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