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    #[must_use]
197    pub const fn new(x: f32, y: f32) -> Self {
198        Self { x, y }
199    }
200
201    pub const fn x(&self) -> f32 {
202        self.x
203    }
204
205    pub const fn y(&self) -> f32 {
206        self.y
207    }
208
209    #[allow(clippy::cast_possible_truncation)]
210    pub fn to_physical(&self, scale_factor: ScaleFactor) -> PhysicalPosition {
211        PhysicalPosition::new(
212            (self.x * scale_factor.value()).round() as i32,
213            (self.y * scale_factor.value()).round() as i32,
214        )
215    }
216
217    pub fn as_tuple(&self) -> (f32, f32) {
218        (self.x, self.y)
219    }
220}
221
222impl Default for LogicalPosition {
223    fn default() -> Self {
224        Self { x: 0.0, y: 0.0 }
225    }
226}
227
228/// Rectangle in logical pixels
229#[derive(Debug, Clone, Copy, PartialEq)]
230pub struct LogicalRect {
231    x: f32,
232    y: f32,
233    width: f32,
234    height: f32,
235}
236
237impl LogicalRect {
238    #[must_use]
239    pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
240        Self {
241            x,
242            y,
243            width,
244            height,
245        }
246    }
247
248    #[must_use]
249    pub const fn x(&self) -> f32 {
250        self.x
251    }
252
253    #[must_use]
254    pub const fn y(&self) -> f32 {
255        self.y
256    }
257
258    #[must_use]
259    pub const fn width(&self) -> f32 {
260        self.width
261    }
262
263    #[must_use]
264    pub const fn height(&self) -> f32 {
265        self.height
266    }
267
268    #[must_use]
269    pub const fn origin(&self) -> LogicalPosition {
270        LogicalPosition::new(self.x, self.y)
271    }
272
273    #[must_use]
274    pub const fn size(&self) -> LogicalSize {
275        LogicalSize::from_raw(self.width, self.height)
276    }
277}
278
279#[derive(Debug, Clone, Copy, PartialEq, Eq)]
280pub struct PhysicalPosition {
281    x: i32,
282    y: i32,
283}
284
285impl PhysicalPosition {
286    pub const fn new(x: i32, y: i32) -> Self {
287        Self { x, y }
288    }
289
290    pub const fn x(&self) -> i32 {
291        self.x
292    }
293
294    pub const fn y(&self) -> i32 {
295        self.y
296    }
297
298    #[allow(clippy::cast_precision_loss)]
299    pub fn to_logical(&self, scale_factor: ScaleFactor) -> LogicalPosition {
300        LogicalPosition::new(
301            self.x as f32 / scale_factor.value(),
302            self.y as f32 / scale_factor.value(),
303        )
304    }
305
306    pub fn as_tuple(&self) -> (i32, i32) {
307        (self.x, self.y)
308    }
309}
310
311#[allow(clippy::derivable_impls)]
312impl Default for PhysicalPosition {
313    fn default() -> Self {
314        Self { x: 0, y: 0 }
315    }
316}