layer_shika_domain/
dimensions.rs

1use crate::errors::DomainError;
2
3/// Size in logical pixels, independent of display scaling
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub struct LogicalSize {
6    width: f32,
7    height: f32,
8}
9
10impl LogicalSize {
11    pub fn new(width: f32, height: f32) -> Result<Self, DomainError> {
12        if width <= 0.0 || height <= 0.0 {
13            return Err(DomainError::InvalidInput {
14                message: format!("Dimensions must be positive, got width={width}, height={height}"),
15            });
16        }
17        if !width.is_finite() || !height.is_finite() {
18            return Err(DomainError::InvalidInput {
19                message: "Dimensions must be finite values".to_string(),
20            });
21        }
22        Ok(Self { width, height })
23    }
24
25    pub const fn from_raw(width: f32, height: f32) -> Self {
26        Self { width, height }
27    }
28
29    pub const fn width(&self) -> f32 {
30        self.width
31    }
32
33    pub const fn height(&self) -> f32 {
34        self.height
35    }
36
37    pub fn to_physical(&self, scale_factor: ScaleFactor) -> PhysicalSize {
38        scale_factor.to_physical(*self)
39    }
40
41    pub fn as_tuple(&self) -> (f32, f32) {
42        (self.width, self.height)
43    }
44
45    pub fn clamp_position(
46        &self,
47        position: LogicalPosition,
48        bounds: LogicalSize,
49    ) -> LogicalPosition {
50        let max_x = (bounds.width - self.width).max(0.0);
51        let max_y = (bounds.height - self.height).max(0.0);
52
53        LogicalPosition::new(
54            position.x().max(0.0).min(max_x),
55            position.y().max(0.0).min(max_y),
56        )
57    }
58}
59
60impl Default for LogicalSize {
61    fn default() -> Self {
62        Self {
63            width: 120.0,
64            height: 120.0,
65        }
66    }
67}
68
69/// Size in physical device pixels
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct PhysicalSize {
72    width: u32,
73    height: u32,
74}
75
76impl PhysicalSize {
77    pub fn new(width: u32, height: u32) -> Result<Self, DomainError> {
78        if width == 0 || height == 0 {
79            return Err(DomainError::InvalidDimensions { width, height });
80        }
81        Ok(Self { width, height })
82    }
83
84    pub const fn from_raw(width: u32, height: u32) -> Self {
85        Self { width, height }
86    }
87
88    pub const fn width(&self) -> u32 {
89        self.width
90    }
91
92    pub const fn height(&self) -> u32 {
93        self.height
94    }
95
96    pub fn to_logical(&self, scale_factor: ScaleFactor) -> LogicalSize {
97        scale_factor.to_logical(*self)
98    }
99
100    pub fn as_tuple(&self) -> (u32, u32) {
101        (self.width, self.height)
102    }
103}
104
105impl Default for PhysicalSize {
106    fn default() -> Self {
107        Self {
108            width: 120,
109            height: 120,
110        }
111    }
112}
113
114/// Display scaling factor for converting between logical and physical pixels
115#[derive(Debug, Clone, Copy, PartialEq)]
116pub struct ScaleFactor(f32);
117
118impl ScaleFactor {
119    pub fn new(factor: f32) -> Result<Self, DomainError> {
120        if factor <= 0.0 {
121            return Err(DomainError::InvalidInput {
122                message: format!("Scale factor must be positive, got {factor}"),
123            });
124        }
125        if !factor.is_finite() {
126            return Err(DomainError::InvalidInput {
127                message: "Scale factor must be a finite value".to_string(),
128            });
129        }
130        Ok(Self(factor))
131    }
132
133    pub const fn from_raw(factor: f32) -> Self {
134        Self(factor)
135    }
136
137    #[allow(clippy::cast_precision_loss)]
138    pub fn from_120ths(scale_120ths: u32) -> Self {
139        Self(scale_120ths as f32 / 120.0)
140    }
141
142    pub const fn value(&self) -> f32 {
143        self.0
144    }
145
146    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
147    pub fn to_physical(&self, logical: LogicalSize) -> PhysicalSize {
148        let width = (logical.width * self.0).round() as u32;
149        let height = (logical.height * self.0).round() as u32;
150        PhysicalSize::from_raw(width.max(1), height.max(1))
151    }
152
153    #[allow(clippy::cast_precision_loss)]
154    pub fn to_logical(&self, physical: PhysicalSize) -> LogicalSize {
155        let width = physical.width as f32 / self.0;
156        let height = physical.height as f32 / self.0;
157        LogicalSize::from_raw(width, height)
158    }
159
160    #[allow(clippy::cast_possible_truncation)]
161    pub fn buffer_scale(&self) -> i32 {
162        self.0.round() as i32
163    }
164
165    pub fn scale_coordinate(&self, logical_coord: f32) -> f32 {
166        logical_coord * self.0
167    }
168
169    pub fn unscale_coordinate(&self, physical_coord: f32) -> f32 {
170        physical_coord / self.0
171    }
172}
173
174impl Default for ScaleFactor {
175    fn default() -> Self {
176        Self(1.0)
177    }
178}
179
180impl TryFrom<f32> for ScaleFactor {
181    type Error = DomainError;
182
183    fn try_from(factor: f32) -> Result<Self, Self::Error> {
184        Self::new(factor)
185    }
186}
187
188/// Position in logical pixels, independent of display scaling
189#[derive(Debug, Clone, Copy, PartialEq)]
190pub struct LogicalPosition {
191    x: f32,
192    y: f32,
193}
194
195impl LogicalPosition {
196    pub fn new(x: f32, y: f32) -> Self {
197        Self { x, y }
198    }
199
200    pub const fn x(&self) -> f32 {
201        self.x
202    }
203
204    pub const fn y(&self) -> f32 {
205        self.y
206    }
207
208    #[allow(clippy::cast_possible_truncation)]
209    pub fn to_physical(&self, scale_factor: ScaleFactor) -> PhysicalPosition {
210        PhysicalPosition::new(
211            (self.x * scale_factor.value()).round() as i32,
212            (self.y * scale_factor.value()).round() as i32,
213        )
214    }
215
216    pub fn as_tuple(&self) -> (f32, f32) {
217        (self.x, self.y)
218    }
219}
220
221impl Default for LogicalPosition {
222    fn default() -> Self {
223        Self { x: 0.0, y: 0.0 }
224    }
225}
226
227#[derive(Debug, Clone, Copy, PartialEq, Eq)]
228pub struct PhysicalPosition {
229    x: i32,
230    y: i32,
231}
232
233impl PhysicalPosition {
234    pub const fn new(x: i32, y: i32) -> Self {
235        Self { x, y }
236    }
237
238    pub const fn x(&self) -> i32 {
239        self.x
240    }
241
242    pub const fn y(&self) -> i32 {
243        self.y
244    }
245
246    #[allow(clippy::cast_precision_loss)]
247    pub fn to_logical(&self, scale_factor: ScaleFactor) -> LogicalPosition {
248        LogicalPosition::new(
249            self.x as f32 / scale_factor.value(),
250            self.y as f32 / scale_factor.value(),
251        )
252    }
253
254    pub fn as_tuple(&self) -> (i32, i32) {
255        (self.x, self.y)
256    }
257}
258
259#[allow(clippy::derivable_impls)]
260impl Default for PhysicalPosition {
261    fn default() -> Self {
262        Self { x: 0, y: 0 }
263    }
264}