use teksilo_canvas::{Canvas, Point, Rect, Size, SizeProposal};
use teksilo_core::WidgetId;
use teksilo_core::accessibility::AccessNodeBuilder;
use teksilo_core::widget::{LayoutContext, PaintContext, PendingChild, Widget, WidgetPlacement};
use teksilo_tokens::Alignment;
#[derive(Debug)]
pub struct ZStack {
child_ids: Vec<WidgetId>,
pending: Vec<PendingChild>,
alignment: Alignment,
}
impl ZStack {
pub fn new() -> Self {
Self {
child_ids: Vec::new(),
pending: Vec::new(),
alignment: Alignment::CENTER,
}
}
pub fn alignment(mut self, alignment: Alignment) -> Self {
self.alignment = alignment;
self
}
pub fn add_child(mut self, id: WidgetId) -> Self {
self.pending.push(PendingChild::Id(id));
self
}
pub fn child(mut self, widget: impl Widget + 'static) -> Self {
self.pending.push(PendingChild::Deferred(Box::new(widget)));
self
}
pub fn children(mut self, iter: impl IntoIterator<Item = impl Widget + 'static>) -> Self {
for widget in iter {
self.pending.push(PendingChild::Deferred(Box::new(widget)));
}
self
}
pub fn child_opt(mut self, widget: Option<impl Widget + 'static>) -> Self {
if let Some(w) = widget {
self.pending.push(PendingChild::Deferred(Box::new(w)));
}
self
}
}
impl Default for ZStack {
fn default() -> Self {
Self::new()
}
}
impl Widget for ZStack {
fn layout_response(
&self,
proposal: SizeProposal,
ctx: &LayoutContext,
) -> teksilo_core::widget::LayoutResponse {
let bounded_for_height = SizeProposal {
width: proposal.width,
height: None,
};
let query_twice = proposal.width.is_some();
let mut max_w: f32 = 0.0;
let mut max_h: f32 = 0.0;
let mut min_w: f32 = 0.0;
let mut min_h: f32 = 0.0;
let mut any_shrink = false;
let mut any_queried = false;
for &child_id in &self.child_ids {
if let Some(r) = ctx.child_layout_response(child_id, SizeProposal::unspecified()) {
max_w = max_w.max(r.size.width);
max_h = max_h.max(r.size.height);
min_w = min_w.max(r.min.width);
min_h = min_h.max(r.min.height);
if r.shrink > 0.0 {
any_shrink = true;
}
any_queried = true;
}
if query_twice && let Some(r) = ctx.child_layout_response(child_id, bounded_for_height)
{
max_h = max_h.max(r.size.height);
any_queried = true;
}
}
if any_queried {
let size = Size::new(max_w, max_h);
let min = Size::new(min_w.min(max_w), min_h.min(max_h));
teksilo_core::widget::LayoutResponse::rigid(size)
.with_shrink(if any_shrink { 1.0 } else { 0.0 })
.with_min(min)
} else {
proposal.resolve(0.0, 0.0).into()
}
}
fn place_children(
&self,
bounds: Rect,
_proposal: SizeProposal,
children: &mut [WidgetPlacement],
ctx: &LayoutContext,
) {
let rtl = ctx.is_rtl();
let exact_proposal = SizeProposal::exact(bounds.width, bounds.height);
for child in children.iter_mut() {
let child_size = ctx
.child_size(child.id, exact_proposal)
.unwrap_or(bounds.size());
let align = ctx.child_alignment(child.id).unwrap_or(self.alignment);
let (dx, dy) = align.resolve(
(child_size.width, child_size.height),
(bounds.width, bounds.height),
rtl,
);
child.origin = Point::new(bounds.x + dx, bounds.y + dy);
child.size = child_size;
}
}
fn paint(&self, _bounds: Rect, _canvas: &mut Canvas, _ctx: &PaintContext) {}
fn accessibility(&self, builder: &mut AccessNodeBuilder) {
builder.set_role(teksilo_core::accesskit::Role::GenericContainer);
}
fn children(&self) -> Vec<WidgetId> {
self.child_ids.clone()
}
fn build(&mut self, ctx: &mut teksilo_core::build_context::BuildContext) -> Vec<WidgetId> {
let pending = std::mem::take(&mut self.pending);
if !pending.is_empty() {
self.child_ids = pending
.into_iter()
.map(|child| match child {
PendingChild::Id(id) => id,
PendingChild::Deferred(w) => ctx.add_boxed(w),
})
.collect();
}
self.child_ids.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
use teksilo_core::widget_tree::WidgetTree;
#[derive(Debug)]
struct FixedLeaf(f32, f32);
impl Widget for FixedLeaf {
fn layout_response(
&self,
_proposal: SizeProposal,
_ctx: &LayoutContext,
) -> teksilo_core::widget::LayoutResponse {
Size::new(self.0, self.1).into()
}
}
#[derive(Debug)]
struct WrappingLeaf {
natural_width: f32,
line_height: f32,
}
impl Widget for WrappingLeaf {
fn layout_response(
&self,
proposal: SizeProposal,
_ctx: &LayoutContext,
) -> teksilo_core::widget::LayoutResponse {
match proposal.width {
Some(w) if w > 0.0 => {
let lines = (self.natural_width / w).ceil().max(1.0);
Size::new(w, lines * self.line_height).into()
}
_ => Size::new(self.natural_width, self.line_height).into(),
}
}
}
#[derive(Debug)]
struct GreedyLeaf;
impl Widget for GreedyLeaf {
fn layout_response(
&self,
proposal: SizeProposal,
_ctx: &LayoutContext,
) -> teksilo_core::widget::LayoutResponse {
Size::new(
proposal.width.unwrap_or(0.0),
proposal.height.unwrap_or(0.0),
)
.into()
}
}
#[test]
fn a_wrapping_child_gets_its_real_height_when_the_width_is_bound() {
let mut tree = WidgetTree::new();
let bg = tree.add(GreedyLeaf);
let text = tree.add(WrappingLeaf {
natural_width: 400.0,
line_height: 16.0,
});
let stack = tree.add(ZStack::new().add_child(bg).add_child(text));
tree.layout(SizeProposal {
width: Some(100.0),
height: None,
});
let b = tree.bounds(stack);
assert!(
(b.height - 64.0).abs() < 0.01,
"400px of content at 100px wide is four 16px lines; got {} \
(16 means the child was only ever measured unbounded)",
b.height
);
}
#[test]
fn a_greedy_background_still_does_not_inflate_the_stack() {
let mut tree = WidgetTree::new();
let bg = tree.add(GreedyLeaf);
let content = tree.add(FixedLeaf(40.0, 20.0));
let stack = tree.add(ZStack::new().add_child(bg).add_child(content));
tree.layout(SizeProposal {
width: Some(300.0),
height: None,
});
let b = tree.bounds(stack);
assert!(
(b.height - 20.0).abs() < 0.01,
"the background must not set the height; got {}",
b.height
);
}
#[test]
fn default_centers_children() {
let mut tree = WidgetTree::new();
let a = tree.add(FixedLeaf(40.0, 20.0));
let _stack = tree.add(ZStack::new().add_child(a));
tree.layout(SizeProposal::exact(100.0, 60.0));
let b = tree.bounds(a);
assert!((b.x - 30.0).abs() < 0.01); assert!((b.y - 20.0).abs() < 0.01); }
#[test]
fn alignment_top_leading() {
let mut tree = WidgetTree::new();
let bg = tree.add(FixedLeaf(100.0, 60.0)); let fg = tree.add(FixedLeaf(40.0, 20.0));
let _stack = tree.add(
ZStack::new()
.alignment(Alignment::TOP_LEADING)
.add_child(bg)
.add_child(fg),
);
tree.layout(SizeProposal::exact(200.0, 200.0));
let b = tree.bounds(fg);
assert!((b.x - 0.0).abs() < 0.01);
assert!((b.y - 0.0).abs() < 0.01);
}
#[test]
fn alignment_bottom_trailing() {
let mut tree = WidgetTree::new();
let bg = tree.add(FixedLeaf(100.0, 60.0));
let fg = tree.add(FixedLeaf(40.0, 20.0));
let _stack = tree.add(
ZStack::new()
.alignment(Alignment::BOTTOM_TRAILING)
.add_child(bg)
.add_child(fg),
);
tree.layout(SizeProposal::exact(200.0, 200.0));
let b = tree.bounds(fg);
assert!((b.x - 160.0).abs() < 0.01); assert!((b.y - 180.0).abs() < 0.01); }
#[test]
fn alignment_center() {
let mut tree = WidgetTree::new();
let bg = tree.add(FixedLeaf(100.0, 60.0));
let fg = tree.add(FixedLeaf(40.0, 20.0));
let _stack = tree.add(ZStack::new().add_child(bg).add_child(fg)); tree.layout(SizeProposal::exact(200.0, 200.0));
let b = tree.bounds(fg);
assert!((b.x - 80.0).abs() < 0.01); assert!((b.y - 90.0).abs() < 0.01); }
#[test]
fn per_child_alignment_override() {
let mut tree = WidgetTree::new();
let bg = tree.add(FixedLeaf(100.0, 60.0));
let a = tree.add(FixedLeaf(40.0, 20.0));
let b = tree.add(FixedLeaf(30.0, 15.0));
let _stack = tree.add(
ZStack::new()
.alignment(Alignment::TOP_LEADING)
.add_child(bg)
.add_child(a)
.add_child(b),
);
tree.set_alignment(b, Alignment::BOTTOM_TRAILING);
tree.layout(SizeProposal::exact(200.0, 200.0));
let ab = tree.bounds(a);
assert!((ab.x - 0.0).abs() < 0.01); assert!((ab.y - 0.0).abs() < 0.01);
let bb = tree.bounds(b);
assert!((bb.x - 170.0).abs() < 0.01); assert!((bb.y - 185.0).abs() < 0.01); }
#[test]
fn size_is_max_of_children() {
let mut tree = WidgetTree::new();
let a = tree.add(FixedLeaf(40.0, 60.0));
let b = tree.add(FixedLeaf(80.0, 30.0));
let stack = tree.add(ZStack::new().add_child(a).add_child(b));
tree.layout(SizeProposal {
width: None,
height: None,
});
let sb = tree.bounds(stack);
assert!((sb.width - 80.0).abs() < 0.01);
assert!((sb.height - 60.0).abs() < 0.01);
}
}