Prune commentary and stale Rust port notes
This commit is contained in:
1 parent
3ae034a47b
commit
1e6d3b1edd
84 files changed
+334
-5648
No files matched your search
@@ -1,16 +1,6 @@
|
||||
//! Pass conditions for LAYOUT.md section 8, exercised as plain unit tests
|
||||
//! rather than through `run-headless.sh`: `UiRenderState` and `Widgets` do
|
||||
//! not touch a GPU or a window, so a tree can be built and driven directly.
|
||||
//! No GPU-backed rendering (`UiRenderNode`) is exercised here -- only the
|
||||
//! CPU-side layout/move machinery LAYOUT.md is about.
|
||||
|
||||
use crate::prelude::*;
|
||||
use std::{cell::Cell, cell::RefCell, rc::Rc};
|
||||
|
||||
/// The minimal `UiRsc` a test needs: just the shared `UiData`, none of the
|
||||
/// event/window/state plumbing `DefaultRsc` carries. `pub(crate)` so
|
||||
/// `access_tests.rs` (I4, RUST.md) can reuse it rather than keeping a
|
||||
/// second copy of the same harness.
|
||||
pub(crate) struct TestRsc {
|
||||
pub(crate) ui: UiData,
|
||||
}
|
||||
@@ -94,14 +84,6 @@ impl Widget for TracedParent {
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal reproduction for a container's child-layer cursor being retained
|
||||
/// as though it were the layer on which the container itself was entered.
|
||||
///
|
||||
/// `Stack` is drawn by `Sized` on layer 0. It advances its painter to layers
|
||||
/// 1 and 2 for its two children. The retained `ActiveData` must still say the
|
||||
/// stack itself is on layer 0; otherwise an ordinary redraw of `Sized` asks
|
||||
/// for the stack on 0 again and turns the invented 2 -> 0 change into a full
|
||||
/// redraw of the stack and both children.
|
||||
#[test]
|
||||
fn a_widget_retains_its_entry_layer_not_its_child_cursor() {
|
||||
let mut rsc = TestRsc {
|
||||
@@ -198,10 +180,6 @@ fn a_parent_that_ignores_child_size_is_not_invalidated_with_it() {
|
||||
assert_eq!(&*trace.borrow(), &["child"]);
|
||||
}
|
||||
|
||||
/// A content-sized vertical container grows vertically when one child grows,
|
||||
/// but that does not invalidate another child's retained height. Its width is
|
||||
/// the context that could change that height (for example through wrapping),
|
||||
/// and that stayed fixed.
|
||||
#[test]
|
||||
fn a_span_reuses_unchanged_sibling_sizes_when_only_its_along_extent_changes() {
|
||||
let mut rsc = TestRsc {
|
||||
@@ -223,9 +201,6 @@ fn a_span_reuses_unchanged_sibling_sizes_when_only_its_along_extent_changes() {
|
||||
dir: Dir::DOWN,
|
||||
gap: Len::ZERO,
|
||||
});
|
||||
// `Sized` settles its child from the full offered box into the content
|
||||
// height, reproducing the retained-region change a nested content-sized
|
||||
// row sees when one of its children grows.
|
||||
let root = rsc
|
||||
.ui
|
||||
.widgets
|
||||
@@ -264,9 +239,6 @@ fn a_child_coordinate_offset_moves_only_the_child_subtree() {
|
||||
offset: vec2(0.0, 15.0),
|
||||
});
|
||||
let parent_weak = parent.weak();
|
||||
// Keep the coordinate-owning widget below the root: a nested widget is
|
||||
// normally redrawn when its parent visits it, which takes a different
|
||||
// retained-state path from `UiRenderState::redraw` on the root itself.
|
||||
let outer = rsc.ui.widgets.add_strong(Sized {
|
||||
inner: parent.any(),
|
||||
x: None,
|
||||
@@ -285,8 +257,6 @@ fn a_child_coordinate_offset_moves_only_the_child_subtree() {
|
||||
assert!((child_before.top_left.y - 15.0).abs() < 0.01);
|
||||
|
||||
rsc.ui.widgets.get_mut(&parent_weak).unwrap().offset.y = 35.0;
|
||||
// Force the ordinary ancestor-redraw path rather than letting the
|
||||
// renderer visit only the dirty descendant directly.
|
||||
rsc.ui.widgets.get_mut(&outer_weak).unwrap().x = None;
|
||||
render.update(&root, &mut rsc);
|
||||
let (draws, rewrites, moves, _shapes) = render.take_counters();
|
||||
@@ -332,13 +302,6 @@ fn a_hinted_rest_draws_once_and_only_moves_the_fixed_child_after_it() {
|
||||
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
|
||||
/// 8, condition 3, using rects rather than glyphs to avoid pulling the font
|
||||
/// stack into a plain unit test). Returns the scroll widget (weak, for
|
||||
/// mutating it later), the erased root to draw, and the rows (weak, for
|
||||
/// hit-testing one of them).
|
||||
fn scrolled_rects(
|
||||
rsc: &mut TestRsc,
|
||||
n: usize,
|
||||
@@ -348,9 +311,6 @@ fn scrolled_rects(
|
||||
for _ in 0..n {
|
||||
let rect = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||
rects.push(rect.weak());
|
||||
// Each row gets a fixed height so the span's total content is
|
||||
// genuinely taller than the viewport -- rest-sized rows would just
|
||||
// divide whatever space is offered and never need scrolling.
|
||||
let row = rsc.ui.widgets.add_strong(Sized {
|
||||
inner: rect.any(),
|
||||
x: None,
|
||||
@@ -362,14 +322,6 @@ fn scrolled_rects(
|
||||
let scroll = rsc
|
||||
.ui
|
||||
.widgets
|
||||
// Anchored at the *start*: every test below scrolls down from
|
||||
// the top and states its sign convention against that. An
|
||||
// end-anchored area now sits at its end from its first drawn
|
||||
// frame (`Scroll::draw` measures and places in the same frame),
|
||||
// so `Pin::End` here would mean scrolling down from a
|
||||
// position that is already the bottom -- a clamped no-op, which
|
||||
// reads as "the move path is broken" rather than as the test
|
||||
// starting somewhere it did not mean to.
|
||||
.add_strong(ScrollArea::new(span.any(), Axis::Y, Pin::Start));
|
||||
let weak = scroll.weak();
|
||||
(weak, scroll.any(), rects)
|
||||
@@ -385,12 +337,6 @@ fn an_unchanged_frame_draws_and_rewrites_nothing() {
|
||||
render.resize((800.0, 20000.0));
|
||||
|
||||
render.update(&root, &mut rsc);
|
||||
// Two, not one: the first offers `ScrollArea`'s content the container's
|
||||
// own length as a placeholder (nothing has been measured yet) and
|
||||
// `Scroll::draw` asks to be drawn again once it knows the real one,
|
||||
// which the second update is. Only after that is the tree settled --
|
||||
// see `scrolling_moves_in_o1_without_a_redraw`'s own note on the
|
||||
// same first draw.
|
||||
render.update(&root, &mut rsc);
|
||||
render.take_counters(); // discard the first, real draws
|
||||
|
||||
@@ -408,35 +354,15 @@ fn scrolling_moves_in_o1_without_a_redraw() {
|
||||
let mut render = UiRenderState::new();
|
||||
render.resize((800.0, 600.0));
|
||||
|
||||
// The first draw offers `ScrollArea`'s content a zero-height region
|
||||
// (nothing has been measured yet) and learns the real content length
|
||||
// from what comes back; `update()` only redraws widgets actually
|
||||
// marked dirty, so that corrected length is not reflected in the
|
||||
// content's own *active* region until something -- here a no-op
|
||||
// scroll tick -- actually asks `ScrollArea` to redraw again. Only after
|
||||
// that warm-up does the content's offered size stop changing between
|
||||
// draws, which is what makes a further, real scroll tick a same-size
|
||||
// move instead of a resize. See scroll.rs.
|
||||
render.update(&root, &mut rsc);
|
||||
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(0.0);
|
||||
render.update(&root, &mut rsc);
|
||||
render.take_counters();
|
||||
|
||||
// Negative: `scroll`'s sign convention subtracts from `amt`, and
|
||||
// `amt` starts at (and is clamped to) 0 at the top of the content, so
|
||||
// a *positive* argument here would be scrolling further up (a no-op,
|
||||
// already clamped) rather than actually moving anything.
|
||||
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(-40.0);
|
||||
render.update(&root, &mut rsc);
|
||||
let (draws, _rewrites, moves, _shapes) = render.take_counters();
|
||||
|
||||
// The pass condition (LAYOUT.md section 8, condition 3) is 0 draws and
|
||||
// 1 move_offsets write, independent of how many rects are in the
|
||||
// scrolled subtree. `draws` here is exactly 1: `ScrollArea` itself is
|
||||
// marked dirty by `scroll()` and its own body is cheap arithmetic with
|
||||
// no primitives of its own, so it is the one real `Widget::draw` this
|
||||
// counts -- the 500 rects underneath move via the O(1) chain and are
|
||||
// never revisited.
|
||||
assert_eq!(draws, 1, "only Scroll itself should redraw");
|
||||
assert_eq!(moves, 1, "the scrolled subtree should move in one write");
|
||||
}
|
||||
@@ -463,25 +389,12 @@ fn hit_testing_follows_a_scrolled_widget() {
|
||||
let before_px = before.to_px((800.0, 600.0).into());
|
||||
let after_px = after.to_px((800.0, 600.0).into());
|
||||
|
||||
// Scrolling by -37 moves `amt` from 0 to 37, sliding the content's
|
||||
// top-left up by 37px -- `resolved_region` (the CPU twin of the vertex
|
||||
// shader's chain walk) must reflect that immediately, not the
|
||||
// pre-scroll position, or a tap routed through it would land on
|
||||
// whatever is now at the old coordinates instead of this widget.
|
||||
assert!(
|
||||
(after_px.top_left.y - (before_px.top_left.y - 37.0)).abs() < 0.01,
|
||||
"before={before_px:?} after={after_px:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// `ActiveData::mask` is the mask a widget was drawn **under**, not the one
|
||||
/// it set for itself -- `redraw` feeds it straight back in as the inherited
|
||||
/// mask, so storing the set one hands a `Masked` its own mask the second
|
||||
/// time round -- which `Painter::set_mask` asserts against, since a mask
|
||||
/// that chains to itself is a clip loop. That was an abort the first time
|
||||
/// the composer's new scroll area was redrawn on the emulator; a targeted
|
||||
/// redraw of a `Masked` is what any real screen does whenever anything
|
||||
/// inside it changes.
|
||||
#[test]
|
||||
fn redrawing_a_masked_widget_does_not_nest_its_own_mask() {
|
||||
let mut rsc = TestRsc {
|
||||
@@ -536,27 +449,10 @@ 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`/`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
|
||||
// (the clipped edge follows the scroll while the viewport border does
|
||||
// not) is `iris/run-headless.sh`'s job to catch in a real frame; this
|
||||
// is the numeric half, on the same data the fragment shader's
|
||||
// `resolve_move` reads. See LAYOUT.md section 2b.
|
||||
assert_eq!(mask_delta_before, [0.0, 0.0]);
|
||||
assert_eq!(mask_delta_after, [0.0, 0.0]);
|
||||
}
|
||||
|
||||
/// Reproduces `transcript_ui::composer::build_composer`'s exact tree shape
|
||||
/// (a `Rect` background stacked behind a `Span::RIGHT`-wrapped, padded,
|
||||
/// `rest`-width `TextEdit`, itself the second child of an outer
|
||||
/// `Span::DOWN` beside a `rest(1)`-height sibling) without the event/
|
||||
/// resource plumbing `composer.rs`'s builders need, to isolate whether the
|
||||
/// bug Iris reported on 2026-09-06 ("text seems to not appear in box")
|
||||
/// is this crate's layout engine or something specific to the real
|
||||
/// composer/screen. `TextEditable::edit` only needs `UiRsc`, so a plain
|
||||
/// insert exercises the exact redraw path a keystroke does.
|
||||
fn composer_like_tree(rsc: &mut TestRsc) -> (WeakWidget<TextEdit>, StrongWidget) {
|
||||
let field = wtext("")
|
||||
.editable(EditMode::MultiLine)
|
||||
@@ -574,12 +470,6 @@ fn composer_like_tree(rsc: &mut TestRsc) -> (WeakWidget<TextEdit>, StrongWidget)
|
||||
(field, tree)
|
||||
}
|
||||
|
||||
/// The reproduction itself. A window this tall stands in for the keyboard
|
||||
/// closed; the second, shorter `resize` stands in for `adjustResize`
|
||||
/// shrinking the surface when the IME opens -- exactly the sequence
|
||||
/// `IrisViewPeer::surface_changed` drives on a real keyboard open. Typing
|
||||
/// happens both before and after, since Iris's report was specifically
|
||||
/// that text typed *after* the keyboard was already up did not appear.
|
||||
#[test]
|
||||
fn composing_text_after_a_keyboard_resize_lands_in_the_bars_own_region() {
|
||||
let mut rsc = TestRsc {
|
||||
@@ -590,11 +480,6 @@ fn composing_text_after_a_keyboard_resize_lands_in_the_bars_own_region() {
|
||||
|
||||
render.resize((1080.0, 2298.0));
|
||||
render.update(&root, &mut rsc);
|
||||
// Focusing a field is what places its caret on a real tap
|
||||
// (`attr.rs`'s `on_press` -> `TextEditCtx::select`), and an insert
|
||||
// with no caret is a routing bug rather than a state to simulate --
|
||||
// `insert_str`'s own `debug_assert!` says so, and caught this test
|
||||
// typing into an unfocused field when it was added.
|
||||
field
|
||||
.edit(&mut rsc)
|
||||
.select(vec2(40.0, 2250.0), vec2(1080.0, 2298.0), false, false);
|
||||
@@ -602,8 +487,6 @@ fn composing_text_after_a_keyboard_resize_lands_in_the_bars_own_region() {
|
||||
render.update(&root, &mut rsc);
|
||||
|
||||
let before_px = render.window_region(&field, &rsc).unwrap();
|
||||
// The field is one line plus 12dp of padding on a 2298-tall window --
|
||||
// nowhere near the whole window's height, and anchored at the bottom.
|
||||
assert!(
|
||||
before_px.bot_right.y - before_px.top_left.y < 200.0,
|
||||
"before a resize: {before_px:?}"
|
||||
@@ -613,10 +496,6 @@ fn composing_text_after_a_keyboard_resize_lands_in_the_bars_own_region() {
|
||||
"expected the bar near the bottom before a resize: {before_px:?}"
|
||||
);
|
||||
|
||||
// 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
|
||||
// provisional placement used along the way.
|
||||
render.resize((1080.0, 1478.0));
|
||||
render.update(&root, &mut rsc);
|
||||
field.edit(&mut rsc).insert("b");
|
||||
@@ -633,13 +512,6 @@ fn composing_text_after_a_keyboard_resize_lands_in_the_bars_own_region() {
|
||||
);
|
||||
}
|
||||
|
||||
/// `ScrollArea` used to be documented as resolving its own lengths against
|
||||
/// `Painter::output_size` -- the window -- which read as if a scroll area
|
||||
/// smaller than the screen could not work, and cost a session's
|
||||
/// investigation before the composer was wired up (docs/RUST.md,
|
||||
/// 2026-09-06). It measures `painter.px_size()` now, so this pins the
|
||||
/// three numbers that follow from the offered box: what it reports
|
||||
/// upward, what its capping parent reports, and how far it can pan.
|
||||
#[test]
|
||||
fn a_scroll_measures_the_box_it_was_offered_not_the_window() {
|
||||
let mut rsc = TestRsc {
|
||||
@@ -654,8 +526,6 @@ fn a_scroll_measures_the_box_it_was_offered_not_the_window() {
|
||||
let scroll = rsc
|
||||
.ui
|
||||
.widgets
|
||||
// Start-anchored, so the `scroll(-37.0)` below has somewhere to
|
||||
// go -- see `scrolled_rects`' note on the same choice.
|
||||
.add_strong(ScrollArea::new(tall.any(), Axis::Y, Pin::Start));
|
||||
let scroll_w = scroll.weak();
|
||||
let scroll_id = scroll.id();
|
||||
@@ -669,17 +539,10 @@ fn a_scroll_measures_the_box_it_was_offered_not_the_window() {
|
||||
|
||||
let mut render = UiRenderState::new();
|
||||
render.resize((800.0, 600.0));
|
||||
// Two passes: the first offers the content a zero-length region
|
||||
// (nothing measured yet) and learns the real content length from what
|
||||
// comes back -- see `scrolling_moves_in_o1_without_a_redraw` for why
|
||||
// that warm-up is deliberate rather than a bug.
|
||||
render.update(&root, &mut rsc);
|
||||
rsc.ui.widgets.get_mut(&scroll_w).unwrap().scroll(0.0);
|
||||
render.update(&root, &mut rsc);
|
||||
|
||||
// Reports the *content*, so the cap above it has something to cap;
|
||||
// reporting the container instead would make the answer a function of
|
||||
// itself, since the container is sized from this very number.
|
||||
assert_eq!(
|
||||
render.active.get(&scroll_id).unwrap().size.y,
|
||||
Len::abs(1000.0)
|
||||
@@ -690,10 +553,6 @@ fn a_scroll_measures_the_box_it_was_offered_not_the_window() {
|
||||
"the cap, not the content and not the window"
|
||||
);
|
||||
|
||||
// Panning is bounded by content minus *container*: 900, not the 400
|
||||
// a 600px window would give. The draw is what spends the delta -- a
|
||||
// controller banks it until the layout that knows where the content
|
||||
// ends (`ScrollController::take_delta`).
|
||||
rsc.ui.widgets.get_mut(&scroll_w).unwrap().scroll(-10_000.0);
|
||||
render.update(&root, &mut rsc);
|
||||
assert!(
|
||||
@@ -703,13 +562,6 @@ fn a_scroll_measures_the_box_it_was_offered_not_the_window() {
|
||||
);
|
||||
}
|
||||
|
||||
/// The half `hit_testing_follows_a_scrolled_widget` could not see: it
|
||||
/// checks a *descendant* of the widget `ScrollArea` actually moves, whose own
|
||||
/// `region` is stale and is corrected entirely by the move chain. The
|
||||
/// moved widget itself had its `region` updated *and* the chain delta
|
||||
/// added on top, so its hit box sat at twice the pan -- which is why a
|
||||
/// finger pan of the composer left its field untappable. See
|
||||
/// `ActiveData::move_applied`.
|
||||
#[test]
|
||||
fn a_panned_widgets_own_hit_box_moves_exactly_once() {
|
||||
let mut rsc = TestRsc {
|
||||
@@ -725,8 +577,6 @@ fn a_panned_widgets_own_hit_box_moves_exactly_once() {
|
||||
let scroll = rsc
|
||||
.ui
|
||||
.widgets
|
||||
// Start-anchored, so the `scroll(-37.0)` below has somewhere to
|
||||
// go -- see `scrolled_rects`' note on the same choice.
|
||||
.add_strong(ScrollArea::new(tall.any(), Axis::Y, Pin::Start));
|
||||
let scroll_w = scroll.weak();
|
||||
let root = scroll.any();
|
||||
@@ -748,16 +598,6 @@ fn a_panned_widgets_own_hit_box_moves_exactly_once() {
|
||||
);
|
||||
}
|
||||
|
||||
/// A `Masked` used to allocate a **new** mask slot on every draw, and
|
||||
/// `draw_inner`'s unchanged-region fast path means its descendants are
|
||||
/// mostly *not* redrawn with it -- so they went on referencing the slot
|
||||
/// they were first drawn under, whose region had since stopped being the
|
||||
/// widget's. Measured 2026-09-06 on the composer's tree: four live mask
|
||||
/// entries, none of them the `Masked`'s current box, and the field it was
|
||||
/// meant to clip drew nothing at all on the emulator. The slot is
|
||||
/// allocated once and rewritten in place now (`ActiveData::own_mask`), so
|
||||
/// this pins both halves: one entry, and that entry is the widget's own
|
||||
/// region.
|
||||
#[test]
|
||||
fn a_masked_widget_keeps_one_mask_slot_that_is_always_its_own_region() {
|
||||
let mut rsc = TestRsc {
|
||||
@@ -769,9 +609,6 @@ fn a_masked_widget_keeps_one_mask_slot_that_is_always_its_own_region() {
|
||||
inner: inner_root,
|
||||
});
|
||||
let masked_id = masked.id();
|
||||
// Placed at the bottom of a `Span::DOWN` behind a `rest(1)` sibling,
|
||||
// which is what moves the bar away from the provisional slot it is
|
||||
// first drawn at -- the move that left the stale mask behind.
|
||||
let filler = rsc.ui.widgets.add_strong(Rect::new(UiColor::BLACK));
|
||||
let filler = rsc.ui.widgets.add_strong(Sized {
|
||||
inner: filler.any(),
|
||||
@@ -808,14 +645,6 @@ fn a_masked_widget_keeps_one_mask_slot_that_is_always_its_own_region() {
|
||||
);
|
||||
}
|
||||
|
||||
/// A `dp` cap that has done its job must be reported in pixels. `Span`
|
||||
/// places a child using the `abs`/`rel` of the length it reported, so a
|
||||
/// `MaxSize` handing back the caller's own `dp(168)` gave the composer's
|
||||
/// bar a slot of **zero** the moment its content grew past six lines --
|
||||
/// and the `ScrollArea` inside then measured its container at -63px (the
|
||||
/// padding, subtracted from nothing) and panned the whole message out of
|
||||
/// view. Measured on this checkout's emulator, 2026-09-06:
|
||||
/// `container=-63 content=415.8 amt=478.8`. See `Len::fold_dp`.
|
||||
#[test]
|
||||
fn a_dp_cap_is_reported_in_pixels_so_a_span_can_place_it() {
|
||||
let mut rsc = TestRsc {
|
||||
@@ -858,15 +687,6 @@ fn a_dp_cap_is_reported_in_pixels_so_a_span_can_place_it() {
|
||||
);
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -879,9 +699,6 @@ fn a_size_independent_widget_moved_by_its_parent_has_the_hit_box_it_is_drawn_at(
|
||||
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);
|
||||
@@ -909,17 +726,9 @@ 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 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").
|
||||
struct MoveThenPlace {
|
||||
inner: StrongWidget,
|
||||
/// Where the child is *offered* a (constant-size) box, moved between
|
||||
/// frames by the test.
|
||||
offer_top: f32,
|
||||
/// Where the child is then placed within this widget's own region.
|
||||
place_top: f32,
|
||||
}
|
||||
|
||||
@@ -945,7 +754,6 @@ impl Widget for MoveThenPlace {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -975,10 +783,6 @@ fn a_widget_moved_by_its_parent_and_then_placed_inside_it_lands_at_the_placement
|
||||
"the child should be drawn where it was placed, not where it was offered: {before:?}"
|
||||
);
|
||||
|
||||
// Move the offered box without changing its size (the `mov` fast path)
|
||||
// and place the child at the same spot as before. Marking the parent
|
||||
// dirty is what a real container's own content change does; the child
|
||||
// itself is untouched, which is the case `mov` exists for.
|
||||
{
|
||||
let parent = rsc.ui.widgets.get_mut(&parent_w).unwrap();
|
||||
parent.offer_top = 200.0;
|
||||
@@ -993,22 +797,8 @@ fn a_widget_moved_by_its_parent_and_then_placed_inside_it_lands_at_the_placement
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// LAYOUT.md's "Masks with a shape" -- its pass conditions, at layer 1.
|
||||
//
|
||||
// The shape a mask clips to is a *primitive already drawn*, never a copy
|
||||
// of one, so "the child's clipped corner" and "the container's own corner"
|
||||
// are the same arithmetic. These say so by evaluating both and demanding
|
||||
// exact equality: an approximate assertion would also pass a second copy
|
||||
// of the radius that merely happened to agree.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
const RADIUS: f32 = 20.0;
|
||||
|
||||
/// A rounded container with `.masked_by` it, holding a `Rect::REST` child
|
||||
/// that fills it -- so the child's own corners are exactly the corners
|
||||
/// being clipped away. Returns the drawn state, the mask, the child, and
|
||||
/// the shape primitive the mask points at.
|
||||
fn rounded_container(rsc: &mut TestRsc) -> (UiRenderState, MaskIdx, WidgetId, u32) {
|
||||
let child = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||
let child_id = child.id();
|
||||
@@ -1046,9 +836,6 @@ fn rounded_container(rsc: &mut TestRsc) -> (UiRenderState, MaskIdx, WidgetId, u3
|
||||
(render, mask, child_id, slot)
|
||||
}
|
||||
|
||||
/// The pass condition: the child's coverage at a corner pixel *equals*
|
||||
/// the container's own coverage there. Exactly equal, because it is the
|
||||
/// same primitive evaluated once -- LAYOUT.md's point 1.
|
||||
#[test]
|
||||
fn a_masked_child_is_clipped_by_its_container_s_own_corner() {
|
||||
let mut rsc = TestRsc {
|
||||
@@ -1062,12 +849,6 @@ fn a_masked_child_is_clipped_by_its_container_s_own_corner() {
|
||||
.expect("a mask's shape is a rect")
|
||||
.radius;
|
||||
|
||||
// Across the whole corner arc, not one point on it: a single sample
|
||||
// is satisfied by a mask that clips to the box and happens to agree
|
||||
// where the two coincide. Swept from the arc's own centre -- the
|
||||
// straight chord between the two ends of the arc lies *inside* the
|
||||
// circle everywhere, so a walk along it never leaves the shape and
|
||||
// the `outside` count below is what caught that.
|
||||
let arc_center = corners.top_left + Vec2::new(radius, radius);
|
||||
let (mut outside, mut inside) = (0, 0);
|
||||
for i in 0..=20 {
|
||||
@@ -1095,9 +876,6 @@ fn a_masked_child_is_clipped_by_its_container_s_own_corner() {
|
||||
);
|
||||
}
|
||||
|
||||
/// A hit test asks the same question the pixels do: the corner the
|
||||
/// container rounded away is not there to be pressed, and a point just
|
||||
/// inside the curve is. LAYOUT.md's point 4.
|
||||
#[test]
|
||||
fn a_mask_s_shape_decides_what_can_be_pressed() {
|
||||
let mut rsc = TestRsc {
|
||||
@@ -1106,20 +884,16 @@ fn a_mask_s_shape_decides_what_can_be_pressed() {
|
||||
let (render, mask, _child, slot) = rounded_container(&mut rsc);
|
||||
let corners = render.primitive_corners(slot, &rsc);
|
||||
|
||||
// The very corner of the box, which the radius cut off.
|
||||
let cut = corners.top_left + Vec2::new(1.0, 1.0);
|
||||
assert!(
|
||||
!render.mask_admits(mask, cut, &rsc),
|
||||
"the corner the container rounded away is still pressable",
|
||||
);
|
||||
// The same distance in along the diagonal, past the curve.
|
||||
let inside = corners.top_left + Vec2::new(RADIUS, RADIUS);
|
||||
assert!(
|
||||
render.mask_admits(mask, inside, &rsc),
|
||||
"a point well inside the curve is not pressable",
|
||||
);
|
||||
// And the middle of an edge, which no radius touches -- the half the
|
||||
// rounding had no reason to change.
|
||||
let edge = Vec2::new(
|
||||
(corners.top_left.x + corners.bot_right.x) / 2.0,
|
||||
corners.top_left.y + 1.0,
|
||||
@@ -1130,11 +904,6 @@ fn a_mask_s_shape_decides_what_can_be_pressed() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Nested masks multiply, so a pixel inside two feathered corners is
|
||||
/// dimmed by both -- LAYOUT.md's point 2, and the "alpha should be
|
||||
/// decreased / multiplied" Iris asked for. Written as a product of the
|
||||
/// two the shader would compute separately, which is what "multiply"
|
||||
/// means and what an intersection test would get wrong.
|
||||
#[test]
|
||||
fn nested_masks_multiply_their_coverage() {
|
||||
let mut rsc = TestRsc {
|
||||
@@ -1182,8 +951,6 @@ fn nested_masks_multiply_their_coverage() {
|
||||
rounded_rect_coverage(pos, c.top_left, c.bot_right, radius)
|
||||
};
|
||||
|
||||
// A point on the corner arc, where both feathers are partial -- the
|
||||
// only place a product and a minimum differ.
|
||||
let slot = render.first_primitive(inner_shape_id).unwrap();
|
||||
let corners = render.primitive_corners(slot, &rsc);
|
||||
let pos = corners.top_left + Vec2::new(RADIUS * 0.3, RADIUS * 0.3);
|
||||
@@ -1200,11 +967,6 @@ fn nested_masks_multiply_their_coverage() {
|
||||
);
|
||||
}
|
||||
|
||||
/// A plain `.masked()` -- no shape given -- still clips to the widget's
|
||||
/// own box with square corners, which is what every list and scroll area
|
||||
/// relies on. The half the shape work had no reason to touch, and the one
|
||||
/// that would silently round every existing clip if `set_mask` ever wrote
|
||||
/// a radius of its own.
|
||||
#[test]
|
||||
fn a_plain_mask_still_clips_to_a_square_box() {
|
||||
let mut rsc = TestRsc {
|
||||
@@ -1236,16 +998,6 @@ fn a_plain_mask_still_clips_to_a_square_box() {
|
||||
);
|
||||
}
|
||||
|
||||
/// A scroll area created to be *read* opens at the beginning of its
|
||||
/// content, however many frames it takes to learn how long that content
|
||||
/// is.
|
||||
///
|
||||
/// The bug this pins: `content_len` was `0.0` both for "nothing here" and
|
||||
/// for "not drawn yet", so the first frame's clamp found a range of zero,
|
||||
/// read `amt == len` as "sitting at the end", and set `snap_end` -- and
|
||||
/// the frame after, now knowing the real length, jumped to it. On screen
|
||||
/// that was a code fence opening at the end of its longest line, in the
|
||||
/// middle of a word (`iris/run-headless.sh phone`, 2026-09-08).
|
||||
#[test]
|
||||
fn a_scroll_area_opens_at_the_start_of_content_it_has_not_measured_yet() {
|
||||
for (name, pin, want) in [("read", Pin::Start, 0.0), ("written", Pin::End, 4900.0)] {
|
||||
@@ -1267,10 +1019,6 @@ fn a_scroll_area_opens_at_the_start_of_content_it_has_not_measured_yet() {
|
||||
|
||||
let mut render = UiRenderState::new();
|
||||
render.resize((800.0, 100.0));
|
||||
// Twice: the first draw is the one that measures the content, and
|
||||
// the defect only showed on the second. The touch in between is
|
||||
// what asks for that second draw -- an unchanged frame draws
|
||||
// nothing at all, which is the point of the frame before it.
|
||||
render.update(&root, &mut rsc);
|
||||
let _ = rsc.ui.widgets.get_mut(&weak);
|
||||
render.update(&root, &mut rsc);
|
||||
@@ -1283,17 +1031,6 @@ fn a_scroll_area_opens_at_the_start_of_content_it_has_not_measured_yet() {
|
||||
}
|
||||
}
|
||||
|
||||
/// docs/IRIS_TODO.md's "A `Span` of `Pad`ded children inside another
|
||||
/// `Span` places those children a slot out of step", worked around in
|
||||
/// `transcript-ui/src/tool.rs` by flattening the two spans into one --
|
||||
/// which costs a tool group the inset its cards should sit inside.
|
||||
///
|
||||
/// The shape is the smallest one that reproduced it there: an outer
|
||||
/// `Span(DOWN)` whose second child is another `Span(DOWN)` whose children
|
||||
/// are each a `Pad` around a fixed-height rect. Each rect is asserted to
|
||||
/// be *drawn* where its own box is -- `primitive_corners` rather than
|
||||
/// `window_region`, since the report is about what is on screen and the
|
||||
/// two resolve the move chain differently.
|
||||
#[test]
|
||||
fn a_span_of_padded_children_inside_a_span_draws_each_where_its_box_is() {
|
||||
const PAD: f32 = 4.0;
|
||||
@@ -1376,11 +1113,6 @@ fn a_span_of_padded_children_inside_a_span_draws_each_where_its_box_is() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Growing an already-drawn row first measures its new child against the
|
||||
/// row's old height. That provisional box can end before it starts when the
|
||||
/// old trailing edge is above the new child's cursor. The size must bubble to
|
||||
/// `LazySpan` and the corrected allocation must travel back down before this
|
||||
/// update is presented; a later stream event is not a layout pass.
|
||||
#[test]
|
||||
fn a_new_child_in_a_growing_lazy_row_uses_its_final_box_immediately() {
|
||||
const FIRST: f32 = 30.0;
|
||||
|
||||
Reference in new issue
Block a user