Redesign span layout around retained placement

This commit is contained in:
iris committed 2026-09-09 15:11:57 -04:00
1 parent 4b69f3cc6b
commit 992482414f
23 files changed
+426 -1005

No files matched your search

+51 -17
View File
@@ -23,6 +23,51 @@ impl UiRsc for TestRsc {
}
}
struct FixedRect(f32);
impl Widget for FixedRect {
fn draw(&mut self, painter: &mut Painter) -> Size {
let size = Size::from_axis(Axis::Y, Len::abs(self.0), Len::REST);
painter.primitive_within(
RectPrimitive::color(UiColor::WHITE),
size.to_uivec2(painter.density())
.align(RegionAlign::TOP_LEFT),
);
size
}
}
#[test]
fn a_hinted_rest_draws_once_and_only_moves_the_fixed_child_after_it() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let first = rsc.ui.widgets.add_strong(FixedRect(40.0));
let fill = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
let fill = rsc.ui.widgets.add_strong(Sized {
inner: fill.any(),
x: None,
y: Some(Len::REST),
});
let last = rsc.ui.widgets.add_strong(FixedRect(40.0));
let last_w = last.weak();
let mut span = Span::empty(Dir::DOWN);
span.push(first.any());
span.push(fill.any());
span.push(last.any());
let root = rsc.ui.widgets.add_strong(span).any();
let mut render = UiRenderState::new();
render.resize((200.0, 300.0));
render.update(&root, &mut rsc);
let (draws, _rewrites, moves, _shapes) = render.take_counters();
assert_eq!(draws, 5);
assert_eq!(moves, 1);
let last = render.window_region(&last_w, &rsc).unwrap();
assert!((last.top_left.y - 260.0).abs() < 0.01, "{last:?}");
}
/// A `ScrollArea` over a `Span` of `n` fixed-height rects -- N primitives large
/// enough that an O(N) regression in the move path would show up as a
/// non-trivial counter rather than being lost in noise (LAYOUT.md section
@@ -227,7 +272,7 @@ fn a_mask_stays_put_while_its_scrolled_content_moves() {
let masked_slot_after = render.active.get(&masked_id).unwrap().move_slot;
let mask_delta_after = rsc.ui.move_offsets[masked_slot_after.idx()].delta;
// `Masked` itself is never the target of a `mov`/`reposition` here --
// `Masked` itself is never the target of a `mov`/`place` here --
// only its scrolled child is -- so the slot its own mask references
// (`Painter::set_mask` bakes in `self.move_slot`, i.e. this one) must
// still read zero after the scroll. The visible counterpart of this
@@ -307,7 +352,7 @@ fn composing_text_after_a_keyboard_resize_lands_in_the_bars_own_region() {
// The keyboard opens: a real `surface_changed`/`resize` to a shorter
// window, then a further keystroke -- the redraw that must land in the
// bar's new (also short) region, not whatever region a provisional
// measurement pass used along the way.
// provisional placement used along the way.
render.resize((1080.0, 1478.0));
render.update(&root, &mut rsc);
field.edit(&mut rsc).insert("b");
@@ -583,9 +628,6 @@ fn a_size_independent_widget_moved_by_its_parent_has_the_hit_box_it_is_drawn_at(
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,
@@ -604,7 +646,7 @@ fn a_size_independent_widget_moved_by_its_parent_has_the_hit_box_it_is_drawn_at(
}
/// A parent that both `mov`s a child (its own layout moved the box it
/// offers) and `reposition`s it inside that box in the same frame -- what
/// offers) and places it inside that box in the same frame -- what
/// `LazySpan::place`'s Bottom-known branch does once a row's cached height
/// stops matching what the row reports, which is reachable as soon as a
/// transcript row's blocks wrap (docs/IRIS_TODO.md's "Found by P1a").
@@ -634,21 +676,12 @@ impl Widget for MoveThenPlace {
UiScalar::abs(self.place_top + 40.0),
),
);
painter.reposition(&self.inner, place);
painter.place(&self.inner, place);
Size::default()
}
}
/// `mov` accumulates a delta onto a widget's move slot and `reposition`
/// overwrites it, and both can legitimately land on one widget in one
/// frame (see `MoveThenPlace`). `reposition` used to write its own delta
/// alone, which dropped the move and put the child back at the position
/// the offered box had *before* it moved; a `debug_assert!` that
/// `move_applied` was zero hid that behind a panic instead of fixing it.
/// The slot has one owner and one meaning now --
/// `move_applied + repositioned` -- so the child stays where it was
/// placed however its offered box moves. Fails at the offer's position
/// (200) rather than the placement's (100) without that.
/// Placement must preserve a move already applied in the same frame.
#[test]
fn a_widget_moved_by_its_parent_and_then_placed_inside_it_lands_at_the_placement() {
let mut rsc = TestRsc {
@@ -1025,6 +1058,7 @@ fn a_span_of_padded_children_inside_a_span_draws_each_where_its_box_is() {
let padded = rsc.ui.widgets.add_strong(Pad {
padding: Padding::uniform(PAD),
inner: sized.any(),
exact_region: false,
});
let fill = rsc.ui.widgets.add_strong(Rect::new(UiColor::BLUE)).any();
let card = rsc.ui.widgets.add_strong(Stack {