1use crate::errors::DomainError;
2
3#[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#[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#[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#[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#[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}