iris: Scroll pans on a finger drag; a vertical drag in a focused field scrolls rather than selects
IRIS_TODO.md's "the composer has no touch-drag scroll". `Scroll::drag`
takes its pan from the same `sense::DragGesture` `List` is driven by --
arbitration, DRAG_SLOP, velocity and pointer capture all stay in sense.rs
and only what a committed pan *means* is decided per caller -- and
`WidgetLike::scrollable()` registers it beside the wheel handler it already
registered, so every scroll area pans on a finger with nothing added at the
call site. No fling: `Scroll` has no per-frame tick to animate one and the
areas it wraps are at most a screenful. `Scroll::amt()` exposes the pan
position.
`attr.rs`'s `on_press` treated an already-focused field as the plain
click_or_drag case, so every Pressing frame extended a selection. It now
applies the same DRAG_SLOP rule its unfocused branch already did: a press
past the slop vertically abandons its pending selection for the rest of the
gesture, so the scroll area around the field wins it. That is Android
EditText's own behaviour and it is what lets a swipe up over the composer
scroll instead of dragging a highlight through what you typed.
Also fixed, found doing it: `ActiveData::mask` stored the mask a widget
*set* rather than the one it was drawn *under*, and `redraw` feeds that
field back in as the inherited mask -- so a targeted redraw of any `Masked`
handed it its own mask and aborted on `set_mask`'s nested-mask assert. A
real abort on the emulator, `assertion failed: self.mask == MaskIdx::NONE`.
And the per-frame orphan guard from 76b1f99 is now a count comparison
(O(active widgets)); the O(primitives) walk only runs to build the failure
message, because running it per frame made a debug build on the emulator too
slow to finish a bench run at all.
Tests: four in scroll.rs (pan past the slop, a tap inside it, a horizontal
drag, the end clamp), `a_finger_drag_over_a_scroll_area_pans_it` in
sense_tests.rs driving the whole registration/dispatch/capture path (fails
with "got 0" without the new registration), and
`redrawing_a_masked_widget_does_not_nest_its_own_mask` in layout_tests.rs
(aborts on the pre-fix code).
The composer itself is deliberately still not `.scrollable()`: `Scroll`
measures against the window rather than its own offered box, so inside the
`MaxSize` capping it at six lines it pans the field out of the bar --
measured, reverted and written down in RUST.md and DECISIONS.md.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
76b1f99277
commit
fb6b459c2c
13 files changed
+608
-31
No files matched your search
@@ -278,6 +278,14 @@ impl Primitives {
|
||||
}
|
||||
}
|
||||
|
||||
/// How many instances are still bound for the GPU -- the O(1) half of
|
||||
/// the orphan check, so the O(primitives) walk below only runs on a
|
||||
/// frame that already looks wrong. See
|
||||
/// [`crate::UiRenderState::orphaned_primitives`].
|
||||
pub fn live_count(&self) -> usize {
|
||||
(self.instances.len() - self.free.len()) + (self.images.len() - self.image_free.len())
|
||||
}
|
||||
|
||||
/// Every instance that is still bound for the GPU, as `(inst_idx,
|
||||
/// owner, is_image)` -- everything except the slots already handed to
|
||||
/// [`Self::free`] and waiting for [`Self::apply_free`] to compact them
|
||||
|
||||
@@ -140,11 +140,7 @@ impl UiRenderState {
|
||||
self.redraw_updates(rsc);
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
debug_assert!(
|
||||
self.orphaned_primitives().is_empty(),
|
||||
"{}",
|
||||
self.orphan_report(rsc),
|
||||
);
|
||||
debug_assert!(self.primitive_counts_agree(), "{}", self.orphan_report(rsc),);
|
||||
}
|
||||
|
||||
fn redraw_all(&mut self, root: Option<&StrongWidget>, rsc: &mut dyn UiRsc) {
|
||||
@@ -291,6 +287,16 @@ impl UiRenderState {
|
||||
}
|
||||
};
|
||||
|
||||
// The mask this widget was drawn *under*, kept aside because
|
||||
// `Painter::set_mask` overwrites `painter.mask` with the widget's
|
||||
// own new one -- and `ActiveData::mask`'s only consumer is
|
||||
// `redraw`, which feeds it back in as the *inherited* mask. Storing
|
||||
// the set one instead handed a `Masked` its own mask on every
|
||||
// targeted redraw, tripping `set_mask`'s nested-mask assert:
|
||||
// `assertion failed: self.mask == MaskIdx::NONE`, an abort the
|
||||
// first time the composer's scroll area was redrawn on the
|
||||
// emulator.
|
||||
let inherited_mask = mask;
|
||||
let mut painter = Painter {
|
||||
state: self,
|
||||
region,
|
||||
@@ -314,7 +320,7 @@ impl UiRenderState {
|
||||
state: _,
|
||||
rsc: _,
|
||||
region,
|
||||
mask,
|
||||
mask: _,
|
||||
move_slot,
|
||||
textures,
|
||||
primitives,
|
||||
@@ -331,7 +337,7 @@ impl UiRenderState {
|
||||
textures,
|
||||
primitives,
|
||||
children,
|
||||
mask,
|
||||
mask: inherited_mask,
|
||||
layer,
|
||||
size,
|
||||
move_slot,
|
||||
@@ -534,6 +540,19 @@ impl UiRenderState {
|
||||
orphans
|
||||
}
|
||||
|
||||
/// Whether every primitive still bound for the GPU is owned by a live
|
||||
/// widget, decided by counting rather than by walking: an orphan is a
|
||||
/// live instance no `ActiveData` names, so it can only ever make the
|
||||
/// live count exceed the owned one. O(active widgets) -- a few dozen --
|
||||
/// against [`Self::orphaned_primitives`]'s O(primitives), which on a
|
||||
/// transcript is tens of thousands and made a debug build on a phone
|
||||
/// too slow to finish a benchmark run.
|
||||
fn primitive_counts_agree(&self) -> bool {
|
||||
let live: usize = self.layers.iter().map(|(_, p)| p.live_count()).sum();
|
||||
let owned: usize = self.active.values().map(|a| a.primitives.len()).sum();
|
||||
live == owned
|
||||
}
|
||||
|
||||
/// The message [`Self::update`]'s orphan assert prints -- built here
|
||||
/// rather than inline so the (allocating, O(primitives)) work only
|
||||
/// happens on the failing path.
|
||||
|
||||
Reference in new issue
Block a user