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

View file

@ -2,16 +2,16 @@ use layer_shika_domain::surface_dimensions::SurfaceDimensions;
use slint::PhysicalSize; use slint::PhysicalSize;
pub trait SurfaceDimensionsExt { pub trait SurfaceDimensionsExt {
fn logical_size(&self) -> PhysicalSize; fn to_slint_logical_size(&self) -> PhysicalSize;
fn physical_size(&self) -> PhysicalSize; fn to_slint_physical_size(&self) -> PhysicalSize;
} }
impl SurfaceDimensionsExt for SurfaceDimensions { impl SurfaceDimensionsExt for SurfaceDimensions {
fn logical_size(&self) -> PhysicalSize { fn to_slint_logical_size(&self) -> PhysicalSize {
PhysicalSize::new(self.logical_width, self.logical_height) PhysicalSize::new(self.logical_width(), self.logical_height())
} }
fn physical_size(&self) -> PhysicalSize { fn to_slint_physical_size(&self) -> PhysicalSize {
PhysicalSize::new(self.physical_width, self.physical_height) 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::errors::DomainError;
use layer_shika_domain::ports::windowing::RuntimeStatePort; use layer_shika_domain::ports::windowing::RuntimeStatePort;
use layer_shika_domain::surface_dimensions::SurfaceDimensions; use layer_shika_domain::surface_dimensions::SurfaceDimensions;
use log::info; use log::{error, info};
use slint::{LogicalPosition, PhysicalSize, ComponentHandle}; use slint::{LogicalPosition, PhysicalSize, ComponentHandle};
use slint::platform::{WindowAdapter, WindowEvent}; use slint::platform::{WindowAdapter, WindowEvent};
use slint_interpreter::{ComponentInstance, CompilationResult}; use slint_interpreter::{ComponentInstance, CompilationResult};
@ -179,22 +179,22 @@ impl WindowState {
self.window.set_scale_factor(self.scale_factor); self.window.set_scale_factor(self.scale_factor);
self.window self.window
.set_size(slint::WindowSize::Logical(slint::LogicalSize::new( .set_size(slint::WindowSize::Logical(slint::LogicalSize::new(
dimensions.logical_width as f32, dimensions.logical_width() as f32,
dimensions.logical_height as f32, dimensions.logical_height() as f32,
))); )));
} }
ScalingMode::FractionalOnly => { ScalingMode::FractionalOnly => {
self.window.set_scale_factor(dimensions.buffer_scale as f32); self.window.set_scale_factor(dimensions.buffer_scale() as f32);
self.window self.window
.set_size(slint::WindowSize::Logical(slint::LogicalSize::new( .set_size(slint::WindowSize::Logical(slint::LogicalSize::new(
dimensions.logical_width as f32, dimensions.logical_width() as f32,
dimensions.logical_height as f32, dimensions.logical_height() as f32,
))); )));
} }
ScalingMode::Integer => { ScalingMode::Integer => {
self.window.set_scale_factor(self.scale_factor); self.window.set_scale_factor(self.scale_factor);
self.window 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); self.surface.set_buffer_scale(1);
if let Some(viewport) = &self.viewport { if let Some(viewport) = &self.viewport {
viewport.set_destination( viewport.set_destination(
dimensions.logical_width as i32, dimensions.logical_width() as i32,
dimensions.logical_height as i32, dimensions.logical_height() as i32,
); );
} }
} }
ScalingMode::FractionalOnly | ScalingMode::Integer => { ScalingMode::FractionalOnly | ScalingMode::Integer => {
self.surface.set_buffer_scale(dimensions.buffer_scale); self.surface.set_buffer_scale(dimensions.buffer_scale());
} }
} }
self.layer_surface 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.layer_surface.set_exclusive_zone(self.exclusive_zone);
self.surface.commit(); self.surface.commit();
} }
@ -229,17 +229,23 @@ impl WindowState {
} }
let scale_factor = self.scale_factor(); 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(); let scaling_mode = self.determine_scaling_mode();
info!( info!(
"Updating window size: logical {}x{}, physical {}x{}, scale {}, buffer_scale {}, mode {:?}", "Updating window size: logical {}x{}, physical {}x{}, scale {}, buffer_scale {}, mode {:?}",
dimensions.logical_width, dimensions.logical_width(),
dimensions.logical_height, dimensions.logical_height(),
dimensions.physical_width, dimensions.physical_width(),
dimensions.physical_height, dimensions.physical_height(),
scale_factor, scale_factor,
dimensions.buffer_scale, dimensions.buffer_scale(),
scaling_mode scaling_mode
); );
@ -248,8 +254,8 @@ impl WindowState {
info!("Window physical size: {:?}", self.window.size()); info!("Window physical size: {:?}", self.window.size());
self.size = dimensions.physical_size(); self.size = dimensions.to_slint_physical_size();
self.logical_size = dimensions.logical_size(); self.logical_size = dimensions.to_slint_logical_size();
self.window.request_redraw(); 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_adapters::platform::slint_interpreter::{CompilationResult, Compiler};
use layer_shika_domain::errors::DomainError; use layer_shika_domain::errors::DomainError;
use layer_shika_domain::prelude::{ use layer_shika_domain::prelude::{
AnchorEdges, KeyboardInteractivity, Layer, Margins, WindowConfig, AnchorEdges, KeyboardInteractivity, Layer, Margins, ScaleFactor, WindowConfig, WindowHeight,
}; };
use spin_on::spin_on; use spin_on::spin_on;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -135,10 +135,9 @@ impl LayerShika<NeedsComponent> {
} }
impl LayerShika<HasComponent> { impl LayerShika<HasComponent> {
#[must_use] pub fn with_height(mut self, height: u32) -> Result<Self> {
pub const fn with_height(mut self, height: u32) -> Self { self.config.height = WindowHeight::new(height)?;
self.config.height = height; Ok(self)
self
} }
#[must_use] #[must_use]
@ -176,10 +175,9 @@ impl LayerShika<HasComponent> {
self self
} }
#[must_use] pub fn with_scale_factor(mut self, scale_factor: f32) -> Result<Self> {
pub const fn with_scale_factor(mut self, scale_factor: f32) -> Self { self.config.scale_factor = ScaleFactor::new(scale_factor)?;
self.config.scale_factor = scale_factor; Ok(self)
self
} }
#[must_use] #[must_use]

