1use super::Point;
4
5#[derive(Clone, Copy, Debug, Default, PartialEq)]
21pub struct Size {
22 pub width: f64,
24 pub height: f64,
26}
27
28impl Size {
29 pub const fn new(width: f64, height: f64) -> Self {
31 Self { width, height }
32 }
33
34 pub fn to_rect(self) -> Rect {
36 Rect::from_size(self)
37 }
38}
39
40#[derive(Clone, Copy, Debug, Default, PartialEq)]
45#[cfg_attr(
46 feature = "serde",
47 derive(serde::Serialize, serde::Deserialize),
48 serde(rename_all = "snake_case")
49)]
50#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
51pub enum Pivot {
52 TopLeft,
54 Top,
56 TopRight,
58 Left,
60 #[default]
62 Center,
63 Right,
65 BottomLeft,
67 Bottom,
69 BottomRight,
71 Baseline,
73 Relative(f64, f64),
75 Point(Point),
77}
78
79#[derive(Clone, Copy, Debug, Default, PartialEq)]
98pub struct Rect {
99 pub left: f64,
101 pub top: f64,
103 pub right: f64,
105 pub bottom: f64,
107}
108
109impl Rect {
110 pub const fn new(left: f64, top: f64, right: f64, bottom: f64) -> Self {
112 Self {
113 left,
114 top,
115 right,
116 bottom,
117 }
118 }
119
120 pub fn from_origin_size(origin: Point, size: Size) -> Self {
122 Self::new(
123 origin.x,
124 origin.y,
125 origin.x + size.width,
126 origin.y + size.height,
127 )
128 }
129
130 pub fn from_center_size(center: Point, size: Size) -> Self {
132 Self::new(
133 center.x - size.width * 0.5,
134 center.y - size.height * 0.5,
135 center.x + size.width * 0.5,
136 center.y + size.height * 0.5,
137 )
138 }
139
140 pub fn from_size(size: Size) -> Self {
142 Self::from_origin_size(Point::ORIGIN, size)
143 }
144
145 pub fn width(self) -> f64 {
147 self.right - self.left
148 }
149
150 pub fn height(self) -> f64 {
152 self.bottom - self.top
153 }
154
155 pub fn size(self) -> Size {
157 Size::new(self.width(), self.height())
158 }
159
160 pub fn center(self) -> Point {
162 Point::new(
163 self.left.midpoint(self.right),
164 self.top.midpoint(self.bottom),
165 )
166 }
167
168 pub fn anchor(self, pivot: Pivot) -> Point {
173 fn relative(start: f64, end: f64, fraction: f64) -> f64 {
174 match fraction {
175 0.0 => start,
176 1.0 => end,
177 0.5 => start.midpoint(end),
178 _ if start.is_sign_negative() != end.is_sign_negative() => {
179 let middle = start.midpoint(end);
180 if fraction < 0.5 {
181 start + (middle - start) * (fraction * 2.0)
182 } else {
183 end + (middle - end) * ((1.0 - fraction) * 2.0)
184 }
185 }
186 _ => start + (end - start) * fraction,
187 }
188 }
189 let center = self.center();
190 match pivot {
191 Pivot::TopLeft => Point::new(self.left, self.top),
192 Pivot::Top => Point::new(center.x, self.top),
193 Pivot::TopRight => Point::new(self.right, self.top),
194 Pivot::Left => Point::new(self.left, center.y),
195 Pivot::Center => center,
196 Pivot::Right => Point::new(self.right, center.y),
197 Pivot::BottomLeft | Pivot::Baseline => Point::new(self.left, self.bottom),
198 Pivot::Bottom => Point::new(center.x, self.bottom),
199 Pivot::BottomRight => Point::new(self.right, self.bottom),
200 Pivot::Relative(horizontal, vertical) => Point::new(
201 relative(self.left, self.right, horizontal),
202 relative(self.top, self.bottom, vertical),
203 ),
204 Pivot::Point(point) => point,
205 }
206 }
207
208 pub fn is_finite(self) -> bool {
210 [self.left, self.top, self.right, self.bottom]
211 .iter()
212 .all(|value| value.is_finite())
213 }
214
215 pub fn is_empty(self) -> bool {
218 self.is_finite() && (self.right <= self.left || self.bottom <= self.top)
219 }
220
221 pub fn contains(self, point: Point) -> bool {
223 self.is_finite()
224 && point.x.is_finite()
225 && point.y.is_finite()
226 && point.x >= self.left
227 && point.x < self.right
228 && point.y >= self.top
229 && point.y < self.bottom
230 }
231
232 pub fn union(self, other: Self) -> Self {
235 if !self.is_finite() {
236 return self;
237 }
238 if !other.is_finite() {
239 return other;
240 }
241 if self.is_empty() {
242 return other;
243 }
244 if other.is_empty() {
245 return self;
246 }
247 Self::new(
248 self.left.min(other.left),
249 self.top.min(other.top),
250 self.right.max(other.right),
251 self.bottom.max(other.bottom),
252 )
253 }
254
255 pub fn intersect(self, other: Self) -> Self {
258 if !self.is_finite() {
259 return self;
260 }
261 if !other.is_finite() {
262 return other;
263 }
264 let left = self.left.max(other.left);
265 let top = self.top.max(other.top);
266 Self::new(
267 left,
268 top,
269 self.right.min(other.right).max(left),
270 self.bottom.min(other.bottom).max(top),
271 )
272 }
273
274 pub fn inset(self, amount: f64) -> Self {
277 Self::new(
278 self.left + amount,
279 self.top + amount,
280 self.right - amount,
281 self.bottom - amount,
282 )
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 #[test]
289 fn pivot_anchors_cover_corners_edges_center_and_nontext_baseline() {
290 use super::{Pivot, Rect};
291 use crate::Point;
292 let bounds = Rect::new(10.0, 20.0, 50.0, 80.0);
293 for (pivot, expected) in [
294 (Pivot::TopLeft, (10.0, 20.0)),
295 (Pivot::Top, (30.0, 20.0)),
296 (Pivot::TopRight, (50.0, 20.0)),
297 (Pivot::Left, (10.0, 50.0)),
298 (Pivot::Center, (30.0, 50.0)),
299 (Pivot::Right, (50.0, 50.0)),
300 (Pivot::BottomLeft, (10.0, 80.0)),
301 (Pivot::Bottom, (30.0, 80.0)),
302 (Pivot::BottomRight, (50.0, 80.0)),
303 (Pivot::Baseline, (10.0, 80.0)),
304 ] {
305 assert_eq!(bounds.anchor(pivot), Point::new(expected.0, expected.1));
306 }
307 assert_eq!(Pivot::default(), Pivot::Center);
308 assert_eq!(
309 bounds.anchor(Pivot::Relative(-0.25, 1.5)),
310 Point::new(0.0, 110.0)
311 );
312 assert_eq!(
313 bounds.anchor(Pivot::Point(Point::new(-8.0, 5.0))),
314 Point::new(-8.0, 5.0)
315 );
316 }
317
318 #[test]
319 fn pivot_relative_positions_preserve_extreme_midpoints_and_subnormals() {
320 use super::{Pivot, Rect};
321 use crate::Point;
322 let wide = Rect::new(-f64::MAX, 0.0, f64::MAX, f64::from_bits(1));
323 assert_eq!(
324 wide.anchor(Pivot::Relative(0.5, 1.0)),
325 Point::new(0.0, f64::from_bits(1))
326 );
327 assert_eq!(wide.anchor(Pivot::Relative(0.25, 0.0)).x, -f64::MAX * 0.5);
328 let tiny = f64::from_bits(1);
329 let equal = Rect::new(tiny, tiny, tiny, tiny);
330 assert_eq!(
331 equal.anchor(Pivot::Relative(0.5, 0.5)),
332 Point::new(tiny, tiny)
333 );
334 let inverted = Rect::new(50.0, 80.0, 10.0, 20.0);
335 assert_eq!(inverted.anchor(Pivot::TopLeft), Point::new(50.0, 80.0));
336 assert_eq!(
337 inverted.anchor(Pivot::Relative(0.25, 0.5)),
338 Point::new(40.0, 50.0)
339 );
340 }
341
342 use super::*;
343 use crate::{Interpolate, Rect, Size};
344
345 #[test]
346 fn constructors_use_opposite_corners_and_preserve_double_precision() {
347 let bounds = Rect::new(10.0, 20.0, 40.0, 30.0);
348 assert_eq!(bounds.size(), Size::new(30.0, 10.0));
349 assert_eq!(
350 Rect::from_origin_size(Point::new(10.0, 20.0), bounds.size()),
351 bounds
352 );
353 assert_eq!(
354 Rect::from_center_size(Point::new(25.0, 25.0), bounds.size()),
355 bounds
356 );
357 assert_eq!(bounds.size().to_rect(), Rect::new(0.0, 0.0, 30.0, 10.0));
358 let origin = 1_000_000_000.0;
359 let precise = Rect::new(origin, 0.0, origin + 0.25, 0.5);
360 assert_eq!(precise.width(), 0.25);
361 assert_eq!(precise.center(), Point::new(origin + 0.125, 0.25));
362 assert_eq!(precise.left as f32, precise.right as f32);
363 assert_eq!(Size::new(-1.0, 2.0).to_rect().width(), -1.0);
364 }
365
366 #[test]
367 fn containment_intersection_union_and_inset_have_explicit_empty_edges() {
368 let bounds = Rect::new(10.0, 20.0, 40.0, 30.0);
369 assert!(bounds.contains(Point::new(10.0, 20.0)));
370 assert!(!bounds.contains(Point::new(40.0, 20.0)));
371 assert!(!bounds.contains(Point::new(10.0, 30.0)));
372 assert!(!bounds.contains(Point::new(f64::NAN, 25.0)));
373 let adjacent = Rect::new(40.0, 20.0, 50.0, 30.0);
374 assert_eq!(
375 bounds.intersect(adjacent),
376 Rect::new(40.0, 20.0, 40.0, 30.0)
377 );
378 assert!(bounds.intersect(adjacent).is_empty());
379 assert_eq!(bounds.union(adjacent), Rect::new(10.0, 20.0, 50.0, 30.0));
380 assert_eq!(bounds.union(Rect::default()), bounds);
381 assert_eq!(Rect::default().union(bounds), bounds);
382 assert_eq!(bounds.inset(2.0), Rect::new(12.0, 22.0, 38.0, 28.0));
383 assert_eq!(bounds.inset(-2.0), Rect::new(8.0, 18.0, 42.0, 32.0));
384 let inverted = bounds.inset(20.0);
385 assert!(inverted.is_empty());
386 assert!(!inverted.contains(inverted.center()));
387 assert!(bounds.intersect(inverted).is_empty());
388 assert_eq!(bounds.union(inverted), bounds);
389 assert_eq!(
390 bounds.intersect(Rect::new(50.0, 50.0, 60.0, 60.0)),
391 Rect::new(50.0, 50.0, 50.0, 50.0)
392 );
393 }
394
395 #[test]
396 fn extreme_midpoints_remain_finite_and_nonfinite_bounds_are_not_silently_discarded() {
397 let large = Rect::new(1e308, -1.4e308, 1.4e308, -1e308);
398 assert_eq!(large.center(), Point::new(1.2e308, -1.2e308));
399 assert_eq!(
400 Rect::new(-f64::MAX, 0.0, f64::MAX, 2.0).center(),
401 Point::new(0.0, 1.0)
402 );
403 let smallest = f64::from_bits(1);
404 assert_eq!(
405 Rect::new(smallest, smallest, smallest, smallest).center(),
406 Point::new(smallest, smallest)
407 );
408 assert_eq!(
409 Rect::new(smallest, smallest, smallest * 2.0, smallest * 2.0).center(),
410 Point::new(smallest * 2.0, smallest * 2.0)
411 );
412 let valid = Rect::new(0.0, 0.0, 10.0, 10.0);
413 for invalid in [
414 Rect::new(f64::NAN, 0.0, 10.0, 10.0),
415 Rect::new(0.0, 0.0, f64::INFINITY, 10.0),
416 ] {
417 assert!(!invalid.is_finite());
418 assert!(!invalid.is_empty());
419 assert!(!invalid.contains(Point::ORIGIN));
420 assert!(!valid.union(invalid).is_finite());
421 assert!(!invalid.union(valid).is_finite());
422 assert!(!valid.intersect(invalid).is_finite());
423 assert!(!invalid.intersect(valid).is_finite());
424 }
425 }
426
427 #[test]
428 fn interpolation_retains_f64_progress_and_signed_extrapolated_values() {
429 let progress = 0.500_000_000_1;
430 let size = Size::default().interpolate(&Size::new(1.0, 2.0), progress);
431 assert_eq!(size, Size::new(progress, 2.0 * progress));
432 assert_ne!(size.width, f64::from(progress as f32));
433 let bounds = Rect::default().interpolate(&Rect::new(1.0, 2.0, 3.0, 4.0), progress);
434 assert_eq!(
435 bounds,
436 Rect::new(progress, 2.0 * progress, 3.0 * progress, 4.0 * progress)
437 );
438 let inverted = Rect::new(0.0, 0.0, 10.0, 20.0).interpolate(&Rect::default(), 2.0);
439 assert_eq!(inverted.size(), Size::new(-10.0, -20.0));
440 assert!(inverted.is_empty());
441 assert_eq!(
442 Size::new(10.0, 20.0).interpolate(&Size::default(), 2.0),
443 Size::new(-10.0, -20.0)
444 );
445 }
446}