//! The smallest trees that laid out differently warm than cold, each shrunk //! by `tests/shrink.rs` from hundreds of widgets. The first two are a cold //! frame that had not settled: a wrapping text shaped at a width it was //! measured in rather than the one it was given. The rest are a widget //! measured again in a box its own answer had decided, where the old answer //! is a fixed point whatever the content now says. The last three are //! neither: one box length, composed two ways, landing either side of the //! boundary that decided whether a child was drawn at all, and two boxes //! reached through a region node's own entry rather than through the offer //! that node was given. use iris::harness::Harness; use iris::prelude::*; use iris::random::Branch; /// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about /// the tree changes -- every widget is marked for redraw and the frame is /// taken again -- so no box may move, and a warm frame has to land where a /// cold one does. fn plant(h: &mut Harness) -> Vec { let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc); let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc); let sized = wrapped.width(76).add(&mut h.rsc); let aligned = sized; h.rsc .widgets_mut() .set_alignment(sized, Axis::X, AxisAlign::POS); h.rsc .widgets_mut() .set_alignment(sized, Axis::Y, AxisAlign::POS); let stack = Stack { children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)], size: StackSize::Child(0), } .add(&mut h.rsc); let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc); h.set_root(root); vec![ plain.id(), wrapped.id(), sized.id(), aligned.id(), stack.id(), root.id(), ] } /// The first frame does not reach the layout a second one does, so "cold" is /// not a fixed point and comparing against it compares against a tree that /// has not settled. #[test] fn one_frame_is_enough() { let mut h = Harness::new((640, 900)); let ids = plant(&mut h); let first = h.region(&ids[1]).unwrap(); for _ in 0..3 { for &id in &ids { h.rsc.widgets_mut().get_dyn_mut(id); } h.frame(); } let settled = h.region(&ids[1]).unwrap(); println!( "first frame {} tall, settled {} tall", first.bot_right.y - first.top_left.y, settled.bot_right.y - settled.top_left.y ); assert_eq!( first.bot_right.y - first.top_left.y, settled.bot_right.y - settled.top_left.y, "the first frame had not finished laying out" ); } #[test] fn repainting_everything_moves_nothing() { let mut warm = Harness::new((640, 900)); let ids = plant(&mut warm); for &id in &ids { warm.rsc.widgets_mut().get_dyn_mut(id); } warm.frame(); let mut cold = Harness::new((640, 900)); let cold_ids = plant(&mut cold); let mut wrong = Vec::new(); for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() { let (got, want) = (warm.region(&w), cold.region(&c)); if got != want { wrong.push(format!("widget {i}: warm {got:?} cold {want:?}")); } } assert!(wrong.is_empty(), "{}", wrong.join("\n")); } /// Six widgets, shrunk from 905. Everything inside the declared 189x176 box /// is the same size whatever the output is, so a resize may not change any of /// it -- but the text comes out 3.92px narrower warm than cold. fn plant_fixed(h: &mut Harness) -> Vec { let words = "Wrapping shapes one source into as many lines as the box leaves"; let text = wtext(words).size(16).wrap(true).add(&mut h.rsc); let aligned = text; h.rsc .widgets_mut() .set_alignment(text, Axis::X, AxisAlign::NEG); let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc); let sized = inner.sized((189, 176)).add(&mut h.rsc); let filler = rect(Color::RED).add(&mut h.rsc); let root = (filler, sized).span(Dir::RIGHT).add(&mut h.rsc); h.state.root = Some(root.add_strong(&mut h.rsc)); vec![ text.id(), aligned.id(), inner.id(), sized.id(), filler.id(), root.id(), ] } #[test] fn a_resize_does_not_reach_inside_a_box_of_declared_pixels() { let mut warm = Harness::new((1920, 1200)); let ids = plant_fixed(&mut warm); warm.frame(); warm.resize((640, 900)); warm.frame(); let mut cold = Harness::new((640, 900)); let cold_ids = plant_fixed(&mut cold); cold.frame(); let mut wrong = Vec::new(); for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() { let (got, want) = (warm.region(&w), cold.region(&c)); if got != want { wrong.push(format!("widget {i}: warm {got:?} cold {want:?}")); } } assert!(wrong.is_empty(), "{}", wrong.join("\n")); } /// Four widgets, shrunk from 486. A span's two children are swapped: warm by /// moving them, cold by growing them that way. Same widgets, same sizes, one /// ends up 29.9px from where the other does. fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec, WeakWidget) { let wrapped = wtext("Wrapping shapes one source into as many lines") .size(16) .wrap(true) .add(&mut h.rsc); let plain = wtext("one line, overflowing whatever it is given") .size(16) .wrap(false) .add(&mut h.rsc); let first: StrongWidget = wrapped.add_strong(&mut h.rsc); let second: StrongWidget = plain.add_strong(&mut h.rsc); let children = match swapped { true => vec![second, first], false => vec![first, second], }; let span = Span { children, dir: Dir::RIGHT, gap: Px::ZERO, } .add(&mut h.rsc); let span_handle = span; let aligned = span; h.rsc .widgets_mut() .set_alignment(span, Axis::X, AxisAlign::CENTER); h.state.root = Some(aligned.add_strong(&mut h.rsc)); ( vec![wrapped.id(), plain.id(), span.id(), aligned.id()], span_handle, ) } #[test] fn swapping_two_children_lands_where_growing_them_that_way_does() { let mut warm = Harness::new((640, 900)); let (ids, span) = plant_pair(&mut warm, false); warm.frame(); warm.rsc[span].children.rotate_left(1); warm.frame(); let mut cold = Harness::new((640, 900)); let (cold_ids, _) = plant_pair(&mut cold, true); cold.frame(); let mut wrong = Vec::new(); for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() { let (got, want) = (warm.region(&w), cold.region(&c)); if got != want { wrong.push(format!("widget {i}: warm {got:?} cold {want:?}")); } } assert!(wrong.is_empty(), "{}", wrong.join("\n")); } /// Eight widgets, shrunk from 80. The scroll decides how wide to make its /// content from what the content says, and hands that box down through a /// pass-through; the span under it was given that box once, so nothing at its /// own edge says the box was its own answer. fn plant_scrolled(h: &mut Harness, swapped: bool) -> (Vec, [WeakWidget; 2]) { let words = "Wrapping shapes one source into as many lines as the box leaves room for,"; let text = wtext(words).size(16).wrap(true).add(&mut h.rsc); let filler = rect(Color::RED).add(&mut h.rsc); let mut inner_children: Vec = vec![text.add_strong(&mut h.rsc), filler.add_strong(&mut h.rsc)]; if swapped { inner_children.rotate_left(1); } let inner = Span { children: inner_children, dir: Dir::RIGHT, gap: Px::ZERO, } .add(&mut h.rsc); let block = rect(Color::RED).add(&mut h.rsc); let fixed = block.width(87).add(&mut h.rsc); let mut outer_children: Vec = vec![fixed.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)]; if swapped { outer_children.rotate_left(1); } let outer = Span { children: outer_children, dir: Dir::RIGHT, gap: Px::ZERO, } .add(&mut h.rsc); // Carried no rule even before rules were a property: it is here to be a // widget between the span and the scroll, not to declare anything. let through = (outer,).span(Dir::RIGHT).add(&mut h.rsc); let scroll = Scroll::new(through.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc); h.state.root = Some(scroll.add_strong(&mut h.rsc)); ( vec![ text.id(), filler.id(), inner.id(), block.id(), fixed.id(), outer.id(), through.id(), scroll.id(), ], [inner, outer], ) } #[test] fn a_span_given_the_box_its_answer_decided_matches_a_cold_layout() { let mut warm = Harness::new((640, 900)); let (ids, spans) = plant_scrolled(&mut warm, false); warm.frame(); for span in spans { warm.rsc[span].children.rotate_left(1); } warm.frame(); let mut cold = Harness::new((640, 900)); let (cold_ids, _) = plant_scrolled(&mut cold, true); cold.frame(); let mut wrong = Vec::new(); for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() { let (got, want) = (warm.region(&w), cold.region(&c)); if got != want { wrong.push(format!("widget {i}: warm {got:?} cold {want:?}")); } } assert!(wrong.is_empty(), "{}", wrong.join("\n")); } /// Reports a width derived from the box it is asked in. Reading through the /// painter is its declaration that the answer holds for that width only. struct Wider { extra: f32, } impl Widget for Wider { fn draw(&mut self, painter: &mut Painter) -> Size { Size { x: LayoutLen { px: painter.px_len(Axis::X) + Px::from_f32(self.extra), ..LayoutLen::ZERO }, y: LayoutLen::LEFTOVER, } } } fn plant_wider(h: &mut Harness, extra: f32) -> (WeakWidget, WidgetId) { let content = Wider { extra }.add(&mut h.rsc); let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc); let root = scroll; h.rsc .widgets_mut() .set_alignment(scroll, Axis::X, AxisAlign::NEG); h.set_root(root); (content, scroll.id()) } #[test] fn a_scrolls_retained_answer_is_the_one_a_cold_layout_asks_for() { let mut warm = Harness::new((100, 100)); let (content, scroll) = plant_wider(&mut warm, 50.0); warm.rsc[content].extra = 70.0; warm.frame(); let mut cold = Harness::new((100, 100)); let (_, cold_scroll) = plant_wider(&mut cold, 70.0); assert_eq!(warm.region(&scroll), cold.region(&cold_scroll)); } /// Six widgets, shrunk from 266. `measured`'s box is exactly the height of its /// one fixed child, which is the box a parent sizing itself from that answer /// hands back -- so whether its leftover-only child was drawn at all came down /// to the 0.00003 px the composed length differs by, one way warm and the /// other cold. fn plant_boundary(h: &mut Harness, swapped: bool) -> (Vec, [WeakWidget; 2]) { let filler = rect(Color::RED).add(&mut h.rsc); let plain = wtext("one line, overflowing whatever it is given") .size(16) .wrap(false) .add(&mut h.rsc); let mut pair: Vec = vec![filler.add_strong(&mut h.rsc), plain.add_strong(&mut h.rsc)]; if swapped { pair.rotate_left(1); } let measured = Span { children: pair, dir: Dir::DOWN, gap: Px::ZERO, } .add(&mut h.rsc); // Takes the whole box on its own, so the span above has nothing left to // divide and `measured` is given exactly the text's height. let whole = rect(Color::RED).add(&mut h.rsc); h.rsc .widgets_mut() .set_size_rules(whole, None, Some(LayoutLen::rel(1.0))); let mut inner_children: Vec = vec![ measured.add_strong(&mut h.rsc), whole.add_strong(&mut h.rsc), ]; if swapped { inner_children.rotate_left(1); } let inner = Span { children: inner_children, dir: Dir::DOWN, gap: Px::ZERO, } .add(&mut h.rsc); h.rsc .widgets_mut() .set_size_rules(inner, None, Some(LayoutLen::px(198.0))); // One more span above it: without a box composed through it, both trees // round the same way and the boundary is never crossed. let outer = (inner,).span(Dir::DOWN).add(&mut h.rsc); h.set_root(outer); ( vec![ filler.id(), plain.id(), measured.id(), whole.id(), inner.id(), outer.id(), ], [measured, inner], ) } #[test] fn a_box_that_only_rounds_past_its_fixed_children_leaves_nothing_over() { let mut warm = Harness::new((640, 900)); let (ids, spans) = plant_boundary(&mut warm, false); warm.frame(); for span in spans { warm.rsc[span].children.rotate_left(1); } warm.frame(); let mut cold = Harness::new((640, 900)); let (cold_ids, _) = plant_boundary(&mut cold, true); cold.frame(); let mut wrong = Vec::new(); for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() { let (got, want) = (warm.region(&w), cold.region(&c)); if got != want { wrong.push(format!("widget {i}: warm {got:?} cold {want:?}")); } } assert!(wrong.is_empty(), "{}", wrong.join("\n")); } /// Five widgets, shrunk by `tests/shrink.rs` from the 277 the oracle's seed /// 18 grows at depth 6. A scroll inside a scroll, the inner one owning a /// movable region of its own, and only its text marked for redraw. Nothing /// about the tree changes, so no box may. fn plant_nested_scrolls(h: &mut Harness) -> Vec { let text = wtext("one line, overflowing whatever it is given") .size(16) .wrap(false) .add(&mut h.rsc); let inner = Scroll::new(text.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc); h.rsc.widgets_mut().set_region_node(inner.id(), true); let filler = rect(Color::RED).add(&mut h.rsc); h.rsc.widgets_mut().set_size_rules( filler.id(), Some(LayoutLen::px(87.0)), Some(LayoutLen::px(24.0)), ); let span = Span { children: vec![inner.add_strong(&mut h.rsc), filler.add_strong(&mut h.rsc)], dir: Dir::DOWN, gap: Px::ZERO, } .add(&mut h.rsc); let root = Scroll::new(span.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc); h.set_root(root); vec![text.id(), inner.id(), filler.id(), span.id(), root.id()] } /// A local redraw asks a dirty widget in the box its parent gave it, and only /// where that box is as long as the one it was offered; anything else is a /// question its parent has to ask. This inner scroll's offer is the outer /// scroll's whole viewport and the box it was given is 24px shorter -- the /// height of the sized child the outer scroll snaps to the end of -- so what /// it must not do is settle itself. It was drawn at its offer once, and the /// inner scroll and its text stayed 24px too low. #[test] fn redrawing_one_widget_does_not_move_what_scrolls_around_it() { let mut warm = Harness::new((900, 1200)); let ids = plant_nested_scrolls(&mut warm); warm.rsc.widgets_mut().get_dyn_mut(ids[0]); warm.frame(); let mut cold = Harness::new((900, 1200)); let cold_ids = plant_nested_scrolls(&mut cold); let mut wrong = Vec::new(); for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() { let (got, want) = (warm.region(&w), cold.region(&c)); if got != want { wrong.push(format!("widget {i}: warm {got:?} cold {want:?}")); } } assert!(wrong.is_empty(), "{}", wrong.join("\n")); } /// Ten widgets, of the shape `tests/shrink.rs` reduces the oracle's seed 220 /// to. The pad owns a movable region and is the scroll's content, so the box /// the scroll places it in is as long as that content while the box it was /// offered is the viewport -- and with no padding to tell those two apart, /// the span inside it looked like it was still at its offer. So everything /// under the pad was asked again in the *placed* box, the offer resolving /// against the node's own entry, which holds that box: the texts kept the /// widths they had, the content stayed the length those widths make, and the /// old answer confirmed itself. What the branch adds is a tree that differs /// rather than a box that moved, since a probe measured at the wrong width /// takes the other side. fn plant_under_a_node(h: &mut Harness, swapped: bool) -> (Vec, [WeakWidget; 2]) { let probe = rect(Color::RED).add(&mut h.rsc); let wide = rect(Color::GREEN).add(&mut h.rsc); let narrow = rect(Color::BLUE).add(&mut h.rsc); let branch = Branch { probe: probe.add_strong(&mut h.rsc), wide: wide.add_strong(&mut h.rsc), narrow: narrow.add_strong(&mut h.rsc), threshold: 213.0, } .add(&mut h.rsc); let wrapped = wtext( "Wrapping shapes one source into as many lines as the box \ leaves room for, so a paragraph's height is an answer and not a setting.", ) .size(16) .wrap(true) .add(&mut h.rsc); let plain = wtext("one line, overflowing whatever it is given") .size(16) .wrap(false) .add(&mut h.rsc); let row = |h: &mut Harness, mut children: Vec| { if swapped { children.rotate_left(1); } Span { children, dir: Dir::RIGHT, gap: Px::ZERO, } .add(&mut h.rsc) }; let texts: Vec = vec![wrapped.add_strong(&mut h.rsc), plain.add_strong(&mut h.rsc)]; let inner = row(h, texts); let pair: Vec = vec![branch.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)]; let outer = row(h, pair); let pad = Pad { padding: Padding::ZERO, inner: outer.add_strong(&mut h.rsc), } .add(&mut h.rsc); h.rsc.widgets_mut().set_region_node(pad.id(), true); let root = Scroll::new(pad.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc); h.set_root(root); ( vec![ probe.id(), wide.id(), narrow.id(), branch.id(), wrapped.id(), plain.id(), inner.id(), outer.id(), pad.id(), root.id(), ], [outer, inner], ) } #[test] fn a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered() { let mut warm = Harness::new((900, 1200)); let (ids, spans) = plant_under_a_node(&mut warm, false); warm.frame(); for span in spans { warm.rsc[span].children.rotate_left(1); } warm.frame(); let mut cold = Harness::new((900, 1200)); let (cold_ids, _) = plant_under_a_node(&mut cold, true); cold.frame(); let mut wrong = Vec::new(); for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() { let (got, want) = (warm.region(&w), cold.region(&c)); if got != want { wrong.push(format!("widget {i}: warm {got:?} cold {want:?}")); } } assert!(wrong.is_empty(), "{}", wrong.join("\n")); }