refactor: unified dimensions system

This commit is contained in:
drendog 2025-11-09 16:43:19 +01:00
parent c3c2690e84
commit acece2dbf3
Signed by: dwenya
GPG key ID: 8DD77074645332D0
12 changed files with 416 additions and 120 deletions

View file

@ -40,7 +40,7 @@ impl WaylandWindowConfig {
domain_config: DomainWindowConfig,
) -> Self {
Self {
height: domain_config.height,
height: domain_config.height.value(),
layer: convert_layer(domain_config.layer),
margin: domain_config.margin,
anchor: convert_anchor(domain_config.anchor),
@ -48,7 +48,7 @@ impl WaylandWindowConfig {
domain_config.keyboard_interactivity,
),
exclusive_zone: domain_config.exclusive_zone,
scale_factor: domain_config.scale_factor,
scale_factor: domain_config.scale_factor.value(),
namespace: domain_config.namespace,
component_definition,
compilation_result,

View file

@ -2,16 +2,16 @@ use layer_shika_domain::surface_dimensions::SurfaceDimensions;
use slint::PhysicalSize;
pub trait SurfaceDimensionsExt {
fn logical_size(&self) -> PhysicalSize;
fn physical_size(&self) -> PhysicalSize;
fn to_slint_logical_size(&self) -> PhysicalSize;
fn to_slint_physical_size(&self) -> PhysicalSize;
}
impl SurfaceDimensionsExt for SurfaceDimensions {
fn logical_size(&self) -> PhysicalSize {
PhysicalSize::new(self.logical_width, self.logical_height)
fn to_slint_logical_size(&self) -> PhysicalSize {
PhysicalSize::new(self.logical_width(), self.logical_height())
}
fn physical_size(&self) -> PhysicalSize {
PhysicalSize::new(self.physical_width, self.physical_height)
fn to_slint_physical_size(&self) -> PhysicalSize {
PhysicalSize::new(self.physical_width(), self.physical_height())
}
}

View file

@ -12,7 +12,7 @@ use core::result::Result as CoreResult;
use layer_shika_domain::errors::DomainError;
use layer_shika_domain::ports::windowing::RuntimeStatePort;
use layer_shika_domain::surface_dimensions::SurfaceDimensions;
use log::info;
use log::{error, info};
use slint::{LogicalPosition, PhysicalSize, ComponentHandle};
use slint::platform::{WindowAdapter, WindowEvent};
use slint_interpreter::{ComponentInstance, CompilationResult};
@ -179,22 +179,22 @@ impl WindowState {
self.window.set_scale_factor(self.scale_factor);
self.window
.set_size(slint::WindowSize::Logical(slint::LogicalSize::new(
dimensions.logical_width as f32,
dimensions.logical_height as f32,
dimensions.logical_width() as f32,
dimensions.logical_height() as f32,
)));
}
ScalingMode::FractionalOnly => {
self.window.set_scale_factor(dimensions.buffer_scale as f32);
self.window.set_scale_factor(dimensions.buffer_scale() as f32);
self.window
.set_size(slint::WindowSize::Logical(slint::LogicalSize::new(
dimensions.logical_width as f32,
dimensions.logical_height as f32,
dimensions.logical_width() as f32,
dimensions.logical_height() as f32,
)));
}
ScalingMode::Integer => {
self.window.set_scale_factor(self.scale_factor);
self.window
.set_size(slint::WindowSize::Physical(dimensions.physical_size()));
.set_size(slint::WindowSize::Physical(dimensions.to_slint_physical_size()));
}
}
}
@ -206,18 +206,18 @@ impl WindowState {
self.surface.set_buffer_scale(1);
if let Some(viewport) = &self.viewport {
viewport.set_destination(
dimensions.logical_width as i32,
dimensions.logical_height as i32,
dimensions.logical_width() as i32,
dimensions.logical_height() as i32,
);
}
}
ScalingMode::FractionalOnly | ScalingMode::Integer => {
self.surface.set_buffer_scale(dimensions.buffer_scale);
self.surface.set_buffer_scale(dimensions.buffer_scale());
}
}
self.layer_surface
.set_size(dimensions.logical_width, dimensions.logical_height);
.set_size(dimensions.logical_width(), dimensions.logical_height());
self.layer_surface.set_exclusive_zone(self.exclusive_zone);
self.surface.commit();
}
@ -229,17 +229,23 @@ impl WindowState {
}
let scale_factor = self.scale_factor();
let dimensions = SurfaceDimensions::calculate(width, height, scale_factor);
let dimensions = match SurfaceDimensions::calculate(width, height, scale_factor) {
Ok(d) => d,
Err(e) => {
error!("Failed to calculate surface dimensions: {e}");
return;
}
};
let scaling_mode = self.determine_scaling_mode();
info!(
"Updating window size: logical {}x{}, physical {}x{}, scale {}, buffer_scale {}, mode {:?}",
dimensions.logical_width,
dimensions.logical_height,
dimensions.physical_width,
dimensions.physical_height,
dimensions.logical_width(),
dimensions.logical_height(),
dimensions.physical_width(),
dimensions.physical_height(),
scale_factor,
dimensions.buffer_scale,
dimensions.buffer_scale(),
scaling_mode
);
@ -248,8 +254,8 @@ impl WindowState {
info!("Window physical size: {:?}", self.window.size());
self.size = dimensions.physical_size();
self.logical_size = dimensions.logical_size();
self.size = dimensions.to_slint_physical_size();
self.logical_size = dimensions.to_slint_logical_size();
self.window.request_redraw();
}

