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}