Skip to main content

frus_widgets/
fractional.rs

1//! [`FractionallySizedBox`]: sizes its box to a **fraction** of the space the
2//! parent offers.
3
4use frus_core::{Curve, Rect, Scene};
5use frus_layout::{Dimension, Style};
6
7use crate::interaction::Status;
8use crate::theme::Theme;
9use crate::widget::Widget;
10
11/// Takes a **fraction** of the parent's size on each axis that is set:
12/// `width_factor(0.5)` = half the parent's width, `height_factor(0.25)` = a
13/// quarter of its height. An axis left **unset** follows its content (its
14/// natural size). The child fills the box (stretch / `flex`).
15///
16/// Rather than constraining the child, the box **sizes itself** as a percentage
17/// of the parent (via `Dimension::Percent`). In the common case — a child that
18/// fills — that gives the same visual result, and it fits the flex model.
19///
20/// [`FractionallySizedBox::animated`] makes each change of factor a movement instead of a
21/// jump; [`crate::AnimatedFractionallySizedBox`] is the name for the same thing.
22pub struct FractionallySizedBox<Msg = crate::callback::Callback> {
23    width_factor: Option<f32>,
24    height_factor: Option<f32>,
25    animated: Option<(f32, Curve)>,
26    children: Vec<Box<dyn Widget<Msg>>>,
27}
28
29impl<Msg> FractionallySizedBox<Msg> {
30    /// Creates a fractional box with no factor (both axes follow the content for
31    /// as long as neither is set).
32    pub fn new() -> Self {
33        Self {
34            width_factor: None,
35            height_factor: None,
36            animated: None,
37            children: Vec::new(),
38        }
39    }
40
41    /// Fraction `0.0..=1.0` of the parent's **width** (clamped to `>= 0`).
42    pub fn width_factor(mut self, factor: f32) -> Self {
43        self.width_factor = Some(factor.max(0.0));
44        self
45    }
46
47    /// Fraction `0.0..=1.0` of the parent's **height** (clamped to `>= 0`).
48    pub fn height_factor(mut self, factor: f32) -> Self {
49        self.height_factor = Some(factor.max(0.0));
50        self
51    }
52
53    /// **Grows and shrinks to each new factor** over `duration` seconds on `curve`,
54    /// instead of arriving at it between one frame and the next.
55    ///
56    /// An axis whose factor is **unset** does not travel: it follows its content, which
57    /// is a different arrangement rather than a different number, and interpolating
58    /// towards it would be interpolating towards a size nobody has computed yet.
59    pub fn animated(mut self, duration: f32, curve: Curve) -> Self {
60        self.animated = Some((duration, curve));
61        self
62    }
63
64    /// Sets the child, which fills the fractional box.
65    pub fn child(mut self, child: impl Widget<Msg> + 'static) -> Self {
66        self.children.clear();
67        self.children.push(Box::new(child));
68        self
69    }
70}
71
72impl<Msg> Default for FractionallySizedBox<Msg> {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78impl<Msg: Clone> Widget<Msg> for FractionallySizedBox<Msg> {
79    fn style(&self) -> Style {
80        // A factor that is set → a dimension as a percentage of the parent; otherwise
81        // `Auto` (the axis follows its content).
82        let dim = |factor: Option<f32>| match factor {
83            Some(f) => Dimension::Percent(f),
84            None => Dimension::Auto,
85        };
86        Style {
87            width: dim(self.width_factor),
88            height: dim(self.height_factor),
89            ..Default::default()
90        }
91    }
92
93    fn children(&self) -> &[Box<dyn Widget<Msg>>] {
94        &self.children
95    }
96
97    fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {
98        // A pure layout widget: no decoration of its own.
99    }
100
101    fn on_click(&self) -> Option<Msg> {
102        None
103    }
104
105    fn anim_fractions(&self) -> Option<(Option<f32>, Option<f32>)> {
106        self.animated
107            .as_ref()
108            .map(|_| (self.width_factor, self.height_factor))
109    }
110
111    fn anim_duration(&self) -> f32 {
112        self.animated
113            .as_ref()
114            .map_or(crate::runtime::ANIM_DURATION, |(d, _)| *d)
115    }
116
117    fn anim_curve(&self) -> Curve {
118        self.animated
119            .as_ref()
120            .map_or(Curve::Linear, |(_, c)| c.clone())
121    }
122
123    fn debug_name(&self) -> &'static str {
124        "FractionallySizedBox"
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use crate::Container;
132    use frus_core::{Color, Primitive, Size};
133
134    /// `width_factor(0.5)` in a column 100 wide → a box 50 wide; the child that
135    /// fills paints a background about 50 wide.
136    #[test]
137    fn width_factor_takes_a_fraction_of_the_parent() {
138        let red = Color::rgb(1.0, 0.0, 0.0);
139        let root = crate::Flex::<()>::column().width(100.0).child(
140            FractionallySizedBox::new()
141                .width_factor(0.5)
142                .child(Container::new().flex(1.0).height(20.0).color(red)),
143        );
144        let rt = crate::runtime::Runtime::default();
145        let theme = crate::Theme::dark();
146        let ui = crate::ui::build_ui(&root, Size::new(100.0, 200.0), &rt, &theme);
147        let rect = ui
148            .scene()
149            .primitives()
150            .iter()
151            .find_map(|p| match p {
152                Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(*rect),
153                _ => None,
154            })
155            .expect("the child's red background");
156        assert!((rect.width - 50.0).abs() < 0.5, "half the width: {rect:?}");
157    }
158
159    /// `height_factor(0.25)` takes a quarter of the parent's height: in a column
160    /// 200 tall, the box is 50 tall.
161    #[test]
162    fn height_factor_takes_a_fraction_of_the_parent() {
163        let red = Color::rgb(1.0, 0.0, 0.0);
164        let root = crate::Flex::<()>::column()
165            .width(100.0)
166            .height(200.0)
167            .child(
168                FractionallySizedBox::new()
169                    .height_factor(0.25)
170                    .child(Container::new().flex(1.0).color(red)),
171            );
172        let rt = crate::runtime::Runtime::default();
173        let theme = crate::Theme::dark();
174        let ui = crate::ui::build_ui(&root, Size::new(100.0, 200.0), &rt, &theme);
175        let rect = ui
176            .scene()
177            .primitives()
178            .iter()
179            .find_map(|p| match p {
180                Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(*rect),
181                _ => None,
182            })
183            .expect("the child's red background");
184        assert!(
185            (rect.height - 50.0).abs() < 0.5,
186            "a quarter of the height: {rect:?}"
187        );
188    }
189}