iris: generalize drag arbitration into a default-input DragGesture, with pointer capture and Drop
Iris asked (2026-09-06) that dragging be part of iris's default input system rather than duplicated per app: "anything that provides good performance and can be generalized well is part of iris rather than the app." DragArbiter and VelocityTracker (both already in iris::sense) are now bundled into a new DragGesture, which also takes exclusive pointer capture (UiRenderState::capture_pointer/release_pointer/captured_pointer) the moment a gesture commits to panning or selecting, and delivers a new CursorSense::Drop -- not PressEnd -- to the captured widget when the button lifts, wherever on screen that happens to be. This directly targets the phone bench's "finger flings do nothing": per-widget hit testing silently drops a gesture the instant the pointer moves off every registered region, which a fast pan/fling does routinely (crossing several virtualised rows, or ending off the loaded content entirely) -- so PressEnd, and the velocity/fling-start decision hanging off it, was frequently never delivered at all. Capture targets List's own stable id (List::key_at resolves the row-under-pointer from its extents), not a row's, since List retires rows mid-drag as content scrolls. transcript-ui::Selection::drag now only decides pan-vs-select from DragGesture's outcome; row.rs's per-row registration is only ever a gesture's first frame, with lib.rs registering the List-level continuation once. New tests: sense_tests.rs's two pointer-capture regressions, list.rs's replacing_the_last_row_many_times_does_not_leak_primitives (a P0 stale-primitives diagnostic -- passes, pinning the widget-arena layer as not the leak). MainActivity.java opts into edge-to-edge (Window::setDecorFitsSystemWindows(false), API 30+, no new dependency) so window insets are redelivered on every change including a pure IME toggle -- the named-but-untried fix for the phone bench's "keyboard: could not be shown" and the emulator's identical non-confirmation. cargo fmt/clippy/test clean across the iris workspace. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
71a3fae655
commit
e12c708246
8 files changed
+571
-86
No files matched your search
@@ -20,6 +20,21 @@ pub struct UiRenderState {
|
||||
resized: bool,
|
||||
draw_started: HashSet<WidgetId>,
|
||||
|
||||
/// The widget currently holding exclusive pointer input, if any --
|
||||
/// `iris::sense::SensorUi::run_sensors` reads and clears this every
|
||||
/// call. Interior mutability (a `Mutex`, not a bare `Cell`, since a
|
||||
/// `CursorData` reaching this through an async `task_on` handler needs
|
||||
/// `Send`/`Sync`) because `run_sensors` takes `&self` (widgets are
|
||||
/// dispatched to, not owned, at that layer) and this render state is
|
||||
/// the one structure both backends (winit, android-view) already hold
|
||||
/// across frames, the same way `old_root`/`resized` are -- see
|
||||
/// `iris::sense`'s pointer-capture doc for why a drag needs this: once
|
||||
/// a gesture has committed to panning or selecting, every later sample
|
||||
/// of it must reach the same widget even if the finger has moved off
|
||||
/// whatever hit region first noticed the press. Never held across an
|
||||
/// await or another lock -- every access here is a single get/set.
|
||||
captured: std::sync::Mutex<Option<WidgetId>>,
|
||||
|
||||
/// `Widget::draw` calls and `Primitives::region_mut` rewrites since the
|
||||
/// last `take_counters`. LAYOUT.md section 8's pass conditions are
|
||||
/// stated in terms of these two: an unchanged frame must cost 0 of
|
||||
@@ -45,6 +60,7 @@ impl UiRenderState {
|
||||
old_root: None,
|
||||
resized: false,
|
||||
draw_started: Default::default(),
|
||||
captured: Default::default(),
|
||||
draw_count: 0,
|
||||
region_mut_count: 0,
|
||||
mov_count: 0,
|
||||
@@ -357,6 +373,13 @@ impl UiRenderState {
|
||||
active.textures.clear();
|
||||
rsc.ui_mut().textures.free();
|
||||
if undraw {
|
||||
// A captured widget that goes away mid-gesture (List's
|
||||
// virtualisation retiring a row, a rebuild) must not leave
|
||||
// the pointer permanently captured by an id nothing will
|
||||
// ever draw again -- `captured`'s own path out.
|
||||
if *self.captured.lock().unwrap() == Some(id) {
|
||||
*self.captured.lock().unwrap() = None;
|
||||
}
|
||||
// Permanent removal: retire this widget's own move slot
|
||||
// (the self-ownership ref taken when it was allocated) and
|
||||
// the up-link ref it held on its parent's slot -- read from
|
||||
@@ -429,6 +452,27 @@ impl UiRenderState {
|
||||
self.active.len()
|
||||
}
|
||||
|
||||
/// Give `id` exclusive pointer input from the next `run_sensors` call
|
||||
/// on -- see `captured`'s field doc. Overwrites any previous capture
|
||||
/// (a gesture that starts a new one has already decided the old one
|
||||
/// is over).
|
||||
pub fn capture_pointer(&self, id: WidgetId) {
|
||||
*self.captured.lock().unwrap() = Some(id);
|
||||
}
|
||||
|
||||
/// Release exclusive pointer input, if any is held -- called once
|
||||
/// `run_sensors` has delivered the terminal `Drop` to the capturing
|
||||
/// widget, or by that widget itself if it decides the gesture is over
|
||||
/// some other way.
|
||||
pub fn release_pointer(&self) {
|
||||
*self.captured.lock().unwrap() = None;
|
||||
}
|
||||
|
||||
/// The widget currently holding exclusive pointer input, if any.
|
||||
pub fn captured_pointer(&self) -> Option<WidgetId> {
|
||||
*self.captured.lock().unwrap()
|
||||
}
|
||||
|
||||
pub fn debug(&self, widgets: &Widgets, label: &str) -> impl Iterator<Item = &ActiveData> {
|
||||
self.active.iter().filter_map(move |(&id, inst)| {
|
||||
let l = widgets.label(id);
|
||||
|
||||
Reference in new issue
Block a user