diff --git a/core/src/ui/active.rs b/core/src/ui/active.rs index 33f208b..daa6084 100644 --- a/core/src/ui/active.rs +++ b/core/src/ui/active.rs @@ -13,6 +13,8 @@ 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, pub mask: MaskIdx, pub layer: LayerId, } diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index 12aa6f0..6ed9cfe 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -20,6 +20,7 @@ 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, pub layer: usize, pub(super) id: WidgetId, } @@ -163,11 +164,17 @@ impl<'a> Painter<'a> { self.region } - pub fn output_size(&self) -> Vec2 { + /// 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.state.output_size } + /// This widget's box in pixels. Resolved against the output's size, so a + /// widget that reads it draws again when the output changes. pub fn px_size(&mut self) -> Vec2 { + self.reads_output = true; self.region.size().to_abs(self.state.output_size) } diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index be6e433..d364e9d 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -53,11 +53,21 @@ impl UiRenderState { ); } let root = root.into(); - if self.needs_full_redraw(root) { + if self.root_changed(root) { self.redraw_all(root, rsc); self.old_root = root.map(|r| r.id()); - self.resized = false; - } else if rsc.widgets().has_updates() { + } else if self.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. + for (&id, active) in &self.active { + if active.reads_output { + rsc.widgets_mut().needs_redraw.insert(id); + } + } + } + self.resized = false; + if rsc.widgets().has_updates() { self.redraw_updates(rsc); } } @@ -106,6 +116,7 @@ impl UiRenderState { primitives: Vec::new(), children: Vec::new(), size_deps: Vec::new(), + reads_output: false, rsc, }; @@ -122,6 +133,7 @@ impl UiRenderState { primitives, children, size_deps, + reads_output, layer, id, } = painter; @@ -142,6 +154,7 @@ impl UiRenderState { primitives, children, size_deps, + reads_output, mask, layer, }; @@ -197,7 +210,11 @@ impl UiRenderState { let offered = region.axis_mut(axis).len(); let had = old.axis_mut(axis).len(); match widget.on_resize(axis) { - OnResize::Scale => true, + // Remapping out of the old box only scales while that box + // had a relative extent; inside a fixed one every part became + // an offset from its start, which `mov` carries but cannot + // stretch. + OnResize::Scale => offered == had || had.rel != 0.0, // `Translate` is not acted on yet, and cannot be until a // drawing can sit somewhere other than its box. `region` is // both the box a widget was given and the box its primitives @@ -283,17 +300,12 @@ impl UiRenderState { root.into().map(|r| r.id()) != self.old_root } - // Scheduling and drawing must use the same full-redraw predicate. - fn needs_full_redraw<'a>(&self, root: impl Into>) -> bool { - self.root_changed(root) || self.resized - } - pub fn needs_redraw<'a>( &self, root: impl Into>, widgets: &Widgets, ) -> bool { - self.needs_full_redraw(root) || widgets.has_updates() + self.root_changed(root) || self.resized || widgets.has_updates() } pub fn active_widgets(&self) -> usize { @@ -328,10 +340,10 @@ impl UiRenderState { pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) { self.draw_started.remove(&id); // Whoever read this widget's size may be a different size now, so the - // highest reader is what draws; it reaches this one on the way down, - // where the mark is what stops it being reused as it stands. Clearing - // it here would hide the change from the draw that came to apply it. - if let Some(top) = self.highest_reader(id) { + // highest reader is what draws. Everything between the two is marked + // as well: their own boxes have not changed, so the mark is the only + // thing stopping the draw reusing its way past this widget. + if let Some(top) = self.mark_readers(id, rsc) { self.redraw(top, rsc); // Cleared by that draw if it reached here; if it did not, this is // no longer drawn and asking again would not end. @@ -360,8 +372,8 @@ impl UiRenderState { } /// The furthest ancestor that read this widget's size, directly or through - /// widgets that did the same. - fn highest_reader(&self, id: WidgetId) -> Option { + /// widgets that did the same, marking everything below it on the way. + fn mark_readers(&self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option { let mut top = None; let mut at = id; while let Some(active) = self.active.get(&at) @@ -371,6 +383,7 @@ impl UiRenderState { .get(&parent) .is_some_and(|p| p.size_deps.contains(&at)) { + rsc.widgets_mut().needs_redraw.insert(at); top = Some(parent); at = parent; } diff --git a/tests/layout.rs b/tests/layout.rs index 960b43e..85ee631 100644 --- a/tests/layout.rs +++ b/tests/layout.rs @@ -59,3 +59,53 @@ fn a_child_drawn_twice_moves_once() { assert_corners!(h, inner, (150, 0), (350, 200)); } + +#[test] +fn a_resize_lands_where_a_cold_start_would() { + let build = |h: &mut Harness| { + let para = wtext( + "Wrapping shapes one source into as many lines as its container leaves room \ + for, so the height of a paragraph is an answer rather than a setting.", + ) + .size(20) + .wrap(true) + .pad(16) + .add(&mut h.rsc); + let below = rect(Color::RED).add(&mut h.rsc); + let root = (para, below).span(Dir::DOWN).pad(12); + h.set_root(root); + (para, below) + }; + + let mut cold = Harness::new((900, 1200)); + let (cold_para, cold_below) = build(&mut cold); + + let mut resized = Harness::new((1920, 1200)); + let (para, below) = build(&mut resized); + resized.resize((900, 1200)); + resized.frame(); + + assert_eq!(resized.region(¶), cold.region(&cold_para), "paragraph"); + assert_eq!(resized.region(&below), cold.region(&cold_below), "below"); +} + +#[test] +fn a_fixed_box_is_drawn_again_rather_than_stretched() { + let mut h = Harness::new((400, 400)); + // The panel fills a stack sized by its sibling, so it is drawn in the + // whole box and then placed in the shorter one. Reusing it in that fixed + // box afterwards would leave it whatever height it happened to have. + let panel = rect(Color::BLUE).add(&mut h.rsc); + let leaf = rect(Color::RED).height(100).add(&mut h.rsc); + let stack = (panel, leaf) + .stack() + .size(StackSize::Child(1)) + .add(&mut h.rsc); + h.set_root(stack.align(Align::TOP)); + assert_corners!(h, panel, (0, 0), (400, 100)); + + h.rsc[leaf].y = Some(Len::abs(250)); + h.frame(); + + assert_corners!(h, panel, (0, 0), (400, 250)); +} diff --git a/tests/retained.rs b/tests/retained.rs index 8404317..6941a48 100644 --- a/tests/retained.rs +++ b/tests/retained.rs @@ -163,3 +163,90 @@ fn a_parent_that_only_read_a_hint_relays_out_when_the_hint_changes() { assert_corners!(h, inner, (0, 0), (400, 120)); } + +/// Reads the output's size, which nothing but its own draw can put right. +struct ReadsOutput { + draws: Rc>, +} + +impl Widget for ReadsOutput { + fn draw(&mut self, painter: &mut Painter) -> Size { + self.draws.set(self.draws.get() + 1); + Size::abs(painter.output_size() / 4.0) + } +} + +#[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); + h.set_root(leaf); + let settled = draws.get(); + + h.resize((800, 100)); + assert!(h.needs_redraw()); + h.frame(); + + assert_eq!( + draws.get(), + settled, + "its box is the same fraction of a different output" + ); + assert_corners!(h, leaf, (0, 0), (800, 100)); +} + +#[test] +fn a_resize_redraws_what_read_the_output() { + let mut h = Harness::new((400, 200)); + let draws = Rc::new(Cell::new(0)); + let leaf = ReadsOutput { + draws: draws.clone(), + } + .add(&mut h.rsc); + h.set_root(leaf); + let settled = draws.get(); + + h.resize((800, 100)); + h.frame(); + + assert_eq!(draws.get(), settled + 1); +} + +#[test] +fn narrowing_the_output_reflows_text_and_relays_out_around_it() { + let mut h = Harness::new((600, 400)); + let para = wtext( + "Wrapping shapes one source into as many lines as its container leaves \ + room for, so the height of a paragraph is an answer rather than a setting.", + ) + .size(20) + .wrap(true) + .add(&mut h.rsc); + let below = rect(Color::RED).add(&mut h.rsc); + h.set_root((para, below).span(Dir::DOWN)); + let top = h.region(&below).expect("drew nothing").top_left.y; + + h.resize((300, 400)); + h.frame(); + + let lower = h.region(&below).expect("drew nothing").top_left.y; + assert!(lower > top, "same words, half the width: {top} -> {lower}"); +} + +#[test] +fn a_change_two_levels_under_its_reader_still_reaches_it() { + let mut h = Harness::new((400, 400)); + // Every wrapper up to the outer pad read the size below it, so the outer + // pad is what draws again -- and the span it hands the box to is the same + // size as before, which is what lets a draw reuse its way past the leaf. + let (leaf, _) = counted(&mut h, Size::abs((100, 100).into()), OnResize::Redraw); + let padded = leaf.pad(10).add(&mut h.rsc); + let below = rect(Color::RED).add(&mut h.rsc); + h.set_root((padded, below).span(Dir::DOWN).pad(12)); + assert_corners!(h, below, (12, 132), (388, 388)); + + h.rsc[leaf].size = Size::abs((100, 200).into()); + h.frame(); + + assert_corners!(h, below, (12, 232), (388, 388)); +}