Move a resized drawing instead of redrawing it
A region is a fraction of the output plus an offset and the shader resolves it against the window every frame, so a resize already moves the whole drawing without the CPU. Wiping the tree and drawing it again was throwing that away. `Painter::output_size` and `px_size` now record that a widget read pixels, the way reading a child's size records a dependency on it, and a resize marks only those. In the `text` example that is the two wrapping paragraphs out of forty-odd widgets; everything else keeps its drawing and the window uniform puts it in the right place. Two defects the change surfaced, both of which made a resize land somewhere a cold start would not: `redraw` climbed to the highest reader of the changed widget and drew from there, trusting that draw to reach back down. It does not: an intermediate whose own box has not changed is reused as it stands and the draw stops there. Everything between the two is now marked as well, which is the only thing that stops the reuse. Not resize-specific -- `a_change_two_levels_under_its_reader_still_reaches_it` fails on the mutation path too. `mov` cannot stretch a drawing out of a box with no relative extent. `UiScalar::within` puts a part into such a box as a plain offset from its start, and `lerp_inv`'s divide-by-zero fallback then returns a rel of 0 rather than saying it cannot invert, so the remap silently leaves the drawing its old size. `OnResize::Scale` now only reuses across a length change when the old box had a relative extent. The underlying loss belongs to the position chain, which separates the drawn box from the offered one; until then this is the honest predicate. Checked: fmt, clippy and 33 tests. `tabs` (with the image replay), `view`, `minimal` still byte-identical to `upstream/main`, and `text` unchanged at 1920x1200 and 900x1200. Driven live under the GPU as well: started at 1920x1200, resized to 900x1200 and back through sway, and each screenshot matches a cold start at that size byte for byte.
This commit is contained in:
1 parent
9520996623
commit
984f482a7f
5 files changed
+175
-16
No files matched your search
@@ -13,6 +13,8 @@ pub struct ActiveData {
|
|||||||
pub children: Vec<WidgetId>,
|
pub children: Vec<WidgetId>,
|
||||||
/// The children whose size this widget read while drawing.
|
/// The children whose size this widget read while drawing.
|
||||||
pub size_deps: Vec<WidgetId>,
|
pub size_deps: Vec<WidgetId>,
|
||||||
|
/// Whether it read the output's size, and so is wrong when that changes.
|
||||||
|
pub reads_output: bool,
|
||||||
pub mask: MaskIdx,
|
pub mask: MaskIdx,
|
||||||
pub layer: LayerId,
|
pub layer: LayerId,
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,7 @@ pub struct Painter<'a> {
|
|||||||
pub(super) children: Vec<WidgetId>,
|
pub(super) children: Vec<WidgetId>,
|
||||||
/// The children whose size this widget read while drawing.
|
/// The children whose size this widget read while drawing.
|
||||||
pub(super) size_deps: Vec<WidgetId>,
|
pub(super) size_deps: Vec<WidgetId>,
|
||||||
|
pub(super) reads_output: bool,
|
||||||
pub layer: usize,
|
pub layer: usize,
|
||||||
pub(super) id: WidgetId,
|
pub(super) id: WidgetId,
|
||||||
}
|
}
|
||||||
@@ -163,11 +164,17 @@ impl<'a> Painter<'a> {
|
|||||||
self.region
|
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
|
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 {
|
pub fn px_size(&mut self) -> Vec2 {
|
||||||
|
self.reads_output = true;
|
||||||
self.region.size().to_abs(self.state.output_size)
|
self.region.size().to_abs(self.state.output_size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+28
-15
@@ -53,11 +53,21 @@ impl UiRenderState {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
let root = root.into();
|
let root = root.into();
|
||||||
if self.needs_full_redraw(root) {
|
if self.root_changed(root) {
|
||||||
self.redraw_all(root, rsc);
|
self.redraw_all(root, rsc);
|
||||||
self.old_root = root.map(|r| r.id());
|
self.old_root = root.map(|r| r.id());
|
||||||
|
} 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;
|
self.resized = false;
|
||||||
} else if rsc.widgets().has_updates() {
|
if rsc.widgets().has_updates() {
|
||||||
self.redraw_updates(rsc);
|
self.redraw_updates(rsc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,6 +116,7 @@ impl UiRenderState {
|
|||||||
primitives: Vec::new(),
|
primitives: Vec::new(),
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
size_deps: Vec::new(),
|
size_deps: Vec::new(),
|
||||||
|
reads_output: false,
|
||||||
rsc,
|
rsc,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -122,6 +133,7 @@ impl UiRenderState {
|
|||||||
primitives,
|
primitives,
|
||||||
children,
|
children,
|
||||||
size_deps,
|
size_deps,
|
||||||
|
reads_output,
|
||||||
layer,
|
layer,
|
||||||
id,
|
id,
|
||||||
} = painter;
|
} = painter;
|
||||||
@@ -142,6 +154,7 @@ impl UiRenderState {
|
|||||||
primitives,
|
primitives,
|
||||||
children,
|
children,
|
||||||
size_deps,
|
size_deps,
|
||||||
|
reads_output,
|
||||||
mask,
|
mask,
|
||||||
layer,
|
layer,
|
||||||
};
|
};
|
||||||
@@ -197,7 +210,11 @@ impl UiRenderState {
|
|||||||
let offered = region.axis_mut(axis).len();
|
let offered = region.axis_mut(axis).len();
|
||||||
let had = old.axis_mut(axis).len();
|
let had = old.axis_mut(axis).len();
|
||||||
match widget.on_resize(axis) {
|
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
|
// `Translate` is not acted on yet, and cannot be until a
|
||||||
// drawing can sit somewhere other than its box. `region` is
|
// drawing can sit somewhere other than its box. `region` is
|
||||||
// both the box a widget was given and the box its primitives
|
// 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
|
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<Option<&'a StrongWidget>>) -> bool {
|
|
||||||
self.root_changed(root) || self.resized
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn needs_redraw<'a>(
|
pub fn needs_redraw<'a>(
|
||||||
&self,
|
&self,
|
||||||
root: impl Into<Option<&'a StrongWidget>>,
|
root: impl Into<Option<&'a StrongWidget>>,
|
||||||
widgets: &Widgets,
|
widgets: &Widgets,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
self.needs_full_redraw(root) || widgets.has_updates()
|
self.root_changed(root) || self.resized || widgets.has_updates()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn active_widgets(&self) -> usize {
|
pub fn active_widgets(&self) -> usize {
|
||||||
@@ -328,10 +340,10 @@ impl UiRenderState {
|
|||||||
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
|
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
|
||||||
self.draw_started.remove(&id);
|
self.draw_started.remove(&id);
|
||||||
// Whoever read this widget's size may be a different size now, so the
|
// 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,
|
// highest reader is what draws. Everything between the two is marked
|
||||||
// where the mark is what stops it being reused as it stands. Clearing
|
// as well: their own boxes have not changed, so the mark is the only
|
||||||
// it here would hide the change from the draw that came to apply it.
|
// thing stopping the draw reusing its way past this widget.
|
||||||
if let Some(top) = self.highest_reader(id) {
|
if let Some(top) = self.mark_readers(id, rsc) {
|
||||||
self.redraw(top, rsc);
|
self.redraw(top, rsc);
|
||||||
// Cleared by that draw if it reached here; if it did not, this is
|
// Cleared by that draw if it reached here; if it did not, this is
|
||||||
// no longer drawn and asking again would not end.
|
// 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
|
/// The furthest ancestor that read this widget's size, directly or through
|
||||||
/// widgets that did the same.
|
/// widgets that did the same, marking everything below it on the way.
|
||||||
fn highest_reader(&self, id: WidgetId) -> Option<WidgetId> {
|
fn mark_readers(&self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<WidgetId> {
|
||||||
let mut top = None;
|
let mut top = None;
|
||||||
let mut at = id;
|
let mut at = id;
|
||||||
while let Some(active) = self.active.get(&at)
|
while let Some(active) = self.active.get(&at)
|
||||||
@@ -371,6 +383,7 @@ impl UiRenderState {
|
|||||||
.get(&parent)
|
.get(&parent)
|
||||||
.is_some_and(|p| p.size_deps.contains(&at))
|
.is_some_and(|p| p.size_deps.contains(&at))
|
||||||
{
|
{
|
||||||
|
rsc.widgets_mut().needs_redraw.insert(at);
|
||||||
top = Some(parent);
|
top = Some(parent);
|
||||||
at = parent;
|
at = parent;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,3 +59,53 @@ fn a_child_drawn_twice_moves_once() {
|
|||||||
|
|
||||||
assert_corners!(h, inner, (150, 0), (350, 200));
|
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));
|
||||||
|
}
|
||||||
@@ -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));
|
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<Cell<usize>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
Reference in new issue
Block a user