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    fn scroll_behavior_override(&self) -> Option<crate::ScrollBehavior> {
239        self.inner.scroll_behavior_override()
240    }
241    /// Forwarded: a wrapper around a form is still that form (milestone 512).
242    fn autofill_group(&self) -> bool {
243        self.inner.autofill_group()
244    }
245    /// Forwarded: the area's bar is the area's, whatever wraps it.
246    fn area_toolbar(
247        &self,
248        context: crate::ToolbarContext,
249    ) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
250        self.inner.area_toolbar(context)
251    }
252});
253
254crate::transparent::forward_transparent!(Expanded {
255    /// Forwarded: expanding a widget says nothing about which widget it is, and the
256    /// identity has to survive the wrapper or the child's retained state moves with its
257    /// position in the list.
258    fn key(&self) -> Option<u64> {
259        self.inner.key()
260    }
261
262    /// Forwarded: a box is not a place.
263    fn positioned(&self) -> Option<crate::positioned::Positioning> {
264        self.inner.positioned()
265    }
266
267    /// Forwarded too: a box is not a palette.
268    fn theme_override(
269        &self,
270        inherited: &crate::theme::Theme,
271    ) -> Option<Box<crate::theme::Theme>> {
272        self.inner.theme_override(inherited)
273    }
274
275    /// Forwarded: a wrapper is its child, and a scoped surface is the child's to impose.
276    fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
277        self.inner.media_override(inherited)
278    }
279});
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284    use crate::interaction::WidgetId;
285    use crate::runtime::Runtime;
286    use crate::text::Text;
287    use crate::theme::Theme;
288    use crate::{Container, Flex};
289
290    /// Lays a row out in `width` pixels and returns each direct child's box.
291    fn boxes(row: &Flex<()>, width: f32) -> Vec<(f32, f32)> {
292        let runtime = Runtime::default();
293        let theme = Theme::light();
294        let mut layout = frus_layout::Layout::new();
295        let node = crate::ui::build_layout(row, WidgetId::ROOT, &runtime, &theme, &mut layout);
296        layout.compute_filled(node, width, 100.0);
297        let rects = layout.absolute_rects(node);
298        // The row itself is first; its children follow, one level down and in order.
299        rects
300            .iter()
301            .skip(1)
302            .take(Widget::<()>::children(row).len())
303            .map(|(r, _)| (r.width, r.height))
304            .collect()
305    }
306
307    /// Lays a row out and returns each direct child's width.
308    fn widths(row: Flex<()>, width: f32) -> Vec<f32> {
309        boxes(&row, width).into_iter().map(|(w, _)| w).collect()
310    }
311
312    /// The cross axis is **not** the wrapper's business, and clearing it cost a
313    /// milestone: a `Text` reports its measured size as its style rather than through a
314    /// measure function, so an `Expanded` that blanked `height` too laid one out 0 px
315    /// tall — invisible in a row aligned to its centre.
316    #[test]
317    fn it_leaves_the_child_s_cross_axis_alone() {
318        let plain = Flex::row().child(Text::new("hello").size(18.0));
319        let natural = boxes(&plain, 200.0)[0].1;
320        assert!(natural > 0.0, "a text has a height to begin with");
321
322        let row = Flex::row().child(Expanded::new(Text::new("hello").size(18.0)));
323        let (width, height) = boxes(&row, 200.0)[0];
324        assert_eq!(width, 200.0, "the main axis is the wrapper's");
325        assert_eq!(height, natural, "the cross axis is the child's");
326    }
327
328    /// The device finding of milestone 333, in three lines: a long label and a 40 px
329    /// button in a row too narrow for both.
330    #[test]
331    fn an_expanded_label_leaves_the_button_its_width() {
332        let long = "a task label far longer than the row it has to live in";
333        let row = Flex::row()
334            .child(Expanded::new(Text::new(long).size(18.0).ellipsis()))
335            .child(Container::<()>::new().width(40.0).height(40.0));
336        let widths = widths(row, 200.0);
337        assert_eq!(widths.len(), 2);
338        assert_eq!(widths[1], 40.0, "the button keeps its width");
339        assert_eq!(widths[0], 160.0, "the label takes exactly what is left");
340    }
341
342    /// Without the wrapper, the same row **overflows** — which is the reference's answer
343    /// and, since milestone 349, this one's. The button keeps its 40 px, the label keeps
344    /// its whole width, and the row wears a striped band saying by how much it did not
345    /// fit. That is the improvement: the defect used to be silent, and it took three
346    /// milestones and a device report to find the button crushed to 13 px.
347    #[test]
348    fn without_it_the_row_overflows_instead_of_crushing_the_button() {
349        let long = "a task label far longer than the row it has to live in";
350        let row = Flex::row()
351            .child(Text::new(long).size(18.0))
352            .child(Container::<()>::new().width(40.0).height(40.0));
353        let widths = widths(row, 200.0);
354        assert_eq!(widths[1], 40.0, "the button keeps its width");
355        assert!(
356            widths[0] > 160.0,
357            "and the label keeps its own, so the row runs over: {widths:?}"
358        );
359    }
360
361    /// Several expanding children share the room in proportion, and the fixed one is
362    /// still untouched.
363    #[test]
364    fn several_expanded_children_split_what_is_left() {
365        let row = Flex::row()
366            .child(Expanded::new(Container::<()>::new()).flex(2.0))
367            .child(Expanded::new(Container::<()>::new()).flex(1.0))
368            .child(Container::<()>::new().width(40.0).height(40.0));
369        let widths = widths(row, 340.0);
370        assert_eq!(widths, vec![200.0, 100.0, 40.0]);
371    }
372
373    /// A fixed width inside an `Expanded` is the wrapper's, not the child's — otherwise
374    /// the basis of zero is overruled and the widget silently does nothing.
375    #[test]
376    fn it_overrules_the_child_s_own_width() {
377        let row = Flex::row()
378            .child(Expanded::new(Container::<()>::new().width(100.0)))
379            .child(Container::<()>::new().width(40.0).height(40.0));
380        let widths = widths(row, 200.0);
381        assert_eq!(widths[0], 160.0, "the expanded child fills, not 100");
382    }
383
384    /// The **loose** fit — the reference's `Flexible`. It never grows a child into room
385    /// it did not ask for; it only lets one give way when there is not enough.
386    #[test]
387    fn a_loose_child_gives_way_but_does_not_fill() {
388        // Room to spare: the tight one fills it, the loose one keeps its size.
389        let tight = Flex::row()
390            .child(Expanded::new(
391                Container::<()>::new().width(40.0).height(20.0),
392            ))
393            .child(Container::<()>::new().width(40.0).height(20.0));
394        assert_eq!(widths(tight, 200.0)[0], 160.0);
395
396        let loose = Flex::row()
397            .child(Expanded::new(Container::<()>::new().width(40.0).height(20.0)).loose())
398            .child(Container::<()>::new().width(40.0).height(20.0));
399        assert_eq!(widths(loose, 200.0)[0], 40.0, "at most its share, not more");
400
401        // Not enough room: it gives way, and the whole deficit lands here because the
402        // sibling is inflexible and inflexible children are not squeezed (milestone 349).
403        // It used to take a `no_shrink()` on the sibling — flexbox shares a deficit in
404        // proportion to basis, and a wrapper cannot speak for its siblings.
405        let squeezed = Flex::row()
406            .child(
407                Expanded::new(
408                    Text::new("a label far longer than this row")
409                        .size(18.0)
410                        .ellipsis(),
411                )
412                .loose(),
413            )
414            .child(Container::<()>::new().width(40.0).height(20.0));
415        let widths = widths(squeezed, 200.0);
416        assert_eq!(widths[1], 40.0, "the fixed sibling keeps its width");
417        assert_eq!(widths[0], 160.0, "and the loose one takes what is left");
418    }
419
420    /// The two names build the **same box**, which is the whole claim: `Expanded` is
421    /// `Flexible(fit: tight)` in the reference and it is here too, in both directions.
422    #[test]
423    fn the_two_names_build_the_same_box() {
424        let by_expanded = Flex::row()
425            .child(Expanded::new(
426                Container::<()>::new().width(40.0).height(20.0),
427            ))
428            .child(Container::<()>::new().width(40.0).height(20.0));
429        let by_flexible = Flex::row()
430            .child(Flexible::new(Container::<()>::new().width(40.0).height(20.0)).tight())
431            .child(Container::<()>::new().width(40.0).height(20.0));
432        assert_eq!(widths(by_expanded, 200.0), widths(by_flexible, 200.0));
433
434        let loose_expanded = Flex::row()
435            .child(Expanded::new(Container::<()>::new().width(40.0).height(20.0)).loose())
436            .child(Container::<()>::new().width(40.0).height(20.0));
437        let loose_flexible = Flex::row()
438            .child(Flexible::new(
439                Container::<()>::new().width(40.0).height(20.0),
440            ))
441            .child(Container::<()>::new().width(40.0).height(20.0));
442        assert_eq!(widths(loose_expanded, 200.0), widths(loose_flexible, 200.0));
443    }
444
445    /// A `Flexible` is **loose** by default, which is the reference's default and the
446    /// opposite of `Expanded`'s: it keeps its size while there is room for it.
447    #[test]
448    fn a_flexible_is_loose_by_default() {
449        let row = Flex::row()
450            .child(Flexible::new(
451                Container::<()>::new().width(40.0).height(20.0),
452            ))
453            .child(Container::<()>::new().width(40.0).height(20.0));
454        assert_eq!(widths(row, 200.0)[0], 40.0, "at most its share, not more");
455    }
456
457    /// `fit()` takes the value, for code ported from the reference's `fit:` argument,
458    /// and says the same thing as `tight()`.
459    #[test]
460    fn the_fit_can_be_given_as_a_value() {
461        let by_method = Flex::row()
462            .child(Flexible::new(Container::<()>::new().width(40.0).height(20.0)).tight())
463            .child(Container::<()>::new().width(40.0).height(20.0));
464        let by_value = Flex::row()
465            .child(
466                Flexible::new(Container::<()>::new().width(40.0).height(20.0)).fit(FlexFit::Tight),
467            )
468            .child(Container::<()>::new().width(40.0).height(20.0));
469        assert_eq!(widths(by_method, 200.0), widths(by_value, 200.0));
470    }
471
472    /// Several flexible children share what is left in proportion, tight or loose.
473    #[test]
474    fn flexible_children_split_in_proportion() {
475        let row = Flex::row()
476            .child(Flexible::new(Container::<()>::new()).tight().flex(2.0))
477            .child(Flexible::new(Container::<()>::new()).tight().flex(1.0))
478            .child(Container::<()>::new().width(40.0).height(40.0));
479        assert_eq!(widths(row, 340.0), vec![200.0, 100.0, 40.0]);
480    }
481
482    /// A `Flexible` is a transparent wrapper too, and the macro is what makes that
483    /// true rather than the author's memory.
484    #[test]
485    fn a_flexible_stays_transparent() {
486        let stack = crate::Stack::<()>::new()
487            .width(100.0)
488            .height(50.0)
489            .layer(Container::new())
490            .layer(Container::new());
491        assert!(Widget::<()>::stack(&Flexible::new(stack)));
492
493        let keyed = crate::Keyed::new(9u64, Container::<()>::new());
494        let key = Widget::<()>::key(&keyed);
495        assert_eq!(Widget::<()>::key(&Flexible::new(keyed)), key);
496    }
497
498    /// It is a wrapper, so the things a wrapper must not eat: the child's identity, and
499    /// the structural answers the walk asks for before it looks at anything.
500    #[test]
501    fn it_stays_transparent() {
502        let stack = crate::Stack::<()>::new()
503            .width(100.0)
504            .height(50.0)
505            .layer(Container::new())
506            .layer(Container::new());
507        assert!(Widget::<()>::stack(&Expanded::new(stack)));
508
509        let keyed = crate::Keyed::new(7u64, Container::<()>::new());
510        let key = Widget::<()>::key(&keyed);
511        assert_eq!(Widget::<()>::key(&Expanded::new(keyed)), key);
512    }
513}