teksilo_widgets/primitives/
zstack.rs1use teksilo_canvas::{Canvas, Point, Rect, Size, SizeProposal};
36
37use teksilo_core::WidgetId;
38use teksilo_core::accessibility::AccessNodeBuilder;
39use teksilo_core::widget::{LayoutContext, PaintContext, PendingChild, Widget, WidgetPlacement};
40use teksilo_tokens::Alignment;
41
42#[derive(Debug)]
46pub struct ZStack {
47 child_ids: Vec<WidgetId>,
48 pending: Vec<PendingChild>,
49 alignment: Alignment,
50}
51
52impl ZStack {
53 pub fn new() -> Self {
55 Self {
56 child_ids: Vec::new(),
57 pending: Vec::new(),
58 alignment: Alignment::CENTER,
59 }
60 }
61
62 pub fn alignment(mut self, alignment: Alignment) -> Self {
65 self.alignment = alignment;
66 self
67 }
68
69 pub fn add_child(mut self, id: WidgetId) -> Self {
71 self.pending.push(PendingChild::Id(id));
72 self
73 }
74
75 pub fn child(mut self, widget: impl Widget + 'static) -> Self {
77 self.pending.push(PendingChild::Deferred(Box::new(widget)));
78 self
79 }
80
81 pub fn children(mut self, iter: impl IntoIterator<Item = impl Widget + 'static>) -> Self {
83 for widget in iter {
84 self.pending.push(PendingChild::Deferred(Box::new(widget)));
85 }
86 self
87 }
88
89 pub fn child_opt(mut self, widget: Option<impl Widget + 'static>) -> Self {
91 if let Some(w) = widget {
92 self.pending.push(PendingChild::Deferred(Box::new(w)));
93 }
94 self
95 }
96}
97
98impl Default for ZStack {
99 fn default() -> Self {
100 Self::new()
101 }
102}
103
104impl Widget for ZStack {
105 fn layout_response(
106 &self,
107 proposal: SizeProposal,
108 ctx: &LayoutContext,
109 ) -> teksilo_core::widget::LayoutResponse {
110 let bounded_for_height = SizeProposal {
136 width: proposal.width,
137 height: None,
138 };
139 let query_twice = proposal.width.is_some();
140
141 let mut max_w: f32 = 0.0;
142 let mut max_h: f32 = 0.0;
143 let mut min_w: f32 = 0.0;
144 let mut min_h: f32 = 0.0;
145 let mut any_shrink = false;
146 let mut any_queried = false;
147 for &child_id in &self.child_ids {
148 if let Some(r) = ctx.child_layout_response(child_id, SizeProposal::unspecified()) {
149 max_w = max_w.max(r.size.width);
150 max_h = max_h.max(r.size.height);
151 min_w = min_w.max(r.min.width);
152 min_h = min_h.max(r.min.height);
153 if r.shrink > 0.0 {
154 any_shrink = true;
155 }
156 any_queried = true;
157 }
158 if query_twice && let Some(r) = ctx.child_layout_response(child_id, bounded_for_height)
161 {
162 max_h = max_h.max(r.size.height);
163 any_queried = true;
164 }
165 }
166 if any_queried {
167 let size = Size::new(max_w, max_h);
172 let min = Size::new(min_w.min(max_w), min_h.min(max_h));
173 teksilo_core::widget::LayoutResponse::rigid(size)
174 .with_shrink(if any_shrink { 1.0 } else { 0.0 })
175 .with_min(min)
176 } else {
177 proposal.resolve(0.0, 0.0).into()
178 }
179 }
180
181 fn place_children(
182 &self,
183 bounds: Rect,
184 _proposal: SizeProposal,
185 children: &mut [WidgetPlacement],
186 ctx: &LayoutContext,
187 ) {
188 let rtl = ctx.is_rtl();
189 let exact_proposal = SizeProposal::exact(bounds.width, bounds.height);
190 for child in children.iter_mut() {
191 let child_size = ctx
195 .child_size(child.id, exact_proposal)
196 .unwrap_or(bounds.size());
197
198 let align = ctx.child_alignment(child.id).unwrap_or(self.alignment);
200 let (dx, dy) = align.resolve(
201 (child_size.width, child_size.height),
202 (bounds.width, bounds.height),
203 rtl,
204 );
205
206 child.origin = Point::new(bounds.x + dx, bounds.y + dy);
207 child.size = child_size;
208 }
209 }
210
211 fn paint(&self, _bounds: Rect, _canvas: &mut Canvas, _ctx: &PaintContext) {}
212
213 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
214 builder.set_role(teksilo_core::accesskit::Role::GenericContainer);
215 }
216
217 fn children(&self) -> Vec<WidgetId> {
218 self.child_ids.clone()
219 }
220
221 fn build(&mut self, ctx: &mut teksilo_core::build_context::BuildContext) -> Vec<WidgetId> {
222 let pending = std::mem::take(&mut self.pending);
223 if !pending.is_empty() {
224 self.child_ids = pending
225 .into_iter()
226 .map(|child| match child {
227 PendingChild::Id(id) => id,
228 PendingChild::Deferred(w) => ctx.add_boxed(w),
229 })
230 .collect();
231 }
232 self.child_ids.clone()
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use teksilo_core::widget_tree::WidgetTree;
240
241 #[derive(Debug)]
242 struct FixedLeaf(f32, f32);
243 impl Widget for FixedLeaf {
244 fn layout_response(
245 &self,
246 _proposal: SizeProposal,
247 _ctx: &LayoutContext,
248 ) -> teksilo_core::widget::LayoutResponse {
249 Size::new(self.0, self.1).into()
250 }
251 }
252
253 #[derive(Debug)]
256 struct WrappingLeaf {
257 natural_width: f32,
258 line_height: f32,
259 }
260 impl Widget for WrappingLeaf {
261 fn layout_response(
262 &self,
263 proposal: SizeProposal,
264 _ctx: &LayoutContext,
265 ) -> teksilo_core::widget::LayoutResponse {
266 match proposal.width {
267 Some(w) if w > 0.0 => {
268 let lines = (self.natural_width / w).ceil().max(1.0);
269 Size::new(w, lines * self.line_height).into()
270 }
271 _ => Size::new(self.natural_width, self.line_height).into(),
272 }
273 }
274 }
275
276 #[derive(Debug)]
279 struct GreedyLeaf;
280 impl Widget for GreedyLeaf {
281 fn layout_response(
282 &self,
283 proposal: SizeProposal,
284 _ctx: &LayoutContext,
285 ) -> teksilo_core::widget::LayoutResponse {
286 Size::new(
287 proposal.width.unwrap_or(0.0),
288 proposal.height.unwrap_or(0.0),
289 )
290 .into()
291 }
292 }
293
294 #[test]
301 fn a_wrapping_child_gets_its_real_height_when_the_width_is_bound() {
302 let mut tree = WidgetTree::new();
303 let bg = tree.add(GreedyLeaf);
304 let text = tree.add(WrappingLeaf {
305 natural_width: 400.0,
306 line_height: 16.0,
307 });
308 let stack = tree.add(ZStack::new().add_child(bg).add_child(text));
309
310 tree.layout(SizeProposal {
312 width: Some(100.0),
313 height: None,
314 });
315
316 let b = tree.bounds(stack);
317 assert!(
318 (b.height - 64.0).abs() < 0.01,
319 "400px of content at 100px wide is four 16px lines; got {} \
320 (16 means the child was only ever measured unbounded)",
321 b.height
322 );
323 }
324
325 #[test]
328 fn a_greedy_background_still_does_not_inflate_the_stack() {
329 let mut tree = WidgetTree::new();
330 let bg = tree.add(GreedyLeaf);
331 let content = tree.add(FixedLeaf(40.0, 20.0));
332 let stack = tree.add(ZStack::new().add_child(bg).add_child(content));
333
334 tree.layout(SizeProposal {
335 width: Some(300.0),
336 height: None,
337 });
338
339 let b = tree.bounds(stack);
340 assert!(
341 (b.height - 20.0).abs() < 0.01,
342 "the background must not set the height; got {}",
343 b.height
344 );
345 }
346
347 #[test]
348 fn default_centers_children() {
349 let mut tree = WidgetTree::new();
350 let a = tree.add(FixedLeaf(40.0, 20.0));
351 let _stack = tree.add(ZStack::new().add_child(a));
352 tree.layout(SizeProposal::exact(100.0, 60.0));
353
354 let b = tree.bounds(a);
357 assert!((b.x - 30.0).abs() < 0.01); assert!((b.y - 20.0).abs() < 0.01); }
360
361 #[test]
362 fn alignment_top_leading() {
363 let mut tree = WidgetTree::new();
364 let bg = tree.add(FixedLeaf(100.0, 60.0)); let fg = tree.add(FixedLeaf(40.0, 20.0));
366 let _stack = tree.add(
367 ZStack::new()
368 .alignment(Alignment::TOP_LEADING)
369 .add_child(bg)
370 .add_child(fg),
371 );
372 tree.layout(SizeProposal::exact(200.0, 200.0));
373
374 let b = tree.bounds(fg);
375 assert!((b.x - 0.0).abs() < 0.01);
376 assert!((b.y - 0.0).abs() < 0.01);
377 }
378
379 #[test]
380 fn alignment_bottom_trailing() {
381 let mut tree = WidgetTree::new();
382 let bg = tree.add(FixedLeaf(100.0, 60.0));
383 let fg = tree.add(FixedLeaf(40.0, 20.0));
384 let _stack = tree.add(
385 ZStack::new()
386 .alignment(Alignment::BOTTOM_TRAILING)
387 .add_child(bg)
388 .add_child(fg),
389 );
390 tree.layout(SizeProposal::exact(200.0, 200.0));
391
392 let b = tree.bounds(fg);
393 assert!((b.x - 160.0).abs() < 0.01); assert!((b.y - 180.0).abs() < 0.01); }
396
397 #[test]
398 fn alignment_center() {
399 let mut tree = WidgetTree::new();
400 let bg = tree.add(FixedLeaf(100.0, 60.0));
401 let fg = tree.add(FixedLeaf(40.0, 20.0));
402 let _stack = tree.add(ZStack::new().add_child(bg).add_child(fg)); tree.layout(SizeProposal::exact(200.0, 200.0));
404
405 let b = tree.bounds(fg);
406 assert!((b.x - 80.0).abs() < 0.01); assert!((b.y - 90.0).abs() < 0.01); }
409
410 #[test]
411 fn per_child_alignment_override() {
412 let mut tree = WidgetTree::new();
413 let bg = tree.add(FixedLeaf(100.0, 60.0));
414 let a = tree.add(FixedLeaf(40.0, 20.0));
415 let b = tree.add(FixedLeaf(30.0, 15.0));
416 let _stack = tree.add(
417 ZStack::new()
418 .alignment(Alignment::TOP_LEADING)
419 .add_child(bg)
420 .add_child(a)
421 .add_child(b),
422 );
423 tree.set_alignment(b, Alignment::BOTTOM_TRAILING);
425 tree.layout(SizeProposal::exact(200.0, 200.0));
426
427 let ab = tree.bounds(a);
428 assert!((ab.x - 0.0).abs() < 0.01); assert!((ab.y - 0.0).abs() < 0.01);
430
431 let bb = tree.bounds(b);
432 assert!((bb.x - 170.0).abs() < 0.01); assert!((bb.y - 185.0).abs() < 0.01); }
435
436 #[test]
437 fn size_is_max_of_children() {
438 let mut tree = WidgetTree::new();
439 let a = tree.add(FixedLeaf(40.0, 60.0));
440 let b = tree.add(FixedLeaf(80.0, 30.0));
441 let stack = tree.add(ZStack::new().add_child(a).add_child(b));
442 tree.layout(SizeProposal {
443 width: None,
444 height: None,
445 });
446
447 let sb = tree.bounds(stack);
448 assert!((sb.width - 80.0).abs() < 0.01);
449 assert!((sb.height - 60.0).abs() < 0.01);
450 }
451}