Skip to main content

frus_widgets/
customlayout.rs

1//! [`CustomMultiChildLayout`]: children placed by a function the application writes, for
2//! arrangements a row, a column, a stack or a grid cannot express (milestone 601).
3
4use std::hash::{Hash, Hasher};
5
6use frus_core::{Point, Rect, Scene, Size};
7use frus_layout::{Dimension, Style};
8
9use crate::interaction::Status;
10use crate::theme::Theme;
11use crate::widget::{FillAxes, Widget};
12
13/// What a child is given on one axis by a [`ChildLayout::layout_child`] call.
14#[derive(Clone, Copy, Debug, PartialEq)]
15pub enum ChildAxis {
16    /// As big as it likes.
17    Free,
18    /// At most this many pixels: a paragraph wraps there, a block smaller than it keeps its
19    /// size.
20    AtMost(f32),
21    /// This many pixels. A child with a size of its own on this axis keeps it, as a stack's
22    /// layer does.
23    Exactly(f32),
24}
25
26/// What a child is given, per axis, by [`ChildLayout::layout_child`].
27#[derive(Clone, Copy, Debug, PartialEq)]
28pub struct ChildConstraints {
29    /// Across.
30    pub width: ChildAxis,
31    /// Down.
32    pub height: ChildAxis,
33}
34
35impl ChildConstraints {
36    /// As big as it likes, on both axes.
37    pub fn free() -> Self {
38        Self {
39            width: ChildAxis::Free,
40            height: ChildAxis::Free,
41        }
42    }
43
44    /// At most `size`.
45    pub fn loose(size: Size) -> Self {
46        Self {
47            width: ChildAxis::AtMost(size.width),
48            height: ChildAxis::AtMost(size.height),
49        }
50    }
51
52    /// Exactly `size`.
53    pub fn tight(size: Size) -> Self {
54        Self {
55            width: ChildAxis::Exactly(size.width),
56            height: ChildAxis::Exactly(size.height),
57        }
58    }
59
60    /// The same, with `width` across.
61    pub fn width(mut self, width: ChildAxis) -> Self {
62        self.width = width;
63        self
64    }
65
66    /// The same, with `height` down.
67    pub fn height(mut self, height: ChildAxis) -> Self {
68        self.height = height;
69        self
70    }
71}
72
73/// The key a child is named by, from whatever the application named it with.
74pub(crate) fn key_of(key: impl Hash) -> u64 {
75    let mut hasher = std::collections::hash_map::DefaultHasher::new();
76    key.hash(&mut hasher);
77    hasher.finish()
78}
79
80/// What a [`CustomMultiChildLayout`]'s function is handed: its children, by key, to measure
81/// and to place.
82pub struct ChildLayout<'a> {
83    keys: &'a [u64],
84    measure: &'a dyn Fn(usize, ChildConstraints) -> Vec<Rect>,
85    pub(crate) laid: Vec<Option<Vec<Rect>>>,
86    pub(crate) at: Vec<Point>,
87}
88
89impl<'a> ChildLayout<'a> {
90    pub(crate) fn new(
91        keys: &'a [u64],
92        measure: &'a dyn Fn(usize, ChildConstraints) -> Vec<Rect>,
93    ) -> Self {
94        Self {
95            keys,
96            measure,
97            laid: vec![None; keys.len()],
98            at: vec![Point::new(0.0, 0.0); keys.len()],
99        }
100    }
101
102    fn index(&self, key: impl Hash) -> Option<usize> {
103        let key = key_of(key);
104        self.keys.iter().position(|k| *k == key)
105    }
106
107    /// Whether a child was given under `key`: a layout whose children come and go asks
108    /// before it measures.
109    pub fn has_child(&self, key: impl Hash) -> bool {
110        self.index(key).is_some()
111    }
112
113    /// Lays the child named `key` out under `constraints` and says how big it came out. A
114    /// child never laid out is not shown; one laid out twice keeps the second answer.
115    ///
116    /// # Panics
117    ///
118    /// If no child was given under `key`, as the reference's does: the layout is asking
119    /// about something that is not there, and a size made up for it would be placed.
120    pub fn layout_child(&mut self, key: impl Hash, constraints: ChildConstraints) -> Size {
121        let i = self
122            .index(key)
123            .expect("layout_child: no child was given under this key");
124        let rects = (self.measure)(i, constraints);
125        let size = rects
126            .first()
127            .map(|r| Size::new(r.width, r.height))
128            .unwrap_or(Size::new(0.0, 0.0));
129        self.laid[i] = Some(rects);
130        size
131    }
132
133    /// Puts the child named `key` with its top left corner at `at`, from the layout's own
134    /// top left. A child laid out and never placed is at the corner.
135    ///
136    /// # Panics
137    ///
138    /// If no child was given under `key`.
139    pub fn position_child(&mut self, key: impl Hash, at: Point) {
140        let i = self
141            .index(key)
142            .expect("position_child: no child was given under this key");
143        self.at[i] = at;
144    }
145}
146
147/// The function that places a [`CustomMultiChildLayout`]'s children: its own size, and the
148/// children to measure and place.
149pub type LayoutFn = dyn Fn(Size, &mut ChildLayout<'_>);
150
151/// A layout a hook hands the walk: the function and the keys, one per child.
152pub struct CustomLayout<'a> {
153    /// The function.
154    pub delegate: &'a LayoutFn,
155    /// One key per child, in order.
156    pub keys: &'a [u64],
157}
158
159/// **Places its children with a function the application writes**: a circle of buttons, a
160/// fan of cards, a bubble whose tail points at its neighbour, nodes placed by a solver.
161///
162/// Each child is named by a key. The function is given the layout's size and, for each
163/// child, asks how big it is under constraints it chooses ([`ChildLayout::layout_child`]),
164/// then says where it goes ([`ChildLayout::position_child`]). A child can be measured before
165/// another is placed, so one can be put against another's size.
166///
167/// ```
168/// use frus_core::{Point, Size};
169/// use frus_widgets::{ChildConstraints, CustomMultiChildLayout, Text};
170///
171/// // A caption under a title, indented by the title's height.
172/// let _card = CustomMultiChildLayout::<()>::new(|size, layout| {
173///     let title = layout.layout_child("title", ChildConstraints::loose(size));
174///     layout.position_child("title", Point::new(0.0, 0.0));
175///     let room = Size::new(size.width - title.height, size.height - title.height);
176///     layout.layout_child("caption", ChildConstraints::loose(room));
177///     layout.position_child("caption", Point::new(title.height, title.height));
178/// })
179/// .child("title", Text::new("Seat map"))
180/// .child("caption", Text::new("Row 12"));
181/// ```
182///
183/// **Its own size does not depend on its children**, as the reference's does not: it takes
184/// the room it is given, or the size set with [`width`](Self::width) and
185/// [`height`](Self::height). Children are painted in the order they were given, and a child
186/// the function never lays out is not shown. Nothing is cut at the layout's edge.
187pub struct CustomMultiChildLayout<Msg = crate::callback::Callback> {
188    delegate: Box<LayoutFn>,
189    keys: Vec<u64>,
190    children: Vec<Box<dyn Widget<Msg>>>,
191    width: Option<f32>,
192    height: Option<f32>,
193}
194
195impl<Msg> CustomMultiChildLayout<Msg> {
196    /// A layout placed by `delegate`, with no children yet.
197    pub fn new(delegate: impl Fn(Size, &mut ChildLayout<'_>) + 'static) -> Self {
198        Self {
199            delegate: Box::new(delegate),
200            keys: Vec::new(),
201            children: Vec::new(),
202            width: None,
203            height: None,
204        }
205    }
206
207    /// Adds `child` under `key`, which the function names it by. A key already given is
208    /// given again: the first child under it is the one the function reaches.
209    pub fn child(mut self, key: impl Hash, child: impl Widget<Msg> + 'static) -> Self {
210        self.keys.push(key_of(key));
211        self.children.push(Box::new(child));
212        self
213    }
214
215    /// A width of its own, instead of the room.
216    pub fn width(mut self, width: f32) -> Self {
217        self.width = Some(width.max(0.0));
218        self
219    }
220
221    /// A height of its own, instead of the room.
222    pub fn height(mut self, height: f32) -> Self {
223        self.height = Some(height.max(0.0));
224        self
225    }
226}
227
228impl<Msg: Clone> Widget<Msg> for CustomMultiChildLayout<Msg> {
229    fn style(&self) -> Style {
230        let dimension = |extent: Option<f32>| extent.map_or(Dimension::Auto, Dimension::Length);
231        Style {
232            width: dimension(self.width),
233            height: dimension(self.height),
234            ..Style::default()
235        }
236    }
237
238    fn children(&self) -> &[Box<dyn Widget<Msg>>] {
239        &self.children
240    }
241
242    fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
243
244    fn on_click(&self) -> Option<Msg> {
245        None
246    }
247
248    fn custom_layout(&self) -> Option<CustomLayout<'_>> {
249        Some(CustomLayout {
250            delegate: self.delegate.as_ref(),
251            keys: &self.keys,
252        })
253    }
254
255    /// The room, on the axes it has no size of its own on: the reference's layout is as big
256    /// as it is allowed.
257    fn fill_axes(&self, _theme: &Theme) -> FillAxes {
258        FillAxes {
259            horizontal: self.width.is_none(),
260            vertical: self.height.is_none(),
261        }
262    }
263
264    fn debug_name(&self) -> &'static str {
265        "CustomMultiChildLayout"
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272    use crate::{build_ui, Container, Flex, Runtime, Text};
273    use frus_core::{Color, Primitive};
274
275    const RED: Color = Color::rgb(1.0, 0.0, 0.0);
276    const BLUE: Color = Color::rgb(0.0, 0.0, 1.0);
277
278    fn block(width: f32, height: f32, color: Color) -> Container<()> {
279        Container::new().width(width).height(height).color(color)
280    }
281
282    /// Every rect of `color` painted.
283    fn painted(root: &dyn Widget<()>, size: Size, color: Color) -> Vec<Rect> {
284        build_ui(root, size, &Runtime::default(), &Theme::dark())
285            .scene()
286            .primitives()
287            .iter()
288            .filter_map(|p| match p {
289                Primitive::Rect { rect, color: c, .. } if *c == color => Some(*rect),
290                _ => None,
291            })
292            .collect()
293    }
294
295    /// **Measured, then placed against another's size**: a blue block goes under the red
296    /// one and against the right edge, from sizes the function asked for.
297    #[test]
298    fn a_child_is_placed_against_another_childs_size() {
299        let root = CustomMultiChildLayout::<()>::new(|size, layout| {
300            let red = layout.layout_child("red", ChildConstraints::free());
301            layout.position_child("red", Point::new(10.0, 10.0));
302            let blue = layout.layout_child("blue", ChildConstraints::free());
303            layout.position_child(
304                "blue",
305                Point::new(size.width - blue.width, 10.0 + red.height),
306            );
307        })
308        .child("red", block(40.0, 30.0, RED))
309        .child("blue", block(20.0, 20.0, BLUE));
310        let size = Size::new(200.0, 100.0);
311        let red = painted(&root, size, RED)[0];
312        let blue = painted(&root, size, BLUE)[0];
313        assert_eq!(
314            (red.x, red.y, red.width, red.height),
315            (10.0, 10.0, 40.0, 30.0)
316        );
317        assert_eq!((blue.x, blue.y), (180.0, 40.0));
318    }
319
320    /// **A child the function never lays out is not shown**; one laid out and never placed
321    /// is at the corner.
322    #[test]
323    fn a_child_left_out_is_not_shown() {
324        let root = CustomMultiChildLayout::<()>::new(|_, layout| {
325            layout.layout_child("red", ChildConstraints::free());
326        })
327        .child("red", block(40.0, 30.0, RED))
328        .child("blue", block(20.0, 20.0, BLUE));
329        let size = Size::new(200.0, 100.0);
330        assert_eq!(painted(&root, size, RED)[0].x, 0.0);
331        assert!(painted(&root, size, BLUE).is_empty());
332    }
333
334    /// **Its own size is the room, or its own**, never its children's: in a column, what
335    /// follows a 50-px-tall layout starts at 50 whatever is inside, and the function is
336    /// handed the column's width.
337    #[test]
338    fn its_size_is_the_room_or_its_own() {
339        let seen = std::rc::Rc::new(std::cell::Cell::new(Size::new(0.0, 0.0)));
340        let told = seen.clone();
341        let root = Flex::<()>::column()
342            .width(160.0)
343            .child(
344                CustomMultiChildLayout::new(move |size, layout| {
345                    told.set(size);
346                    layout.layout_child("red", ChildConstraints::free());
347                })
348                .height(50.0)
349                .child("red", block(40.0, 300.0, RED)),
350            )
351            .child(block(10.0, 10.0, BLUE));
352        let blue = painted(&root, Size::new(300.0, 400.0), BLUE)[0];
353        assert_eq!(blue.y, 50.0);
354        assert_eq!(seen.get(), Size::new(160.0, 50.0));
355    }
356
357    /// **Exactly** gives a child with no size of its own that size; **at most** lets a
358    /// paragraph wrap there.
359    #[test]
360    fn exact_and_at_most_constraints() {
361        let words = "one two three four five six seven eight nine ten eleven twelve";
362        let root = CustomMultiChildLayout::<()>::new(|_, layout| {
363            layout.layout_child("red", ChildConstraints::tight(Size::new(70.0, 30.0)));
364            let text =
365                layout.layout_child("text", ChildConstraints::loose(Size::new(120.0, 400.0)));
366            assert!(text.width <= 120.0 && text.height > 50.0, "{text:?}");
367            layout.position_child("text", Point::new(0.0, 40.0));
368        })
369        .child("red", Container::new().color(RED))
370        .child("text", Text::new(words));
371        let red = painted(&root, Size::new(300.0, 300.0), RED)[0];
372        assert_eq!((red.width, red.height), (70.0, 30.0));
373    }
374
375    /// **A placed child takes taps where it was put**, and not where it would have been.
376    #[test]
377    fn a_placed_child_takes_taps_where_it_is() {
378        let root = CustomMultiChildLayout::<()>::new(|_, layout| {
379            layout.layout_child("button", ChildConstraints::free());
380            layout.position_child("button", Point::new(100.0, 50.0));
381        })
382        .child("button", block(40.0, 20.0, RED).on_click(()));
383        let ui = build_ui(
384            &root,
385            Size::new(200.0, 100.0),
386            &Runtime::default(),
387            &Theme::dark(),
388        );
389        assert!(ui.hit(Point::new(110.0, 60.0)).is_some());
390        assert!(ui.hit(Point::new(10.0, 10.0)).is_none());
391    }
392
393    /// **Asking about a key that was never given** is a mistake in the function, and says so.
394    #[test]
395    #[should_panic(expected = "no child was given under this key")]
396    fn an_unknown_key_panics() {
397        let root = CustomMultiChildLayout::<()>::new(|_, layout| {
398            assert!(!layout.has_child("missing"));
399            layout.layout_child("missing", ChildConstraints::free());
400        });
401        painted(&root, Size::new(100.0, 100.0), RED);
402    }
403
404    /// **Through a key**, the layout is still one: a keyed wrapper passes the question on.
405    #[test]
406    fn a_keyed_layout_still_places_its_children() {
407        let root = crate::keyed(
408            7,
409            CustomMultiChildLayout::<()>::new(|_, layout| {
410                layout.layout_child("red", ChildConstraints::free());
411                layout.position_child("red", Point::new(30.0, 20.0));
412            })
413            .child("red", block(40.0, 30.0, RED)),
414        );
415        let red = painted(&root, Size::new(200.0, 100.0), RED)[0];
416        assert_eq!((red.x, red.y), (30.0, 20.0));
417    }
418}