//! 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. The last is a wrapping text handed back the width //! it measured, rounded to a step below the line it measured there. //! //! Each says which seed it was shrunk from, of the generator as it stood when //! it was found. Those numbers no longer grow those trees -- a seed names one //! only while the generator draws the same things in the same order, and the //! leaves have grown an image since -- so what is written out below is the //! record of the case, and the seed is where it came from. use std::collections::HashSet; use iris::harness::Harness; use iris::prelude::*; use iris::random::Branch; /// Every widget in the same place warm as cold, reported all at once: which /// of a dozen boxes moved is the whole of what a shrunk case has to say. /// /// A list that names one widget twice is an error rather than a redundant /// check. `width`, `sized` and `align` give back the widget they were handed, /// so a fixture built through them can name one text three times, and then a /// case comparing six boxes compares four and says nothing about it. One /// fixture builds both lists, so checking the warm one checks both. #[track_caller] fn assert_same_regions( warm: &Harness, warm_ids: &[WidgetId], cold: &Harness, cold_ids: &[WidgetId], ) { assert_eq!( warm_ids.len(), cold_ids.len(), "the warm and cold fixtures list different widgets" ); let named: HashSet<&WidgetId> = warm_ids.iter().collect(); assert_eq!( named.len(), warm_ids.len(), "a widget is listed twice: {warm_ids:?}" ); let mut wrong = Vec::new(); for (i, (&w, &c)) in warm_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, shrunk from seed 2 at depth 5. The stack is as tall as its /// first child, so its other children belong in that one-line box. A cold /// layout used to keep the span's answer from the larger measuring box while /// a repaint asked it in the stack's final box. fn plant_stack_in_its_sizing_childs_box(h: &mut Harness) -> Vec { let sizing = wtext("one line, overflowing whatever it is given") .size(16) .wrap(false) .add(&mut h.rsc); let filler = rect(Color::CYAN.alpha(252)).add(&mut h.rsc); let plain = wtext("one line, overflowing whatever it is given") .size(16) .wrap(false) .add(&mut h.rsc); let span = (filler, plain).span(Dir::DOWN).add(&mut h.rsc); let pad = Pad { padding: Padding::ZERO, inner: span.add_strong(&mut h.rsc), } .add(&mut h.rsc); let probe = rect(Color::RED).add(&mut h.rsc); let wide = rect(Color::YELLOW.alpha(252)).add(&mut h.rsc); let narrow = rect(Color::RED).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: 55.0, } .add(&mut h.rsc); let stack = Stack { children: vec![ sizing.add_strong(&mut h.rsc), pad.add_strong(&mut h.rsc), branch.add_strong(&mut h.rsc), ], size: StackSize::Child(0), } .add(&mut h.rsc); h.rsc .widgets_mut() .set_size_rules(stack.id(), Some(LayoutLen::LEFTOVER), None); h.set_root(stack); vec![ sizing.id(), filler.id(), plain.id(), span.id(), pad.id(), probe.id(), wide.id(), narrow.id(), branch.id(), stack.id(), ] } #[test] fn repainting_a_stack_uses_the_box_its_sizing_child_decided() { let mut warm = Harness::new((900, 1200)); let ids = plant_stack_in_its_sizing_childs_box(&mut warm); for &id in &ids { warm.rsc.widgets_mut().mark_for_redraw(id); } warm.frame(); let mut cold = Harness::new((900, 1200)); let cold_ids = plant_stack_in_its_sizing_childs_box(&mut cold); assert_same_regions(&warm, &ids, &cold, &cold_ids); } /// Ten widgets, shrunk from seed 108 at depth 5. The nested reverse spans /// evaluate the branch in successively narrower boxes. The answer from the /// final, decided box must be the one retained after every span is reordered. fn plant_branch_in_nested_reverse_spans( h: &mut Harness, reordered: bool, ) -> (Vec, [WeakWidget; 3]) { let pair = |first: StrongWidget, second: StrongWidget| match reordered { true => vec![second, first], false => vec![first, second], }; let probe = rect(Color::RED.alpha(63)).add(&mut h.rsc); let wide = rect(Color::RED).add(&mut h.rsc); let narrow = wtext(PARAGRAPH).size(16).wrap(true).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: 483.0, } .add(&mut h.rsc); let wrapped = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc); let down = Span { children: pair( branch.add_strong(&mut h.rsc), wrapped.add_strong(&mut h.rsc), ), dir: Dir::DOWN, gap: Px::ZERO, } .add(&mut h.rsc); let inner_filler = rect(Color::CYAN.alpha(63)).add(&mut h.rsc); let inner = Span { children: pair( down.add_strong(&mut h.rsc), inner_filler.add_strong(&mut h.rsc), ), dir: Dir::LEFT, gap: Px::ZERO, } .height(LayoutLen::rel(1.0)) .add(&mut h.rsc); let outer_filler = rect(Color::GREEN.alpha(63)).add(&mut h.rsc); let outer = Span { children: pair( inner.add_strong(&mut h.rsc), outer_filler.add_strong(&mut h.rsc), ), dir: Dir::LEFT, gap: Px::ZERO, } .height(LayoutLen::rel(1.0)) .add(&mut h.rsc); h.set_root(outer); ( vec![ probe.id(), wide.id(), narrow.id(), branch.id(), wrapped.id(), down.id(), inner_filler.id(), inner.id(), outer_filler.id(), outer.id(), ], [down, inner, outer], ) } #[test] fn reordering_nested_spans_keeps_the_answer_from_the_decided_box() { let mut warm = Harness::new((900, 1200)); let (ids, spans) = plant_branch_in_nested_reverse_spans(&mut warm, false); for span in spans { warm.rsc[span].children.rotate_left(1); } warm.frame(); let mut cold = Harness::new((900, 1200)); let (cold_ids, _) = plant_branch_in_nested_reverse_spans(&mut cold, true); assert_same_regions(&warm, &ids, &cold, &cold_ids); } /// Four 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) .width(76) .add(&mut h.rsc); h.rsc .widgets_mut() .set_alignment(wrapped, Axis::X, AxisAlign::POS); h.rsc .widgets_mut() .set_alignment(wrapped, Axis::Y, AxisAlign::POS); let stack = Stack { children: vec![plain.add_strong(&mut h.rsc), wrapped.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(), 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().mark_for_redraw(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().mark_for_redraw(id); } warm.frame(); let mut cold = Harness::new((640, 900)); let cold_ids = plant(&mut cold); assert_same_regions(&warm, &ids, &cold, &cold_ids); } /// Four 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); h.rsc .widgets_mut() .set_alignment(text, Axis::X, AxisAlign::NEG); let inner = (text,).span(Dir::RIGHT).sized((189, 176)).add(&mut h.rsc); let filler = rect(Color::RED).add(&mut h.rsc); let root = (filler, inner).span(Dir::RIGHT).add(&mut h.rsc); h.state.root = Some(root.add_strong(&mut h.rsc)); vec![text.id(), inner.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(); assert_same_regions(&warm, &ids, &cold, &cold_ids); } /// Three 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); h.rsc .widgets_mut() .set_alignment(span, Axis::X, AxisAlign::CENTER); h.state.root = Some(span.add_strong(&mut h.rsc)); (vec![wrapped.id(), plain.id(), span.id()], span) } #[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(); assert_same_regions(&warm, &ids, &cold, &cold_ids); } /// Seven 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 fixed = rect(Color::RED).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(), 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(); assert_same_regions(&warm, &ids, &cold, &cold_ids); } /// 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); h.rsc .widgets_mut() .set_alignment(scroll, Axis::X, AxisAlign::NEG); h.set_root(scroll); (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(); assert_same_regions(&warm, &ids, &cold, &cold_ids); } /// 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().mark_for_redraw(ids[0]); warm.frame(); let mut cold = Harness::new((900, 1200)); let cold_ids = plant_nested_scrolls(&mut cold); assert_same_regions(&warm, &ids, &cold, &cold_ids); } /// 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(); assert_same_regions(&warm, &ids, &cold, &cold_ids); } const PARAGRAPH: &str = "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."; fn plant_stack_resized_from_free(h: &mut Harness, fixed: bool) -> (Vec, WidgetId) { let sizing = rect(Color::CYAN.alpha(126)).add(&mut h.rsc); h.rsc.widgets_mut().set_size_rules(sizing.id(), None, None); if fixed { h.rsc.widgets_mut().set_size_rules( sizing.id(), Some(LayoutLen::px(112)), Some(LayoutLen::px(101)), ); } let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc); let pad = Pad { padding: Padding::ZERO, inner: text.add_strong(&mut h.rsc), } .add(&mut h.rsc); let stack = Stack { children: vec![sizing.add_strong(&mut h.rsc), pad.add_strong(&mut h.rsc)], size: StackSize::Child(0), } .add(&mut h.rsc); h.set_root(stack); ( vec![sizing.id(), text.id(), pad.id(), stack.id()], sizing.id(), ) } #[test] fn fixing_a_stacks_sizing_child_repositions_its_overlay() { let mut warm = Harness::new((900, 1200)); let (ids, sizing) = plant_stack_resized_from_free(&mut warm, false); warm.frame(); warm.rsc.widgets_mut().set_size_rules( sizing, Some(LayoutLen::px(112)), Some(LayoutLen::px(101)), ); warm.frame(); let mut cold = Harness::new((900, 1200)); let (cold_ids, _) = plant_stack_resized_from_free(&mut cold, true); assert_same_regions(&warm, &ids, &cold, &cold_ids); } /// Eight widgets, shrunk from a 118-widget tree (seed 1121, depth 4, /// `shuffle-swap-for-three`). The stack takes its size from the span above, /// the span takes its width from the longest line of the texts in it, and /// the text below the span is then wrapped at that width -- so a width the /// shaper measured comes back to it as the box to break in. fn plant_a_measured_width(h: &mut Harness, swapped: bool) -> (WeakWidget, WidgetId) { let first: StrongWidget = rect(Color::YELLOW).add_strong(&mut h.rsc); let mut inner = Span::empty(Dir::UP); inner.children = match swapped { true => swapped_in(h), false => vec![first], }; let inner = inner.height(142).add(&mut h.rsc); let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc); let stack = Stack { children: vec![inner.add_strong(&mut h.rsc), text.add_strong(&mut h.rsc)], size: StackSize::Child(0), } .add(&mut h.rsc); h.set_root((stack,).span(Dir::DOWN).width(195)); (inner, text.id()) } /// What the span holds once its children have been swapped, which is what /// the warm tree is changed to and what the cold one is grown with. fn swapped_in(h: &mut Harness) -> Vec { let paragraph = |h: &mut Harness| -> StrongWidget { wtext(PARAGRAPH).size(16).wrap(true).add_strong(&mut h.rsc) }; vec![ paragraph(h), rect(Color::YELLOW).add_strong(&mut h.rsc), paragraph(h), ] } /// A text handed back the width it measured breaks there the way it broke /// when it measured it. The width the shaper answers is not on the grid, and /// a report rounded to the nearest step is under the longest line half the /// time: a warm tree then keeps a break made in a wider box while a cold one /// makes a narrower break in the same box, and the paragraph gains a line. #[test] fn a_text_is_given_back_a_box_the_line_it_measured_fits_in() { let mut warm = Harness::new((900, 1200)); let (inner, text) = plant_a_measured_width(&mut warm, false); warm.frame(); warm.rsc[inner].children = swapped_in(&mut warm); warm.frame(); let mut cold = Harness::new((900, 1200)); let (_, cold_text) = plant_a_measured_width(&mut cold, true); cold.frame(); assert_eq!(warm.region(&text), cold.region(&cold_text)); } #[test] fn adding_text_to_a_reverse_row_keeps_its_shared_height() { fn build( h: &mut Harness, changed: bool, ) -> (WeakWidget, WeakWidget, Vec) { let wrap = 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_strong(&mut h.rsc); let one = || { wtext("one line, overflowing whatever it is given") .size(16) .wrap(false) }; let plain = one().add_strong(&mut h.rsc); let shared = one() .width(LayoutLen::LEFTOVER) .height(LayoutLen::LEFTOVER) .add(&mut h.rsc); let mut extra: Vec = vec![ rect(Color::RED).add_strong(&mut h.rsc), one().add_strong(&mut h.rsc), one().add_strong(&mut h.rsc), ]; let children: Vec = if changed { let mut children: Vec = vec![plain, shared.add_strong(&mut h.rsc)]; children.append(&mut extra); children } else { vec![wrap, plain, shared.add_strong(&mut h.rsc)] }; let row = Span { children, dir: Dir::LEFT, gap: Px::ZERO, } .height(LayoutLen::rel(1.0)) .add(&mut h.rsc); let fill: StrongWidget = rect(Color::BLUE).add_strong(&mut h.rsc); let children: Vec = vec![fill, row.add_strong(&mut h.rsc)]; let root = Span { children, dir: Dir::RIGHT, gap: Px::from_int(4), } .height(LayoutLen::rel(1.0)) .add(&mut h.rsc); h.set_root(root); (row, shared, extra) } let mut warm = Harness::new((900, 1200)); let (row, shared, extra) = build(&mut warm, false); warm.rsc[row].children.remove(0); warm.rsc[row].children.extend(extra); warm.frame(); let mut cold = Harness::new((900, 1200)); let (_, other, _) = build(&mut cold, true); assert_eq!(warm.region(&shared), cold.region(&other)); } /// Nine widgets, shrunk from seed 946 at depth 6. The column is a share of /// the row while its rect has room to draw and a fixed width once it has /// not, so the row asks it twice: in the room, where it answers a share, /// and in its slot, where it answers its text's width. Emptying the column /// changes only the first answer. A local redraw that asked only the second /// question kept the row as it was; the column has to defer to the row. fn plant_column_that_is_a_share_only_while_its_rect_fits( h: &mut Harness, emptied: bool, ) -> (Vec, WeakWidget, Vec) { let first = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc); let filler = rect(Color::CYAN.alpha(126)).add(&mut h.rsc); let second = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc); let mut spare: Vec = vec![filler.add_strong(&mut h.rsc), second.add_strong(&mut h.rsc)]; let mut children: Vec = vec![first.add_strong(&mut h.rsc)]; if !emptied { children.append(&mut spare); } let column = Span { children, dir: Dir::DOWN, gap: Px::ZERO, } .height(159) .add(&mut h.rsc); let left = rect(Color::MAGENTA.alpha(189)).add(&mut h.rsc); let right = rect(Color::BLUE.alpha(0)).add(&mut h.rsc); let row = Span { children: vec![ left.add_strong(&mut h.rsc), column.add_strong(&mut h.rsc), right.add_strong(&mut h.rsc), ], dir: Dir::RIGHT, gap: Px::ZERO, } .add(&mut h.rsc); let end = rect(Color::MAGENTA.alpha(189)).add(&mut h.rsc); let root = Span { children: vec![end.add_strong(&mut h.rsc), row.add_strong(&mut h.rsc)], dir: Dir::LEFT, gap: Px::ZERO, } .add(&mut h.rsc); h.set_root(root); ( vec![ first.id(), filler.id(), second.id(), column.id(), left.id(), right.id(), row.id(), end.id(), root.id(), ], column, spare, ) } #[test] fn emptying_a_column_the_row_asked_twice_asks_the_row_again() { let mut warm = Harness::new((900, 1200)); let (ids, column, _spare) = plant_column_that_is_a_share_only_while_its_rect_fits(&mut warm, false); warm.frame(); // Kept alive: dropping the last share of a widget frees its id. let _removed: Vec = warm.rsc[column].children.drain(1..).collect(); warm.frame(); let mut cold = Harness::new((900, 1200)); let (cold_ids, _, _spare) = plant_column_that_is_a_share_only_while_its_rect_fits(&mut cold, true); cold.frame(); assert_same_regions(&warm, &ids, &cold, &cold_ids); } /// Six widgets, shrunk from seed 59 at depth 5 (`resize-size`). The column /// divides the box it is given between two shares, so its drawing holds for /// that box's length alone, and the pads above it pass that dependency up: /// each one's box is a part of the box it was asked in. Padding narrowing /// the frame it hands down does not change that, and while it was taken to, /// changing the rule over the pads relocated the column's drawing into the /// new box instead of dividing it again. fn plant_two_shares_under_two_pads(h: &mut Harness, height: f32) -> Vec { let top = rect(Color::CYAN.alpha(126)).add(&mut h.rsc); let bottom = rect(Color::RED).add(&mut h.rsc); let column = (top, bottom).span(Dir::DOWN).add(&mut h.rsc); let inner = Pad { padding: Padding::ZERO, inner: column.add_strong(&mut h.rsc), } .add(&mut h.rsc); let outer = Pad { padding: Padding::ZERO, inner: inner.add_strong(&mut h.rsc), } .height(height) .add(&mut h.rsc); let beside = rect(Color::BLUE).add(&mut h.rsc); h.set_root((outer, beside).span(Dir::RIGHT)); vec![ top.id(), bottom.id(), column.id(), inner.id(), outer.id(), beside.id(), ] } #[test] fn changing_a_rule_over_two_pads_divides_the_column_again() { let mut warm = Harness::new((900, 1200)); let ids = plant_two_shares_under_two_pads(&mut warm, 88.0); warm.frame(); warm.rsc .widgets_mut() .set_size_rules(ids[4], None, Some(LayoutLen::px(105))); warm.frame(); let mut cold = Harness::new((900, 1200)); let cold_ids = plant_two_shares_under_two_pads(&mut cold, 105.0); cold.frame(); assert_same_regions(&warm, &ids, &cold, &cold_ids); } /// Six widgets, shrunk from seed 942 at depth 6 (`resize`). A `Branch` asks /// its probe in the top 40 px of its box and forwards the frame, so the /// scroll's own box is 40 px tall whatever the window is -- but its content /// is as tall as the frame, which is the window, and a scroll kept to its /// end has to be told when that changes. Resolving a length against the /// window is what reads it, so that is where the dependency is taken. fn plant_a_window_tall_column_in_a_short_scroll(h: &mut Harness) -> Vec { let leaf = rect(Color::RED).add(&mut h.rsc); let column = Span { children: vec![leaf.add_strong(&mut h.rsc)], dir: Dir::RIGHT, gap: Px::ZERO, } .height(rel(1.0)) .add(&mut h.rsc); let scroll = Scroll::new(column.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc); let wide = rect(Color::BLUE).add(&mut h.rsc); let narrow = rect(Color::GREEN).add(&mut h.rsc); let root = Branch { probe: scroll.add_strong(&mut h.rsc), wide: wide.add_strong(&mut h.rsc), narrow: narrow.add_strong(&mut h.rsc), threshold: 55.0, } .add(&mut h.rsc); h.set_root(root); vec![leaf.id(), column.id(), scroll.id(), root.id()] } #[test] fn resizing_under_a_short_scroll_snaps_its_window_tall_content_again() { let mut warm = Harness::new((1920, 1200)); let ids = plant_a_window_tall_column_in_a_short_scroll(&mut warm); warm.frame(); warm.resize((640, 900)); warm.frame(); let mut cold = Harness::new((640, 900)); let cold_ids = plant_a_window_tall_column_in_a_short_scroll(&mut cold); cold.frame(); assert_same_regions(&warm, &ids, &cold, &cold_ids); } /// A scroll clamps its position against the box it is drawn in, so drawing it /// once at one viewport and again at another writes state the second draw then /// reads. That the answer is still the one a cold layout gives is a property /// of the clamp, not something the layout enforces. #[test] fn a_scrolled_view_resized_lands_where_a_cold_layout_puts_it() { for amt in [10.0, 40.0, 90.0, 140.0] { let mut warm = Harness::new((100, 100)); let (_, warm_scroll) = plant_wider(&mut warm, 100.0); warm.move_to((50.0, 50.0)); warm.scroll((-amt, 0.0)); warm.frame(); warm.resize((160, 100)); warm.frame(); let mut cold = Harness::new((160, 100)); let (_, cold_scroll) = plant_wider(&mut cold, 100.0); cold.move_to((50.0, 50.0)); cold.scroll((-amt, 0.0)); cold.frame(); assert_eq!( warm.region(&warm_scroll), cold.region(&cold_scroll), "scrolled by {amt} then widened" ); } }