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#![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#[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_or_nil(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#[property(CHILD, default(ui_vec![]), widget_impl(Stack))]
90pub fn children(wgt: &mut WidgetBuilding, children: impl IntoUiNode) {
91    let _ = children;
92    wgt.expect_property_capture();
93}
94
95#[property(LAYOUT, widget_impl(Stack))]
97pub fn direction(wgt: &mut WidgetBuilding, direction: impl IntoVar<StackDirection>) {
98    let _ = direction;
99    wgt.expect_property_capture();
100}
101
102#[property(LAYOUT, widget_impl(Stack))]
110pub fn spacing(wgt: &mut WidgetBuilding, spacing: impl IntoVar<Length>) {
111    let _ = spacing;
112    wgt.expect_property_capture();
113}
114
115#[property(LAYOUT, default(Align::FILL), widget_impl(Stack))]
124pub fn children_align(wgt: &mut WidgetBuilding, align: impl IntoVar<Align>) {
125    let _ = align;
126    wgt.expect_property_capture();
127}
128
129pub fn node(
134    children: impl IntoUiNode,
135    direction: impl IntoVar<StackDirection>,
136    spacing: impl IntoVar<Length>,
137    children_align: impl IntoVar<Align>,
138) -> UiNode {
139    let children = PanelList::new(children).track_info_range(*PANEL_LIST_ID);
140    let direction = direction.into_var();
141    let spacing = spacing.into_var();
142    let children_align = children_align.into_var();
143
144    match_node(children, move |c, op| match op {
145        UiNodeOp::Init => {
146            WIDGET
147                .sub_var_layout(&direction)
148                .sub_var_layout(&spacing)
149                .sub_var_layout(&children_align);
150        }
151        UiNodeOp::Update { updates } => {
152            let mut changed = false;
153            c.update_list(updates, &mut changed);
154
155            if changed {
156                WIDGET.layout();
157            }
158        }
159        UiNodeOp::Measure { wm, desired_size } => {
160            c.delegated();
161            *desired_size = measure(wm, c.node_impl::<PanelList>(), direction.get(), spacing.get(), children_align.get());
162        }
163        UiNodeOp::Layout { wl, final_size } => {
164            c.delegated();
165            *final_size = layout(wl, c.node_impl::<PanelList>(), direction.get(), spacing.get(), children_align.get());
166        }
167        _ => {}
168    })
169}
170
171pub fn lazy_size(
175    children_len: impl IntoVar<usize>,
176    direction: impl IntoVar<StackDirection>,
177    spacing: impl IntoVar<Length>,
178    child_size: impl IntoVar<Size>,
179) -> UiNode {
180    lazy_sample(
181        children_len,
182        direction,
183        spacing,
184        zng_wgt_size_offset::size(UiNode::nil(), child_size),
185    )
186}
187
188pub fn lazy_sample(
192    children_len: impl IntoVar<usize>,
193    direction: impl IntoVar<StackDirection>,
194    spacing: impl IntoVar<Length>,
195    child_sample: impl IntoUiNode,
196) -> UiNode {
197    let children_len = children_len.into_var();
198    let direction = direction.into_var();
199    let spacing = spacing.into_var();
200
201    match_node(child_sample, move |child, op| match op {
202        UiNodeOp::Init => {
203            WIDGET
204                .sub_var_layout(&children_len)
205                .sub_var_layout(&direction)
206                .sub_var_layout(&spacing);
207        }
208        op @ UiNodeOp::Measure { .. } | op @ UiNodeOp::Layout { .. } => {
209            let mut measure = |wm| {
210                let constraints = LAYOUT.constraints().inner();
211                if let Some(known) = constraints.fill_or_exact() {
212                    child.delegated();
213                    return known;
214                }
215
216                let len = Px(children_len.get() as i32);
217                if len.0 == 0 {
218                    child.delegated();
219                    return PxSize::zero();
220                }
221
222                let child_size = child.measure(wm);
223
224                let direction = direction.get();
225                let dv = direction.direction_factor(LayoutDirection::LTR);
226                let ds = if dv.x == 0.fct() && dv.y != 0.fct() {
227                    let spacing = spacing.layout_y();
229                    PxSize::new(child_size.width, (len - Px(1)) * (child_size.height + spacing) + child_size.height)
230                } else if dv.x != 0.fct() && dv.y == 0.fct() {
231                    let spacing = spacing.layout_x();
233                    PxSize::new((len - Px(1)) * (child_size.width + spacing) + child_size.width, child_size.height)
234                } else {
235                    let spacing = spacing_from_direction(dv, spacing.get());
237
238                    let mut item_rect = PxRect::from_size(child_size);
239                    let mut item_bounds = euclid::Box2D::zero();
240                    let mut child_spacing = PxVector::zero();
241                    for _ in 0..len.0 {
242                        let offset = direction.layout(item_rect, child_size) + child_spacing;
243                        item_rect.origin = offset.to_point();
244                        let item_box = item_rect.to_box2d();
245                        item_bounds.min = item_bounds.min.min(item_box.min);
246                        item_bounds.max = item_bounds.max.max(item_box.max);
247                        child_spacing = spacing;
248                    }
249
250                    item_bounds.size()
251                };
252
253                constraints.fill_size_or(ds)
254            };
255
256            match op {
257                UiNodeOp::Measure { wm, desired_size } => {
258                    *desired_size = measure(wm);
259                }
260                UiNodeOp::Layout { wl, final_size } => {
261                    *final_size = measure(&mut wl.to_measure(None));
262                }
263                _ => unreachable!(),
264            }
265        }
266        _ => {}
267    })
268}
269
270fn measure(wm: &mut WidgetMeasure, children: &mut PanelList, direction: StackDirection, spacing: Length, children_align: Align) -> PxSize {
271    let metrics = LAYOUT.metrics();
272    let constraints = metrics.constraints();
273    if let Some(known) = constraints.inner().fill_or_exact() {
274        return known;
275    }
276
277    let child_align = children_align * direction.direction_scale();
278
279    let spacing = layout_spacing(&metrics, &direction, spacing);
280    let max_size = child_max_size(wm, children, child_align);
281
282    let mut item_bounds = euclid::Box2D::zero();
284    LAYOUT.with_constraints(
285        constraints
286            .with_fill(child_align.is_fill_x(), child_align.is_fill_y())
287            .with_max_size(max_size)
288            .with_new_min(Px(0), Px(0)),
289        || {
290            children.measure_list(
292                wm,
293                |_, c, _, wm| {
294                    if c.as_widget().is_some() { c.measure(wm) } else { PxSize::zero() }
295                },
296                |_, _| PxSize::zero(),
297            );
298
299            let mut item_rect = PxRect::zero();
300            let mut child_spacing = PxVector::zero();
301            children.for_each_child(|_, c, _| {
302                let size = match c.as_widget() {
303                    Some(mut w) => w.with_context(WidgetUpdateMode::Ignore, || WIDGET.bounds().measure_outer_size()),
305                    None => c.measure(wm),
306                };
307                if size.is_empty() {
308                    return; }
310
311                let offset = direction.layout(item_rect, size) + child_spacing;
312
313                item_rect.origin = offset.to_point();
314                item_rect.size = size;
315
316                let item_box = item_rect.to_box2d();
317                item_bounds.min = item_bounds.min.min(item_box.min);
318                item_bounds.max = item_bounds.max.max(item_box.max);
319                child_spacing = spacing;
320            });
321        },
322    );
323
324    constraints.inner().fill_size_or(item_bounds.size())
325}
326fn layout(wl: &mut WidgetLayout, children: &mut PanelList, direction: StackDirection, spacing: Length, children_align: Align) -> PxSize {
327    let metrics = LAYOUT.metrics();
328    let constraints = metrics.constraints();
329    let child_align = children_align * direction.direction_scale();
330
331    let spacing = layout_spacing(&metrics, &direction, spacing);
332    let max_size = child_max_size(&mut wl.to_measure(None), children, child_align);
333
334    let mut item_bounds = euclid::Box2D::zero();
336    LAYOUT.with_constraints(
337        constraints
338            .with_fill(child_align.is_fill_x(), child_align.is_fill_y())
339            .with_max_size(max_size)
340            .with_new_min(Px(0), Px(0)),
341        || {
342            children.layout_list(
344                wl,
345                |_, c, o, wl| {
346                    if c.as_widget().is_some() {
347                        let (size, define_ref_frame) = wl.with_child(|wl| c.layout(wl));
348                        debug_assert!(!define_ref_frame); o.define_reference_frame = define_ref_frame;
350                        size
351                    } else {
352                        PxSize::zero()
353                    }
354                },
355                |_, _| PxSize::zero(),
356            );
357
358            let mut item_rect = PxRect::zero();
360            let mut child_spacing = PxVector::zero();
361            children.for_each_child(|_, c, o| {
362                let size = match c.as_widget() {
363                    Some(mut w) => w.with_context(WidgetUpdateMode::Ignore, || WIDGET.bounds().outer_size()),
364                    None => {
365                        let (size, define_ref_frame) = wl.with_child(|wl| c.layout(wl));
366                        o.define_reference_frame = define_ref_frame;
367                        size
368                    }
369                };
370                if size.is_empty() {
371                    o.child_offset = PxVector::zero();
372                    o.define_reference_frame = false;
373                    return; }
375
376                let offset = direction.layout(item_rect, size) + child_spacing;
377                o.child_offset = offset;
378
379                item_rect.origin = offset.to_point();
380                item_rect.size = size;
381
382                let item_box = item_rect.to_box2d();
383                item_bounds.min = item_bounds.min.min(item_box.min);
384                item_bounds.max = item_bounds.max.max(item_box.max);
385                child_spacing = spacing;
386            });
387        },
388    );
389
390    let items_size = item_bounds.size();
392    let panel_size = constraints.inner().fill_size_or(items_size);
393    let children_offset = -item_bounds.min.to_vector() + (panel_size - items_size).to_vector() * children_align.xy(LAYOUT.direction());
394    let align_baseline = children_align.is_baseline();
395    let child_align = child_align.xy(LAYOUT.direction());
396
397    children.for_each_child(|_, c, o| {
398        match c.as_widget() {
399            Some(mut w) => {
400                let (size, baseline) = w.with_context(WidgetUpdateMode::Ignore, || {
401                    let bounds = WIDGET.bounds();
402                    (bounds.outer_size(), bounds.final_baseline())
403                });
404                let child_offset = (items_size - size).to_vector() * child_align;
405                o.child_offset += children_offset + child_offset;
406
407                if align_baseline {
408                    o.child_offset.y += baseline;
409                }
410            }
411            None => {
412                o.child_offset += children_offset;
414            }
415        }
416    });
417
418    children.commit_data().request_render();
419
420    panel_size
421}
422
423fn layout_spacing(ctx: &LayoutMetrics, direction: &StackDirection, spacing: Length) -> PxVector {
425    let factor = direction.direction_factor(ctx.direction());
426    spacing_from_direction(factor, spacing)
427}
428fn spacing_from_direction(factor: Factor2d, spacing: Length) -> PxVector {
429    PxVector::new(spacing.layout_x(), spacing.layout_y()) * factor
430}
431
432fn child_max_size(wm: &mut WidgetMeasure, children: &mut PanelList, child_align: Align) -> PxSize {
434    let constraints = LAYOUT.constraints();
435
436    let mut need_measure = false;
438    let mut max_size = PxSize::zero();
439    let mut measure_constraints = constraints;
440    match (constraints.x.fill_or_exact(), constraints.y.fill_or_exact()) {
441        (None, None) => {
442            need_measure = child_align.is_fill_x() || child_align.is_fill_y();
443            if !need_measure {
444                max_size = constraints.max_size().unwrap_or_else(|| PxSize::new(Px::MAX, Px::MAX));
445            }
446        }
447        (None, Some(h)) => {
448            max_size.height = h;
449            need_measure = child_align.is_fill_x();
450
451            if need_measure {
452                measure_constraints = constraints.with_fill_x(false);
453            } else {
454                max_size.width = Px::MAX;
455            }
456        }
457        (Some(w), None) => {
458            max_size.width = w;
459            need_measure = child_align.is_fill_y();
460
461            if need_measure {
462                measure_constraints = constraints.with_fill_y(false);
463            } else {
464                max_size.height = Px::MAX;
465            }
466        }
467        (Some(w), Some(h)) => max_size = PxSize::new(w, h),
468    }
469
470    if need_measure {
472        let max_items = LAYOUT.with_constraints(measure_constraints.with_new_min(Px(0), Px(0)), || {
473            children.measure_list(wm, |_, c, _, wm| c.measure(wm), PxSize::max)
474        });
475
476        max_size = constraints.clamp_size(max_size.max(max_items));
477    }
478
479    max_size
480}
481
482pub fn stack_nodes(
491    nodes: impl IntoUiNode,
492    index: impl IntoVar<usize>,
493    constraints: impl Fn(PxConstraints2d, usize, PxSize) -> PxConstraints2d + Send + 'static,
494) -> UiNode {
495    stack_nodes_impl(nodes.into_node(), index, constraints)
496}
497
498fn stack_nodes_impl(
499    nodes: UiNode,
500    index: impl IntoVar<usize>,
501    constraints: impl Fn(PxConstraints2d, usize, PxSize) -> PxConstraints2d + Send + 'static,
502) -> UiNode {
503    let nodes = nodes.into_list();
504    let index = index.into_var();
505
506    match_node(nodes, move |c, op| match op {
507        UiNodeOp::Init => {
508            WIDGET.sub_var_layout(&index);
509        }
510        UiNodeOp::Measure { wm, desired_size } => {
511            let index = index.get();
512            let len = c.node().children_len();
513            *desired_size = if index >= len {
514                tracing::error!("index {} out of range for length {} in `{:?}#stack_nodes`", index, len, WIDGET.id());
515
516                c.measure(wm)
517            } else {
518                c.delegated();
519                let index_size = c.node().with_child(index, |n| n.measure(wm));
520                let constraints = constraints(LAYOUT.metrics().constraints(), index, index_size);
521                LAYOUT.with_constraints(constraints, || {
522                    c.measure_list(
523                        wm,
524                        |i, n, wm| {
525                            if i != index { n.measure(wm) } else { index_size }
526                        },
527                        PxSize::max,
528                    )
529                })
530            };
531        }
532        UiNodeOp::Layout { wl, final_size } => {
533            let index = index.get();
534            let len = c.node().children_len();
535            *final_size = if index >= len {
536                tracing::error!(
537                    "index {} out of range for length {} in `{:?}#stack_nodes_layout_by`",
538                    index,
539                    len,
540                    WIDGET.id()
541                );
542
543                c.layout(wl)
544            } else {
545                c.delegated();
546                let index_size = c.node().with_child(index, |n| n.layout(wl));
547                let constraints = constraints(LAYOUT.metrics().constraints(), index, index_size);
548                LAYOUT.with_constraints(constraints, || {
549                    c.layout_list(
550                        wl,
551                        |i, n, wl| {
552                            if i != index { n.layout(wl) } else { index_size }
553                        },
554                        PxSize::max,
555                    )
556                })
557            };
558        }
559        _ => {}
560    })
561}
562
563static_id! {
564    static ref PANEL_LIST_ID: StateId<zng_app::widget::node::PanelListRange>;
565}
566
567#[property(CONTEXT)]
571pub fn get_index(child: impl IntoUiNode, state: impl IntoVar<usize>) -> UiNode {
572    let state = state.into_var();
573    zng_wgt::node::with_index_node(child, *PANEL_LIST_ID, move |id| {
574        state.set(id.unwrap_or(0));
575    })
576}
577
578#[property(CONTEXT)]
580pub fn get_index_len(child: impl IntoUiNode, state: impl IntoVar<(usize, usize)>) -> UiNode {
581    let state = state.into_var();
582    zng_wgt::node::with_index_len_node(child, *PANEL_LIST_ID, move |id_len| {
583        state.set(id_len.unwrap_or((0, 0)));
584    })
585}
586
587#[property(CONTEXT)]
589pub fn get_rev_index(child: impl IntoUiNode, state: impl IntoVar<usize>) -> UiNode {
590    let state = state.into_var();
591    zng_wgt::node::with_rev_index_node(child, *PANEL_LIST_ID, move |id| {
592        state.set(id.unwrap_or(0));
593    })
594}
595
596#[property(CONTEXT)]
602pub fn is_even(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
603    let state = state.into_var();
604    zng_wgt::node::with_index_node(child, *PANEL_LIST_ID, move |id| {
605        state.set(id.map(|i| i % 2 == 0).unwrap_or(false));
606    })
607}
608
609#[property(CONTEXT)]
615pub fn is_odd(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
616    let state = state.into_var();
617    zng_wgt::node::with_index_node(child, *PANEL_LIST_ID, move |id| {
618        state.set(id.map(|i| i % 2 != 0).unwrap_or(false));
619    })
620}
621
622#[property(CONTEXT)]
624pub fn is_first(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
625    let state = state.into_var();
626    zng_wgt::node::with_index_node(child, *PANEL_LIST_ID, move |id| {
627        state.set(id == Some(0));
628    })
629}
630
631#[property(CONTEXT)]
633pub fn is_last(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
634    let state = state.into_var();
635    zng_wgt::node::with_rev_index_node(child, *PANEL_LIST_ID, move |id| {
636        state.set(id == Some(0));
637    })
638}
639
640pub trait WidgetInfoStackExt {
645    fn stack_children(&self) -> Option<zng_app::widget::info::iter::Children>;
649}
650impl WidgetInfoStackExt for zng_app::widget::info::WidgetInfo {
651    fn stack_children(&self) -> Option<zng_app::widget::info::iter::Children> {
652        zng_app::widget::node::PanelListRange::get(self, *PANEL_LIST_ID)
653    }
654}