Skip to main content

telar_geometry_core/
object_fit.rs

1use crate::Rect;
2
3/// How a sized piece of content (an image or SVG) is scaled into its layout box, mirroring CSS `object-fit`.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5pub enum ObjectFit {
6    /// Stretch to fill the box exactly, ignoring the intrinsic aspect ratio (may distort).
7    Fill,
8    /// Scale uniformly to fit inside the box, centered, leaving letterbox gaps (no clipping).
9    #[default]
10    Contain,
11    /// Scale uniformly to cover the box, centered; the overflow must be clipped to the box.
12    Cover,
13}
14
15/// Places `intrinsic`-sized content into `container` per `fit`.
16///
17/// Returns the content rect (in `container`'s coordinate space) and whether the caller must clip
18/// it to `container` — true only for `Cover`, whose content deliberately overflows.
19pub fn fit_rect(intrinsic: (f32, f32), container: Rect, fit: ObjectFit) -> (Rect, bool) {
20    let (iw, ih) = intrinsic;
21    // A zero-area intrinsic or container has no defined aspect ratio to preserve; fall back to filling the box.
22    if iw <= 0.0 || ih <= 0.0 || container.width <= 0.0 || container.height <= 0.0 {
23        return (container, false);
24    }
25    match fit {
26        ObjectFit::Fill => (container, false),
27        ObjectFit::Contain | ObjectFit::Cover => {
28            let sx = container.width / iw;
29            let sy = container.height / ih;
30            let (s, clip) = if fit == ObjectFit::Contain {
31                (sx.min(sy), false)
32            } else {
33                (sx.max(sy), true)
34            };
35            let w = iw * s;
36            let h = ih * s;
37            let x = container.x + (container.width - w) * 0.5;
38            let y = container.y + (container.height - h) * 0.5;
39            (Rect::new(x, y, w, h), clip)
40        }
41    }
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47
48    #[test]
49    fn fill_returns_container_and_no_clip() {
50        let c = Rect::new(0.0, 0.0, 120.0, 60.0);
51        let (rect, clip) = fit_rect((10.0, 10.0), c, ObjectFit::Fill);
52        assert_eq!(rect, c);
53        assert!(!clip);
54    }
55
56    #[test]
57    fn contain_letterboxes_wide_box() {
58        // 10x10 into 120x60: uniform scale = min(12, 6) = 6, fitted 60x60, centered horizontally.
59        let c = Rect::new(0.0, 0.0, 120.0, 60.0);
60        let (rect, clip) = fit_rect((10.0, 10.0), c, ObjectFit::Contain);
61        assert_eq!(rect, Rect::new(30.0, 0.0, 60.0, 60.0));
62        assert!(!clip);
63    }
64
65    #[test]
66    fn cover_overflows_and_clips() {
67        // 10x10 into 120x60: uniform scale = max(12, 6) = 12, fitted 120x120, centered (overflows top/bottom).
68        let c = Rect::new(0.0, 0.0, 120.0, 60.0);
69        let (rect, clip) = fit_rect((10.0, 10.0), c, ObjectFit::Cover);
70        assert_eq!(rect, Rect::new(0.0, -30.0, 120.0, 120.0));
71        assert!(clip);
72    }
73
74    #[test]
75    fn contain_respects_container_origin() {
76        let c = Rect::new(5.0, 7.0, 120.0, 60.0);
77        let (rect, _) = fit_rect((10.0, 10.0), c, ObjectFit::Contain);
78        assert_eq!(rect, Rect::new(35.0, 7.0, 60.0, 60.0));
79    }
80
81    #[test]
82    fn degenerate_intrinsic_fills() {
83        let c = Rect::new(0.0, 0.0, 120.0, 60.0);
84        let (rect, clip) = fit_rect((0.0, 10.0), c, ObjectFit::Contain);
85        assert_eq!(rect, c);
86        assert!(!clip);
87    }
88
89    #[test]
90    fn default_is_contain() {
91        assert_eq!(ObjectFit::default(), ObjectFit::Contain);
92    }
93}