Skip to main content

frus_widgets/
expanded.rs

1//! [`Expanded`]: the child of a row or a column that takes **the room the others left**.
2//!
3//! A row sizes its children by their content first, and only then shares out what is
4//! spare. So a row of a checkbox, a long label and a delete button asks the label how
5//! wide it would like to be, is told *558 pixels*, and the row is already over budget
6//! before the button is measured. Flexbox then takes the deficit out of every child in
7//! proportion — and the label, whose automatic minimum size is its own content, refuses,
8//! so the whole of it lands on the button. That is how a delete button 40 px wide came to
9//! be laid out at 13, then off the card entirely, and with it out of the hit registry:
10//! the task could no longer be deleted (milestone 333).
11//!
12//! `Expanded` is the fix, and it is the reference's: the label is not a box that must be
13//! squeezed, it is the one that takes **what is left**.
14//!
15//! ```ignore
16//! row![
17//!     Checkbox::new(done),
18//!     Expanded::new(text(&task.title).ellipsis()),   // ← takes the rest
19//!     IconButton::new(Icons::CLOSE),              // ← keeps its 40 px
20//! ]
21//! ```
22//!
23//! Three things at once, none of which is useful without the other two:
24//!
25//! - **a basis of zero**, so the child no longer tells the row how wide it wants to be;
26//! - **a grow factor**, so it then takes the spare room — all of it, or a share, if
27//!   several children expand;
28//! - **no automatic minimum**, which is the one everybody forgets. `flex: 1` on its own
29//!   still floors an item at its content width, on the web as here; without lifting that
30//!   floor the child grows but never yields, and the row overflows exactly as before.
31//!
32//! It changes only the box. Everything else — identity, hit testing, scrolling, the
33//! structural questions — is the child's, forwarded by the transparent-wrapper macro.
34//!
35//! ## Two widgets, one box
36//!
37//! The reference has [`Flexible`] with a **fit**, and `Expanded` is the tight one —
38//! `Flexible(fit: FlexFit.tight)`, literally a subclass. Both names are here, they build
39//! the same box, and which one you reach for is a matter of which the code you are
40//! porting used:
41//!
42//! | | takes its whole share | may take less |
43//! |---|---|---|
44//! | [`Expanded`] | `Expanded::new(child)` | `Expanded::new(child).loose()` |
45//! | [`Flexible`] | `Flexible::new(child).tight()` | `Flexible::new(child)` |
46//!
47//! The defaults are the reference's on both: an `Expanded` is tight, a `Flexible` is
48//! loose. The difference between the two fits is the **basis** — tight starts the child
49//! at nothing and grows it into the whole share, loose starts it at its content and only
50//! lets it give way when the share is smaller than that.
51
52use frus_layout::{Dimension, Style};
53
54use crate::widget::Widget;
55
56/// Wraps a child so it takes the spare room along its parent's main axis.
57///
58/// Three properties at once — a basis of zero, a grow factor, and no automatic minimum
59/// — because a bare grow factor does nothing for the case this exists for: a flex item's
60/// automatic minimum size is its own content, so `flex: 1` on a long label grows it and
61/// still lets it refuse to be narrower than its text.
62pub struct Expanded<Msg = crate::callback::Callback> {
63    inner: Box<dyn Widget<Msg>>,
64    flex: f32,
65    /// A **loose** fit: the child may take less than its share. The reference calls this
66    /// `Flexible`; `Expanded` is the tight one, and the difference is one line here.
67    loose: bool,
68}
69
70impl<Msg> Expanded<Msg> {
71    /// Wraps `inner`, which then takes all the spare room (a flex factor of 1).
72    pub fn new(inner: impl Widget<Msg> + 'static) -> Self {
73        Self {
74            inner: Box::new(inner),
75            flex: 1.0,
76            loose: false,
77        }
78    }
79
80    /// A **loose** fit: the child takes *at most* its share, and less if that is all it
81    /// wants — the reference's `Flexible`, where [`Expanded`] is `Flexible(fit: tight)`.
82    ///
83    /// The difference is the basis: tight starts the child at nothing and grows it into
84    /// its whole share, loose starts it at its content and only lets it *give way* when
85    /// the share is smaller than that.
86    ///
87    /// It had a caveat the reference does not until milestone 349: flexbox shares a
88    /// deficit in proportion to basis, so a fixed sibling used to give up its share too
89    /// unless it said so. Inflexible children are never squeezed now, here as there, and
90    /// the deficit lands where it was aimed.
91    pub fn loose(mut self) -> Self {
92        self.loose = true;
93        self
94    }
95
96    /// The share of the spare room, when several children expand: two children at
97    /// `flex(2.0)` and `flex(1.0)` split it two to one.
98    pub fn flex(mut self, flex: f32) -> Self {
99        self.flex = flex;
100        self
101    }
102
103    /// The one thing this wrapper changes: the flex item its child is.
104    fn restyle(&self, base: Style) -> Style {
105        flex_item(base, self.flex, self.fit())
106    }
107
108    /// Which fit this is, in the reference's vocabulary.
109    fn fit(&self) -> FlexFit {
110        if self.loose {
111            FlexFit::Loose
112        } else {
113            FlexFit::Tight
114        }
115    }
116}
117
118/// How a flexible child fills the share of the spare room it was given — the
119/// reference's `FlexFit`.
120#[derive(Clone, Copy, Debug, PartialEq, Eq)]
121pub enum FlexFit {
122    /// **At most** its share, and less if that is all it wants. The child keeps its
123    /// content size until the share is smaller than that, and only then gives way.
124    Loose,
125    /// **Exactly** its share. The child stops sizing the line at all and is grown into
126    /// the whole of what is left.
127    Tight,
128}
129
130/// The flex item a flexible child becomes: three properties at once, none of which is
131/// useful without the other two.
132///
133/// Shared by [`Expanded`] and [`Flexible`] because they *are* the same box — the
134/// reference makes one a subclass of the other, and two copies of this would be two
135/// places for the next fix to miss.
136pub(crate) fn flex_item(base: Style, flex: f32, fit: FlexFit) -> Style {
137    let tight = fit == FlexFit::Tight;
138    Style {
139        flex_grow: if tight { flex } else { 0.0 },
140        flex_shrink: flex,
141        flex_basis: if tight {
142            Dimension::Length(0.0)
143        } else {
144            Dimension::Auto
145        },
146        // The floor that makes `flex: 1` alone a no-op. `Auto` here means *the
147        // content*, so the child would grow to its share and then refuse to go below
148        // its natural width — which is the whole of the bug this widget exists for.
149        min_width: Dimension::Length(0.0),
150        min_height: Dimension::Length(0.0),
151        // `width` and `height` are **left alone**. A basis already overrules the size
152        // on the main axis, and on the cross axis the child's own is the one that is
153        // wanted: clearing both made an expanded `Text` — which reports its measured
154        // size as its style rather than through a measure function — lose its height
155        // and lay out 0 px tall in a centred row.
156        ..base
157    }
158}
159
160/// The child of a row or a column that takes **at most** a share of the room the others
161/// left — the reference's `Flexible`, with [`Expanded`] as its tight case.
162///
163/// `Flexible::new(child)` is the reference's default fit, `Loose`: the child keeps the
164/// size it asked for while there is room for it, and gives way when there is not. That
165/// is the widget for a label that should stay its natural width in a wide row and
166/// ellipsise in a narrow one. Reach for [`Expanded`] — or `Flexible::new(child).tight()`,
167/// the same box — when the child should fill its share whether it wanted to or not.
168pub struct Flexible<Msg = crate::callback::Callback> {
169    inner: Box<dyn Widget<Msg>>,
170    flex: f32,
171    fit: FlexFit,
172}
173
174impl<Msg> Flexible<Msg> {
175    /// Wraps `inner` with the reference's defaults: a flex factor of 1 and a **loose**
176    /// fit.
177    pub fn new(inner: impl Widget<Msg> + 'static) -> Self {
178        Self {
179            inner: Box::new(inner),
180            flex: 1.0,
181            fit: FlexFit::Loose,
182        }
183    }
184
185    /// The share of the spare room, when several children are flexible: two children at
186    /// `flex(2.0)` and `flex(1.0)` split it two to one.
187    pub fn flex(mut self, flex: f32) -> Self {
188        self.flex = flex;
189        self
190    }
191
192    /// A **tight** fit: the child takes its whole share whether it asked for it or not.
193    /// The same box as [`Expanded::new`].
194    pub fn tight(mut self) -> Self {
195        self.fit = FlexFit::Tight;
196        self
197    }
198
199    /// The fit, given as a value — for code ported from the reference's `fit:` argument.
200    pub fn fit(mut self, fit: FlexFit) -> Self {
201        self.fit = fit;
202        self
203    }
204
205    /// The one thing this wrapper changes: the flex item its child is.
206    fn restyle(&self, base: Style) -> Style {
207        flex_item(base, self.flex, self.fit)
208    }
209}
210
211crate::transparent::forward_transparent!(Flexible {
212    /// Forwarded, as [`Expanded`]'s is: making a widget flexible says nothing about
213    /// which widget it is.
214    fn key(&self) -> Option<u64> {
215        self.inner.key()
216    }
217
218    /// Forwarded: a box is not a place.
219    fn positioned(&self) -> Option<crate::positioned::Positioning> {
220        self.inner.positioned()
221    }
222
223    /// Forwarded too: a box is not a palette.
224    fn theme_override(
225        &self,
226        inherited: &crate::theme::Theme,
227    ) -> Option<Box<crate::theme::Theme>> {
228        self.inner.theme_override(inherited)
229    }
230
231    /// Forwarded: a wrapper is its child, and a scoped surface is the child's to impose.
232    fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
233        self.inner.media_override(inherited)
234    }
235    fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
236        self.inner.scaffold_override()
237    }
238    /// Forwarded: a wrapper around a form is still that form (milestone 512).
239    fn autofill_group(&self) -> bool {
240        self.inner.autofill_group()
241    }
242});
243
244crate::transparent::forward_transparent!(Expanded {
245    /// Forwarded: expanding a widget says nothing about which widget it is, and the
246    /// identity has to survive the wrapper or the child's retained state moves with its
247    /// position in the list.
248    fn key(&self) -> Option<u64> {
249        self.inner.key()
250    }
251
252    /// Forwarded: a box is not a place.
253    fn positioned(&self) -> Option<crate::positioned::Positioning> {
254        self.inner.positioned()
255    }
256
257    /// Forwarded too: a box is not a palette.
258    fn theme_override(
259        &self,
260        inherited: &crate::theme::Theme,
261    ) -> Option<Box<crate::theme::Theme>> {
262        self.inner.theme_override(inherited)
263    }
264
265    /// Forwarded: a wrapper is its child, and a scoped surface is the child's to impose.
266    fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
267        self.inner.media_override(inherited)
268    }
269});
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274    use crate::interaction::WidgetId;
275    use crate::runtime::Runtime;
276    use crate::text::Text;
277    use crate::theme::Theme;
278    use crate::{Container, Flex};
279
280    /// Lays a row out in `width` pixels and returns each direct child's box.
281    fn boxes(row: &Flex<()>, width: f32) -> Vec<(f32, f32)> {
282        let runtime = Runtime::default();
283        let theme = Theme::light();
284        let mut layout = frus_layout::Layout::new();
285        let node = crate::ui::build_layout(row, WidgetId::ROOT, &runtime, &theme, &mut layout);
286        layout.compute_filled(node, width, 100.0);
287        let rects = layout.absolute_rects(node);
288        // The row itself is first; its children follow, one level down and in order.
289        rects
290            .iter()
291            .skip(1)
292            .take(Widget::<()>::children(row).len())
293            .map(|(r, _)| (r.width, r.height))
294            .collect()
295    }
296
297    /// Lays a row out and returns each direct child's width.
298    fn widths(row: Flex<()>, width: f32) -> Vec<f32> {
299        boxes(&row, width).into_iter().map(|(w, _)| w).collect()
300    }
301
302    /// The cross axis is **not** the wrapper's business, and clearing it cost a
303    /// milestone: a `Text` reports its measured size as its style rather than through a
304    /// measure function, so an `Expanded` that blanked `height` too laid one out 0 px
305    /// tall — invisible in a row aligned to its centre.
306    #[test]
307    fn it_leaves_the_child_s_cross_axis_alone() {
308        let plain = Flex::row().child(Text::new("hello").size(18.0));
309        let natural = boxes(&plain, 200.0)[0].1;
310        assert!(natural > 0.0, "a text has a height to begin with");
311
312        let row = Flex::row().child(Expanded::new(Text::new("hello").size(18.0)));
313        let (width, height) = boxes(&row, 200.0)[0];
314        assert_eq!(width, 200.0, "the main axis is the wrapper's");
315        assert_eq!(height, natural, "the cross axis is the child's");
316    }
317
318    /// The device finding of milestone 333, in three lines: a long label and a 40 px
319    /// button in a row too narrow for both.
320    #[test]
321    fn an_expanded_label_leaves_the_button_its_width() {
322        let long = "a task label far longer than the row it has to live in";
323        let row = Flex::row()
324            .child(Expanded::new(Text::new(long).size(18.0).ellipsis()))
325            .child(Container::<()>::new().width(40.0).height(40.0));
326        let widths = widths(row, 200.0);
327        assert_eq!(widths.len(), 2);
328        assert_eq!(widths[1], 40.0, "the button keeps its width");
329        assert_eq!(widths[0], 160.0, "the label takes exactly what is left");
330    }
331
332    /// Without the wrapper, the same row **overflows** — which is the reference's answer
333    /// and, since milestone 349, this one's. The button keeps its 40 px, the label keeps
334    /// its whole width, and the row wears a striped band saying by how much it did not
335    /// fit. That is the improvement: the defect used to be silent, and it took three
336    /// milestones and a device report to find the button crushed to 13 px.
337    #[test]
338    fn without_it_the_row_overflows_instead_of_crushing_the_button() {
339        let long = "a task label far longer than the row it has to live in";
340        let row = Flex::row()
341            .child(Text::new(long).size(18.0))
342            .child(Container::<()>::new().width(40.0).height(40.0));
343        let widths = widths(row, 200.0);
344        assert_eq!(widths[1], 40.0, "the button keeps its width");
345        assert!(
346            widths[0] > 160.0,
347            "and the label keeps its own, so the row runs over: {widths:?}"
348        );
349    }
350
351    /// Several expanding children share the room in proportion, and the fixed one is
352    /// still untouched.
353    #[test]
354    fn several_expanded_children_split_what_is_left() {
355        let row = Flex::row()
356            .child(Expanded::new(Container::<()>::new()).flex(2.0))
357            .child(Expanded::new(Container::<()>::new()).flex(1.0))
358            .child(Container::<()>::new().width(40.0).height(40.0));
359        let widths = widths(row, 340.0);
360        assert_eq!(widths, vec![200.0, 100.0, 40.0]);
361    }
362
363    /// A fixed width inside an `Expanded` is the wrapper's, not the child's — otherwise
364    /// the basis of zero is overruled and the widget silently does nothing.
365    #[test]
366    fn it_overrules_the_child_s_own_width() {
367        let row = Flex::row()
368            .child(Expanded::new(Container::<()>::new().width(100.0)))
369            .child(Container::<()>::new().width(40.0).height(40.0));
370        let widths = widths(row, 200.0);
371        assert_eq!(widths[0], 160.0, "the expanded child fills, not 100");
372    }
373
374    /// The **loose** fit — the reference's `Flexible`. It never grows a child into room
375    /// it did not ask for; it only lets one give way when there is not enough.
376    #[test]
377    fn a_loose_child_gives_way_but_does_not_fill() {
378        // Room to spare: the tight one fills it, the loose one keeps its size.
379        let tight = Flex::row()
380            .child(Expanded::new(
381                Container::<()>::new().width(40.0).height(20.0),
382            ))
383            .child(Container::<()>::new().width(40.0).height(20.0));
384        assert_eq!(widths(tight, 200.0)[0], 160.0);
385
386        let loose = Flex::row()
387            .child(Expanded::new(Container::<()>::new().width(40.0).height(20.0)).loose())
388            .child(Container::<()>::new().width(40.0).height(20.0));
389        assert_eq!(widths(loose, 200.0)[0], 40.0, "at most its share, not more");
390
391        // Not enough room: it gives way, and the whole deficit lands here because the
392        // sibling is inflexible and inflexible children are not squeezed (milestone 349).
393        // It used to take a `no_shrink()` on the sibling — flexbox shares a deficit in
394        // proportion to basis, and a wrapper cannot speak for its siblings.
395        let squeezed = Flex::row()
396            .child(
397                Expanded::new(
398                    Text::new("a label far longer than this row")
399                        .size(18.0)
400                        .ellipsis(),
401                )
402                .loose(),
403            )
404            .child(Container::<()>::new().width(40.0).height(20.0));
405        let widths = widths(squeezed, 200.0);
406        assert_eq!(widths[1], 40.0, "the fixed sibling keeps its width");
407        assert_eq!(widths[0], 160.0, "and the loose one takes what is left");
408    }
409
410    /// The two names build the **same box**, which is the whole claim: `Expanded` is
411    /// `Flexible(fit: tight)` in the reference and it is here too, in both directions.
412    #[test]
413    fn the_two_names_build_the_same_box() {
414        let by_expanded = Flex::row()
415            .child(Expanded::new(
416                Container::<()>::new().width(40.0).height(20.0),
417            ))
418            .child(Container::<()>::new().width(40.0).height(20.0));
419        let by_flexible = Flex::row()
420            .child(Flexible::new(Container::<()>::new().width(40.0).height(20.0)).tight())
421            .child(Container::<()>::new().width(40.0).height(20.0));
422        assert_eq!(widths(by_expanded, 200.0), widths(by_flexible, 200.0));
423
424        let loose_expanded = Flex::row()
425            .child(Expanded::new(Container::<()>::new().width(40.0).height(20.0)).loose())
426            .child(Container::<()>::new().width(40.0).height(20.0));
427        let loose_flexible = Flex::row()
428            .child(Flexible::new(
429                Container::<()>::new().width(40.0).height(20.0),
430            ))
431            .child(Container::<()>::new().width(40.0).height(20.0));
432        assert_eq!(widths(loose_expanded, 200.0), widths(loose_flexible, 200.0));
433    }
434
435    /// A `Flexible` is **loose** by default, which is the reference's default and the
436    /// opposite of `Expanded`'s: it keeps its size while there is room for it.
437    #[test]
438    fn a_flexible_is_loose_by_default() {
439        let row = Flex::row()
440            .child(Flexible::new(
441                Container::<()>::new().width(40.0).height(20.0),
442            ))
443            .child(Container::<()>::new().width(40.0).height(20.0));
444        assert_eq!(widths(row, 200.0)[0], 40.0, "at most its share, not more");
445    }
446
447    /// `fit()` takes the value, for code ported from the reference's `fit:` argument,
448    /// and says the same thing as `tight()`.
449    #[test]
450    fn the_fit_can_be_given_as_a_value() {
451        let by_method = Flex::row()
452            .child(Flexible::new(Container::<()>::new().width(40.0).height(20.0)).tight())
453            .child(Container::<()>::new().width(40.0).height(20.0));
454        let by_value = Flex::row()
455            .child(
456                Flexible::new(Container::<()>::new().width(40.0).height(20.0)).fit(FlexFit::Tight),
457            )
458            .child(Container::<()>::new().width(40.0).height(20.0));
459        assert_eq!(widths(by_method, 200.0), widths(by_value, 200.0));
460    }
461
462    /// Several flexible children share what is left in proportion, tight or loose.
463    #[test]
464    fn flexible_children_split_in_proportion() {
465        let row = Flex::row()
466            .child(Flexible::new(Container::<()>::new()).tight().flex(2.0))
467            .child(Flexible::new(Container::<()>::new()).tight().flex(1.0))
468            .child(Container::<()>::new().width(40.0).height(40.0));
469        assert_eq!(widths(row, 340.0), vec![200.0, 100.0, 40.0]);
470    }
471
472    /// A `Flexible` is a transparent wrapper too, and the macro is what makes that
473    /// true rather than the author's memory.
474    #[test]
475    fn a_flexible_stays_transparent() {
476        let stack = crate::Stack::<()>::new()
477            .width(100.0)
478            .height(50.0)
479            .layer(Container::new())
480            .layer(Container::new());
481        assert!(Widget::<()>::stack(&Flexible::new(stack)));
482
483        let keyed = crate::Keyed::new(9u64, Container::<()>::new());
484        let key = Widget::<()>::key(&keyed);
485        assert_eq!(Widget::<()>::key(&Flexible::new(keyed)), key);
486    }
487
488    /// It is a wrapper, so the things a wrapper must not eat: the child's identity, and
489    /// the structural answers the walk asks for before it looks at anything.
490    #[test]
491    fn it_stays_transparent() {
492        let stack = crate::Stack::<()>::new()
493            .width(100.0)
494            .height(50.0)
495            .layer(Container::new())
496            .layer(Container::new());
497        assert!(Widget::<()>::stack(&Expanded::new(stack)));
498
499        let keyed = crate::Keyed::new(7u64, Container::<()>::new());
500        let key = Widget::<()>::key(&keyed);
501        assert_eq!(Widget::<()>::key(&Expanded::new(keyed)), key);
502    }
503}