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}