Measure a dirty widget where its parent asked, not in a box its answer decided
A local redraw drew a dirty widget in the box it was placed in. When a reader decided that box from the widget's own answer -- an aligned span sized to its children, a text at the tail of a row, a scroll's content -- the old answer is a fixed point of measuring there whatever the content now says, so the layout had two stable answers and which one it reached depended on the tree's history. `tests/unsettled.rs` has the two shrunk cases: the four-widget aligned span, and a scroll placing a pass-through `SetSize` in a box the content decided, where the span under it was placed once and nothing at its own edge said so. `ActiveData::offered_px` keeps the pixel size of the box the parent first asked about the child in, whether through `known_len` or a first `place`, beside `px`, the box it drew against. A dirty widget whose size reads an axis on which some reader up its chain gave what it read a box other than the one it asked in is not drawn locally: the chain is marked and the parent of the highest such placement draws, since above it every box is a constraint rather than an answer. The walk goes up the whole reader chain because a pass-through hands a derived box down unchanged. `Scroll` read its box's length for the clamp through `px_len`, which records the reported size as depending on it, and it does not: its size is its content's. That made every scroll tick a size question asked in a derived box, at 34x the instructions. `Painter::px_len_for_draw` is the read that records nothing. Instructions per frame on the depth-8 rig against the previous head: `many` at 32 dirty 0.66M to 0.74M, at 130 dirty 27.7M to 26.5M, `resize` 15.8M to 15.0M, `scroll`, `repaint` and `size` unchanged. The shrinking fuzzer passes 200 trees at depth 7 in all four cases, the hundred-seed sweep passes, and the five reference renders and the resize render are byte-identical. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
65f68bbb8a
commit
02ff8c7454
6 files changed
+200
-51
No files matched your search
@@ -13,6 +13,10 @@ pub struct ActiveData {
|
||||
/// it is a fraction of a slot's box, and the same fraction of a box that
|
||||
/// has since changed is a different number of pixels.
|
||||
pub px: Vec2,
|
||||
/// The pixel size of the box its parent first asked about it in, before
|
||||
/// knowing what it came to. `px` may be a box derived from that answer,
|
||||
/// and a size measured there is only the same answer asked again.
|
||||
pub offered_px: Vec2,
|
||||
pub parent: Option<WidgetId>,
|
||||
/// How far down the tree it was drawn, the root being 1. Carried down a
|
||||
/// draw rather than worked out by walking up, so it is right for every
|
||||
|
||||
+28
-1
@@ -21,6 +21,9 @@ pub struct Painter<'a> {
|
||||
pub(super) textures: Vec<TextureHandle>,
|
||||
pub(super) primitives: Vec<PrimitiveHandle>,
|
||||
pub(super) children: Vec<WidgetId>,
|
||||
/// The children asked about so far, so the first box each was asked
|
||||
/// about is the one recorded as its offer.
|
||||
pub(super) offered: Vec<WidgetId>,
|
||||
/// The children whose size this widget read while drawing.
|
||||
pub(super) size_deps: Vec<WidgetId>,
|
||||
/// Offered pixel axes which can affect the size this draw reports.
|
||||
@@ -141,6 +144,7 @@ impl<'a> Painter<'a> {
|
||||
None,
|
||||
self.rsc,
|
||||
);
|
||||
self.offer(id.id(), region);
|
||||
DrawResult {
|
||||
child: id,
|
||||
painter: self,
|
||||
@@ -182,6 +186,8 @@ impl<'a> Painter<'a> {
|
||||
axis: Axis,
|
||||
region: UiRegion,
|
||||
) -> Option<Len> {
|
||||
let region = region.within(&self.region);
|
||||
self.offer(child.id(), region);
|
||||
if let Some(hint) = self.size_hint(child, axis) {
|
||||
return Some(hint);
|
||||
}
|
||||
@@ -189,12 +195,12 @@ impl<'a> Painter<'a> {
|
||||
.map(|size| size.axis(axis))
|
||||
}
|
||||
|
||||
/// `region` in this widget's own coordinates.
|
||||
fn retained_size<W: ?Sized>(
|
||||
&mut self,
|
||||
child: &StrongWidget<W>,
|
||||
region: UiRegion,
|
||||
) -> Option<Size> {
|
||||
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())?;
|
||||
@@ -204,6 +210,20 @@ impl<'a> Painter<'a> {
|
||||
Some(size)
|
||||
}
|
||||
|
||||
/// Records the box a child was first asked about in this draw. Any later
|
||||
/// box this draw gives it was decided knowing its answer, so a size the
|
||||
/// child measures there is not an answer to this widget's question.
|
||||
fn offer(&mut self, child: WidgetId, region: UiRegion) {
|
||||
if self.offered.contains(&child) {
|
||||
return;
|
||||
}
|
||||
self.offered.push(child);
|
||||
let px = self.state.px_of(self.move_idx, region);
|
||||
if let Some(active) = self.state.active.get_mut(&child) {
|
||||
active.offered_px = px;
|
||||
}
|
||||
}
|
||||
|
||||
/// Depends on a length the child gave without being drawn. A hint is
|
||||
/// context-free, so this depends on the child but on no pixel axis.
|
||||
fn depend_on_hint<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
|
||||
@@ -309,6 +329,13 @@ impl<'a> Painter<'a> {
|
||||
/// [`Self::px_size`] when the other axis cannot affect the reported size.
|
||||
pub fn px_len(&mut self, axis: Axis) -> f32 {
|
||||
self.size_box_inputs[axis as usize] = true;
|
||||
self.px_len_for_draw(axis)
|
||||
}
|
||||
|
||||
/// One axis of this widget's box in pixels, for a draw whose reported
|
||||
/// size does not follow from it -- a clamp or a position. Nothing records
|
||||
/// the read, so a size that does depend on it would go stale.
|
||||
pub fn px_len_for_draw(&self, axis: Axis) -> f32 {
|
||||
let region = self.state.moves.resolve(self.move_idx, self.region);
|
||||
region
|
||||
.size()
|
||||
|
||||
+71
-23
@@ -203,7 +203,7 @@ impl UiRenderState {
|
||||
parent_move: MoveIdx,
|
||||
slotted: bool,
|
||||
mask: MaskIdx,
|
||||
old_children: Option<Vec<WidgetId>>,
|
||||
mut old: Option<ActiveData>,
|
||||
rsc: &mut dyn UiRsc,
|
||||
) -> Size {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
@@ -211,14 +211,12 @@ impl UiRenderState {
|
||||
diag::bump(Counter::DrawRequests);
|
||||
diag::draw_request(id, parent, region, self.px_of(parent_move, region), slotted);
|
||||
}
|
||||
let mut old_children = old_children.unwrap_or_default();
|
||||
if self.active.contains_key(&id) {
|
||||
if let Some(size) = self.try_reuse(id, region, depth, parent_move, rsc) {
|
||||
return size;
|
||||
}
|
||||
// if not, then maintain resize and track old children to remove unneeded
|
||||
let active = self.remove(id, false, rsc).unwrap();
|
||||
old_children = active.children;
|
||||
old = self.remove(id, false, rsc);
|
||||
}
|
||||
|
||||
// draw widget
|
||||
@@ -232,6 +230,12 @@ impl UiRenderState {
|
||||
}
|
||||
};
|
||||
let px = self.px_of(move_idx, local);
|
||||
// Drawn again in a box its parent already decided: the offer is the
|
||||
// one recorded when the parent first asked, not this box.
|
||||
let (old_children, offered_px) = match old {
|
||||
Some(old) => (old.children, old.offered_px),
|
||||
None => (Vec::new(), px),
|
||||
};
|
||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||
self.draw_started.insert(id);
|
||||
|
||||
@@ -244,6 +248,7 @@ impl UiRenderState {
|
||||
textures: Vec::new(),
|
||||
primitives: Vec::new(),
|
||||
children: Vec::new(),
|
||||
offered: Vec::new(),
|
||||
size_deps: Vec::new(),
|
||||
depth,
|
||||
size_box_inputs: [false; 2],
|
||||
@@ -271,6 +276,7 @@ impl UiRenderState {
|
||||
textures,
|
||||
primitives,
|
||||
children,
|
||||
offered: _,
|
||||
size_deps,
|
||||
size_box_inputs,
|
||||
size_output_inputs,
|
||||
@@ -292,6 +298,7 @@ impl UiRenderState {
|
||||
region,
|
||||
size,
|
||||
px,
|
||||
offered_px,
|
||||
parent,
|
||||
depth,
|
||||
textures,
|
||||
@@ -347,7 +354,7 @@ impl UiRenderState {
|
||||
}
|
||||
|
||||
/// The pixel size of a region held in `slot`'s coordinates.
|
||||
fn px_of(&self, slot: MoveIdx, region: UiRegion) -> Vec2 {
|
||||
pub(super) fn px_of(&self, slot: MoveIdx, region: UiRegion) -> Vec2 {
|
||||
self.moves
|
||||
.resolve(slot, region)
|
||||
.size()
|
||||
@@ -736,16 +743,25 @@ impl UiRenderState {
|
||||
// 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.
|
||||
// box first. The same holds when the box was decided from the
|
||||
// widget's own answer: measuring there again can only repeat it,
|
||||
// whatever the content now says. 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| {
|
||||
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 box_changed && let Some(top) = self.mark_readers(id, rsc) {
|
||||
let top = match box_changed {
|
||||
true => self.top_reader(id),
|
||||
false => None,
|
||||
}
|
||||
.or_else(|| self.derived_box_reader(id));
|
||||
if let Some(top) = top {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::EagerReaderRedraws);
|
||||
self.mark_below(id, top, rsc);
|
||||
self.redraw(top, rsc);
|
||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||
return;
|
||||
@@ -772,7 +788,7 @@ impl UiRenderState {
|
||||
active.parent_move,
|
||||
active.move_idx != active.parent_move,
|
||||
active.mask,
|
||||
Some(active.children),
|
||||
Some(active),
|
||||
rsc,
|
||||
);
|
||||
|
||||
@@ -795,24 +811,56 @@ impl UiRenderState {
|
||||
}
|
||||
}
|
||||
|
||||
/// The furthest ancestor that read this widget's size, directly or through
|
||||
/// widgets that did the same, marking everything below it on the way.
|
||||
fn mark_readers(&self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<WidgetId> {
|
||||
/// The highest reader up the chain that gave what it read a box other
|
||||
/// than the one it asked in, on an axis this widget's size reads. Above
|
||||
/// it every box is a constraint rather than an answer. It is the highest
|
||||
/// and not the nearest because a pass-through hands a derived box down
|
||||
/// unchanged.
|
||||
fn derived_box_reader(&self, id: WidgetId) -> Option<WidgetId> {
|
||||
let reads = self.active.get(&id)?.size_box_inputs;
|
||||
let mut top = None;
|
||||
let mut at = id;
|
||||
while let Some(active) = self.active.get(&at)
|
||||
&& let Some(parent) = active.parent
|
||||
&& self
|
||||
.active
|
||||
.get(&parent)
|
||||
.is_some_and(|p| p.size_deps.contains(&at))
|
||||
{
|
||||
rsc.widgets_mut().needs_redraw.insert(at);
|
||||
top = Some(parent);
|
||||
at = parent;
|
||||
for (active, parent) in self.reader_chain(id) {
|
||||
let px = self.px_of(active.parent_move, active.region);
|
||||
if AXES.into_iter().zip(reads).any(|(axis, r)| {
|
||||
r && pixel_len_changed(active.offered_px.axis(axis), px.axis(axis))
|
||||
}) {
|
||||
top = Some(parent);
|
||||
}
|
||||
}
|
||||
top
|
||||
}
|
||||
|
||||
/// The furthest ancestor that read this widget's size, directly or through
|
||||
/// widgets that did the same.
|
||||
fn top_reader(&self, id: WidgetId) -> Option<WidgetId> {
|
||||
self.reader_chain(id).last().map(|(_, parent)| parent)
|
||||
}
|
||||
|
||||
/// Each widget from `id` upward whose parent read its size, with that
|
||||
/// parent.
|
||||
fn reader_chain(&self, id: WidgetId) -> impl Iterator<Item = (&ActiveData, WidgetId)> {
|
||||
let mut at = Some(id);
|
||||
std::iter::from_fn(move || {
|
||||
let active = self.active.get(&at?)?;
|
||||
let parent = active.parent?;
|
||||
let read = self.active.get(&parent)?.size_deps.contains(&active.id);
|
||||
at = read.then_some(parent);
|
||||
read.then_some((active, parent))
|
||||
})
|
||||
}
|
||||
|
||||
/// Marks everything from `id` up to, and not including, `top`, so that
|
||||
/// drawing `top` draws each of them rather than reusing it.
|
||||
fn mark_below(&self, id: WidgetId, top: WidgetId, rsc: &mut dyn UiRsc) {
|
||||
let mut at = id;
|
||||
while at != top {
|
||||
rsc.widgets_mut().needs_redraw.insert(at);
|
||||
let Some(parent) = self.active.get(&at).and_then(|active| active.parent) else {
|
||||
return;
|
||||
};
|
||||
at = parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UiRenderState {
|
||||
|
||||
Reference in new issue
Block a user