#![doc(html_favicon_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo-icon.png")]
#![doc(html_logo_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo.png")]
#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
zng_wgt::enable_widget_macros!();
use zng_wgt::prelude::*;
mod types;
pub use types::*;
#[widget($crate::Stack {
($children:expr) => {
children = $children;
};
($direction:ident, $children:expr) => {
direction = $crate::StackDirection::$direction();
children = $children;
};
($direction:ident, $spacing:expr, $children:expr) => {
direction = $crate::StackDirection::$direction();
spacing = $spacing;
children = $children;
};
($direction:expr, $children:expr) => {
direction = $direction;
children = $children;
};
($direction:expr, $spacing:expr, $children:expr) => {
direction = $direction;
spacing = $spacing;
children = $children;
};
})]
pub struct Stack(WidgetBase);
impl Stack {
fn widget_intrinsic(&mut self) {
self.widget_builder().push_build_action(|wgt| {
let child = node(
wgt.capture_ui_node_list_or_empty(property_id!(Self::children)),
wgt.capture_var_or_default(property_id!(Self::direction)),
wgt.capture_var_or_default(property_id!(Self::spacing)),
wgt.capture_var_or_else(property_id!(Self::children_align), || Align::FILL),
);
wgt.set_child(child);
});
}
}
#[property(CHILD, capture, default(ui_vec![]), widget_impl(Stack))]
pub fn children(children: impl UiNodeList) {}
#[property(LAYOUT, capture, widget_impl(Stack))]
pub fn direction(direction: impl IntoVar<StackDirection>) {}
#[property(LAYOUT, capture, widget_impl(Stack))]
pub fn spacing(spacing: impl IntoVar<Length>) {}
#[property(LAYOUT, capture, default(Align::FILL), widget_impl(Stack))]
pub fn children_align(align: impl IntoVar<Align>) {}
pub fn node(
children: impl UiNodeList,
direction: impl IntoVar<StackDirection>,
spacing: impl IntoVar<Length>,
children_align: impl IntoVar<Align>,
) -> impl UiNode {
let children = PanelList::new(children).track_info_range(*PANEL_LIST_ID);
let direction = direction.into_var();
let spacing = spacing.into_var();
let children_align = children_align.into_var();
match_node_list(children, move |c, op| match op {
UiNodeOp::Init => {
WIDGET
.sub_var_layout(&direction)
.sub_var_layout(&spacing)
.sub_var_layout(&children_align);
}
UiNodeOp::Update { updates } => {
let mut changed = false;
c.update_all(updates, &mut changed);
if changed {
WIDGET.layout();
}
}
UiNodeOp::Measure { wm, desired_size } => {
c.delegated();
*desired_size = measure(wm, c.children(), direction.get(), spacing.get(), children_align.get());
}
UiNodeOp::Layout { wl, final_size } => {
c.delegated();
*final_size = layout(wl, c.children(), direction.get(), spacing.get(), children_align.get());
}
_ => {}
})
}
pub fn lazy_size(
children_len: impl IntoVar<usize>,
direction: impl IntoVar<StackDirection>,
spacing: impl IntoVar<Length>,
child_size: impl IntoVar<Size>,
) -> impl UiNode {
lazy_sample(children_len, direction, spacing, zng_wgt_size_offset::size(NilUiNode, child_size))
}
pub fn lazy_sample(
children_len: impl IntoVar<usize>,
direction: impl IntoVar<StackDirection>,
spacing: impl IntoVar<Length>,
child_sample: impl UiNode,
) -> impl UiNode {
let children_len = children_len.into_var();
let direction = direction.into_var();
let spacing = spacing.into_var();
match_node(child_sample, move |child, op| match op {
UiNodeOp::Init => {
WIDGET
.sub_var_layout(&children_len)
.sub_var_layout(&direction)
.sub_var_layout(&spacing);
}
op @ UiNodeOp::Measure { .. } | op @ UiNodeOp::Layout { .. } => {
let mut measure = |wm| {
let constraints = LAYOUT.constraints();
if let Some(known) = constraints.fill_or_exact() {
child.delegated();
return known;
}
let len = Px(children_len.get() as i32);
if len.0 == 0 {
child.delegated();
return PxSize::zero();
}
let child_size = child.measure(wm);
let direction = direction.get();
let dv = direction.direction_factor(LayoutDirection::LTR);
let ds = if dv.x == 0.fct() && dv.y != 0.fct() {
let spacing = spacing.layout_y();
PxSize::new(child_size.width, (len - Px(1)) * (child_size.height + spacing) + child_size.height)
} else if dv.x != 0.fct() && dv.y == 0.fct() {
let spacing = spacing.layout_x();
PxSize::new((len - Px(1)) * (child_size.width + spacing) + child_size.width, child_size.height)
} else {
let spacing = spacing_from_direction(dv, spacing.get());
let mut item_rect = PxRect::from_size(child_size);
let mut item_bounds = euclid::Box2D::zero();
let mut child_spacing = PxVector::zero();
for _ in 0..len.0 {
let offset = direction.layout(item_rect, child_size) + child_spacing;
item_rect.origin = offset.to_point();
let item_box = item_rect.to_box2d();
item_bounds.min = item_bounds.min.min(item_box.min);
item_bounds.max = item_bounds.max.max(item_box.max);
child_spacing = spacing;
}
item_bounds.size()
};
constraints.fill_size_or(ds)
};
match op {
UiNodeOp::Measure { wm, desired_size } => {
*desired_size = measure(wm);
}
UiNodeOp::Layout { wl, final_size } => {
*final_size = measure(&mut wl.to_measure(None));
}
_ => unreachable!(),
}
}
_ => {}
})
}
fn measure(wm: &mut WidgetMeasure, children: &mut PanelList, direction: StackDirection, spacing: Length, children_align: Align) -> PxSize {
let metrics = LAYOUT.metrics();
let constraints = metrics.constraints();
if let Some(known) = constraints.fill_or_exact() {
return known;
}
let child_align = children_align * direction.direction_scale();
let spacing = layout_spacing(&metrics, &direction, spacing);
let max_size = child_max_size(wm, children, child_align);
let mut item_bounds = euclid::Box2D::zero();
LAYOUT.with_constraints(
constraints
.with_fill(child_align.is_fill_x(), child_align.is_fill_y())
.with_max_size(max_size)
.with_new_min(Px(0), Px(0)),
|| {
children.measure_each(
wm,
|_, c, _, wm| {
if c.is_widget() {
c.measure(wm)
} else {
PxSize::zero()
}
},
|_, _| PxSize::zero(),
);
let mut item_rect = PxRect::zero();
let mut child_spacing = PxVector::zero();
children.for_each(|_, c, _| {
let size = match c.with_context(WidgetUpdateMode::Ignore, || WIDGET.bounds().measure_outer_size()) {
Some(wgt_size) => wgt_size,
None => c.measure(wm),
};
if size.is_empty() {
return; }
let offset = direction.layout(item_rect, size) + child_spacing;
item_rect.origin = offset.to_point();
item_rect.size = size;
let item_box = item_rect.to_box2d();
item_bounds.min = item_bounds.min.min(item_box.min);
item_bounds.max = item_bounds.max.max(item_box.max);
child_spacing = spacing;
});
},
);
constraints.fill_size_or(item_bounds.size())
}
fn layout(wl: &mut WidgetLayout, children: &mut PanelList, direction: StackDirection, spacing: Length, children_align: Align) -> PxSize {
let metrics = LAYOUT.metrics();
let constraints = metrics.constraints();
let child_align = children_align * direction.direction_scale();
let spacing = layout_spacing(&metrics, &direction, spacing);
let max_size = child_max_size(&mut wl.to_measure(None), children, child_align);
let mut item_bounds = euclid::Box2D::zero();
LAYOUT.with_constraints(
constraints
.with_fill(child_align.is_fill_x(), child_align.is_fill_y())
.with_max_size(max_size)
.with_new_min(Px(0), Px(0)),
|| {
children.layout_each(
wl,
|_, c, o, wl| {
if c.is_widget() {
let (size, define_ref_frame) = wl.with_child(|wl| c.layout(wl));
debug_assert!(!define_ref_frame); o.define_reference_frame = define_ref_frame;
size
} else {
PxSize::zero()
}
},
|_, _| PxSize::zero(),
);
let mut item_rect = PxRect::zero();
let mut child_spacing = PxVector::zero();
children.for_each(|_, c, o| {
let size = match c.with_context(WidgetUpdateMode::Ignore, || WIDGET.bounds().outer_size()) {
Some(wgt_size) => wgt_size,
None => {
let (size, define_ref_frame) = wl.with_child(|wl| c.layout(wl));
o.define_reference_frame = define_ref_frame;
size
}
};
if size.is_empty() {
o.child_offset = PxVector::zero();
o.define_reference_frame = false;
return; }
let offset = direction.layout(item_rect, size) + child_spacing;
o.child_offset = offset;
item_rect.origin = offset.to_point();
item_rect.size = size;
let item_box = item_rect.to_box2d();
item_bounds.min = item_bounds.min.min(item_box.min);
item_bounds.max = item_bounds.max.max(item_box.max);
child_spacing = spacing;
});
},
);
let items_size = item_bounds.size();
let panel_size = constraints.fill_size_or(items_size);
let children_offset = -item_bounds.min.to_vector() + (panel_size - items_size).to_vector() * children_align.xy(LAYOUT.direction());
let align_baseline = children_align.is_baseline();
let child_align = child_align.xy(LAYOUT.direction());
children.for_each(|_, c, o| {
if let Some((size, baseline)) = c.with_context(WidgetUpdateMode::Ignore, || {
let bounds = WIDGET.bounds();
(bounds.outer_size(), bounds.final_baseline())
}) {
let child_offset = (items_size - size).to_vector() * child_align;
o.child_offset += children_offset + child_offset;
if align_baseline {
o.child_offset.y += baseline;
}
} else {
o.child_offset += children_offset;
}
});
children.commit_data().request_render();
panel_size
}
fn layout_spacing(ctx: &LayoutMetrics, direction: &StackDirection, spacing: Length) -> PxVector {
let factor = direction.direction_factor(ctx.direction());
spacing_from_direction(factor, spacing)
}
fn spacing_from_direction(factor: Factor2d, spacing: Length) -> PxVector {
PxVector::new(spacing.layout_x(), spacing.layout_y()) * factor
}
fn child_max_size(wm: &mut WidgetMeasure, children: &mut PanelList, child_align: Align) -> PxSize {
let constraints = LAYOUT.constraints();
let mut need_measure = false;
let mut max_size = PxSize::zero();
let mut measure_constraints = constraints;
match (constraints.x.fill_or_exact(), constraints.y.fill_or_exact()) {
(None, None) => {
need_measure = child_align.is_fill_x() || child_align.is_fill_y();
if !need_measure {
max_size = constraints.max_size().unwrap_or_else(|| PxSize::new(Px::MAX, Px::MAX));
}
}
(None, Some(h)) => {
max_size.height = h;
need_measure = child_align.is_fill_x();
if need_measure {
measure_constraints = constraints.with_fill_x(false);
} else {
max_size.width = Px::MAX;
}
}
(Some(w), None) => {
max_size.width = w;
need_measure = child_align.is_fill_y();
if need_measure {
measure_constraints = constraints.with_fill_y(false);
} else {
max_size.height = Px::MAX;
}
}
(Some(w), Some(h)) => max_size = PxSize::new(w, h),
}
if need_measure {
let max_items = LAYOUT.with_constraints(measure_constraints.with_new_min(Px(0), Px(0)), || {
children.measure_each(wm, |_, c, _, wm| c.measure(wm), PxSize::max)
});
max_size = constraints.clamp_size(max_size.max(max_items));
}
max_size
}
pub fn stack_nodes(nodes: impl UiNodeList) -> impl UiNode {
match_node_list(nodes, |_, _| {})
}
pub fn stack_nodes_layout_by(
nodes: impl UiNodeList,
index: impl IntoVar<usize>,
constraints: impl Fn(PxConstraints2d, usize, PxSize) -> PxConstraints2d + Send + 'static,
) -> impl UiNode {
#[cfg(feature = "dyn_closure")]
let constraints: Box<dyn Fn(PxConstraints2d, usize, PxSize) -> PxConstraints2d + Send> = Box::new(constraints);
stack_nodes_layout_by_impl(nodes, index, constraints)
}
fn stack_nodes_layout_by_impl(
nodes: impl UiNodeList,
index: impl IntoVar<usize>,
constraints: impl Fn(PxConstraints2d, usize, PxSize) -> PxConstraints2d + Send + 'static,
) -> impl UiNode {
let index = index.into_var();
match_node_list(nodes, move |children, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_layout(&index);
}
UiNodeOp::Measure { wm, desired_size } => {
let index = index.get();
let len = children.len();
*desired_size = if index >= len {
tracing::error!(
"index {} out of range for length {} in `{:?}#stack_nodes_layout_by`",
index,
len,
WIDGET.id()
);
children.measure_each(wm, |_, n, wm| n.measure(wm), PxSize::max)
} else {
let index_size = children.with_node(index, |n| n.measure(wm));
let constraints = constraints(LAYOUT.metrics().constraints(), index, index_size);
LAYOUT.with_constraints(constraints, || {
children.measure_each(
wm,
|i, n, wm| {
if i != index {
n.measure(wm)
} else {
index_size
}
},
PxSize::max,
)
})
};
}
UiNodeOp::Layout { wl, final_size } => {
let index = index.get();
let len = children.len();
*final_size = if index >= len {
tracing::error!(
"index {} out of range for length {} in `{:?}#stack_nodes_layout_by`",
index,
len,
WIDGET.id()
);
children.layout_each(wl, |_, n, wl| n.layout(wl), PxSize::max)
} else {
let index_size = children.with_node(index, |n| n.layout(wl));
let constraints = constraints(LAYOUT.metrics().constraints(), index, index_size);
LAYOUT.with_constraints(constraints, || {
children.layout_each(
wl,
|i, n, wl| {
if i != index {
n.layout(wl)
} else {
index_size
}
},
PxSize::max,
)
})
};
}
_ => {}
})
}
static_id! {
static ref PANEL_LIST_ID: StateId<zng_app::widget::node::PanelListRange>;
}
#[property(CONTEXT)]
pub fn get_index(child: impl UiNode, state: impl IntoVar<usize>) -> impl UiNode {
let state = state.into_var();
zng_wgt::node::with_index_node(child, *PANEL_LIST_ID, move |id| {
let _ = state.set(id.unwrap_or(0));
})
}
#[property(CONTEXT)]
pub fn get_index_len(child: impl UiNode, state: impl IntoVar<(usize, usize)>) -> impl UiNode {
let state = state.into_var();
zng_wgt::node::with_index_len_node(child, *PANEL_LIST_ID, move |id_len| {
let _ = state.set(id_len.unwrap_or((0, 0)));
})
}
#[property(CONTEXT)]
pub fn get_rev_index(child: impl UiNode, state: impl IntoVar<usize>) -> impl UiNode {
let state = state.into_var();
zng_wgt::node::with_rev_index_node(child, *PANEL_LIST_ID, move |id| {
let _ = state.set(id.unwrap_or(0));
})
}
#[property(CONTEXT)]
pub fn is_even(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
let state = state.into_var();
zng_wgt::node::with_index_node(child, *PANEL_LIST_ID, move |id| {
let _ = state.set(id.map(|i| i % 2 == 0).unwrap_or(false));
})
}
#[property(CONTEXT)]
pub fn is_odd(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
let state = state.into_var();
zng_wgt::node::with_index_node(child, *PANEL_LIST_ID, move |id| {
let _ = state.set(id.map(|i| i % 2 != 0).unwrap_or(false));
})
}
#[property(CONTEXT)]
pub fn is_first(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
let state = state.into_var();
zng_wgt::node::with_index_node(child, *PANEL_LIST_ID, move |id| {
let _ = state.set(id == Some(0));
})
}
#[property(CONTEXT)]
pub fn is_last(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
let state = state.into_var();
zng_wgt::node::with_rev_index_node(child, *PANEL_LIST_ID, move |id| {
let _ = state.set(id == Some(0));
})
}
pub trait WidgetInfoStackExt {
fn stack_children(&self) -> Option<zng_app::widget::info::iter::Children>;
}
impl WidgetInfoStackExt for zng_app::widget::info::WidgetInfo {
fn stack_children(&self) -> Option<zng_app::widget::info::iter::Children> {
zng_app::widget::node::PanelListRange::get(self, *PANEL_LIST_ID)
}
}