1#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://zng-ui.github.io/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}