Skip to main content

frus_widgets/
aspectratio.rs

1//! [`AspectRatio`]: constrains its child to a given **width-to-height ratio**.
2
3use frus_core::{Rect, Scene};
4use frus_layout::{Dimension, Style};
5
6use crate::interaction::Status;
7use crate::theme::Theme;
8use crate::widget::Widget;
9
10/// Imposes a `width / height` ratio on its box: the box **takes the full available
11/// width**, then derives its height from the ratio (`height = width / ratio`). In a
12/// column 100 wide, `AspectRatio::new(2.0)` gives a 100×50 box and
13/// `AspectRatio::new(0.5)` a 100×200 one.
14///
15/// The ratio follows the usual convention (taffy's included): `width / height`
16/// — `2.0` is twice as wide as it is tall, `0.5` twice as tall as it is wide.
17///
18/// The child inherits the box: it stretches in height, along the cross axis, and
19/// fills the width if it grows (`flex`) — typically an image or a solid background.
20pub struct AspectRatio<Msg = crate::callback::Callback> {
21    ratio: f32,
22    children: Vec<Box<dyn Widget<Msg>>>,
23}
24
25impl<Msg> AspectRatio<Msg> {
26    /// Creates a box at the given `width / height` ratio, clamped to `> 0`.
27    pub fn new(ratio: f32) -> Self {
28        Self {
29            ratio: ratio.max(f32::MIN_POSITIVE),
30            children: Vec::new(),
31        }
32    }
33
34    /// Sets the child constrained to the ratio.
35    pub fn child(mut self, child: impl Widget<Msg> + 'static) -> Self {
36        self.children.clear();
37        self.children.push(Box::new(child));
38        self
39    }
40}
41
42impl<Msg: Clone> Widget<Msg> for AspectRatio<Msg> {
43    fn style(&self) -> Style {
44        // The box takes the parent's full width, a dimension taffy **knows**, and the
45        // ratio derives the height from it. A width that is merely "stretched", through
46        // align stretch, is not enough for taffy to derive the other axis.
47        Style {
48            width: Dimension::Percent(1.0),
49            aspect_ratio: Some(self.ratio),
50            ..Default::default()
51        }
52    }
53
54    fn children(&self) -> &[Box<dyn Widget<Msg>>] {
55        &self.children
56    }
57
58    fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {
59        // A pure layout widget: no decoration of its own.
60    }
61
62    fn on_click(&self) -> Option<Msg> {
63        None
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use crate::Container;
71    use frus_core::{Color, Primitive, Size};
72
73    /// In a column 100 wide, an `AspectRatio(2.0)` gives a 100×50 box — full width,
74    /// derived height — and its child, which fills, paints a background of about
75    /// 100×50.
76    #[test]
77    fn derives_free_dimension_from_ratio() {
78        let red = Color::rgb(1.0, 0.0, 0.0);
79        let root = crate::Flex::<()>::column()
80            .width(100.0)
81            .child(AspectRatio::new(2.0).child(Container::new().flex(1.0).color(red)));
82        let rt = crate::runtime::Runtime::default();
83        let theme = crate::Theme::dark();
84        let ui = crate::ui::build_ui(&root, Size::new(100.0, 200.0), &rt, &theme);
85        let rect = ui
86            .scene()
87            .primitives()
88            .iter()
89            .find_map(|p| match p {
90                Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(*rect),
91                _ => None,
92            })
93            .expect("the child's red background");
94        assert!(
95            (rect.width - 100.0).abs() < 0.5 && (rect.height - 50.0).abs() < 0.5,
96            "ratio 2.0 → 100×50: {rect:?}"
97        );
98    }
99
100    /// A ratio below 1 makes the box **taller than it is wide**: `0.5` in a column of
101    /// 100 gives 100×200.
102    #[test]
103    fn ratio_below_one_is_taller_than_wide() {
104        let red = Color::rgb(1.0, 0.0, 0.0);
105        let root = crate::Flex::<()>::column()
106            .width(100.0)
107            .child(AspectRatio::new(0.5).child(Container::new().flex(1.0).color(red)));
108        let rt = crate::runtime::Runtime::default();
109        let theme = crate::Theme::dark();
110        let ui = crate::ui::build_ui(&root, Size::new(100.0, 400.0), &rt, &theme);
111        let rect = ui
112            .scene()
113            .primitives()
114            .iter()
115            .find_map(|p| match p {
116                Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(*rect),
117                _ => None,
118            })
119            .expect("the child's red background");
120        assert!(
121            (rect.width - 100.0).abs() < 0.5 && (rect.height - 200.0).abs() < 0.5,
122            "ratio 0.5 → 100×200: {rect:?}"
123        );
124    }
125}