Avoid speculative layout when retained answers suffice
This commit is contained in:
1 parent
cdec29351a
commit
a640c6cce2
5 files changed
+116
-27
No files matched your search
@@ -22,6 +22,8 @@ pub struct Painter<'a> {
|
|||||||
/// 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(super) reads_output: bool,
|
||||||
|
/// Whether this widget is drawing in the same pixel-sized box as before.
|
||||||
|
pub(super) same_box: bool,
|
||||||
/// The slot this widget's primitives are positioned through: its own if
|
/// The slot this widget's primitives are positioned through: its own if
|
||||||
/// its parent placed it, otherwise the nearest ancestor that has one.
|
/// its parent placed it, otherwise the nearest ancestor that has one.
|
||||||
pub(super) move_idx: MoveIdx,
|
pub(super) move_idx: MoveIdx,
|
||||||
@@ -144,6 +146,21 @@ impl<'a> Painter<'a> {
|
|||||||
Some(hint)
|
Some(hint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A clean child's retained size, when this widget's own constraints are
|
||||||
|
/// unchanged. This is the answer from its last real draw, not a guess.
|
||||||
|
pub fn retained_size<W: ?Sized>(&mut self, id: &StrongWidget<W>) -> Option<Size> {
|
||||||
|
if !self.same_box || self.rsc.widgets().needs_redraw.contains(&id.id()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let active = self.state.active.get(&id.id())?;
|
||||||
|
if active.parent != Some(self.id) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let size = active.size;
|
||||||
|
self.depend_on_size(id);
|
||||||
|
Some(size)
|
||||||
|
}
|
||||||
|
|
||||||
fn depend_on_size<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
|
fn depend_on_size<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
|
||||||
if !self.size_deps.contains(&child.id()) {
|
if !self.size_deps.contains(&child.id()) {
|
||||||
self.size_deps.push(child.id());
|
self.size_deps.push(child.id());
|
||||||
|
|||||||
+28
-13
@@ -109,18 +109,19 @@ impl UiRenderState {
|
|||||||
parent_move: MoveIdx,
|
parent_move: MoveIdx,
|
||||||
slotted: bool,
|
slotted: bool,
|
||||||
mask: MaskIdx,
|
mask: MaskIdx,
|
||||||
old_children: Option<Vec<WidgetId>>,
|
old_active: Option<ActiveData>,
|
||||||
rsc: &mut dyn UiRsc,
|
rsc: &mut dyn UiRsc,
|
||||||
) -> Size {
|
) -> Size {
|
||||||
let mut old_children = old_children.unwrap_or_default();
|
let mut old_active = old_active;
|
||||||
if self.active.contains_key(&id) {
|
if self.active.contains_key(&id) {
|
||||||
if let Some(size) = self.try_reuse(id, region, parent_move, rsc) {
|
if let Some(size) = self.try_reuse(id, region, parent_move, rsc) {
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
// if not, then maintain resize and track old children to remove unneeded
|
// if not, then maintain resize and track old children to remove unneeded
|
||||||
let active = self.remove(id, false, rsc).unwrap();
|
old_active = self.remove(id, false, rsc);
|
||||||
old_children = active.children;
|
|
||||||
}
|
}
|
||||||
|
let previous_px = old_active.as_ref().map(|active| active.px);
|
||||||
|
let old_children = old_active.map(|active| active.children).unwrap_or_default();
|
||||||
|
|
||||||
// draw widget
|
// draw widget
|
||||||
let (move_idx, local) = match slotted {
|
let (move_idx, local) = match slotted {
|
||||||
@@ -133,6 +134,7 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let px = self.px_of(move_idx, local);
|
let px = self.px_of(move_idx, local);
|
||||||
|
let same_box = previous_px == Some(px);
|
||||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||||
self.draw_started.insert(id);
|
self.draw_started.insert(id);
|
||||||
|
|
||||||
@@ -147,6 +149,7 @@ impl UiRenderState {
|
|||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
size_deps: Vec::new(),
|
size_deps: Vec::new(),
|
||||||
reads_output: false,
|
reads_output: false,
|
||||||
|
same_box,
|
||||||
move_idx,
|
move_idx,
|
||||||
rsc,
|
rsc,
|
||||||
};
|
};
|
||||||
@@ -165,6 +168,7 @@ impl UiRenderState {
|
|||||||
children,
|
children,
|
||||||
size_deps,
|
size_deps,
|
||||||
reads_output,
|
reads_output,
|
||||||
|
same_box: _,
|
||||||
move_idx,
|
move_idx,
|
||||||
layer,
|
layer,
|
||||||
id,
|
id,
|
||||||
@@ -449,14 +453,17 @@ impl UiRenderState {
|
|||||||
/// redraws a widget that's currently active (drawn)
|
/// redraws a widget that's currently active (drawn)
|
||||||
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
|
// A widget can only answer whether its size changed by drawing in the
|
||||||
// highest reader is what draws. Everything between the two is marked
|
// box its parent chose. If that box changed in pixels, its retained
|
||||||
// as well: their own boxes have not changed, so the mark is the only
|
// placement is stale and the highest size reader must choose the new
|
||||||
// thing stopping the draw reusing its way past this widget.
|
// box first. Otherwise the widget can draw locally, and its readers
|
||||||
if let Some(top) = self.mark_readers(id, rsc) {
|
// 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 box_changed && 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
|
|
||||||
// no longer drawn and asking again would not end.
|
|
||||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -470,7 +477,8 @@ impl UiRenderState {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.draw_inner(
|
let was = active.size;
|
||||||
|
let size = self.draw_inner(
|
||||||
active.layer,
|
active.layer,
|
||||||
id,
|
id,
|
||||||
active.region,
|
active.region,
|
||||||
@@ -478,9 +486,16 @@ impl UiRenderState {
|
|||||||
active.parent_move,
|
active.parent_move,
|
||||||
active.move_idx != active.parent_move,
|
active.move_idx != active.parent_move,
|
||||||
active.mask,
|
active.mask,
|
||||||
Some(active.children),
|
Some(active),
|
||||||
rsc,
|
rsc,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if size != was
|
||||||
|
&& let Some(top) = self.mark_readers(id, rsc)
|
||||||
|
{
|
||||||
|
self.redraw(top, rsc);
|
||||||
|
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The furthest ancestor that read this widget's size, directly or through
|
/// The furthest ancestor that read this widget's size, directly or through
|
||||||
|
|||||||
@@ -13,9 +13,13 @@ impl Widget for Scroll {
|
|||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let output_len = painter.output_size().axis(self.axis);
|
let output_len = painter.output_size().axis(self.axis);
|
||||||
let container_len = UiScalar::abs(painter.px_size().axis(self.axis));
|
let container_len = UiScalar::abs(painter.px_size().axis(self.axis));
|
||||||
// Drawn in the whole container to learn its length, then placed at
|
// Its last measured size stays valid while neither the child nor this
|
||||||
// the scrolled offset.
|
// container's constraints changed. Otherwise draw it in the whole
|
||||||
let child = painter.place(&self.inner, UiRegion::FULL).size();
|
// container to learn its length, then place it at the scrolled offset.
|
||||||
|
let child = match painter.retained_size(&self.inner) {
|
||||||
|
Some(size) => size,
|
||||||
|
None => painter.place(&self.inner, UiRegion::FULL).size(),
|
||||||
|
};
|
||||||
let content_len = child
|
let content_len = child
|
||||||
.axis(self.axis)
|
.axis(self.axis)
|
||||||
.apply_rest()
|
.apply_rest()
|
||||||
|
|||||||
@@ -12,14 +12,24 @@ impl Widget for Span {
|
|||||||
let axis = self.dir.axis;
|
let axis = self.dir.axis;
|
||||||
// A length for every child before any is placed: from its own hint
|
// A length for every child before any is placed: from its own hint
|
||||||
// where it has one, and from drawing it where it does not.
|
// where it has one, and from drawing it where it does not.
|
||||||
let lens: Vec<Len> = self
|
let mut cursor = UiScalar::rel_min();
|
||||||
.children
|
let mut lens = Vec::with_capacity(self.children.len());
|
||||||
.iter()
|
for child in &self.children {
|
||||||
.map(|child| match painter.size_hint(child, axis) {
|
let len = match painter.size_hint(child, axis) {
|
||||||
Some(len) => len,
|
Some(len) => len,
|
||||||
None => painter.place(child, UiRegion::FULL).len(axis),
|
None => {
|
||||||
})
|
let mut span = UiSpan::new(cursor, UiScalar::rel_max());
|
||||||
.collect();
|
if self.dir.sign == Sign::Neg {
|
||||||
|
span.flip();
|
||||||
|
}
|
||||||
|
let region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
||||||
|
painter.place(child, region).len(axis)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
cursor.abs += len.abs + self.gap;
|
||||||
|
cursor.rel += len.rel;
|
||||||
|
lens.push(len);
|
||||||
|
}
|
||||||
|
|
||||||
let gap = self.gap * self.children.len().saturating_sub(1) as f32;
|
let gap = self.gap * self.children.len().saturating_sub(1) as f32;
|
||||||
let total = lens.iter().fold(Len::abs(gap), |sum, len| sum + *len);
|
let total = lens.iter().fold(Len::abs(gap), |sum, len| sum + *len);
|
||||||
|
|||||||
+47
-4
@@ -79,10 +79,9 @@ fn a_leaf_that_depends_on_its_box_is_drawn_again_when_the_box_changes() {
|
|||||||
h.rsc[first].size = Size::from((150, 200));
|
h.rsc[first].size = Size::from((150, 200));
|
||||||
h.frame();
|
h.frame();
|
||||||
|
|
||||||
// Twice: once for the span to measure it, once for its real box. A child
|
// The preceding fixed child makes the remaining box this child's real
|
||||||
// that can hint its length is spared the first, and a smaller number here
|
// box, so measuring it also draws it in its final place.
|
||||||
// means someone has made that cheaper rather than broken it.
|
assert_eq!(draws.get(), settled + 1);
|
||||||
assert_eq!(draws.get(), settled + 2);
|
|
||||||
assert_corners!(h, second, (150, 0), (400, 200));
|
assert_corners!(h, second, (150, 0), (400, 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,6 +115,50 @@ fn a_span_relays_out_when_a_child_it_measured_changes() {
|
|||||||
assert_corners!(h, second, (250, 0), (400, 200));
|
assert_corners!(h, second, (250, 0), (400, 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_repaint_that_keeps_its_size_does_not_relay_out() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let (first, draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate);
|
||||||
|
let (second, _) = counted(&mut h, Size::REST, OnResize::Translate);
|
||||||
|
h.set_root((first, second).span(Dir::RIGHT));
|
||||||
|
let settled = draws.get();
|
||||||
|
|
||||||
|
// Taking mutable access is the ordinary content-change signal. This
|
||||||
|
// widget returns the same size, so the parent has nothing to lay out.
|
||||||
|
let _ = h.rsc.widgets_mut().get_dyn_mut(first.id());
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert_eq!(draws.get(), settled + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scrolling_reuses_the_clean_contents_size() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let (inner, draws) = counted(&mut h, Size::from((400, 600)), OnResize::Translate);
|
||||||
|
let scroll = Scroll::new(inner.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc);
|
||||||
|
h.set_root(scroll);
|
||||||
|
let settled = draws.get();
|
||||||
|
|
||||||
|
h.rsc[scroll].scroll(40.0);
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert_eq!(draws.get(), settled);
|
||||||
|
assert_corners!(h, inner, (0, -360), (400, 240));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scrolling_remeasures_changed_contents() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let (inner, _) = counted(&mut h, Size::from((400, 600)), OnResize::Translate);
|
||||||
|
let scroll = Scroll::new(inner.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc);
|
||||||
|
h.set_root(scroll);
|
||||||
|
|
||||||
|
h.rsc[inner].size = Size::from((400, 800));
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert_corners!(h, inner, (0, -600), (400, 200));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_placed_child_survives_the_next_frame() {
|
fn a_placed_child_survives_the_next_frame() {
|
||||||
let mut h = Harness::new((400, 200));
|
let mut h = Harness::new((400, 200));
|
||||||
|
|||||||
Reference in new issue
Block a user