kurbo/
size.rs

1// Copyright 2019 the Kurbo Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! A 2D size.
5
6use core::fmt;
7use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign};
8
9use crate::common::FloatExt;
10use crate::{Axis, Rect, RoundedRect, RoundedRectRadii, Vec2};
11
12#[cfg(not(feature = "std"))]
13use crate::common::FloatFuncs;
14
15/// A 2D size.
16#[derive(Clone, Copy, Default, PartialEq)]
17#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub struct Size {
20    /// The width.
21    pub width: f64,
22    /// The height.
23    pub height: f64,
24}
25
26impl Size {
27    /// A size with zero width or height.
28    pub const ZERO: Size = Size::new(0., 0.);
29
30    /// A size with width and height set to `f64::INFINITY`.
31    pub const INFINITY: Size = Size::new(f64::INFINITY, f64::INFINITY);
32
33    /// Create a new `Size` with the provided `width` and `height`.
34    #[inline(always)]
35    pub const fn new(width: f64, height: f64) -> Self {
36        Size { width, height }
37    }
38
39    /// Returns the max of `width` and `height`.
40    ///
41    /// # Examples
42    ///
43    /// ```
44    /// use kurbo::Size;
45    /// let size = Size::new(-10.5, 42.0);
46    /// assert_eq!(size.max_side(), 42.0);
47    /// ```
48    pub fn max_side(self) -> f64 {
49        self.width.max(self.height)
50    }
51
52    /// Returns the min of `width` and `height`.
53    ///
54    /// # Examples
55    ///
56    /// ```
57    /// use kurbo::Size;
58    /// let size = Size::new(-10.5, 42.0);
59    /// assert_eq!(size.min_side(), -10.5);
60    /// ```
61    pub fn min_side(self) -> f64 {
62        self.width.min(self.height)
63    }
64
65    /// The area covered by this size.
66    #[inline]
67    pub fn area(self) -> f64 {
68        self.width * self.height
69    }
70
71    /// Whether this size has zero area.
72    #[doc(alias = "is_empty")]
73    #[inline]
74    pub fn is_zero_area(self) -> bool {
75        self.area() == 0.0
76    }
77
78    /// Returns the component-wise minimum of `self` and `other`.
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// use kurbo::Size;
84    ///
85    /// let this = Size::new(0., 100.);
86    /// let other = Size::new(10., 10.);
87    ///
88    /// assert_eq!(this.min(other), Size::new(0., 10.));
89    /// ```
90    pub fn min(self, other: Size) -> Self {
91        Size {
92            width: self.width.min(other.width),
93            height: self.height.min(other.height),
94        }
95    }
96
97    /// Returns the component-wise maximum of `self` and `other`.
98    ///
99    /// # Examples
100    ///
101    /// ```
102    /// use kurbo::Size;
103    ///
104    /// let this = Size::new(0., 100.);
105    /// let other = Size::new(10., 10.);
106    ///
107    /// assert_eq!(this.max(other), Size::new(10., 100.));
108    /// ```
109    pub fn max(self, other: Size) -> Self {
110        Size {
111            width: self.width.max(other.width),
112            height: self.height.max(other.height),
113        }
114    }
115
116    /// Returns a new size bounded by `min` and `max.`
117    ///
118    /// # Examples
119    ///
120    /// ```
121    /// use kurbo::Size;
122    ///
123    /// let this = Size::new(0., 100.);
124    /// let min = Size::new(10., 10.,);
125    /// let max = Size::new(50., 50.);
126    /// assert_eq!(this.clamp(min, max), Size::new(10., 50.))
127    /// ```
128    pub fn clamp(self, min: Size, max: Size) -> Self {
129        self.max(min).min(max)
130    }
131
132    /// Convert this size into a [`Vec2`], with `width` mapped to `x` and `height`
133    /// mapped to `y`.
134    #[inline(always)]
135    pub const fn to_vec2(self) -> Vec2 {
136        Vec2::new(self.width, self.height)
137    }
138
139    /// Returns a new `Size`,
140    /// with `width` and `height` [rounded] to the nearest integer.
141    ///
142    /// # Examples
143    ///
144    /// ```
145    /// use kurbo::Size;
146    /// let size_pos = Size::new(3.3, 3.6).round();
147    /// assert_eq!(size_pos.width, 3.0);
148    /// assert_eq!(size_pos.height, 4.0);
149    /// let size_neg = Size::new(-3.3, -3.6).round();
150    /// assert_eq!(size_neg.width, -3.0);
151    /// assert_eq!(size_neg.height, -4.0);
152    /// ```
153    ///
154    /// [rounded]: f64::round
155    #[inline]
156    pub fn round(self) -> Size {
157        Size::new(self.width.round(), self.height.round())
158    }
159
160    /// Returns a new `Size`,
161    /// with `width` and `height` [rounded up] to the nearest integer,
162    /// unless they are already an integer.
163    ///
164    /// # Examples
165    ///
166    /// ```
167    /// use kurbo::Size;
168    /// let size_pos = Size::new(3.3, 3.6).ceil();
169    /// assert_eq!(size_pos.width, 4.0);
170    /// assert_eq!(size_pos.height, 4.0);
171    /// let size_neg = Size::new(-3.3, -3.6).ceil();
172    /// assert_eq!(size_neg.width, -3.0);
173    /// assert_eq!(size_neg.height, -3.0);
174    /// ```
175    ///
176    /// [rounded up]: f64::ceil
177    #[inline]
178    pub fn ceil(self) -> Size {
179        Size::new(self.width.ceil(), self.height.ceil())
180    }
181
182    /// Returns a new `Size`,
183    /// with `width` and `height` [rounded down] to the nearest integer,
184    /// unless they are already an integer.
185    ///
186    /// # Examples
187    ///
188    /// ```
189    /// use kurbo::Size;
190    /// let size_pos = Size::new(3.3, 3.6).floor();
191    /// assert_eq!(size_pos.width, 3.0);
192    /// assert_eq!(size_pos.height, 3.0);
193    /// let size_neg = Size::new(-3.3, -3.6).floor();
194    /// assert_eq!(size_neg.width, -4.0);
195    /// assert_eq!(size_neg.height, -4.0);
196    /// ```
197    ///
198    /// [rounded down]: f64::floor
199    #[inline]
200    pub fn floor(self) -> Size {
201        Size::new(self.width.floor(), self.height.floor())
202    }
203
204    /// Returns a new `Size`,
205    /// with `width` and `height` [rounded away] from zero to the nearest integer,
206    /// unless they are already an integer.
207    ///
208    /// # Examples
209    ///
210    /// ```
211    /// use kurbo::Size;
212    /// let size_pos = Size::new(3.3, 3.6).expand();
213    /// assert_eq!(size_pos.width, 4.0);
214    /// assert_eq!(size_pos.height, 4.0);
215    /// let size_neg = Size::new(-3.3, -3.6).expand();
216    /// assert_eq!(size_neg.width, -4.0);
217    /// assert_eq!(size_neg.height, -4.0);
218    /// ```
219    ///
220    /// [rounded away]: FloatExt::expand
221    #[inline]
222    pub fn expand(self) -> Size {
223        Size::new(self.width.expand(), self.height.expand())
224    }
225
226    /// Returns a new `Size`,
227    /// with `width` and `height` [rounded towards] zero to the nearest integer,
228    /// unless they are already an integer.
229    ///
230    /// # Examples
231    ///
232    /// ```
233    /// use kurbo::Size;
234    /// let size_pos = Size::new(3.3, 3.6).trunc();
235    /// assert_eq!(size_pos.width, 3.0);
236    /// assert_eq!(size_pos.height, 3.0);
237    /// let size_neg = Size::new(-3.3, -3.6).trunc();
238    /// assert_eq!(size_neg.width, -3.0);
239    /// assert_eq!(size_neg.height, -3.0);
240    /// ```
241    ///
242    /// [rounded towards]: f64::trunc
243    #[inline]
244    pub fn trunc(self) -> Size {
245        Size::new(self.width.trunc(), self.height.trunc())
246    }
247
248    /// Returns the aspect ratio of a rectangle with this size.
249    ///
250    /// The aspect ratio is the ratio of the width to the height.
251    ///
252    /// If the height is `0`, the output will be `sign(self.width) * infinity`. If the width and
253    /// height are both `0`, then the output will be `NaN`.
254    #[inline]
255    pub fn aspect_ratio_width(self) -> f64 {
256        // ratio is determined by width / height
257        // https://en.wikipedia.org/wiki/Aspect_ratio_(image)
258        // https://en.wikipedia.org/wiki/Ratio
259        self.width / self.height
260    }
261
262    /// Returns **the inverse** of the aspect ratio of a rectangle with this size.
263    ///
264    /// Aspect ratios are usually defined as the ratio of the width to the height, but
265    /// this method incorrectly returns the ratio of height to width.
266    /// You should generally prefer [`aspect_ratio_width`](Self::aspect_ratio_width).
267    ///
268    /// If the width is `0`, the output will be `sign(self.height) * infinity`. If the width and
269    /// height are both `0`, then the output will be `NaN`.
270    #[deprecated(
271        note = "You should use `aspect_ratio_width` instead, as this method returns a potentially unexpected value.",
272        since = "0.12.0"
273    )]
274    #[inline]
275    // TODO: When we remove this, we should also work out what to do with aspect_ratio_width
276    // The tentatitive plan is to rename `aspect_ratio_width` back to this name
277    pub fn aspect_ratio(self) -> f64 {
278        self.height / self.width
279    }
280
281    /// Convert this `Size` into a [`Rect`] with origin `(0.0, 0.0)`.
282    #[inline(always)]
283    pub const fn to_rect(self) -> Rect {
284        Rect::new(0., 0., self.width, self.height)
285    }
286
287    /// Convert this `Size` into a [`RoundedRect`] with origin `(0.0, 0.0)` and
288    /// the provided corner radius.
289    #[inline]
290    pub fn to_rounded_rect(self, radii: impl Into<RoundedRectRadii>) -> RoundedRect {
291        self.to_rect().to_rounded_rect(radii)
292    }
293
294    /// Is this size [finite]?
295    ///
296    /// [finite]: f64::is_finite
297    #[inline]
298    pub fn is_finite(self) -> bool {
299        self.width.is_finite() && self.height.is_finite()
300    }
301
302    /// Is this size [NaN]?
303    ///
304    /// [NaN]: f64::is_nan
305    #[inline]
306    pub fn is_nan(self) -> bool {
307        self.width.is_nan() || self.height.is_nan()
308    }
309
310    /// Get the member matching the given axis.
311    #[inline]
312    pub fn get_coord(self, axis: Axis) -> f64 {
313        match axis {
314            Axis::Horizontal => self.width,
315            Axis::Vertical => self.height,
316        }
317    }
318
319    /// Get a mutable reference to the member matching the given axis.
320    #[inline]
321    pub fn get_coord_mut(&mut self, axis: Axis) -> &mut f64 {
322        match axis {
323            Axis::Horizontal => &mut self.width,
324            Axis::Vertical => &mut self.height,
325        }
326    }
327
328    /// Set the member matching the given axis to the given value.
329    #[inline]
330    pub fn set_coord(&mut self, axis: Axis, value: f64) {
331        match axis {
332            Axis::Horizontal => self.width = value,
333            Axis::Vertical => self.height = value,
334        }
335    }
336}
337
338impl fmt::Debug for Size {
339    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
340        write!(f, "{:?}W×{:?}H", self.width, self.height)
341    }
342}
343
344impl fmt::Display for Size {
345    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
346        write!(formatter, "(")?;
347        fmt::Display::fmt(&self.width, formatter)?;
348        write!(formatter, "×")?;
349        fmt::Display::fmt(&self.height, formatter)?;
350        write!(formatter, ")")
351    }
352}
353
354impl MulAssign<f64> for Size {
355    #[inline]
356    fn mul_assign(&mut self, other: f64) {
357        *self = Size {
358            width: self.width * other,
359            height: self.height * other,
360        };
361    }
362}
363
364impl Mul<Size> for f64 {
365    type Output = Size;
366
367    #[inline]
368    fn mul(self, other: Size) -> Size {
369        other * self
370    }
371}
372
373impl Mul<f64> for Size {
374    type Output = Size;
375
376    #[inline]
377    fn mul(self, other: f64) -> Size {
378        Size {
379            width: self.width * other,
380            height: self.height * other,
381        }
382    }
383}
384
385impl DivAssign<f64> for Size {
386    #[inline]
387    fn div_assign(&mut self, other: f64) {
388        *self = Size {
389            width: self.width / other,
390            height: self.height / other,
391        };
392    }
393}
394
395impl Div<f64> for Size {
396    type Output = Size;
397
398    #[inline]
399    fn div(self, other: f64) -> Size {
400        Size {
401            width: self.width / other,
402            height: self.height / other,
403        }
404    }
405}
406
407impl Add<Size> for Size {
408    type Output = Size;
409    #[inline]
410    fn add(self, other: Size) -> Size {
411        Size {
412            width: self.width + other.width,
413            height: self.height + other.height,
414        }
415    }
416}
417
418impl AddAssign<Size> for Size {
419    #[inline]
420    fn add_assign(&mut self, other: Size) {
421        *self = *self + other;
422    }
423}
424
425impl Sub<Size> for Size {
426    type Output = Size;
427    #[inline]
428    fn sub(self, other: Size) -> Size {
429        Size {
430            width: self.width - other.width,
431            height: self.height - other.height,
432        }
433    }
434}
435
436impl SubAssign<Size> for Size {
437    #[inline]
438    fn sub_assign(&mut self, other: Size) {
439        *self = *self - other;
440    }
441}
442
443impl From<(f64, f64)> for Size {
444    #[inline(always)]
445    fn from(v: (f64, f64)) -> Size {
446        Size {
447            width: v.0,
448            height: v.1,
449        }
450    }
451}
452
453impl From<Size> for (f64, f64) {
454    #[inline(always)]
455    fn from(v: Size) -> (f64, f64) {
456        (v.width, v.height)
457    }
458}
459
460#[cfg(test)]
461mod tests {
462    use super::*;
463
464    #[test]
465    fn display() {
466        let s = Size::new(-0.12345, 9.87654);
467        assert_eq!(format!("{s}"), "(-0.12345×9.87654)");
468
469        let s = Size::new(-0.12345, 9.87654);
470        assert_eq!(format!("{s:+6.2}"), "( -0.12× +9.88)");
471    }
472
473    #[test]
474    #[expect(deprecated, reason = "Testing deprecated function.")]
475    fn aspect_ratio() {
476        let s = Size::new(1.0, 1.0);
477        assert!((s.aspect_ratio() - 1.0).abs() < 1e-6);
478    }
479
480    #[test]
481    fn aspect_ratio_width() {
482        let s = Size::new(1.0, 1.0);
483        assert!((s.aspect_ratio_width() - 1.0).abs() < 1e-6);
484
485        // 3:2 film (mm)
486        let s = Size::new(36.0, 24.0);
487        assert!((s.aspect_ratio_width() - 1.5).abs() < 1e-6);
488        // 4k screen
489        let s = Size::new(3840.0, 2160.0);
490        assert!((s.aspect_ratio_width() - (16. / 9.)).abs() < 1e-6);
491    }
492}