diff --git a/core/src/ui/active.rs b/core/src/ui/active.rs index a984207..f8fb0e5 100644 --- a/core/src/ui/active.rs +++ b/core/src/ui/active.rs @@ -31,8 +31,6 @@ pub struct ActiveData { 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 db3ad0e..e3e9c1b 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -26,7 +26,6 @@ pub struct Painter<'a> { /// 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, @@ -286,7 +285,6 @@ 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; 2]; self.size_output_inputs = [true; 2]; self.state.output_size } @@ -294,7 +292,6 @@ impl<'a> Painter<'a> { /// 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) } @@ -303,7 +300,6 @@ impl<'a> Painter<'a> { /// 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; 2]; self.size_box_inputs = [true; 2]; let region = self.state.moves.resolve(self.move_idx, self.region); region.size().to_px(self.state.output_size) @@ -312,7 +308,6 @@ impl<'a> Painter<'a> { /// 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 diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 8667dcf..630b82c 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -21,14 +21,12 @@ pub struct UiRenderState { old_root: Option, /// The slot every chain bottoms out in, holding the output as a box. root_move: MoveIdx, - resized: [bool; 2], + /// Widgets whose reported size depends on the root box rather than on + /// their own, so nothing below them changing length can reach them. + root_readers: HashSet, /// 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, /// What has already been drawn during the pass under way, so a widget /// reached by redrawing an ancestor is not drawn again on its own /// account. Emptied when the pass ends. @@ -46,13 +44,12 @@ impl UiRenderState { layers: Default::default(), output_size: Vec2::ZERO, old_root: None, - resized: [false; 2], invalid_sizes: Default::default(), - resize_marks: Default::default(), draw_started: Default::default(), slots: Default::default(), moves: Default::default(), root_move: MoveIdx::NONE, + root_readers: Default::default(), } } @@ -72,14 +69,25 @@ impl UiRenderState { } pub fn resize(&mut self, size: impl Into) { - let size = size.into(); - 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; + self.output_size = size.into(); self.write_root(); } + /// Which axes of the root widget's box are no longer the ones the root + /// slot holds, which is all a resize now is: one slot written, found by + /// the same comparison every other box change is found by. + fn root_axes_changed(&self) -> [bool; 2] { + let Some(active) = self.old_root.and_then(|root| self.active.get(&root)) else { + return [false; 2]; + }; + let px = self.px_of(active.parent_move, active.region); + let mut changed = [false; 2]; + for (axis, c) in AXES.into_iter().zip(changed.iter_mut()) { + *c = pixel_len_changed(active.px.axis(axis), px.axis(axis)); + } + changed + } + pub fn output_size(&self) -> Vec2 { self.output_size } @@ -92,7 +100,6 @@ impl UiRenderState { 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() { @@ -113,53 +120,52 @@ impl UiRenderState { if self.root_changed(root) { self.redraw_all(root, rsc); self.old_root = root.map(|r| r.id()); - } 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. - { - #[cfg(feature = "layout-diagnostics")] - let _marking = diag::timer(TimerKind::ResizeMarking); - let dependents: Vec<_> = self + } else if self.root_axes_changed().iter().any(|&c| c) { + // Every box is a part of the root box, so writing it is a box + // that changed length like any other. Offering the root widget + // its box again puts that through `try_reuse`, which answers per + // axis and lets `redraws_under` price the subtree -- rather than + // marking it, which would redraw it whichever axis moved. What + // that cannot reach is a widget whose size came from the root box + // instead of its own, since its own box need not have changed. + #[cfg(feature = "layout-diagnostics")] + let _marking = diag::timer(TimerKind::ResizeMarking); + let changed = self.root_axes_changed(); + for id in self.root_readers.clone() { + let reads = self .active - .iter() - .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")] - diag::bump(Counter::ResizeDependents); - rsc.widgets_mut().needs_redraw.insert(id); - if let Some(top) = self.mark_readers(id, rsc) { - rsc.widgets_mut().needs_redraw.insert(top); - } + .get(&id) + .map_or([false; 2], |active| active.size_output_inputs); + if !AXES + .into_iter() + .zip(changed) + .any(|(axis, c)| c && reads[axis as usize]) + { + continue; } - self.resize_marks.extend( - rsc.widgets() - .needs_redraw - .iter() - .filter(|id| !self.invalid_sizes.contains(id)) - .copied(), + #[cfg(feature = "layout-diagnostics")] + diag::bump(Counter::ResizeDependents); + rsc.widgets_mut().needs_redraw.insert(id); + } + if let Some(root) = root { + self.draw_inner( + 0, + root.id(), + UiRegion::FULL, + None, + 1, + self.root_move, + false, + MaskIdx::NONE, + None, + rsc, ); } } if rsc.widgets().has_updates() { self.redraw_updates(rsc); } - self.resized = [false; 2]; self.invalid_sizes.clear(); - self.resize_marks.clear(); self.draw_started.clear(); } @@ -242,7 +248,6 @@ impl UiRenderState { depth, size_box_inputs: [false; 2], size_output_inputs: [false; 2], - reads_output: [false; 2], move_idx, rsc, }; @@ -269,7 +274,6 @@ impl UiRenderState { size_deps, size_box_inputs, size_output_inputs, - reads_output, move_idx, layer, depth: _, @@ -297,7 +301,6 @@ impl UiRenderState { size_box_inputs, size_output_inputs, output_px: self.output_size, - reads_output, move_idx, parent_move, mask, @@ -310,10 +313,13 @@ impl UiRenderState { } } + match active.size_output_inputs.iter().any(|&reads| reads) { + true => self.root_readers.insert(id), + false => self.root_readers.remove(&id), + }; rsc.on_draw(&active); self.active.insert(id, active); self.invalid_sizes.remove(&id); - self.resize_marks.remove(&id); size } @@ -390,8 +396,7 @@ impl UiRenderState { } 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)) + self.invalid_sizes.contains(&id) || widgets.needs_redraw.contains(&id) } fn dirty_size_under(&self, id: WidgetId, widgets: &Widgets) -> bool { @@ -458,10 +463,14 @@ impl UiRenderState { self.keep_depth(id, depth); return Some(size); } - // Only a placed widget can be given a different box without drawing - // again: everything it drew is a fraction of its slot's box, so one - // entry says where all of it went. - if slot == parent_move { + // Only a placed widget can be given a different *region* without + // drawing again: it has an entry of its own to say where it went, + // where an unslotted one shares its parent's and has nothing to + // write. Its parent's box changing length is not that -- everything + // it drew is a fraction of that box, so the slot already above it + // carries the change and `on_resize` below decides whether the + // drawing survives it. + if slot == parent_move && old_region != region { #[cfg(feature = "layout-diagnostics")] { diag::bump(Counter::ReuseUnslotted); @@ -495,7 +504,9 @@ impl UiRenderState { return None; } } - self.moves.set(slot, region); + if slot != parent_move { + self.moves.set(slot, region); + } self.keep_depth(id, depth); let active = self.active.get_mut(&id).unwrap(); active.region = region; @@ -612,7 +623,6 @@ impl UiRenderState { self.root_move = MoveIdx::NONE; self.layers.clear(); self.invalid_sizes.clear(); - self.resize_marks.clear(); self.draw_started.clear(); rsc.widgets_mut().needs_redraw.clear(); rsc.free(); @@ -631,10 +641,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.iter().any(|&resized| resized) { - true => dirty.min_by_key(|&id| self.depth(id)), - false => dirty.max_by_key(|&id| self.depth(id)), - } + dirty.max_by_key(|&id| self.depth(id)) } { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::QueuePops); @@ -685,7 +692,7 @@ impl UiRenderState { widgets: &Widgets, ) -> bool { self.root_changed(root) - || self.resized.iter().any(|&resized| resized) + || self.root_axes_changed().iter().any(|&c| c) || widgets.has_updates() } @@ -723,7 +730,7 @@ 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) { + if rsc.widgets().needs_redraw.contains(&id) { self.invalid_sizes.insert(id); } // A widget can only answer whether its size changed by drawing in the @@ -736,9 +743,7 @@ impl UiRenderState { 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) - { + if box_changed && let Some(top) = self.mark_readers(id, rsc) { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::EagerReaderRedraws); self.redraw(top, rsc); diff --git a/tests/retained.rs b/tests/retained.rs index 816a62b..86edb82 100644 --- a/tests/retained.rs +++ b/tests/retained.rs @@ -191,6 +191,8 @@ impl Widget for ReadsOutput { } } +/// Reads the output across one axis only, and says so: its drawing follows +/// a taller box on its own, so only a wider one is worth a draw. struct ReadsWidth { draws: Rc>, } @@ -200,12 +202,19 @@ impl Widget for ReadsWidth { self.draws.set(self.draws.get() + 1); Size::px((painter.output_len(Axis::X) / 4.0, 20.0).into()) } + + fn on_resize(&self, axis: Axis) -> OnResize { + match axis { + Axis::X => OnResize::Redraw, + Axis::Y => OnResize::Scale, + } + } } #[test] fn a_resize_does_not_redraw_what_the_shader_can_move() { let mut h = Harness::new((400, 200)); - let (leaf, draws) = counted(&mut h, Size::REST, OnResize::Redraw); + let (leaf, draws) = counted(&mut h, Size::REST, OnResize::Scale); h.set_root(leaf); let settled = draws.get(); @@ -216,11 +225,28 @@ fn a_resize_does_not_redraw_what_the_shader_can_move() { assert_eq!( draws.get(), settled, - "its box is the same fraction of a different output" + "a scaling drawing follows its box, and the output is one" ); assert_corners!(h, leaf, (0, 0), (800, 100)); } +/// The output is the root of the box chain, so a resize is a box that changed +/// length and `OnResize` answers for it -- there is not a second rule for the +/// window. A drawing that does not scale is redrawn whichever box moved. +#[test] +fn a_resize_redraws_what_does_not_scale() { + let mut h = Harness::new((400, 200)); + let (leaf, draws) = counted(&mut h, Size::REST, OnResize::Redraw); + h.set_root(leaf); + let settled = draws.get(); + + h.resize((800, 100)); + h.frame(); + + assert_eq!(draws.get(), settled + 1, "its box is a different length"); + assert_corners!(h, leaf, (0, 0), (800, 100)); +} + #[test] fn a_resize_redraws_what_read_the_output() { let mut h = Harness::new((400, 200));