diff --git a/core/src/layout_diagnostics.rs b/core/src/layout_diagnostics.rs index b922e31..d5d8823 100644 --- a/core/src/layout_diagnostics.rs +++ b/core/src/layout_diagnostics.rs @@ -33,6 +33,7 @@ pub(crate) enum Counter { SizeReads, HintHits, HintMisses, + RetainedSizeHits, ReuseAttempts, ReuseExact, ReuseMoved, @@ -67,6 +68,7 @@ impl Counter { "draw-result size reads", "hint hits", "hint misses", + "retained size hits", "reuse attempts", "reuse exact", "reuse moved", diff --git a/core/src/ui/active.rs b/core/src/ui/active.rs index fb61086..df61ef0 100644 --- a/core/src/ui/active.rs +++ b/core/src/ui/active.rs @@ -19,8 +19,16 @@ pub struct ActiveData { pub children: Vec, /// The children whose size this widget read while drawing. pub size_deps: Vec, - /// Whether it read the output's size, and so is wrong when that changes. - pub reads_output: bool, + /// Offered pixel axes which flowed into this widget's reported size, + /// directly or through a child size it read. + pub size_box_inputs: [bool; 2], + /// Output axes read while producing `size`, distinct from the widget's + /// own box when that box has a fixed pixel length. + pub size_output_inputs: [bool; 2], + /// The output dimensions against which those dependencies were observed. + pub output_px: Vec2, + /// Output axes it read directly or while resolving its offered box. + pub reads_output: [bool; 2], /// The slot its primitives are positioned through: its own if its parent /// placed it, otherwise the nearest ancestor that has one. pub move_idx: MoveIdx, diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index ae1a2d5..24fdce8 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -23,7 +23,10 @@ pub struct Painter<'a> { pub(super) children: Vec, /// The children whose size this widget read while drawing. pub(super) size_deps: Vec, - pub(super) reads_output: bool, + /// Offered pixel axes which can affect the size this draw reports. + pub(super) size_box_inputs: [bool; 2], + pub(super) size_output_inputs: [bool; 2], + pub(super) reads_output: [bool; 2], /// The slot this widget's primitives are positioned through: its own if /// its parent placed it, otherwise the nearest ancestor that has one. pub(super) move_idx: MoveIdx, @@ -158,7 +161,7 @@ impl<'a> Painter<'a> { Some(hint) => { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::HintHits); - self.depend_on_size(id); + self.depend_on_size(id, false); Some(hint) } None => { @@ -169,10 +172,66 @@ impl<'a> Painter<'a> { } } - fn depend_on_size(&mut self, child: &StrongWidget) { + /// A retained child length valid under the region it is about to be + /// offered. Unlike a hint, this is contextual: it is kept only when none + /// of the offered pixel axes which produced it changed. + pub fn known_len( + &mut self, + child: &StrongWidget, + axis: Axis, + region: UiRegion, + ) -> Option { + if let Some(hint) = self.size_hint(child, axis) { + return Some(hint); + } + self.retained_size(child, region) + .map(|size| size.axis(axis)) + } + + fn retained_size( + &mut self, + child: &StrongWidget, + region: UiRegion, + ) -> Option { + let region = region.within(&self.region); + let (size, box_inputs, output_inputs) = + self.state + .retained_size(child.id(), region, self.move_idx, self.rsc.widgets())?; + #[cfg(feature = "layout-diagnostics")] + diag::bump(Counter::RetainedSizeHits); + self.depend_on_size_inputs(child, box_inputs, output_inputs); + Some(size) + } + + fn depend_on_size(&mut self, child: &StrongWidget, inherit_inputs: bool) { + let (box_inputs, output_inputs) = match inherit_inputs { + true => self + .state + .active + .get(&child.id()) + .map_or(([false; 2], [false; 2]), |active| { + (active.size_box_inputs, active.size_output_inputs) + }), + false => ([false; 2], [false; 2]), + }; + self.depend_on_size_inputs(child, box_inputs, output_inputs); + } + + fn depend_on_size_inputs( + &mut self, + child: &StrongWidget, + box_inputs: [bool; 2], + output_inputs: [bool; 2], + ) { if !self.size_deps.contains(&child.id()) { self.size_deps.push(child.id()); } + for (own, child) in self.size_box_inputs.iter_mut().zip(box_inputs) { + *own |= child; + } + for (own, child) in self.size_output_inputs.iter_mut().zip(output_inputs) { + *own |= child; + } } pub fn render_text( @@ -220,19 +279,41 @@ impl<'a> Painter<'a> { /// The output's size in pixels. A widget that reads it draws again when /// the output changes, since nothing else can put that right. pub fn output_size(&mut self) -> Vec2 { - self.reads_output = true; + self.reads_output = [true; 2]; + self.size_output_inputs = [true; 2]; self.state.output_size } + /// One axis of the output in pixels. Prefer this to [`Self::output_size`] + /// when the other axis cannot affect the size this widget reports. + pub fn output_len(&mut self, axis: Axis) -> f32 { + self.reads_output[axis as usize] = true; + self.size_output_inputs[axis as usize] = true; + self.state.output_size.axis(axis) + } + /// This widget's box in pixels. Resolved against the output's size and /// the boxes it sits within, so a widget that reads it draws again when /// the output changes. pub fn px_size(&mut self) -> Vec2 { - self.reads_output = true; + self.reads_output = [true; 2]; + self.size_box_inputs = [true; 2]; let region = self.state.moves.resolve(self.move_idx, self.region); region.size().to_abs(self.state.output_size) } + /// One axis of this widget's box in pixels. Prefer this to + /// [`Self::px_size`] when the other axis cannot affect the reported size. + pub fn px_len(&mut self, axis: Axis) -> f32 { + self.reads_output[axis as usize] = true; + self.size_box_inputs[axis as usize] = true; + let region = self.state.moves.resolve(self.move_idx, self.region); + region + .size() + .axis(axis) + .to_abs(self.state.output_size.axis(axis)) + } + pub fn text_data(&mut self) -> &mut TextData { &mut self.rsc.ui_mut().text } @@ -270,7 +351,7 @@ impl DrawResult<'_, '_, W> { diag::bump(Counter::SizeReads); diag::size_read(self.child.id(), self.painter.id, self.size); } - self.painter.depend_on_size(self.child); + self.painter.depend_on_size(self.child, true); self.size } diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 97c47b6..5c52aff 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -7,6 +7,11 @@ use crate::{ }; const AXES: [Axis; 2] = [Axis::X, Axis::Y]; +const LAYOUT_EPSILON_PX: f32 = 0.05; + +fn pixel_len_changed(old: f32, new: f32) -> bool { + (old - new).abs() > LAYOUT_EPSILON_PX +} pub struct UiRenderState { pub active: HashMap, @@ -14,7 +19,14 @@ pub struct UiRenderState { pub(super) output_size: Vec2, old_root: Option, - resized: bool, + resized: [bool; 2], + /// Content/state dirtiness whose retained size cannot answer a layout + /// question until that widget has drawn again. + invalid_sizes: HashSet, + /// Marks introduced only to traverse resize dependency paths. Unlike + /// content dirtiness, these may retain an answer whose observed pixel + /// axes did not change. + resize_marks: HashSet, draw_started: HashSet, /// A widget's move slot, which outlives any one `ActiveData`: a redraw /// replaces that while its children go on pointing at the slot. @@ -29,7 +41,9 @@ impl UiRenderState { layers: Default::default(), output_size: Vec2::ZERO, old_root: None, - resized: false, + resized: [false; 2], + invalid_sizes: Default::default(), + resize_marks: Default::default(), draw_started: Default::default(), slots: Default::default(), moves: Default::default(), @@ -38,10 +52,10 @@ impl UiRenderState { pub fn resize(&mut self, size: impl Into) { let size = size.into(); - if size != self.output_size { - self.output_size = size; - self.resized = true; + for (axis, resized) in AXES.into_iter().zip(self.resized.iter_mut()) { + *resized |= size.axis(axis) != self.output_size.axis(axis); } + self.output_size = size; } pub fn output_size(&self) -> Vec2 { @@ -53,6 +67,10 @@ impl UiRenderState { diag::bump(Counter::Updates); #[cfg(feature = "layout-diagnostics")] let _update = diag::timer(TimerKind::Update); + self.invalid_sizes.clear(); + self.invalid_sizes + .extend(rsc.widgets().needs_redraw.iter().copied()); + self.resize_marks.clear(); // safety mechanism for memory leaks; might wanna return a result instead so user can // decide whether to panic or not if !rsc.widgets().waiting.is_empty() { @@ -73,7 +91,7 @@ impl UiRenderState { if self.root_changed(root) { self.redraw_all(root, rsc); self.old_root = root.map(|r| r.id()); - } else if self.resized { + } else if self.resized.iter().any(|&resized| resized) { // A region is a fraction of the output plus an offset, resolved // against the window in the shader, so a resize moves the whole // drawing on its own. Only a widget that read pixels can be wrong. @@ -83,7 +101,19 @@ impl UiRenderState { let dependents: Vec<_> = self .active .iter() - .filter_map(|(&id, active)| active.reads_output.then_some(id)) + .filter_map(|(&id, active)| { + AXES.into_iter() + .zip(self.resized) + .any(|(axis, changed)| { + changed + && active.reads_output[axis as usize] + && pixel_len_changed( + active.output_px.axis(axis), + self.output_size.axis(axis), + ) + }) + .then_some(id) + }) .collect(); for id in dependents { #[cfg(feature = "layout-diagnostics")] @@ -93,12 +123,21 @@ impl UiRenderState { rsc.widgets_mut().needs_redraw.insert(top); } } + self.resize_marks.extend( + rsc.widgets() + .needs_redraw + .iter() + .filter(|id| !self.invalid_sizes.contains(id)) + .copied(), + ); } } if rsc.widgets().has_updates() { self.redraw_updates(rsc); } - self.resized = false; + self.resized = [false; 2]; + self.invalid_sizes.clear(); + self.resize_marks.clear(); } fn redraw_all(&mut self, root: Option<&StrongWidget>, rsc: &mut dyn UiRsc) { @@ -174,7 +213,9 @@ impl UiRenderState { primitives: Vec::new(), children: Vec::new(), size_deps: Vec::new(), - reads_output: false, + size_box_inputs: [false; 2], + size_output_inputs: [false; 2], + reads_output: [false; 2], move_idx, rsc, }; @@ -199,6 +240,8 @@ impl UiRenderState { primitives, children, size_deps, + size_box_inputs, + size_output_inputs, reads_output, move_idx, layer, @@ -222,13 +265,15 @@ impl UiRenderState { primitives, children, size_deps, + size_box_inputs, + size_output_inputs, + output_px: self.output_size, reads_output, move_idx, parent_move, mask, layer, }; - // remove old children that weren't kept for c in &old_children { if !active.children.contains(c) { @@ -238,6 +283,8 @@ impl UiRenderState { rsc.on_draw(&active); self.active.insert(id, active); + self.invalid_sizes.remove(&id); + self.resize_marks.remove(&id); size } @@ -272,6 +319,60 @@ impl UiRenderState { .to_abs(self.output_size) } + /// A clean widget's retained size, when the offered pixel axes which + /// produced that answer are unchanged. This observes the old answer only; + /// it does not move or otherwise reuse the widget's drawing. + pub(super) fn retained_size( + &self, + id: WidgetId, + region: UiRegion, + parent_move: MoveIdx, + widgets: &Widgets, + ) -> Option<(Size, [bool; 2], [bool; 2])> { + if self.size_is_invalid(id, widgets) || self.dirty_size_under(id, widgets) { + return None; + } + let active = self.active.get(&id)?; + if active.parent_move != parent_move { + return None; + } + let px = self.px_of(parent_move, region); + let valid_box = AXES + .into_iter() + .zip(active.size_box_inputs) + .all(|(axis, depends)| { + !depends || !pixel_len_changed(active.px.axis(axis), px.axis(axis)) + }); + let valid_output = + AXES.into_iter() + .zip(active.size_output_inputs) + .all(|(axis, depends)| { + !depends + || !pixel_len_changed( + active.output_px.axis(axis), + self.output_size.axis(axis), + ) + }); + (valid_box && valid_output).then_some(( + active.size, + active.size_box_inputs, + active.size_output_inputs, + )) + } + + fn size_is_invalid(&self, id: WidgetId, widgets: &Widgets) -> bool { + self.invalid_sizes.contains(&id) + || (widgets.needs_redraw.contains(&id) && !self.resize_marks.contains(&id)) + } + + fn dirty_size_under(&self, id: WidgetId, widgets: &Widgets) -> bool { + self.active.get(&id).is_some_and(|active| { + active.size_deps.iter().any(|child| { + self.size_is_invalid(*child, widgets) || self.dirty_size_under(*child, widgets) + }) + }) + } + /// The drawing a widget already has, kept for a new box if the box has not /// changed in a way it depends on. fn try_reuse( @@ -310,7 +411,7 @@ impl UiRenderState { let px = self.px_of(parent_move, region); let mut changed = [false; 2]; for (axis, c) in AXES.into_iter().zip(changed.iter_mut()) { - *c = px.axis(axis) != old_px.axis(axis); + *c = pixel_len_changed(old_px.axis(axis), px.axis(axis)); } if !changed.iter().any(|&c| c) && old_region == region { #[cfg(feature = "layout-diagnostics")] @@ -360,7 +461,6 @@ impl UiRenderState { self.moves.set(slot, region); let active = self.active.get_mut(&id).unwrap(); active.region = region; - active.px = px; #[cfg(feature = "layout-diagnostics")] { diag::bump(Counter::ReuseMoved); @@ -472,6 +572,8 @@ impl UiRenderState { self.slots.clear(); self.moves.clear(); self.layers.clear(); + self.invalid_sizes.clear(); + self.resize_marks.clear(); rsc.widgets_mut().needs_redraw.clear(); rsc.free(); } @@ -486,7 +588,7 @@ impl UiRenderState { // reader and gives each changing box its final constraints first. while let Some(id) = { let dirty = rsc.widgets().needs_redraw.iter().copied(); - match self.resized { + match self.resized.iter().any(|&resized| resized) { true => dirty.min_by_key(|&id| self.depth(id)), false => dirty.max_by_key(|&id| self.depth(id)), } @@ -519,7 +621,9 @@ impl UiRenderState { root: impl Into>, widgets: &Widgets, ) -> bool { - self.root_changed(root) || self.resized || widgets.has_updates() + self.root_changed(root) + || self.resized.iter().any(|&resized| resized) + || widgets.has_updates() } pub fn active_widgets(&self) -> usize { @@ -556,16 +660,20 @@ impl UiRenderState { /// redraws a widget that's currently active (drawn) pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) { self.draw_started.remove(&id); + if rsc.widgets().needs_redraw.contains(&id) && !self.resize_marks.contains(&id) { + self.invalid_sizes.insert(id); + } // A widget can only answer whether its size changed by drawing in the // box its parent chose. If that box changed in pixels, its retained // placement is stale and the highest size reader must choose the new // box first. Otherwise the widget can draw locally, and its readers // only matter if the returned size actually changed. - let box_changed = self - .active - .get(&id) - .is_some_and(|active| self.px_of(active.parent_move, active.region) != active.px); - if (self.resized || box_changed) + let box_changed = self.active.get(&id).is_some_and(|active| { + let px = self.px_of(active.parent_move, active.region); + AXES.into_iter() + .any(|axis| pixel_len_changed(active.px.axis(axis), px.axis(axis))) + }); + if (self.resized.iter().any(|&resized| resized) || box_changed) && let Some(top) = self.mark_readers(id, rsc) { #[cfg(feature = "layout-diagnostics")] @@ -611,6 +719,7 @@ impl UiRenderState { // Propagate one dependency edge at a time. If drawing the reader // does not change its own size, nothing above it can observe this. rsc.widgets_mut().needs_redraw.insert(parent); + self.invalid_sizes.insert(parent); #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::ReaderEdges); } diff --git a/src/widget/position/align.rs b/src/widget/position/align.rs index 17d15c9..806e377 100644 --- a/src/widget/position/align.rs +++ b/src/widget/position/align.rs @@ -7,17 +7,31 @@ pub struct Aligned { impl Widget for Aligned { fn draw(&mut self, painter: &mut Painter) -> Size { - // Drawn where it may be too big, then given its aligned box once its - // size is known. - let size = painter.place(&self.inner, UiRegion::FULL).size(); + let known = match self.align.tuple() { + (Some(_), Some(_)) => painter + .known_len(&self.inner, Axis::X, UiRegion::FULL) + .zip(painter.known_len(&self.inner, Axis::Y, UiRegion::FULL)) + .map(|(x, y)| Size { x, y }), + (Some(_), None) => painter + .known_len(&self.inner, Axis::X, UiRegion::FULL) + .map(|x| Size { x, y: Len::REST }), + (None, Some(_)) => painter + .known_len(&self.inner, Axis::Y, UiRegion::FULL) + .map(|y| Size { x: Len::REST, y }), + (None, None) => Some(Size::REST), + }; + // Drawn where it may be too big only when the aligned axes are not + // already known, then given its aligned box once its size is known. + let had_size = known.is_some(); + let size = known.unwrap_or_else(|| painter.place(&self.inner, UiRegion::FULL).size()); let region = match self.align.tuple() { (Some(x), Some(y)) => size.to_uivec2().align(RegionAlign { x, y }), (Some(x), None) => UiRegion::new(size.x.apply_rest().align(x), UiSpan::FULL), (None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_rest().align(y)), (None, None) => UiRegion::FULL, }; - painter.place(&self.inner, region); - size + let placed = painter.place(&self.inner, region).size(); + if had_size { placed } else { size } } /// The aligned box is a fraction of its own, so the child keeps its diff --git a/src/widget/position/scroll.rs b/src/widget/position/scroll.rs index 0b033e8..cfac561 100644 --- a/src/widget/position/scroll.rs +++ b/src/widget/position/scroll.rs @@ -11,13 +11,15 @@ pub struct Scroll { impl Widget for Scroll { fn draw(&mut self, painter: &mut Painter) -> Size { - let output_len = painter.output_size().axis(self.axis); - let container_len = UiScalar::abs(painter.px_size().axis(self.axis)); - // Draw in the whole container to learn the content's length, then - // place it at the scrolled offset. - let child = painter.place(&self.inner, UiRegion::FULL).size(); - let content_len = child - .axis(self.axis) + let output_len = painter.output_len(self.axis); + let container_len = UiScalar::abs(painter.px_len(self.axis)); + // Draw in the whole container only when its scrolling-axis length is + // not already known, then place it at the scrolled offset. + let known_len = painter.known_len(&self.inner, self.axis, UiRegion::FULL); + let measured = known_len.is_none(); + let child = measured.then(|| painter.place(&self.inner, UiRegion::FULL).size()); + let content_len = known_len + .unwrap_or_else(|| child.unwrap().axis(self.axis)) .apply_rest() .within_len(container_len) .to_abs(output_len); @@ -31,8 +33,8 @@ impl Widget for Scroll { let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0)); region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len); - painter.place(&self.inner, region); - child + let placed = painter.place(&self.inner, region).size(); + child.unwrap_or(placed) } } diff --git a/src/widget/position/span.rs b/src/widget/position/span.rs index 886fec2..7fe29cc 100644 --- a/src/widget/position/span.rs +++ b/src/widget/position/span.rs @@ -15,16 +15,14 @@ impl Widget for Span { let mut cursor = UiScalar::rel_min(); let mut lens = Vec::with_capacity(self.children.len()); for child in &self.children { - let len = match painter.size_hint(child, axis) { + let mut span = UiSpan::new(cursor, UiScalar::rel_max()); + if self.dir.sign == Sign::Neg { + span.flip(); + } + let region = UiRegion::from_axis(axis, span, UiSpan::FULL); + let len = match painter.known_len(child, axis, region) { Some(len) => len, - None => { - let mut span = UiSpan::new(cursor, UiScalar::rel_max()); - if self.dir.sign == Sign::Neg { - span.flip(); - } - let region = UiRegion::from_axis(axis, span, UiSpan::FULL); - painter.place(child, region).len(axis) - } + None => painter.place(child, region).len(axis), }; cursor.abs += len.abs + self.gap; cursor.rel += len.rel; diff --git a/src/widget/text/mod.rs b/src/widget/text/mod.rs index 0601b24..49a3fde 100644 --- a/src/widget/text/mod.rs +++ b/src/widget/text/mod.rs @@ -54,7 +54,7 @@ impl TextView { fn render(&mut self, painter: &mut Painter) -> &RenderedText { let width = if self.attrs.wrap { - Some(painter.px_size().x) + Some(painter.px_len(Axis::X)) } else { None }; diff --git a/tests/generated.rs b/tests/generated.rs index dc22fa8..5d561d0 100644 --- a/tests/generated.rs +++ b/tests/generated.rs @@ -17,20 +17,11 @@ use iris::prelude::*; use iris::random::{Edits, Lens, Rng, SpanEdit, Tree, grow}; const DEPTH: usize = 4; -const SEEDS: [u64; 6] = [1, 2, 3, 5, 8, 13]; -const REGION_ULPS: u32 = 4; - -fn ordered_bits(value: f32) -> u32 { - const SIGN: u32 = 1 << 31; - let bits = value.to_bits(); - match bits & SIGN { - 0 => bits | SIGN, - _ => !bits, - } -} +const SEEDS: [u64; 7] = [1, 2, 3, 5, 8, 13, 98]; +const REGION_EPSILON_PX: f32 = 0.05; fn same_coordinate(got: f32, want: f32) -> bool { - got == want || ordered_bits(got).abs_diff(ordered_bits(want)) <= REGION_ULPS + (got - want).abs() <= REGION_EPSILON_PX } fn same_region(got: Option, want: Option) -> bool { @@ -165,9 +156,10 @@ fn assert_same(seed: u64, what: &str, warm: (&Harness, &Tree), cold: (&Harness, for (i, (&w, &c)) in wt.ids.iter().zip(&ct.ids).enumerate() { let (got, want) = (wh.region(&w), ch.region(&c)); drawn += usize::from(got.is_some()); - // Equivalent composition orders can differ by a few f32 ULPs. Bound - // that representation drift directly, while whether a widget drew - // remains exact. + // This oracle cares where rasterization lands, not whether equivalent + // arithmetic produced the same f32. Keep the tolerance to one + // twentieth of a physical pixel, while whether a widget drew remains + // exact. if same_region(got, want) { continue; } @@ -327,9 +319,14 @@ fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() { /// same defect, and it wants fixing where the two draws meet -- LAYOUT.md ยง4 -- /// rather than anywhere in the chain. #[test] -#[ignore = "a hundred seeds, rather than the six the others check"] +#[ignore = "a hundred seeds, rather than the seven the others check"] fn a_long_run_of_seeds_agrees() { - for seed in 1..=100 { + let seeds = std::env::var("IRIS_GENERATED_SEED") + .ok() + .and_then(|seed| seed.parse().ok()) + .map(|seed| seed..=seed) + .unwrap_or(1..=100); + for seed in seeds { changed_size(seed); resized(seed); resized_then_changed(seed); diff --git a/tests/layout_diagnostics.rs b/tests/layout_diagnostics.rs index 54b208a..3748198 100644 --- a/tests/layout_diagnostics.rs +++ b/tests/layout_diagnostics.rs @@ -75,6 +75,22 @@ fn env(name: &str, fallback: T) -> T { .unwrap_or(fallback) } +#[cfg(feature = "layout-diagnostics")] +fn trace_selected(tree: &Tree) { + let Ok(value) = std::env::var("IRIS_TRACE_INDEX") else { + return; + }; + let index = value + .parse::() + .expect("IRIS_TRACE_INDEX must be a tree.ids index"); + let id = tree.ids[index]; + iris::core::layout_diagnostics::trace_widget(id); + println!("tracing tree.ids[{index}] = {id:?}"); +} + +#[cfg(not(feature = "layout-diagnostics"))] +fn trace_selected(_: &Tree) {} + fn warm(seed: u64, depth: usize) -> (Harness, Tree) { let mut harness = Harness::new(OUTPUT); let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default()); @@ -157,6 +173,7 @@ fn layout_cost() { "fixture: seed {seed}, depth {depth}, {} widgets", tree.ids.len() ); + trace_selected(&tree); #[cfg(feature = "layout-diagnostics")] let _ = iris::core::layout_diagnostics::take(); run("cold", 1, &mut harness, |_, _| {}); @@ -165,6 +182,7 @@ fn layout_cost() { if selected("repaint") { let (mut harness, tree) = warm(seed, depth); + trace_selected(&tree); let leaf = tree.ids[0]; run("repaint", frames, &mut harness, move |harness, _| { let _ = harness.rsc.widgets_mut().get_dyn_mut(leaf); @@ -173,6 +191,7 @@ fn layout_cost() { if selected("size") { let (mut harness, tree) = warm(seed, depth); + trace_selected(&tree); let sized = tree.sized[0]; run("size", frames, &mut harness, move |harness, frame| { harness.rsc[sized].x = Some(Len::abs(100.0 + (frame % 2) as f32 * 40.0)); @@ -181,6 +200,7 @@ fn layout_cost() { if selected("scroll") { let (mut harness, tree) = warm(seed, depth); + trace_selected(&tree); let scroll = tree.scrolls[0]; run("scroll", frames, &mut harness, move |harness, frame| { harness.rsc[scroll].scroll(if frame % 2 == 0 { 12.0 } else { -12.0 }); @@ -189,8 +209,9 @@ fn layout_cost() { if selected("resize") { let (mut harness, tree) = warm(seed, depth); + trace_selected(&tree); run("resize", frames, &mut harness, |harness, frame| { - harness.resize((OUTPUT.0 - (frame % 2) as f32 * 8.0, OUTPUT.1)); + harness.resize((OUTPUT.0 - ((frame + 1) % 2) as f32 * 8.0, OUTPUT.1)); }); drop(tree); } diff --git a/tests/retained.rs b/tests/retained.rs index 69f10dc..ac4437f 100644 --- a/tests/retained.rs +++ b/tests/retained.rs @@ -191,6 +191,17 @@ impl Widget for ReadsOutput { } } +struct ReadsWidth { + draws: Rc>, +} + +impl Widget for ReadsWidth { + fn draw(&mut self, painter: &mut Painter) -> Size { + self.draws.set(self.draws.get() + 1); + Size::abs((painter.output_len(Axis::X) / 4.0, 20.0).into()) + } +} + #[test] fn a_resize_does_not_redraw_what_the_shader_can_move() { let mut h = Harness::new((400, 200)); @@ -227,6 +238,65 @@ fn a_resize_redraws_what_read_the_output() { assert_eq!(draws.get(), settled + 1); } +#[test] +fn a_resize_only_redraws_read_output_axes() { + let mut h = Harness::new((400, 200)); + let draws = Rc::new(Cell::new(0)); + let leaf = ReadsWidth { + draws: draws.clone(), + } + .add(&mut h.rsc); + h.set_root(leaf); + let settled = draws.get(); + + h.resize((400, 300)); + h.frame(); + assert_eq!(draws.get(), settled, "height was never read"); + + h.resize((800, 300)); + h.frame(); + assert_eq!(draws.get(), settled + 1, "width changes its answer"); +} + +#[test] +fn subpixel_resize_changes_accumulate_from_the_last_layout() { + let mut h = Harness::new((400, 200)); + let draws = Rc::new(Cell::new(0)); + let leaf = ReadsWidth { + draws: draws.clone(), + } + .add(&mut h.rsc); + h.set_root(leaf); + let settled = draws.get(); + + for width in [400.02, 400.04, 400.05] { + h.resize((width, 200.0)); + h.frame(); + assert_eq!(draws.get(), settled); + } + + h.resize((400.06, 200.0)); + h.frame(); + assert_eq!(draws.get(), settled + 1); +} + +#[test] +fn subpixel_box_changes_accumulate_from_the_last_draw() { + let mut h = Harness::new((400, 200)); + let (first, draws, _) = pair(&mut h, OnResize::Redraw); + let settled = draws.get(); + + for width in [100.02, 100.04, 100.05] { + h.rsc[first].size.x = Len::abs(width); + h.frame(); + assert_eq!(draws.get(), settled); + } + + h.rsc[first].size.x = Len::abs(100.06); + h.frame(); + assert_eq!(draws.get(), settled + 1); +} + #[test] fn reporting_the_same_output_size_does_not_start_a_resize() { let mut h = Harness::new((400, 200));