1use frus_core::{AlignmentDirectional, AlignmentGeometry, Rect, Scene};
21use frus_layout::{Dimension, Style};
22
23use crate::interaction::Status;
24use crate::theme::Theme;
25use crate::widget::Widget;
26
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
30pub enum StackFit {
31 #[default]
34 Expand,
35 Loose,
38}
39
40pub struct Stack<Msg = crate::callback::Callback> {
42 width: Dimension,
43 height: Dimension,
44 flex_grow: f32,
45 fit: StackFit,
46 alignment: AlignmentGeometry,
47 layers: Vec<Box<dyn Widget<Msg>>>,
48}
49
50impl<Msg> Stack<Msg> {
51 pub fn new() -> Self {
53 Self {
54 width: Dimension::Auto,
55 height: Dimension::Auto,
56 flex_grow: 0.0,
57 fit: StackFit::Expand,
58 alignment: AlignmentGeometry::Directional(AlignmentDirectional::TOP_START),
62 layers: Vec::new(),
63 }
64 }
65
66 pub fn fit(mut self, fit: StackFit) -> Self {
68 self.fit = fit;
69 self
70 }
71
72 pub fn alignment(mut self, alignment: impl Into<AlignmentGeometry>) -> Self {
79 self.alignment = alignment.into();
80 self
81 }
82
83 pub fn width(mut self, width: f32) -> Self {
85 self.width = Dimension::Length(width);
86 self
87 }
88
89 pub fn height(mut self, height: f32) -> Self {
91 self.height = Dimension::Length(height);
92 self
93 }
94
95 pub fn flex(mut self, grow: f32) -> Self {
97 self.flex_grow = grow;
98 self
99 }
100
101 pub fn layer(mut self, layer: impl Widget<Msg> + 'static) -> Self {
103 self.layers.push(Box::new(layer));
104 self
105 }
106}
107
108impl<Msg> Default for Stack<Msg> {
109 fn default() -> Self {
110 Self::new()
111 }
112}
113
114impl<Msg: Clone> Widget<Msg> for Stack<Msg> {
115 fn style(&self) -> Style {
116 Style {
117 width: self.width,
118 height: self.height,
119 flex_grow: self.flex_grow,
120 ..Default::default()
121 }
122 }
123
124 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
125 &self.layers
126 }
127
128 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
129
130 fn on_click(&self) -> Option<Msg> {
131 None
132 }
133
134 fn stack(&self) -> bool {
135 true
136 }
137
138 fn stack_loose(&self) -> bool {
139 self.fit == StackFit::Loose
140 }
141
142 fn alignment_geometry(&self) -> Option<AlignmentGeometry> {
143 Some(self.alignment)
144 }
145}
146
147pub struct IndexedStack<Msg = crate::callback::Callback> {
172 index: usize,
173 width: Dimension,
174 height: Dimension,
175 flex_grow: f32,
176 fit: StackFit,
177 alignment: AlignmentGeometry,
178 children: Vec<Box<dyn Widget<Msg>>>,
179}
180
181impl<Msg> IndexedStack<Msg> {
182 pub fn new(index: usize) -> Self {
184 Self {
185 index,
186 width: Dimension::Auto,
187 height: Dimension::Auto,
188 flex_grow: 0.0,
189 fit: StackFit::Loose,
194 alignment: AlignmentGeometry::Directional(frus_core::AlignmentDirectional::TOP_START),
195 children: Vec::new(),
196 }
197 }
198
199 pub fn index(mut self, index: usize) -> Self {
201 self.index = index;
202 self
203 }
204
205 pub fn child(mut self, child: impl Widget<Msg> + 'static) -> Self {
207 self.children.push(Box::new(child));
208 self
209 }
210
211 pub fn fit(mut self, fit: StackFit) -> Self {
213 self.fit = fit;
214 self
215 }
216
217 pub fn alignment(mut self, alignment: impl Into<AlignmentGeometry>) -> Self {
219 self.alignment = alignment.into();
220 self
221 }
222
223 pub fn width(mut self, width: f32) -> Self {
225 self.width = Dimension::Length(width);
226 self
227 }
228
229 pub fn height(mut self, height: f32) -> Self {
231 self.height = Dimension::Length(height);
232 self
233 }
234
235 pub fn flex(mut self, grow: f32) -> Self {
237 self.flex_grow = grow;
238 self
239 }
240}
241
242impl<Msg: Clone> Widget<Msg> for IndexedStack<Msg> {
243 fn style(&self) -> Style {
244 Style {
245 width: self.width,
246 height: self.height,
247 flex_grow: self.flex_grow,
248 ..Default::default()
249 }
250 }
251
252 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
253 &self.children
254 }
255
256 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
257
258 fn on_click(&self) -> Option<Msg> {
259 None
260 }
261
262 fn stack(&self) -> bool {
263 true
264 }
265
266 fn stack_loose(&self) -> bool {
267 self.fit == StackFit::Loose
268 }
269
270 fn stack_visible(&self) -> Option<usize> {
271 Some(self.index)
272 }
273
274 fn alignment_geometry(&self) -> Option<AlignmentGeometry> {
275 Some(self.alignment)
276 }
277
278 fn debug_name(&self) -> &'static str {
279 "IndexedStack"
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use crate::{build_ui, Container, Runtime, Size};
287 use frus_core::{Color, Primitive};
288
289 #[test]
290 fn layers_overlap_in_same_box() {
291 let red = Color::rgb(1.0, 0.0, 0.0);
292 let blue = Color::rgb(0.0, 0.0, 1.0);
293 let stack = Stack::<()>::new()
294 .width(100.0)
295 .height(100.0)
296 .layer(Container::<()>::new().width(100.0).height(100.0).color(red))
297 .layer(Container::<()>::new().width(40.0).height(40.0).color(blue));
298 let ui = build_ui(
299 &stack,
300 Size::new(100.0, 100.0),
301 &Runtime::default(),
302 &Theme::default(),
303 );
304
305 let rect_of = |c: Color| {
306 ui.scene()
307 .primitives()
308 .iter()
309 .find_map(|p| match p {
310 Primitive::Rect { color, rect, .. } if *color == c => Some(*rect),
311 _ => None,
312 })
313 .expect("the layer is present")
314 };
315 assert_eq!(rect_of(red).origin(), rect_of(blue).origin());
317 }
318
319 fn rects(stack: Stack<()>, of: Color) -> Vec<Rect> {
321 let ui = build_ui(
322 &stack,
323 Size::new(100.0, 100.0),
324 &Runtime::default(),
325 &Theme::default(),
326 );
327 ui.scene()
328 .primitives()
329 .iter()
330 .filter_map(|p| match p {
331 Primitive::Rect { color, rect, .. } if *color == of => Some(*rect),
332 _ => None,
333 })
334 .collect()
335 }
336
337 const BADGE: Color = Color::rgb(0.0, 0.0, 1.0);
338
339 fn badge() -> Container<()> {
340 Container::<()>::new().width(20.0).height(20.0).color(BADGE)
341 }
342
343 #[test]
345 fn a_pinned_layer_sits_against_the_edges_it_names() {
346 let stack = Stack::<()>::new()
347 .width(100.0)
348 .height(100.0)
349 .layer(crate::Positioned::new(badge()).top(8.0).right(12.0));
350 let r = rects(stack, BADGE)[0];
351 assert_eq!(r.y, 8.0, "8 below the top: {r:?}");
352 assert_eq!(r.x, 100.0 - 12.0 - 20.0, "12 in from the right: {r:?}");
353 assert_eq!((r.width, r.height), (20.0, 20.0), "its own size");
354 }
355
356 #[test]
359 fn opposite_pins_decide_the_extent() {
360 let stack = Stack::<()>::new().width(100.0).height(100.0).layer(
361 crate::Positioned::new(badge())
362 .left(10.0)
363 .right(30.0)
364 .bottom(0.0),
365 );
366 let r = rects(stack, BADGE)[0];
367 assert_eq!(r.x, 10.0);
368 assert_eq!(r.width, 60.0, "100 - 10 - 30: {r:?}");
369 assert_eq!(r.y, 80.0, "its own height, off the bottom: {r:?}");
370 }
371
372 #[test]
374 fn an_inset_layer_fills_what_is_left() {
375 let stack = Stack::<()>::new()
376 .width(100.0)
377 .height(100.0)
378 .layer(crate::Positioned::new(badge()).inset(5.0));
379 let r = rects(stack, BADGE)[0];
380 assert_eq!((r.x, r.y, r.width, r.height), (5.0, 5.0, 90.0, 90.0));
381 }
382
383 #[test]
385 fn an_unpinned_axis_follows_the_alignment() {
386 let stack = Stack::<()>::new()
387 .width(100.0)
388 .height(100.0)
389 .alignment(frus_core::Alignment::CENTER)
390 .layer(crate::Positioned::new(badge()).bottom(0.0));
391 let r = rects(stack, BADGE)[0];
392 assert_eq!(r.x, 40.0, "centred across: {r:?}");
393 assert_eq!(r.y, 80.0, "and pinned down: {r:?}");
394 }
395
396 #[test]
399 fn a_loose_layer_keeps_its_size_and_is_aligned() {
400 let unsized_layer = || {
401 Container::<()>::new()
402 .color(BADGE)
403 .child(crate::Text::new("x"))
404 };
405
406 let expanded = Stack::<()>::new()
407 .width(100.0)
408 .height(100.0)
409 .layer(unsized_layer());
410 let r = rects(expanded, BADGE)[0];
411 assert_eq!((r.width, r.height), (100.0, 100.0), "given the box: {r:?}");
412
413 let loose = Stack::<()>::new()
414 .width(100.0)
415 .height(100.0)
416 .fit(StackFit::Loose)
417 .alignment(frus_core::Alignment::BOTTOM_RIGHT)
418 .layer(unsized_layer());
419 let r = rects(loose, BADGE)[0];
420 assert!(r.width < 60.0, "asked, and it hugged its text: {r:?}");
421 assert_eq!(r.x + r.width, 100.0, "against the right edge: {r:?}");
422 assert_eq!(r.y + r.height, 100.0, "and the bottom: {r:?}");
423 }
424
425 #[test]
428 fn a_wrapped_pin_still_pins() {
429 let stack = Stack::<()>::new()
430 .width(100.0)
431 .height(100.0)
432 .layer(crate::Keyed::new(
433 3u64,
434 crate::Positioned::new(badge()).top(8.0).left(8.0),
435 ));
436 let r = rects(stack, BADGE)[0];
437 assert_eq!((r.x, r.y), (8.0, 8.0));
438 }
439
440 fn page(colour: Color, side: f32, msg: i32) -> Container<i32> {
442 Container::<i32>::new()
443 .width(side)
444 .height(side)
445 .color(colour)
446 .on_click(msg)
447 }
448
449 const PAGE_ONE: Color = Color::rgb(1.0, 0.0, 0.0);
450 const PAGE_TWO: Color = Color::rgb(0.0, 0.0, 1.0);
451
452 fn pages(shown: usize) -> IndexedStack<i32> {
454 IndexedStack::<i32>::new(shown)
455 .width(100.0)
456 .height(100.0)
457 .child(page(PAGE_ONE, 40.0, 1))
458 .child(page(PAGE_TWO, 80.0, 2))
459 }
460
461 fn built(root: &dyn Widget<i32>) -> (crate::Ui<i32>, Vec<crate::InspectorNode>) {
462 crate::build_ui_inspected(
463 root,
464 Size::new(100.0, 100.0),
465 &Runtime::default(),
466 &Theme::default(),
467 )
468 }
469
470 fn boxes(nodes: &[crate::InspectorNode], side: f32) -> usize {
472 nodes
473 .iter()
474 .filter(|n| (n.rect.width - side).abs() < 0.5 && (n.rect.height - side).abs() < 0.5)
475 .count()
476 }
477
478 fn painted(ui: &crate::Ui<i32>, of: Color) -> bool {
479 ui.scene()
480 .primitives()
481 .iter()
482 .any(|p| matches!(p, Primitive::Rect { color, .. } if *color == of))
483 }
484
485 #[test]
486 fn an_indexed_stack_lays_every_page_out_and_paints_one() {
487 let (ui, nodes) = built(&pages(0));
488 assert!(painted(&ui, PAGE_ONE), "the shown page is drawn");
489 assert!(!painted(&ui, PAGE_TWO), "the others are not");
490 assert_eq!(
491 boxes(&nodes, 80.0),
492 1,
493 "the hidden page still has its box: {}",
494 crate::dump_tree(&nodes)
495 );
496 }
497
498 #[test]
501 fn a_hidden_page_keeps_the_size_it_had() {
502 let (_, showing_first) = built(&pages(0));
503 let (_, showing_second) = built(&pages(1));
504 for side in [40.0, 80.0] {
505 assert_eq!(
506 boxes(&showing_first, side),
507 boxes(&showing_second, side),
508 "the {side} px page changed box when the other one was shown"
509 );
510 }
511 }
512
513 #[test]
516 fn an_offstage_page_has_no_box_at_all() {
517 let stack = Stack::<i32>::new()
518 .width(100.0)
519 .height(100.0)
520 .fit(StackFit::Loose)
521 .layer(page(PAGE_ONE, 40.0, 1))
522 .layer(crate::Offstage::new(page(PAGE_TWO, 80.0, 2)));
523 let (_, nodes) = built(&stack);
524 assert_eq!(boxes(&nodes, 80.0), 0, "{}", crate::dump_tree(&nodes));
525 }
526
527 #[test]
528 fn a_hidden_page_takes_no_input() {
529 let point = frus_core::Point::new(60.0, 60.0);
531 let showing_first = built(&pages(0)).0;
532 assert!(
533 showing_first
534 .hit(point)
535 .and_then(|id| showing_first.msg_for(id))
536 != Some(2),
537 "the hidden page answered a click"
538 );
539 let showing_second = built(&pages(1)).0;
540 assert_eq!(
541 showing_second
542 .hit(point)
543 .and_then(|id| showing_second.msg_for(id)),
544 Some(2),
545 "and the shown one does"
546 );
547 }
548}