View file

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

View file

@ -1,6 +1,9 @@
#![allow(clippy::pub_use)] #![allow(clippy::pub_use)]
pub use crate::config::WindowConfig; pub use crate::config::WindowConfig;
pub use crate::dimensions::{
LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, ScaleFactor,
};
pub use crate::entities::component::UiComponentHandle; pub use crate::entities::component::UiComponentHandle;
pub use crate::entities::surface::SurfaceHandle; pub use crate::entities::surface::SurfaceHandle;
pub use crate::entities::window::WindowHandle; 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)] #[derive(Debug, Clone, Copy)]
pub struct SurfaceDimensions { pub struct SurfaceDimensions {
pub logical_width: u32, logical: LogicalSize,
pub logical_height: u32, physical: PhysicalSize,
pub physical_width: u32, scale_factor: ScaleFactor,
pub physical_height: u32,
pub buffer_scale: i32,
} }
impl SurfaceDimensions { impl SurfaceDimensions {
#[must_use] #[allow(clippy::cast_precision_loss)]
#[allow( pub fn calculate(
clippy::cast_possible_truncation, logical_width: u32,
clippy::cast_sign_loss, logical_height: u32,
clippy::cast_precision_loss scale_factor: f32,
)] ) -> Result<Self> {
pub fn calculate(logical_width: u32, logical_height: u32, scale_factor: f32) -> Self { let logical = LogicalSize::new(logical_width as f32, logical_height as f32)?;
let physical_width = (logical_width as f32 * scale_factor).round() as u32; let scale = ScaleFactor::new(scale_factor)?;
let physical_height = (logical_height as f32 * scale_factor).round() as u32; let physical = scale.to_physical(logical);
let buffer_scale = scale_factor.round() as i32;
Self { Ok(Self {
logical_width, logical,
logical_height, physical,
physical_width, scale_factor: scale,
physical_height, })
buffer_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); pub struct WindowHeight(u32);
impl WindowHeight { impl WindowHeight {
#[must_use] pub fn new(height: u32) -> Result<Self> {
pub const fn new(height: u32) -> 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) Self(height)
} }
#[must_use]
pub const fn value(&self) -> u32 { pub const fn value(&self) -> u32 {
self.0 self.0
} }
@ -18,3 +28,11 @@ impl Default for WindowHeight {
Self(30) 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 super::popup_positioning_mode::PopupPositioningMode;
use crate::dimensions::{LogicalPosition, LogicalSize};
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct PopupConfig { pub struct PopupConfig {
reference_x: f32, reference_position: LogicalPosition,
reference_y: f32, popup_size: LogicalSize,
width: f32, output_size: LogicalSize,
height: f32,
positioning_mode: PopupPositioningMode, positioning_mode: PopupPositioningMode,
output_width: f32,
output_height: f32,
} }
impl PopupConfig { impl PopupConfig {
#[must_use] pub fn new(
pub const fn new(
reference_x: f32, reference_x: f32,
reference_y: f32, reference_y: f32,
width: f32, width: f32,
@ -23,62 +20,68 @@ impl PopupConfig {
output_height: f32, output_height: f32,
) -> Self { ) -> Self {
Self { Self {
reference_x, reference_position: LogicalPosition::new(reference_x, reference_y),
reference_y, popup_size: LogicalSize::from_raw(width, height),
width, output_size: LogicalSize::from_raw(output_width, output_height),
height,
positioning_mode, 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 { pub const fn reference_x(&self) -> f32 {
self.reference_x self.reference_position.x()
} }
#[must_use]
pub const fn reference_y(&self) -> f32 { 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 { pub const fn width(&self) -> f32 {
self.width self.popup_size.width()
} }
#[must_use]
pub const fn height(&self) -> f32 { 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 { pub const fn positioning_mode(&self) -> PopupPositioningMode {
self.positioning_mode 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 { pub fn calculated_top_left_x(&self) -> f32 {
let unclamped_x = if self.positioning_mode.center_x() { let unclamped_x = if self.positioning_mode.center_x() {
self.reference_x - (self.width / 2.0) self.reference_x() - (self.width() / 2.0)
} else { } 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) unclamped_x.max(0.0).min(max_x)
} }
#[must_use]
pub fn calculated_top_left_y(&self) -> f32 { pub fn calculated_top_left_y(&self) -> f32 {
let unclamped_y = if self.positioning_mode.center_y() { let unclamped_y = if self.positioning_mode.center_y() {
self.reference_y - (self.height / 2.0) self.reference_y() - (self.height() / 2.0)
} else { } 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) unclamped_y.max(0.0).min(max_y)
} }
} }

View file

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