iris: a size-independent widget's hit box lands where it is drawn
draw_inner's third fast path -- offered region changed shape, widget's
output does not depend on it -- rewrites the widget's own primitives in
place and writes no move-slot delta at all. 167862c added a
move_applied increment there, copied from mov, where region and the slot
delta really do move together. Here only region moves, so resolved_region
subtracted a distance the chain never held and every such widget's hit
box sat short of its drawing by exactly the last step it took.
Span reaches this on the first frame of any tree it is in: it measures
each child at the full region and then places it, which for a Rect (the
.background(rect(..)) idiom, list row tints) is a size change through this
branch. So the hit box was wrong from the start, with the drawing correct
-- nothing on screen to say so.
a_size_independent_widget_moved_by_its_parent_has_the_hit_box_it_is_drawn_at
is the sibling of a_panned_widgets_own_hit_box_moves_exactly_once on the
branch that fix had no reason to touch; it fails on both frames without
this.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
a56a928b0c
commit
e63e923d44
2 files changed
+63
-8
No files matched your search
@@ -210,7 +210,6 @@ impl UiRenderState {
|
||||
// the doubled `Compacted:` row in docs/bench/iris-phone-v2-2026-09-06.md.
|
||||
// The same shape reaches any dirty widget an ancestor redraws first.
|
||||
let dirty = rsc.widgets_mut().needs_redraw.remove(&id);
|
||||
let output_size = self.output_size;
|
||||
if let Some(active) = self.active.get_mut(&id)
|
||||
&& !dirty
|
||||
{
|
||||
@@ -239,13 +238,15 @@ impl UiRenderState {
|
||||
*r = r.outside(&from).within(®ion);
|
||||
self.region_mut_count += 1;
|
||||
}
|
||||
// Same bookkeeping `mov` does below and for the same
|
||||
// reason: `region` moves, this widget's own slot delta
|
||||
// does not, so the part of that delta `region` accounts
|
||||
// for grows by exactly this step. See
|
||||
// `ActiveData::move_applied`.
|
||||
active.move_applied +=
|
||||
region.top_left().to_abs(output_size) - from.top_left().to_abs(output_size);
|
||||
// `move_applied` is deliberately **not** touched here,
|
||||
// unlike in `mov`: it counts the part of this widget's own
|
||||
// move-slot delta that `region` has already absorbed, and
|
||||
// this branch writes no delta at all -- the primitives were
|
||||
// moved directly. Counting one would make
|
||||
// `resolved_region` subtract a distance the chain never
|
||||
// held, putting the hit box short of the drawing by
|
||||
// exactly this step. See `ActiveData::move_applied`, and
|
||||
// `a_size_independent_widget_moved_by_its_parent_has_the_hit_box_it_is_drawn_at`.
|
||||
active.region = region;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -508,3 +508,57 @@ fn a_dp_cap_is_reported_in_pixels_so_a_span_can_place_it() {
|
||||
"expected the 100dp cap at density 2.5 to be a 250px slot, got {height} ({box_px:?})"
|
||||
);
|
||||
}
|
||||
|
||||
/// The sibling of `a_panned_widgets_own_hit_box_moves_exactly_once`, on
|
||||
/// the branch that fix had no reason to touch: `draw_inner`'s
|
||||
/// size-independent fast path rewrites a widget's primitives *in place*
|
||||
/// and leaves its move slot alone, so unlike `mov` there is no slot delta
|
||||
/// for `region` to have absorbed. Counting one there anyway makes
|
||||
/// `resolved_region` subtract a delta the chain never held, and the
|
||||
/// widget's hit box lands short of where it is drawn by exactly the
|
||||
/// distance it just moved -- with nothing on screen to say so, since the
|
||||
/// primitives are in the right place.
|
||||
#[test]
|
||||
fn a_size_independent_widget_moved_by_its_parent_has_the_hit_box_it_is_drawn_at() {
|
||||
let mut rsc = TestRsc {
|
||||
ui: UiData::default(),
|
||||
};
|
||||
let top = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||
let spacer = rsc.ui.widgets.add_strong(Sized {
|
||||
inner: top.any(),
|
||||
x: None,
|
||||
y: Some(Len::abs(100.0)),
|
||||
});
|
||||
let spacer_w = spacer.weak();
|
||||
// `Rect` is `is_size_independent`, so growing the spacer above it
|
||||
// offers this one a region that changed *both* position and size --
|
||||
// the one shape that reaches the branch under test.
|
||||
let below = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||
let below_w = below.weak();
|
||||
let mut span = Span::empty(Dir::DOWN);
|
||||
span.push(spacer.any());
|
||||
span.push(below.any());
|
||||
let root = rsc.ui.widgets.add_strong(span).any();
|
||||
|
||||
let mut render = UiRenderState::new();
|
||||
render.resize((800.0, 600.0));
|
||||
render.update(&root, &mut rsc);
|
||||
// `Span` draws each child once at the full region to measure it and
|
||||
// then places it, so this widget has already been through the branch
|
||||
// once by the end of the very first frame.
|
||||
let first = render.window_region(&below_w, &rsc).unwrap();
|
||||
assert!(
|
||||
(first.top_left.y - 100.0).abs() < 0.01,
|
||||
"hit box at {:?}, drawn at y=100",
|
||||
first.top_left
|
||||
);
|
||||
|
||||
rsc.ui.widgets.get_mut(&spacer_w).unwrap().y = Some(Len::abs(250.0));
|
||||
render.update(&root, &mut rsc);
|
||||
let after = render.window_region(&below_w, &rsc).unwrap();
|
||||
assert!(
|
||||
(after.top_left.y - 250.0).abs() < 0.01,
|
||||
"hit box at {:?}, drawn at y=250",
|
||||
after.top_left
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user