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_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#[property(CHILD, capture, default(ui_vec![]), widget_impl(Stack))]
90pub fn children(children: impl UiNodeList) {}
91
92#[property(LAYOUT, capture, widget_impl(Stack))]
94pub fn direction(direction: impl IntoVar<StackDirection>) {}
95
96#[property(LAYOUT, capture, widget_impl(Stack))]
104pub fn spacing(spacing: impl IntoVar<Length>) {}
105
106#[property(LAYOUT, capture, default(Align::FILL), widget_impl(Stack))]
115pub fn children_align(align: impl IntoVar<Align>) {}
116
117pub 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
159pub 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
171pub 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 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 let spacing = spacing.layout_x();
216 PxSize::new((len - Px(1)) * (child_size.width + spacing) + child_size.width, child_size.height)
217 } else {
218 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 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 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 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; }
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 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 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); o.define_reference_frame = define_ref_frame;
333 size
334 } else {
335 PxSize::zero()
336 }
337 },
338 |_, _| PxSize::zero(),
339 );
340
341 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; }
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 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 o.child_offset += children_offset;
395 }
396 });
397
398 children.commit_data().request_render();
399
400 panel_size
401}
402
403fn 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
412fn child_max_size(wm: &mut WidgetMeasure, children: &mut PanelList, child_align: Align) -> PxSize {
414 let constraints = LAYOUT.constraints();
415
416 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 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
462pub fn stack_nodes(nodes: impl UiNodeList) -> impl UiNode {
473 match_node_list(nodes, |_, _| {})
474}
475
476pub 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#[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#[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#[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#[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#[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#[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#[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
638pub trait WidgetInfoStackExt {
643 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}