View file

@ -3,7 +3,7 @@ use crate::system::WindowingSystem;
use layer_shika_adapters::platform::slint_interpreter::{CompilationResult, Compiler};
use layer_shika_domain::errors::DomainError;
use layer_shika_domain::prelude::{
AnchorEdges, KeyboardInteractivity, Layer, Margins, WindowConfig,
AnchorEdges, KeyboardInteractivity, Layer, Margins, ScaleFactor, WindowConfig, WindowHeight,
};
use spin_on::spin_on;
use std::path::{Path, PathBuf};
@ -135,10 +135,9 @@ impl LayerShika<NeedsComponent> {
}
impl LayerShika<HasComponent> {
#[must_use]
pub const fn with_height(mut self, height: u32) -> Self {
self.config.height = height;
self
pub fn with_height(mut self, height: u32) -> Result<Self> {
self.config.height = WindowHeight::new(height)?;
Ok(self)
}
#[must_use]
@ -176,10 +175,9 @@ impl LayerShika<HasComponent> {
self
}
#[must_use]
pub const fn with_scale_factor(mut self, scale_factor: f32) -> Self {
self.config.scale_factor = scale_factor;
self
pub fn with_scale_factor(mut self, scale_factor: f32) -> Result<Self> {
self.config.scale_factor = ScaleFactor::new(scale_factor)?;
Ok(self)
}
#[must_use]

View file

@ -1,14 +1,16 @@
use crate::dimensions::ScaleFactor;
use crate::value_objects::anchor::AnchorEdges;
use crate::value_objects::dimensions::WindowHeight;
use crate::value_objects::keyboard_interactivity::KeyboardInteractivity;
use crate::value_objects::layer::Layer;
use crate::value_objects::margins::Margins;
#[derive(Debug, Clone)]
pub struct WindowConfig {
pub height: u32,
pub height: WindowHeight,
pub margin: Margins,
pub exclusive_zone: i32,
pub scale_factor: f32,
pub scale_factor: ScaleFactor,
pub namespace: String,
pub layer: Layer,
pub anchor: AnchorEdges,
@ -19,11 +21,11 @@ impl WindowConfig {
#[must_use]
pub fn new() -> Self {
Self {
height: 30,
height: WindowHeight::default(),
margin: Margins::default(),
exclusive_zone: -1,
namespace: "layer-shika".to_owned(),
scale_factor: 1.0,
scale_factor: ScaleFactor::default(),
layer: Layer::default(),
anchor: AnchorEdges::default(),
keyboard_interactivity: KeyboardInteractivity::default(),

233
domain/src/dimensions.rs Normal file
View file

@ -0,0 +1,233 @@
use crate::errors::DomainError;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LogicalSize {
width: f32,
height: f32,
}
impl LogicalSize {
pub fn new(width: f32, height: f32) -> Result<Self, DomainError> {
if width <= 0.0 || height <= 0.0 {
return Err(DomainError::InvalidInput {
message: format!("Dimensions must be positive, got width={width}, height={height}"),
});
}
if !width.is_finite() || !height.is_finite() {
return Err(DomainError::InvalidInput {
message: "Dimensions must be finite values".to_string(),
});
}
Ok(Self { width, height })
}
pub const fn from_raw(width: f32, height: f32) -> Self {
Self { width, height }
}
pub const fn width(&self) -> f32 {
self.width
}
pub const fn height(&self) -> f32 {
self.height
}
pub fn to_physical(&self, scale_factor: ScaleFactor) -> PhysicalSize {
scale_factor.to_physical(*self)
}
pub fn as_tuple(&self) -> (f32, f32) {
(self.width, self.height)
}
}
impl Default for LogicalSize {
fn default() -> Self {
Self {
width: 120.0,
height: 120.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PhysicalSize {
width: u32,
height: u32,
}
impl PhysicalSize {
pub fn new(width: u32, height: u32) -> Result<Self, DomainError> {
if width == 0 || height == 0 {
return Err(DomainError::InvalidDimensions { width, height });
}
Ok(Self { width, height })
}
pub const fn from_raw(width: u32, height: u32) -> Self {
Self { width, height }
}
pub const fn width(&self) -> u32 {
self.width
}
pub const fn height(&self) -> u32 {
self.height
}
pub fn to_logical(&self, scale_factor: ScaleFactor) -> LogicalSize {
scale_factor.to_logical(*self)
}
pub fn as_tuple(&self) -> (u32, u32) {
(self.width, self.height)
}
}
impl Default for PhysicalSize {
fn default() -> Self {
Self {
width: 120,
height: 120,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ScaleFactor(f32);
impl ScaleFactor {
pub fn new(factor: f32) -> Result<Self, DomainError> {
if factor <= 0.0 {
return Err(DomainError::InvalidInput {
message: format!("Scale factor must be positive, got {factor}"),
});
}
if !factor.is_finite() {
return Err(DomainError::InvalidInput {
message: "Scale factor must be a finite value".to_string(),
});
}
Ok(Self(factor))
}
pub const fn from_raw(factor: f32) -> Self {
Self(factor)
}
pub const fn value(&self) -> f32 {
self.0
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn to_physical(&self, logical: LogicalSize) -> PhysicalSize {
let width = (logical.width * self.0).round() as u32;
let height = (logical.height * self.0).round() as u32;
PhysicalSize::from_raw(width.max(1), height.max(1))
}
#[allow(clippy::cast_precision_loss)]
pub fn to_logical(&self, physical: PhysicalSize) -> LogicalSize {
let width = physical.width as f32 / self.0;
let height = physical.height as f32 / self.0;
LogicalSize::from_raw(width, height)
}
#[allow(clippy::cast_possible_truncation)]
pub fn buffer_scale(&self) -> i32 {
self.0.round() as i32
}
pub fn scale_coordinate(&self, logical_coord: f32) -> f32 {
logical_coord * self.0
}
pub fn unscale_coordinate(&self, physical_coord: f32) -> f32 {
physical_coord / self.0
}
}
impl Default for ScaleFactor {
fn default() -> Self {
Self(1.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LogicalPosition {
x: f32,
y: f32,
}
impl LogicalPosition {
pub fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
pub const fn x(&self) -> f32 {
self.x
}
pub const fn y(&self) -> f32 {
self.y
}
#[allow(clippy::cast_possible_truncation)]
pub fn to_physical(&self, scale_factor: ScaleFactor) -> PhysicalPosition {
PhysicalPosition::new(
(self.x * scale_factor.value()).round() as i32,
(self.y * scale_factor.value()).round() as i32,
)
}
pub fn as_tuple(&self) -> (f32, f32) {
(self.x, self.y)
}
}
impl Default for LogicalPosition {
fn default() -> Self {
Self { x: 0.0, y: 0.0 }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PhysicalPosition {
x: i32,
y: i32,
}
impl PhysicalPosition {
pub const fn new(x: i32, y: i32) -> Self {
Self { x, y }
}
pub const fn x(&self) -> i32 {
self.x
}
pub const fn y(&self) -> i32 {
self.y
}
#[allow(clippy::cast_precision_loss)]
pub fn to_logical(&self, scale_factor: ScaleFactor) -> LogicalPosition {
LogicalPosition::new(
self.x as f32 / scale_factor.value(),
self.y as f32 / scale_factor.value(),
)
}
pub fn as_tuple(&self) -> (i32, i32) {
(self.x, self.y)
}
}
#[allow(clippy::derivable_impls)]
impl Default for PhysicalPosition {
fn default() -> Self {
Self { x: 0, y: 0 }
}
}

View file

@ -1,4 +1,5 @@
pub mod config;
pub mod dimensions;
pub mod entities;
pub mod errors;
pub mod ports;

View file

@ -1,6 +1,9 @@
#![allow(clippy::pub_use)]
pub use crate::config::WindowConfig;
pub use crate::dimensions::{
LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, ScaleFactor,
};
pub use crate::entities::component::UiComponentHandle;
pub use crate::entities::surface::SurfaceHandle;
pub use crate::entities::window::WindowHandle;

View file

@ -1,30 +1,62 @@
use crate::dimensions::{LogicalSize, PhysicalSize, ScaleFactor};
use crate::errors::Result;
#[derive(Debug, Clone, Copy)]
pub struct SurfaceDimensions {
pub logical_width: u32,
pub logical_height: u32,
pub physical_width: u32,
pub physical_height: u32,
pub buffer_scale: i32,
logical: LogicalSize,
physical: PhysicalSize,
scale_factor: ScaleFactor,
}
impl SurfaceDimensions {
#[must_use]
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
pub fn calculate(logical_width: u32, logical_height: u32, scale_factor: f32) -> Self {
let physical_width = (logical_width as f32 * scale_factor).round() as u32;
let physical_height = (logical_height as f32 * scale_factor).round() as u32;
let buffer_scale = scale_factor.round() as i32;
#[allow(clippy::cast_precision_loss)]
pub fn calculate(
logical_width: u32,
logical_height: u32,
scale_factor: f32,
) -> Result<Self> {
let logical = LogicalSize::new(logical_width as f32, logical_height as f32)?;
let scale = ScaleFactor::new(scale_factor)?;
let physical = scale.to_physical(logical);
Self {
logical_width,
logical_height,
physical_width,
physical_height,
buffer_scale,
}
Ok(Self {
logical,
physical,
scale_factor: scale,
})
}
pub const fn logical_size(&self) -> LogicalSize {
self.logical
}
pub const fn physical_size(&self) -> PhysicalSize {
self.physical
}
pub const fn scale_factor(&self) -> ScaleFactor {
self.scale_factor
}
pub fn buffer_scale(&self) -> i32 {
self.scale_factor.buffer_scale()
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn logical_width(&self) -> u32 {
self.logical.width() as u32
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn logical_height(&self) -> u32 {
self.logical.height() as u32
}
pub fn physical_width(&self) -> u32 {
self.physical.width()
}
pub fn physical_height(&self) -> u32 {
self.physical.height()
}
}

View file

@ -1,13 +1,23 @@
#[derive(Debug, Clone, Copy)]
use crate::errors::{DomainError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WindowHeight(u32);
impl WindowHeight {
#[must_use]
pub const fn new(height: u32) -> Self {
pub fn new(height: u32) -> Result<Self> {
if height == 0 {
return Err(DomainError::InvalidDimensions {
width: 0,
height: 0,
});
}
Ok(Self(height))
}
pub const fn from_raw(height: u32) -> Self {
Self(height)
}
#[must_use]
pub const fn value(&self) -> u32 {
self.0
}
@ -18,3 +28,11 @@ impl Default for WindowHeight {
Self(30)
}
}
impl TryFrom<u32> for WindowHeight {
type Error = DomainError;
fn try_from(height: u32) -> Result<Self> {
Self::new(height)
}
}

View file

@ -1,19 +1,16 @@
use super::popup_positioning_mode::PopupPositioningMode;
use crate::dimensions::{LogicalPosition, LogicalSize};
#[derive(Debug, Clone, Copy)]
pub struct PopupConfig {
reference_x: f32,
reference_y: f32,
width: f32,
height: f32,
reference_position: LogicalPosition,
popup_size: LogicalSize,
output_size: LogicalSize,
positioning_mode: PopupPositioningMode,
output_width: f32,
output_height: f32,
}
impl PopupConfig {
#[must_use]
pub const fn new(
pub fn new(
reference_x: f32,
reference_y: f32,
width: f32,
@ -23,62 +20,68 @@ impl PopupConfig {
output_height: f32,
) -> Self {
Self {
reference_x,
reference_y,
width,
height,
reference_position: LogicalPosition::new(reference_x, reference_y),
popup_size: LogicalSize::from_raw(width, height),
output_size: LogicalSize::from_raw(output_width, output_height),
positioning_mode,
output_width,
output_height,
}
}
#[must_use]
pub const fn reference_position(&self) -> LogicalPosition {
self.reference_position
}
pub const fn reference_x(&self) -> f32 {
self.reference_x
self.reference_position.x()
}
#[must_use]
pub const fn reference_y(&self) -> f32 {
self.reference_y
self.reference_position.y()
}
pub const fn popup_size(&self) -> LogicalSize {
self.popup_size
}
#[must_use]
pub const fn width(&self) -> f32 {
self.width
self.popup_size.width()
}
#[must_use]
pub const fn height(&self) -> f32 {
self.height
self.popup_size.height()
}
pub const fn output_size(&self) -> LogicalSize {
self.output_size
}
#[must_use]
pub const fn positioning_mode(&self) -> PopupPositioningMode {
self.positioning_mode
}
#[must_use]
pub fn calculated_top_left_position(&self) -> LogicalPosition {
LogicalPosition::new(self.calculated_top_left_x(), self.calculated_top_left_y())
}
pub fn calculated_top_left_x(&self) -> f32 {
let unclamped_x = if self.positioning_mode.center_x() {
self.reference_x - (self.width / 2.0)
self.reference_x() - (self.width() / 2.0)
} else {
self.reference_x
self.reference_x()
};
let max_x = self.output_width - self.width;
let max_x = self.output_size.width() - self.width();
unclamped_x.max(0.0).min(max_x)
}
#[must_use]
pub fn calculated_top_left_y(&self) -> f32 {
let unclamped_y = if self.positioning_mode.center_y() {
self.reference_y - (self.height / 2.0)
self.reference_y() - (self.height() / 2.0)
} else {
self.reference_y
self.reference_y()
};
let max_y = self.output_height - self.height;
let max_y = self.output_size.height() - self.height();
unclamped_y.max(0.0).min(max_y)
}
}

View file

@ -1,42 +1,42 @@
use crate::dimensions::LogicalSize;
use crate::errors::{DomainError, Result};
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct PopupDimensions {
width: f32,
height: f32,
size: LogicalSize,
}
impl PopupDimensions {
#[must_use]
pub const fn new(width: f32, height: f32) -> Self {
Self { width, height }
pub fn new(width: f32, height: f32) -> Result<Self> {
let size = LogicalSize::new(width, height)?;
Ok(Self { size })
}
pub const fn from_logical(size: LogicalSize) -> Self {
Self { size }
}
#[must_use]
pub const fn width(&self) -> f32 {
self.width
self.size.width()
}
#[must_use]
pub const fn height(&self) -> f32 {
self.height
self.size.height()
}
pub fn validate(&self) -> Result<()> {
if self.width <= 0.0 || self.height <= 0.0 {
return Err(DomainError::Configuration {
message: format!(
"Invalid popup dimensions: width={}, height={}. Both must be positive.",
self.width, self.height
),
});
pub const fn logical_size(&self) -> LogicalSize {
self.size
}
Ok(())
pub fn as_tuple(&self) -> (f32, f32) {
self.size.as_tuple()
}
}
impl Default for PopupDimensions {
fn default() -> Self {
Self::new(120.0, 120.0)
impl TryFrom<(f32, f32)> for PopupDimensions {
type Error = DomainError;
fn try_from((width, height): (f32, f32)) -> Result<Self> {
Self::new(width, height)
}
}