1use frus_core::{AlignmentDirectional, AlignmentGeometry, Rect, Scene, TextDirection};
18use frus_layout::{Dimension, Style};
19
20use crate::interaction::Status;
21use crate::theme::Theme;
22use crate::widget::{FillAxes, Widget};
23
24#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
27pub enum StackFit {
28 #[default]
31 Loose,
32 Expand,
35 Passthrough,
39}
40
41#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
44pub enum Clip {
45 None,
47 #[default]
49 HardEdge,
50 AntiAlias,
53 AntiAliasWithSaveLayer,
56}
57
58impl Clip {
59 pub fn clips(self) -> bool {
61 self != Clip::None
62 }
63}
64
65fn takes_the_room<Msg>(fit: StackFit, layers: &[Box<dyn Widget<Msg>>]) -> FillAxes {
68 if fit == StackFit::Expand || layers.iter().all(|layer| layer.positioned().is_some()) {
69 FillAxes::BOTH
70 } else {
71 FillAxes::NONE
72 }
73}
74
75pub struct Stack<Msg = crate::callback::Callback> {
77 width: Dimension,
78 height: Dimension,
79 flex_grow: f32,
80 fit: StackFit,
81 alignment: AlignmentGeometry,
82 clip: Clip,
83 direction: Option<TextDirection>,
84 layers: Vec<Box<dyn Widget<Msg>>>,
85}
86
87impl<Msg> Stack<Msg> {
88 pub fn new() -> Self {
90 Self {
91 width: Dimension::Auto,
92 height: Dimension::Auto,
93 flex_grow: 0.0,
94 fit: StackFit::Loose,
95 alignment: AlignmentGeometry::Directional(AlignmentDirectional::TOP_START),
99 clip: Clip::HardEdge,
100 direction: None,
101 layers: Vec::new(),
102 }
103 }
104
105 pub fn fit(mut self, fit: StackFit) -> Self {
108 self.fit = fit;
109 self
110 }
111
112 pub fn alignment(mut self, alignment: impl Into<AlignmentGeometry>) -> Self {
116 self.alignment = alignment.into();
117 self
118 }
119
120 pub fn clip_behavior(mut self, clip: Clip) -> Self {
123 self.clip = clip;
124 self
125 }
126
127 pub fn text_direction(mut self, direction: TextDirection) -> Self {
130 self.direction = Some(direction);
131 self
132 }
133
134 pub fn width(mut self, width: f32) -> Self {
136 self.width = Dimension::Length(width);
137 self
138 }
139
140 pub fn height(mut self, height: f32) -> Self {
142 self.height = Dimension::Length(height);
143 self
144 }
145
146 pub fn flex(mut self, grow: f32) -> Self {
148 self.flex_grow = grow;
149 self
150 }
151
152 pub fn layer(mut self, layer: impl Widget<Msg> + 'static) -> Self {
154 self.layers.push(Box::new(layer));
155 self
156 }
157}
158
159impl<Msg> Default for Stack<Msg> {
160 fn default() -> Self {
161 Self::new()
162 }
163}
164
165impl<Msg: Clone> Widget<Msg> for Stack<Msg> {
166 fn style(&self) -> Style {
167 Style {
168 width: self.width,
169 height: self.height,
170 flex_grow: self.flex_grow,
171 ..Default::default()
172 }
173 }
174
175 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
176 &self.layers
177 }
178
179 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
180
181 fn on_click(&self) -> Option<Msg> {
182 None
183 }
184
185 fn stack(&self) -> bool {
186 true
187 }
188
189 fn stack_fit(&self) -> StackFit {
190 self.fit
191 }
192
193 fn stack_measured(&self) -> bool {
194 true
195 }
196
197 fn stack_clips(&self) -> bool {
198 self.clip.clips()
199 }
200
201 fn stack_direction(&self) -> Option<TextDirection> {
202 self.direction
203 }
204
205 fn fill_axes(&self, _theme: &Theme) -> FillAxes {
207 takes_the_room(self.fit, &self.layers)
208 }
209
210 fn alignment_geometry(&self) -> Option<AlignmentGeometry> {
211 Some(self.alignment)
212 }
213}
214
215pub struct IndexedStack<Msg = crate::callback::Callback> {
240 index: usize,
241 width: Dimension,
242 height: Dimension,
243 flex_grow: f32,
244 fit: StackFit,
245 alignment: AlignmentGeometry,
246 clip: Clip,
247 direction: Option<TextDirection>,
248 children: Vec<Box<dyn Widget<Msg>>>,
249}
250
251impl<Msg> IndexedStack<Msg> {
252 pub fn new(index: usize) -> Self {
254 Self {
255 index,
256 width: Dimension::Auto,
257 height: Dimension::Auto,
258 flex_grow: 0.0,
259 fit: StackFit::Loose,
260 alignment: AlignmentGeometry::Directional(frus_core::AlignmentDirectional::TOP_START),
261 clip: Clip::HardEdge,
262 direction: None,
263 children: Vec::new(),
264 }
265 }
266
267 pub fn index(mut self, index: usize) -> Self {
269 self.index = index;
270 self
271 }
272
273 pub fn child(mut self, child: impl Widget<Msg> + 'static) -> Self {
275 self.children.push(Box::new(child));
276 self
277 }
278
279 pub fn fit(mut self, fit: StackFit) -> Self {
281 self.fit = fit;
282 self
283 }
284
285 pub fn alignment(mut self, alignment: impl Into<AlignmentGeometry>) -> Self {
287 self.alignment = alignment.into();
288 self
289 }
290
291 pub fn clip_behavior(mut self, clip: Clip) -> Self {
293 self.clip = clip;
294 self
295 }
296
297 pub fn text_direction(mut self, direction: TextDirection) -> Self {
299 self.direction = Some(direction);
300 self
301 }
302
303 pub fn width(mut self, width: f32) -> Self {
305 self.width = Dimension::Length(width);
306 self
307 }
308
309 pub fn height(mut self, height: f32) -> Self {
311 self.height = Dimension::Length(height);
312 self
313 }
314
315 pub fn flex(mut self, grow: f32) -> Self {
317 self.flex_grow = grow;
318 self
319 }
320}
321
322impl<Msg: Clone> Widget<Msg> for IndexedStack<Msg> {
323 fn style(&self) -> Style {
324 Style {
325 width: self.width,
326 height: self.height,
327 flex_grow: self.flex_grow,
328 ..Default::default()
329 }
330 }
331
332 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
333 &self.children
334 }
335
336 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
337
338 fn on_click(&self) -> Option<Msg> {
339 None
340 }
341
342 fn stack(&self) -> bool {
343 true
344 }
345
346 fn stack_fit(&self) -> StackFit {
347 self.fit
348 }
349
350 fn stack_measured(&self) -> bool {
353 true
354 }
355
356 fn stack_clips(&self) -> bool {
357 self.clip.clips()
358 }
359
360 fn stack_direction(&self) -> Option<TextDirection> {
361 self.direction
362 }
363
364 fn fill_axes(&self, _theme: &Theme) -> FillAxes {
365 takes_the_room(self.fit, &self.children)
366 }
367
368 fn stack_visible(&self) -> Option<usize> {
369 Some(self.index)
370 }
371
372 fn alignment_geometry(&self) -> Option<AlignmentGeometry> {
373 Some(self.alignment)
374 }
375
376 fn debug_name(&self) -> &'static str {
377 "IndexedStack"
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384 use crate::{build_ui, Container, Runtime, Size};
385 use frus_core::{Color, Primitive};
386
387 #[test]
388 fn layers_overlap_in_same_box() {
389 let red = Color::rgb(1.0, 0.0, 0.0);
390 let blue = Color::rgb(0.0, 0.0, 1.0);
391 let stack = Stack::<()>::new()
392 .width(100.0)
393 .height(100.0)
394 .layer(Container::<()>::new().width(100.0).height(100.0).color(red))
395 .layer(Container::<()>::new().width(40.0).height(40.0).color(blue));
396 let ui = build_ui(
397 &stack,
398 Size::new(100.0, 100.0),
399 &Runtime::default(),
400 &Theme::default(),
401 );
402
403 let rect_of = |c: Color| {
404 ui.scene()
405 .primitives()
406 .iter()
407 .find_map(|p| match p {
408 Primitive::Rect { color, rect, .. } if *color == c => Some(*rect),
409 _ => None,
410 })
411 .expect("the layer is present")
412 };
413 assert_eq!(rect_of(red).origin(), rect_of(blue).origin());
415 }
416
417 fn rects(stack: Stack<()>, of: Color) -> Vec<Rect> {
419 let ui = build_ui(
420 &stack,
421 Size::new(100.0, 100.0),
422 &Runtime::default(),
423 &Theme::default(),
424 );
425 ui.scene()
426 .primitives()
427 .iter()
428 .filter_map(|p| match p {
429 Primitive::Rect { color, rect, .. } if *color == of => Some(*rect),
430 _ => None,
431 })
432 .collect()
433 }
434
435 const BADGE: Color = Color::rgb(0.0, 0.0, 1.0);
436
437 fn badge() -> Container<()> {
438 Container::<()>::new().width(20.0).height(20.0).color(BADGE)
439 }
440
441 #[test]
443 fn a_pinned_layer_sits_against_the_edges_it_names() {
444 let stack = Stack::<()>::new()
445 .width(100.0)
446 .height(100.0)
447 .layer(crate::Positioned::new(badge()).top(8.0).right(12.0));
448 let r = rects(stack, BADGE)[0];
449 assert_eq!(r.y, 8.0, "8 below the top: {r:?}");
450 assert_eq!(r.x, 100.0 - 12.0 - 20.0, "12 in from the right: {r:?}");
451 assert_eq!((r.width, r.height), (20.0, 20.0), "its own size");
452 }
453
454 #[test]
457 fn opposite_pins_decide_the_extent() {
458 let stack = Stack::<()>::new().width(100.0).height(100.0).layer(
459 crate::Positioned::new(badge())
460 .left(10.0)
461 .right(30.0)
462 .bottom(0.0),
463 );
464 let r = rects(stack, BADGE)[0];
465 assert_eq!(r.x, 10.0);
466 assert_eq!(r.width, 60.0, "100 - 10 - 30: {r:?}");
467 assert_eq!(r.y, 80.0, "its own height, off the bottom: {r:?}");
468 }
469
470 #[test]
472 fn an_inset_layer_fills_what_is_left() {
473 let stack = Stack::<()>::new()
474 .width(100.0)
475 .height(100.0)
476 .layer(crate::Positioned::new(badge()).inset(5.0));
477 let r = rects(stack, BADGE)[0];
478 assert_eq!((r.x, r.y, r.width, r.height), (5.0, 5.0, 90.0, 90.0));
479 }
480
481 #[test]
483 fn an_unpinned_axis_follows_the_alignment() {
484 let stack = Stack::<()>::new()
485 .width(100.0)
486 .height(100.0)
487 .alignment(frus_core::Alignment::CENTER)
488 .layer(crate::Positioned::new(badge()).bottom(0.0));
489 let r = rects(stack, BADGE)[0];
490 assert_eq!(r.x, 40.0, "centred across: {r:?}");
491 assert_eq!(r.y, 80.0, "and pinned down: {r:?}");
492 }
493
494 #[test]
499 fn the_three_fits_are_the_reference_s() {
500 let unsized_layer = || {
501 Container::<()>::new()
502 .color(BADGE)
503 .child(crate::Text::new("x"))
504 };
505 let sized_layer = || Container::<()>::new().width(40.0).height(40.0).color(BADGE);
506 let in_box = |fit: StackFit, layer: Container<()>| {
507 let stack = Stack::<()>::new()
508 .width(100.0)
509 .height(100.0)
510 .fit(fit)
511 .alignment(frus_core::Alignment::BOTTOM_RIGHT)
512 .layer(layer);
513 rects(stack, BADGE)[0]
514 };
515
516 let r = in_box(StackFit::Loose, unsized_layer());
517 assert!(r.width < 60.0, "asked, and it hugged its text: {r:?}");
518 assert_eq!(r.x + r.width, 100.0, "against the right edge: {r:?}");
519 assert_eq!(r.y + r.height, 100.0, "and the bottom: {r:?}");
520 assert_eq!(
521 Stack::<()>::new().stack_fit(),
522 StackFit::Loose,
523 "the default, as in the reference"
524 );
525
526 let r = in_box(StackFit::Expand, sized_layer());
527 assert_eq!((r.width, r.height), (100.0, 100.0), "forced: {r:?}");
528 let r = in_box(StackFit::Expand, unsized_layer());
529 assert_eq!((r.width, r.height), (100.0, 100.0), "forced: {r:?}");
530
531 let r = in_box(StackFit::Passthrough, unsized_layer());
532 assert_eq!((r.width, r.height), (100.0, 100.0), "handed the box: {r:?}");
533 let r = in_box(StackFit::Passthrough, sized_layer());
534 assert_eq!(
535 (r.width, r.height),
536 (40.0, 40.0),
537 "and a size of its own kept: {r:?}"
538 );
539 let r = in_box(StackFit::Loose, sized_layer());
540 assert_eq!((r.width, r.height), (40.0, 40.0));
541 let r = in_box(StackFit::Loose, Container::<()>::new().color(BADGE));
544 assert_eq!((r.width, r.height), (100.0, 100.0), "it asked: {r:?}");
545 }
546
547 fn in_a_column(stack: Stack<()>, colour: Color) -> (Vec<Rect>, Rect) {
550 let under = Color::rgb(0.1, 0.7, 0.3);
551 let col = crate::Flex::<()>::column()
552 .child(stack)
553 .child(Container::new().width(10.0).height(10.0).color(under));
554 let ui = build_ui(
555 &col,
556 Size::new(200.0, 200.0),
557 &Runtime::default(),
558 &crate::Theme::default(),
559 );
560 let painted = |c: Color| -> Vec<Rect> {
561 ui.scene()
562 .primitives()
563 .iter()
564 .filter_map(|p| match p {
565 Primitive::Rect { rect, color, .. } if *color == c => Some(*rect),
566 _ => None,
567 })
568 .collect()
569 };
570 let next = painted(under)[0];
571 (painted(colour), next)
572 }
573
574 #[test]
578 fn a_stack_is_as_big_as_its_largest_unpinned_layer() {
579 let stack = Stack::<()>::new()
581 .layer(Container::new().width(20.0).height(50.0).color(BADGE))
582 .layer(Container::new().width(40.0).height(30.0).color(BADGE))
583 .layer(
584 crate::Positioned::new(Container::new().width(90.0).height(90.0).color(BADGE))
585 .top(0.0)
586 .left(0.0),
587 );
588 let (_, next) = in_a_column(stack, BADGE);
589 assert_eq!(
590 next.y, 50.0,
591 "under the tallest unpinned layer, not the pinned one"
592 );
593 }
594
595 #[test]
598 fn with_only_pinned_layers_it_takes_the_room() {
599 let pinned = Stack::<()>::new().height(60.0).layer(
600 crate::Positioned::new(Container::new().width(10.0).height(10.0).color(BADGE))
601 .top(0.0)
602 .left(0.0),
603 );
604 assert_eq!(
605 Widget::<()>::fill_axes(&pinned, &crate::Theme::default()),
606 FillAxes::BOTH
607 );
608 let expanded = Stack::<()>::new()
609 .fit(StackFit::Expand)
610 .layer(Container::new().width(10.0).height(10.0).color(BADGE));
611 assert_eq!(
612 Widget::<()>::fill_axes(&expanded, &crate::Theme::default()),
613 FillAxes::BOTH
614 );
615 let loose = Stack::<()>::new().layer(Container::new().width(10.0).height(10.0));
616 assert_eq!(
617 Widget::<()>::fill_axes(&loose, &crate::Theme::default()),
618 FillAxes::NONE,
619 "a layer that is not pinned sizes it"
620 );
621 let boxed = Container::<()>::new().width(120.0).height(70.0).child(
623 Stack::new().layer(
624 crate::Positioned::new(Container::new().color(BADGE))
625 .right(0.0)
626 .bottom(0.0)
627 .width(10.0)
628 .height(10.0),
629 ),
630 );
631 let ui = build_ui(
632 &boxed,
633 Size::new(200.0, 200.0),
634 &Runtime::default(),
635 &crate::Theme::default(),
636 );
637 let badge = ui
638 .scene()
639 .primitives()
640 .iter()
641 .find_map(|p| match p {
642 Primitive::Rect { rect, color, .. } if *color == BADGE => Some(*rect),
643 _ => None,
644 })
645 .expect("the badge");
646 assert_eq!((badge.x, badge.y), (110.0, 60.0), "in the box's corner");
647 }
648
649 #[test]
652 fn a_layer_hangs_over_only_when_told() {
653 let hanging = |clip: Clip| {
654 Stack::<()>::new()
655 .width(40.0)
656 .height(40.0)
657 .clip_behavior(clip)
658 .layer(
659 crate::Positioned::new(Container::new().width(20.0).height(20.0).color(BADGE))
660 .top(-10.0)
661 .right(-10.0),
662 )
663 };
664 assert!(Widget::<()>::stack_clips(&hanging(Clip::HardEdge)));
665 assert!(!Widget::<()>::stack_clips(&hanging(Clip::None)));
666 assert!(Clip::AntiAlias.clips() && Clip::AntiAliasWithSaveLayer.clips());
667 assert_eq!(Clip::default(), Clip::HardEdge);
668 let r = rects(hanging(Clip::None), BADGE)[0];
669 assert_eq!(
670 (r.x, r.y),
671 (30.0, -10.0),
672 "past the top right corner: {r:?}"
673 );
674 let clip_of = |clip: Clip| {
676 let stack = hanging(clip);
677 let ui = build_ui(
678 &stack,
679 Size::new(100.0, 100.0),
680 &Runtime::default(),
681 &crate::Theme::default(),
682 );
683 ui.scene()
684 .primitives()
685 .iter()
686 .find_map(|p| match p {
687 Primitive::Rect { color, clip, .. } if *color == BADGE => Some(*clip),
688 _ => None,
689 })
690 .expect("the badge")
691 };
692 assert_eq!(clip_of(Clip::HardEdge), Rect::new(0.0, 0.0, 40.0, 40.0));
693 assert!(
694 clip_of(Clip::None).contains(frus_core::Point::new(45.0, 5.0)),
695 "nothing cut past the edge"
696 );
697 }
698
699 #[test]
703 fn a_hanging_layer_is_only_touched_inside_the_stack() {
704 #[derive(Clone, Debug, PartialEq)]
705 struct Tap;
706 let page = Container::<Tap>::new().padding(50.0).child(
707 Stack::<Tap>::new()
708 .width(40.0)
709 .height(40.0)
710 .clip_behavior(Clip::None)
711 .layer(
712 crate::Positioned::new(Container::new().width(20.0).height(20.0).on_click(Tap))
713 .top(-10.0)
714 .right(-10.0),
715 ),
716 );
717 let ui = build_ui(
718 &page,
719 Size::new(200.0, 200.0),
720 &Runtime::default(),
721 &crate::Theme::default(),
722 );
723 let tap = |x: f32, y: f32| {
724 ui.hit(frus_core::Point::new(x, y))
725 .and_then(|id| ui.msg_for(id))
726 };
727 assert_eq!(tap(85.0, 55.0), Some(Tap), "inside the stack");
728 assert_eq!(
729 tap(95.0, 45.0),
730 None,
731 "outside it, where only the drawing is"
732 );
733 }
734
735 #[test]
737 fn the_stack_s_own_direction_is_read() {
738 let stack = Stack::<()>::new()
739 .width(100.0)
740 .height(100.0)
741 .text_direction(TextDirection::Rtl)
742 .layer(Container::new().width(20.0).height(20.0).color(BADGE));
743 let r = rects(stack, BADGE)[0];
744 assert_eq!(r.x, 80.0, "the start is the right: {r:?}");
745 }
746
747 #[test]
750 fn the_positioned_shorthands() {
751 let at = |layer: crate::Positioned<()>| {
752 rects(
753 Stack::<()>::new().width(100.0).height(100.0).layer(layer),
754 BADGE,
755 )[0]
756 };
757 let square = || Container::<()>::new().color(BADGE);
758 assert_eq!(
759 at(crate::Positioned::fill(square())),
760 Rect::new(0.0, 0.0, 100.0, 100.0)
761 );
762 assert_eq!(
763 at(crate::Positioned::from_rect(
764 square(),
765 Rect::new(10.0, 20.0, 30.0, 40.0)
766 )),
767 Rect::new(10.0, 20.0, 30.0, 40.0)
768 );
769 assert_eq!(
770 at(crate::Positioned::from_relative_rect(
771 square(),
772 frus_core::Insets::new(5.0, 10.0, 15.0, 20.0)
773 )),
774 Rect::new(20.0, 5.0, 70.0, 80.0)
775 );
776 }
777
778 #[test]
781 fn an_indexed_stack_is_as_big_as_its_largest_child() {
782 let pages = |index: usize| {
783 IndexedStack::<()>::new(index)
784 .child(Container::new().width(30.0).height(20.0).color(BADGE))
785 .child(Container::new().width(30.0).height(60.0).color(BADGE))
786 };
787 for index in [0, 1] {
788 let col = crate::Flex::<()>::column().child(pages(index)).child(
789 Container::new()
790 .width(10.0)
791 .height(10.0)
792 .color(Color::rgb(0.1, 0.7, 0.3)),
793 );
794 let ui = build_ui(
795 &col,
796 Size::new(200.0, 200.0),
797 &Runtime::default(),
798 &crate::Theme::default(),
799 );
800 let next = ui
801 .scene()
802 .primitives()
803 .iter()
804 .find_map(|p| match p {
805 Primitive::Rect { rect, color, .. } if *color == Color::rgb(0.1, 0.7, 0.3) => {
806 Some(*rect)
807 }
808 _ => None,
809 })
810 .expect("the square under it");
811 assert_eq!(next.y, 60.0, "page {index}: the tallest page's height");
812 }
813 }
814
815 #[test]
818 fn a_wrapped_pin_still_pins() {
819 let stack = Stack::<()>::new()
820 .width(100.0)
821 .height(100.0)
822 .layer(crate::Keyed::new(
823 3u64,
824 crate::Positioned::new(badge()).top(8.0).left(8.0),
825 ));
826 let r = rects(stack, BADGE)[0];
827 assert_eq!((r.x, r.y), (8.0, 8.0));
828 }
829
830 fn page(colour: Color, side: f32, msg: i32) -> Container<i32> {
832 Container::<i32>::new()
833 .width(side)
834 .height(side)
835 .color(colour)
836 .on_click(msg)
837 }
838
839 const PAGE_ONE: Color = Color::rgb(1.0, 0.0, 0.0);
840 const PAGE_TWO: Color = Color::rgb(0.0, 0.0, 1.0);
841
842 fn pages(shown: usize) -> IndexedStack<i32> {
844 IndexedStack::<i32>::new(shown)
845 .width(100.0)
846 .height(100.0)
847 .child(page(PAGE_ONE, 40.0, 1))
848 .child(page(PAGE_TWO, 80.0, 2))
849 }
850
851 fn built(root: &dyn Widget<i32>) -> (crate::Ui<i32>, Vec<crate::InspectorNode>) {
852 crate::build_ui_inspected(
853 root,
854 Size::new(100.0, 100.0),
855 &Runtime::default(),
856 &Theme::default(),
857 )
858 }
859
860 fn boxes(nodes: &[crate::InspectorNode], side: f32) -> usize {
862 nodes
863 .iter()
864 .filter(|n| (n.rect.width - side).abs() < 0.5 && (n.rect.height - side).abs() < 0.5)
865 .count()
866 }
867
868 fn painted(ui: &crate::Ui<i32>, of: Color) -> bool {
869 ui.scene()
870 .primitives()
871 .iter()
872 .any(|p| matches!(p, Primitive::Rect { color, .. } if *color == of))
873 }
874
875 #[test]
876 fn an_indexed_stack_lays_every_page_out_and_paints_one() {
877 let (ui, nodes) = built(&pages(0));
878 assert!(painted(&ui, PAGE_ONE), "the shown page is drawn");
879 assert!(!painted(&ui, PAGE_TWO), "the others are not");
880 assert_eq!(
881 boxes(&nodes, 80.0),
882 1,
883 "the hidden page still has its box: {}",
884 crate::dump_tree(&nodes)
885 );
886 }
887
888 #[test]
891 fn a_hidden_page_keeps_the_size_it_had() {
892 let (_, showing_first) = built(&pages(0));
893 let (_, showing_second) = built(&pages(1));
894 for side in [40.0, 80.0] {
895 assert_eq!(
896 boxes(&showing_first, side),
897 boxes(&showing_second, side),
898 "the {side} px page changed box when the other one was shown"
899 );
900 }
901 }
902
903 #[test]
906 fn an_offstage_page_has_no_box_at_all() {
907 let stack = Stack::<i32>::new()
908 .width(100.0)
909 .height(100.0)
910 .fit(StackFit::Loose)
911 .layer(page(PAGE_ONE, 40.0, 1))
912 .layer(crate::Offstage::new(page(PAGE_TWO, 80.0, 2)));
913 let (_, nodes) = built(&stack);
914 assert_eq!(boxes(&nodes, 80.0), 0, "{}", crate::dump_tree(&nodes));
915 }
916
917 #[test]
918 fn a_hidden_page_takes_no_input() {
919 let point = frus_core::Point::new(60.0, 60.0);
921 let showing_first = built(&pages(0)).0;
922 assert!(
923 showing_first
924 .hit(point)
925 .and_then(|id| showing_first.msg_for(id))
926 != Some(2),
927 "the hidden page answered a click"
928 );
929 let showing_second = built(&pages(1)).0;
930 assert_eq!(
931 showing_second
932 .hit(point)
933 .and_then(|id| showing_second.msg_for(id)),
934 Some(2),
935 "and the shown one does"
936 );
937 }
938}