zng_wgt_stack/
lib.rs

1#![doc(html_favicon_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo.png")]
3//!
4//! Stack widgets, properties and nodes.
5//!
6//! # Crate
7//!
8#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9
10zng_wgt::enable_widget_macros!();
11
12use zng_wgt::prelude::*;
13
14mod types;
15pub use types::*;
16
17/// Stack layout.
18///
19/// Without [`direction`] this is a Z layering stack, with direction the traditional vertical and horizontal *stack panels*
20/// can be created, other custom layouts are also supported, diagonal stacks, partially layered stacks and more. See
21/// [`StackDirection`] for more details.
22///
23/// # Z-Index
24///
25/// By default the widgets are rendered in their logical order, the last widget renders in front of the others,
26/// you can change this by setting the [`z_index`] property in the item widget.
27///
28/// # Shorthand
29///
30/// The `Stack!` macro provides shorthand syntax:
31///
32/// * `Stack!($children:expr)` creates a Z stack.
33/// * `Stack!($direction:ident, $children:expr)` create stack on the given direction. The first parameter is
34///   the name of one of the [`LayoutDirection`] associated functions.
35/// * `Stack!($direction:ident, $spacing:expr, $children:expr)` create stack with the given direction, spacing between items and the items.
36/// * `Stack!($direction:expr, $children:expr)` create stack on the given direction. The first parameter is an expression of
37/// type [`LayoutDirection`]. Note that to avoid conflict with the alternative (`$direction:ident`) you can use braces `{my_direction}`.
38/// * `Stack!($direction:expr, $spacing:expr, $children:expr)` create stack with the given direction expression, spacing between items
39/// and the items.
40///
41/// # `stack_nodes`
42///
43/// If you only want to create an overlaying effect composed of multiple nodes you can use the [`stack_nodes`] function.
44///
45/// [`stack_nodes`]: fn@stack_nodes
46///
47/// [`direction`]: fn@direction
48/// [`z_index`]: fn@zng_wgt::z_index
49/// [`LayoutDirection`]: zng_wgt::prelude::LayoutDirection
50#[widget($crate::Stack {
51    ($children:expr) => {
52        children = $children;
53    };
54    ($direction:ident, $children:expr) => {
55        direction = $crate::StackDirection::$direction();
56        children = $children;
57    };
58    ($direction:ident, $spacing:expr, $children:expr) => {
59        direction = $crate::StackDirection::$direction();
60        spacing = $spacing;
61        children = $children;
62    };
63    ($direction:expr, $children:expr) => {
64        direction = $direction;
65        children = $children;
66    };
67    ($direction:expr, $spacing:expr, $children:expr) => {
68        direction = $direction;
69        spacing = $spacing;
70        children = $children;
71    };
72})]
73pub struct Stack(WidgetBase);
74impl Stack {
75    fn widget_intrinsic(&mut self) {
76        self.widget_builder().push_build_action(|wgt| {
77            let child = node(
78                wgt.capture_ui_node_list_or_empty(property_id!(Self::children)),
79                wgt.capture_var_or_default(property_id!(Self::direction)),
80                wgt.capture_var_or_default(property_id!(Self::spacing)),
81                wgt.capture_var_or_else(property_id!(Self::children_align), || Align::FILL),
82            );
83            wgt.set_child(child);
84        });
85    }
86}
87
88/// Stack items.
89#[property(CHILD, capture, default(ui_vec![]), widget_impl(Stack))]
90pub fn children(children: impl UiNodeList) {}
91
92/// Stack direction.
93#[property(LAYOUT, capture, widget_impl(Stack))]
94pub fn direction(direction: impl IntoVar<StackDirection>) {}
95
96/// Space in-between items.
97///
98/// The spacing is added along non-zero axis for each item offset after the first, the spacing is
99/// scaled by the [direction factor].
100///
101/// [`direction`]: fn@direction
102/// [direction factor]: StackDirection::direction_factor
103#[property(LAYOUT, capture, widget_impl(Stack))]
104pub fn spacing(spacing: impl IntoVar<Length>) {}
105
106/// Items alignment.
107///
108/// The items are aligned along axis that don't change, as defined by the [`direction`].
109///
110/// The default is [`FILL`].
111///
112/// [`FILL`]: Align::FILL
113/// [`direction`]: fn@direction
114#[property(LAYOUT, capture, default(Align::FILL), widget_impl(Stack))]
115pub fn children_align(align: impl IntoVar<Align>) {}
116
117/// Stack node.
118///
119/// Can be used directly to stack widgets without declaring a stack widget info. This node is the child
120/// of the `Stack!` widget.
121pub fn node(
122    children: impl UiNodeList,
123    direction: impl IntoVar<StackDirection>,
124    spacing: impl IntoVar<Length>,
125    children_align: impl IntoVar<Align>,
126) -> impl UiNode {
127    let children = PanelList::new(children).track_info_range(*PANEL_LIST_ID);
128    let direction = direction.into_var();
129    let spacing = spacing.into_var();
130    let children_align = children_align.into_var();
131
132    match_node_list(children, move |c, op| match op {
133        UiNodeOp::Init => {
134            WIDGET
135                .sub_var_layout(&direction)
136                .sub_var_layout(&spacing)
137                .sub_var_layout(&children_align);
138        }
139        UiNodeOp::Update { updates } => {
140            let mut changed = false;
141            c.update_all(updates, &mut changed);
142
143            if changed {
144                WIDGET.layout();
145            }
146        }
147        UiNodeOp::Measure { wm, desired_size } => {
148            c.delegated();
149            *desired_size = measure(wm, c.children(), direction.get(), spacing.get(), children_align.get());
150        }
151        UiNodeOp::Layout { wl, final_size } => {
152            c.delegated();
153            *final_size = layout(wl, c.children(), direction.get(), spacing.get(), children_align.get());
154        }
155        _ => {}
156    })
157}
158
159/// Create a node that estimates the size of stack panel children.
160///
161/// The estimation assumes that all items have a size of `child_size`.
162pub fn lazy_size(
163    children_len: impl IntoVar<usize>,
164    direction: impl IntoVar<StackDirection>,
165    spacing: impl IntoVar<Length>,
166    child_size: impl IntoVar<Size>,
167) -> impl UiNode {
168    lazy_sample(children_len, direction, spacing, zng_wgt_size_offset::size(NilUiNode, child_size))
169}
170
171/// Create a node that estimates the size of stack panel children.
172///
173/// The estimation assumes that all items have the size of `child_sample`.
174pub fn lazy_sample(
175    children_len: impl IntoVar<usize>,
176    direction: impl IntoVar<StackDirection>,
177    spacing: impl IntoVar<Length>,
178    child_sample: impl UiNode,
179) -> impl UiNode {
180    let children_len = children_len.into_var();
181    let direction = direction.into_var();
182    let spacing = spacing.into_var();
183
184    match_node(child_sample, move |child, op| match op {
185        UiNodeOp::Init => {
186            WIDGET
187                .sub_var_layout(&children_len)
188                .sub_var_layout(&direction)
189                .sub_var_layout(&spacing);
190        }
191        op @ UiNodeOp::Measure { .. } | op @ UiNodeOp::Layout { .. } => {
192            let mut measure = |wm| {
193                let constraints = LAYOUT.constraints();
194                if let Some(known) = constraints.fill_or_exact() {
195                    child.delegated();
196                    return known;
197                }
198
199                let len = Px(children_len.get() as i32);
200                if len.0 == 0 {
201                    child.delegated();
202                    return PxSize::zero();
203                }
204
205                let child_size = child.measure(wm);
206
207                let direction = direction.get();
208                let dv = direction.direction_factor(LayoutDirection::LTR);
209                let ds = if dv.x == 0.fct() && dv.y != 0.fct() {
210                    // vertical stack
211                    let spacing = spacing.layout_y();
212                    PxSize::new(child_size.width, (len - Px(1)) * (child_size.height + spacing) + child_size.height)
213                } else if dv.x != 0.fct() && dv.y == 0.fct() {
214                    // horizontal stack
215                    let spacing = spacing.layout_x();
216                    PxSize::new((len - Px(1)) * (child_size.width + spacing) + child_size.width, child_size.height)
217                } else {
218                    // unusual stack
219                    let spacing = spacing_from_direction(dv, spacing.get());
220
221                    let mut item_rect = PxRect::from_size(child_size);
222                    let mut item_bounds = euclid::Box2D::zero();
223                    let mut child_spacing = PxVector::zero();
224                    for _ in 0..len.0 {
225                        let offset = direction.layout(item_rect, child_size) + child_spacing;
226                        item_rect.origin = offset.to_point();
227                        let item_box = item_rect.to_box2d();
228                        item_bounds.min = item_bounds.min.min(item_box.min);
229                        item_bounds.max = item_bounds.max.max(item_box.max);
230                        child_spacing = spacing;
231                    }
232
233                    item_bounds.size()
234                };
235
236                constraints.fill_size_or(ds)
237            };
238
239            match op {
240                UiNodeOp::Measure { wm, desired_size } => {
241                    *desired_size = measure(wm);
242                }
243                UiNodeOp::Layout { wl, final_size } => {
244                    *final_size = measure(&mut wl.to_measure(None));
245                }
246                _ => unreachable!(),
247            }
248        }
249        _ => {}
250    })
251}
252
253fn measure(wm: &mut WidgetMeasure, children: &mut PanelList, direction: StackDirection, spacing: Length, children_align: Align) -> PxSize {
254    let metrics = LAYOUT.metrics();
255    let constraints = metrics.constraints();
256    if let Some(known) = constraints.fill_or_exact() {
257        return known;
258    }
259
260    let child_align = children_align * direction.direction_scale();
261
262    let spacing = layout_spacing(&metrics, &direction, spacing);
263    let max_size = child_max_size(wm, children, child_align);
264
265    // layout children, size, raw position + spacing only.
266    let mut item_bounds = euclid::Box2D::zero();
267    LAYOUT.with_constraints(
268        constraints
269            .with_fill(child_align.is_fill_x(), child_align.is_fill_y())
270            .with_max_size(max_size)
271            .with_new_min(Px(0), Px(0)),
272        || {
273            // parallel measure full widgets first
274            children.measure_each(
275                wm,
276                |_, c, _, wm| {
277                    if c.is_widget() { c.measure(wm) } else { PxSize::zero() }
278                },
279                |_, _| PxSize::zero(),
280            );
281
282            let mut item_rect = PxRect::zero();
283            let mut child_spacing = PxVector::zero();
284            children.for_each(|_, c, _| {
285                // already parallel measured widgets, only measure other nodes.
286                let size = match c.with_context(WidgetUpdateMode::Ignore, || WIDGET.bounds().measure_outer_size()) {
287                    Some(wgt_size) => wgt_size,
288                    None => c.measure(wm),
289                };
290                if size.is_empty() {
291                    return; // continue, skip collapsed
292                }
293
294                let offset = direction.layout(item_rect, size) + child_spacing;
295
296                item_rect.origin = offset.to_point();
297                item_rect.size = size;
298
299                let item_box = item_rect.to_box2d();
300                item_bounds.min = item_bounds.min.min(item_box.min);
301                item_bounds.max = item_bounds.max.max(item_box.max);
302                child_spacing = spacing;
303            });
304        },
305    );
306
307    constraints.fill_size_or(item_bounds.size())
308}
309fn layout(wl: &mut WidgetLayout, children: &mut PanelList, direction: StackDirection, spacing: Length, children_align: Align) -> PxSize {
310    let metrics = LAYOUT.metrics();
311    let constraints = metrics.constraints();
312    let child_align = children_align * direction.direction_scale();
313
314    let spacing = layout_spacing(&metrics, &direction, spacing);
315    let max_size = child_max_size(&mut wl.to_measure(None), children, child_align);
316
317    // layout children, size, raw position + spacing only.
318    let mut item_bounds = euclid::Box2D::zero();
319    LAYOUT.with_constraints(
320        constraints
321            .with_fill(child_align.is_fill_x(), child_align.is_fill_y())
322            .with_max_size(max_size)
323            .with_new_min(Px(0), Px(0)),
324        || {
325            // parallel layout widgets
326            children.layout_each(
327                wl,
328                |_, c, o, wl| {
329                    if c.is_widget() {
330                        let (size, define_ref_frame) = wl.with_child(|wl| c.layout(wl));
331                        debug_assert!(!define_ref_frame); // is widget, should define own frame.
332                        o.define_reference_frame = define_ref_frame;
333                        size
334                    } else {
335                        PxSize::zero()
336                    }
337                },
338                |_, _| PxSize::zero(),
339            );
340
341            // layout other nodes and position everything.
342            let mut item_rect = PxRect::zero();
343            let mut child_spacing = PxVector::zero();
344            children.for_each(|_, c, o| {
345                let size = match c.with_context(WidgetUpdateMode::Ignore, || WIDGET.bounds().outer_size()) {
346                    Some(wgt_size) => wgt_size,
347                    None => {
348                        let (size, define_ref_frame) = wl.with_child(|wl| c.layout(wl));
349                        o.define_reference_frame = define_ref_frame;
350                        size
351                    }
352                };
353
354                if size.is_empty() {
355                    o.child_offset = PxVector::zero();
356                    o.define_reference_frame = false;
357                    return; // continue, skip collapsed
358                }
359
360                let offset = direction.layout(item_rect, size) + child_spacing;
361                o.child_offset = offset;
362
363                item_rect.origin = offset.to_point();
364                item_rect.size = size;
365
366                let item_box = item_rect.to_box2d();
367                item_bounds.min = item_bounds.min.min(item_box.min);
368                item_bounds.max = item_bounds.max.max(item_box.max);
369                child_spacing = spacing;
370            });
371        },
372    );
373
374    // final position, align child inside item_bounds and item_bounds in the panel area.
375    let items_size = item_bounds.size();
376    let panel_size = constraints.fill_size_or(items_size);
377    let children_offset = -item_bounds.min.to_vector() + (panel_size - items_size).to_vector() * children_align.xy(LAYOUT.direction());
378    let align_baseline = children_align.is_baseline();
379    let child_align = child_align.xy(LAYOUT.direction());
380
381    children.for_each(|_, c, o| {
382        if let Some((size, baseline)) = c.with_context(WidgetUpdateMode::Ignore, || {
383            let bounds = WIDGET.bounds();
384            (bounds.outer_size(), bounds.final_baseline())
385        }) {
386            let child_offset = (items_size - size).to_vector() * child_align;
387            o.child_offset += children_offset + child_offset;
388
389            if align_baseline {
390                o.child_offset.y += baseline;
391            }
392        } else {
393            // non-widgets only align with item_bounds
394            o.child_offset += children_offset;
395        }
396    });
397
398    children.commit_data().request_render();
399
400    panel_size
401}
402
403/// Spacing to add on each axis.
404fn layout_spacing(ctx: &LayoutMetrics, direction: &StackDirection, spacing: Length) -> PxVector {
405    let factor = direction.direction_factor(ctx.direction());
406    spacing_from_direction(factor, spacing)
407}
408fn spacing_from_direction(factor: Factor2d, spacing: Length) -> PxVector {
409    PxVector::new(spacing.layout_x(), spacing.layout_y()) * factor
410}
411
412/// Max size to layout each child with.
413fn child_max_size(wm: &mut WidgetMeasure, children: &mut PanelList, child_align: Align) -> PxSize {
414    let constraints = LAYOUT.constraints();
415
416    // need measure when children fill, but the panel does not.
417    let mut need_measure = false;
418    let mut max_size = PxSize::zero();
419    let mut measure_constraints = constraints;
420    match (constraints.x.fill_or_exact(), constraints.y.fill_or_exact()) {
421        (None, None) => {
422            need_measure = child_align.is_fill_x() || child_align.is_fill_y();
423            if !need_measure {
424                max_size = constraints.max_size().unwrap_or_else(|| PxSize::new(Px::MAX, Px::MAX));
425            }
426        }
427        (None, Some(h)) => {
428            max_size.height = h;
429            need_measure = child_align.is_fill_x();
430
431            if need_measure {
432                measure_constraints = constraints.with_fill_x(false);
433            } else {
434                max_size.width = Px::MAX;
435            }
436        }
437        (Some(w), None) => {
438            max_size.width = w;
439            need_measure = child_align.is_fill_y();
440
441            if need_measure {
442                measure_constraints = constraints.with_fill_y(false);
443            } else {
444                max_size.height = Px::MAX;
445            }
446        }
447        (Some(w), Some(h)) => max_size = PxSize::new(w, h),
448    }
449
450    // find largest child, the others will fill to its size.
451    if need_measure {
452        let max_items = LAYOUT.with_constraints(measure_constraints.with_new_min(Px(0), Px(0)), || {
453            children.measure_each(wm, |_, c, _, wm| c.measure(wm), PxSize::max)
454        });
455
456        max_size = constraints.clamp_size(max_size.max(max_items));
457    }
458
459    max_size
460}
461
462/// Basic z-stack node.
463///
464/// Creates a node that updates and layouts the `nodes` in the logical order they appear in the list
465/// and renders them one on top of the other from back(0) to front(len-1). The layout size is the largest item width and height,
466/// the parent constraints are used for the layout of each item.
467///
468/// This is the most simple *z-stack* implementation possible, it is a building block useful for quickly declaring
469/// overlaying effects composed of multiple nodes, it does not do any alignment layout or z-sorting render.
470///
471/// [`Stack!`]: struct@Stack
472pub fn stack_nodes(nodes: impl UiNodeList) -> impl UiNode {
473    match_node_list(nodes, |_, _| {})
474}
475
476/// Basic z-stack node sized by one of the items.
477///
478/// Creates a node that updates the `nodes` in the logical order they appear, renders them one on top of the other from back(0)
479/// to front(len-1), but layouts the `index` item first and uses its size to get `constraints` for the other items.
480///
481/// The layout size is the largest item width and height, usually the `index` size.
482///
483/// If the `index` is out of range the node logs an error and behaves like [`stack_nodes`].
484pub fn stack_nodes_layout_by(
485    nodes: impl UiNodeList,
486    index: impl IntoVar<usize>,
487    constraints: impl Fn(PxConstraints2d, usize, PxSize) -> PxConstraints2d + Send + 'static,
488) -> impl UiNode {
489    #[cfg(feature = "dyn_closure")]
490    let constraints: Box<dyn Fn(PxConstraints2d, usize, PxSize) -> PxConstraints2d + Send> = Box::new(constraints);
491    stack_nodes_layout_by_impl(nodes, index, constraints)
492}
493
494fn stack_nodes_layout_by_impl(
495    nodes: impl UiNodeList,
496    index: impl IntoVar<usize>,
497    constraints: impl Fn(PxConstraints2d, usize, PxSize) -> PxConstraints2d + Send + 'static,
498) -> impl UiNode {
499    let index = index.into_var();
500
501    match_node_list(nodes, move |children, op| match op {
502        UiNodeOp::Init => {
503            WIDGET.sub_var_layout(&index);
504        }
505        UiNodeOp::Measure { wm, desired_size } => {
506            let index = index.get();
507            let len = children.len();
508            *desired_size = if index >= len {
509                tracing::error!(
510                    "index {} out of range for length {} in `{:?}#stack_nodes_layout_by`",
511                    index,
512                    len,
513                    WIDGET.id()
514                );
515
516                children.measure_each(wm, |_, n, wm| n.measure(wm), PxSize::max)
517            } else {
518                let index_size = children.with_node(index, |n| n.measure(wm));
519                let constraints = constraints(LAYOUT.metrics().constraints(), index, index_size);
520                LAYOUT.with_constraints(constraints, || {
521                    children.measure_each(
522                        wm,
523                        |i, n, wm| {
524                            if i != index { n.measure(wm) } else { index_size }
525                        },
526                        PxSize::max,
527                    )
528                })
529            };
530        }
531        UiNodeOp::Layout { wl, final_size } => {
532            let index = index.get();
533            let len = children.len();
534            *final_size = if index >= len {
535                tracing::error!(
536                    "index {} out of range for length {} in `{:?}#stack_nodes_layout_by`",
537                    index,
538                    len,
539                    WIDGET.id()
540                );
541
542                children.layout_each(wl, |_, n, wl| n.layout(wl), PxSize::max)
543            } else {
544                let index_size = children.with_node(index, |n| n.layout(wl));
545                let constraints = constraints(LAYOUT.metrics().constraints(), index, index_size);
546                LAYOUT.with_constraints(constraints, || {
547                    children.layout_each(
548                        wl,
549                        |i, n, wl| {
550                            if i != index { n.layout(wl) } else { index_size }
551                        },
552                        PxSize::max,
553                    )
554                })
555            };
556        }
557        _ => {}
558    })
559}
560
561static_id! {
562    static ref PANEL_LIST_ID: StateId<zng_app::widget::node::PanelListRange>;
563}
564
565/// Get the child index in the parent stack.
566///
567/// The child index is zero-based.
568#[property(CONTEXT)]
569pub fn get_index(child: impl UiNode, state: impl IntoVar<usize>) -> impl UiNode {
570    let state = state.into_var();
571    zng_wgt::node::with_index_node(child, *PANEL_LIST_ID, move |id| {
572        let _ = state.set(id.unwrap_or(0));
573    })
574}
575
576/// Get the child index and number of children.
577#[property(CONTEXT)]
578pub fn get_index_len(child: impl UiNode, state: impl IntoVar<(usize, usize)>) -> impl UiNode {
579    let state = state.into_var();
580    zng_wgt::node::with_index_len_node(child, *PANEL_LIST_ID, move |id_len| {
581        let _ = state.set(id_len.unwrap_or((0, 0)));
582    })
583}
584
585/// Get the child index, starting from the last child at `0`.
586#[property(CONTEXT)]
587pub fn get_rev_index(child: impl UiNode, state: impl IntoVar<usize>) -> impl UiNode {
588    let state = state.into_var();
589    zng_wgt::node::with_rev_index_node(child, *PANEL_LIST_ID, move |id| {
590        let _ = state.set(id.unwrap_or(0));
591    })
592}
593
594/// If the child index is even.
595///
596/// Child index is zero-based, so the first is even, the next [`is_odd`].
597///
598/// [`is_odd`]: fn@is_odd
599#[property(CONTEXT)]
600pub fn is_even(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
601    let state = state.into_var();
602    zng_wgt::node::with_index_node(child, *PANEL_LIST_ID, move |id| {
603        let _ = state.set(id.map(|i| i % 2 == 0).unwrap_or(false));
604    })
605}
606
607/// If the child index is odd.
608///
609/// Child index is zero-based, so the first [`is_even`], the next one is odd.
610///
611/// [`is_even`]: fn@is_even
612#[property(CONTEXT)]
613pub fn is_odd(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
614    let state = state.into_var();
615    zng_wgt::node::with_index_node(child, *PANEL_LIST_ID, move |id| {
616        let _ = state.set(id.map(|i| i % 2 != 0).unwrap_or(false));
617    })
618}
619
620/// If the child is the first.
621#[property(CONTEXT)]
622pub fn is_first(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
623    let state = state.into_var();
624    zng_wgt::node::with_index_node(child, *PANEL_LIST_ID, move |id| {
625        let _ = state.set(id == Some(0));
626    })
627}
628
629/// If the child is the last.
630#[property(CONTEXT)]
631pub fn is_last(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
632    let state = state.into_var();
633    zng_wgt::node::with_rev_index_node(child, *PANEL_LIST_ID, move |id| {
634        let _ = state.set(id == Some(0));
635    })
636}
637
638/// Extension methods for [`WidgetInfo`] that may represent a [`Stack!`] instance.
639///
640/// [`Stack!`]: struct@Stack
641/// [`WidgetInfo`]: zng_app::widget::info::WidgetInfo
642pub trait WidgetInfoStackExt {
643    /// Gets the stack children, if this widget is a [`Stack!`] instance.
644    ///
645    /// [`Stack!`]: struct@Stack
646    fn stack_children(&self) -> Option<zng_app::widget::info::iter::Children>;
647}
648impl WidgetInfoStackExt for zng_app::widget::info::WidgetInfo {
649    fn stack_children(&self) -> Option<zng_app::widget::info::iter::Children> {
650        zng_app::widget::node::PanelListRange::get(self, *PANEL_LIST_ID)
651    }
652}