iris: a capture cancels every other gesture, and the pointer leaves UiRenderState
Two defects Iris reported from her phone on 2026-09-08, one root cause each, both in how a gesture ends. A widget that takes pointer capture cuts every other widget off from the press completely -- no PressEnd, no Drop -- so anything else tracking it was left with an open gesture at a stale origin, and the *next* touch anywhere was measured from that origin. That is the transcript jumping on a tap after a code fence was panned sideways. CursorSense::Cancel is the missing state: delivered once to each loser of a capture race, the way Android sends ACTION_CANCEL and the web sends pointercancel. And registered click_or_drag|unclick, which never matches a Drop, so a Scroll that had captured never saw its own gesture end and stayed panning from where the finger left. That is the horizontal snap back. CursorSense::drag_senses() states the rule once for every widget driving a DragGesture instead of per call site. The pointer's own state (who holds capture, who is tracking the press) no longer lives in a Mutex on UiRenderState. It is Event::Global for the cursor senses -- owned by the event manager that runs the dispatch, reached by &mut, with a per-dispatch PointerRequests slot for handlers -- per Iris: never reach for locks first, and input-wide state belongs to the general input handler. What had forced the lock was a Data: Send bound on task_on that nothing needed; the spawned future never sees the event's data. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cdeb7b0857
commit
b863f9f3df
13 files changed
+554
-129
No files matched your search
@@ -79,6 +79,8 @@ type EventData<Rsc, E> = (E, Rc<dyn for<'a> EventFn<Rsc, <E as Event>::Data<'a>>
|
|||||||
pub struct TypeEventManager<Rsc: HasEvents, E: Event> {
|
pub struct TypeEventManager<Rsc: HasEvents, E: Event> {
|
||||||
// TODO: reduce visiblity!!
|
// TODO: reduce visiblity!!
|
||||||
pub active: HashMap<LayerId, HashMap<WidgetId, E::State>>,
|
pub active: HashMap<LayerId, HashMap<WidgetId, E::State>>,
|
||||||
|
/// This event's own input-wide state -- see [`Event::Global`].
|
||||||
|
pub global: E::Global,
|
||||||
map: HashMap<WidgetId, Vec<EventData<Rsc, E>>>,
|
map: HashMap<WidgetId, Vec<EventData<Rsc, E>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +109,7 @@ impl<Rsc: HasEvents, E: Event> Default for TypeEventManager<Rsc, E> {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
active: Default::default(),
|
active: Default::default(),
|
||||||
|
global: Default::default(),
|
||||||
map: Default::default(),
|
map: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,20 @@ pub use rsc::*;
|
|||||||
pub trait Event: Sized + 'static + Clone {
|
pub trait Event: Sized + 'static + Clone {
|
||||||
type Data<'a>: Clone = ();
|
type Data<'a>: Clone = ();
|
||||||
type State: Default = ();
|
type State: Default = ();
|
||||||
|
/// State this event owns that belongs to no single widget -- what the
|
||||||
|
/// thing dispatching the event knows about the *input*, rather than
|
||||||
|
/// about a listener. `()` for almost every event; the cursor's is
|
||||||
|
/// `iris::sense::PointerInput` (which widget holds pointer capture,
|
||||||
|
/// and who is tracking the press in flight).
|
||||||
|
///
|
||||||
|
/// It lives here so that such state has one owner, reached by `&mut`
|
||||||
|
/// through the event manager, instead of being parked on whatever
|
||||||
|
/// structure a handler happens to be able to reach and guarded with a
|
||||||
|
/// lock. Iris asked for that on 2026-09-08, of the pointer capture
|
||||||
|
/// that used to sit in a `Mutex` on `UiRenderState`: "everything
|
||||||
|
/// global should be stored in the general input handler, not in
|
||||||
|
/// specific senses with locking stuff."
|
||||||
|
type Global: Default = ();
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
|
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
|
||||||
Some(data.clone())
|
Some(data.clone())
|
||||||
|
|||||||
@@ -57,21 +57,6 @@ pub struct UiRenderState {
|
|||||||
/// ever drawn and was never emptied.
|
/// ever drawn and was never emptied.
|
||||||
draw_started: HashSet<WidgetId>,
|
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
|
/// `Widget::draw` calls and `Primitives::region_mut` rewrites since the
|
||||||
/// last `take_counters`. LAYOUT.md section 8's pass conditions are
|
/// last `take_counters`. LAYOUT.md section 8's pass conditions are
|
||||||
/// stated in terms of these two: an unchanged frame must cost 0 of
|
/// stated in terms of these two: an unchanged frame must cost 0 of
|
||||||
@@ -103,10 +88,9 @@ pub struct UiRenderState {
|
|||||||
/// When the sensor dispatch (`SensorUi::run_sensors`, in the `iris`
|
/// When the sensor dispatch (`SensorUi::run_sensors`, in the `iris`
|
||||||
/// crate) last saw an input sample, dated by the sample's own clock
|
/// crate) last saw an input sample, dated by the sample's own clock
|
||||||
/// (`CursorState::time`) rather than when the dispatch ran -- same
|
/// (`CursorState::time`) rather than when the dispatch ran -- same
|
||||||
/// reasoning as that field's own doc. A `Mutex` rather than a
|
/// reasoning as that field's own doc. A `Mutex` because `run_sensors`
|
||||||
/// `Cell` for the same reason `captured` is: `run_sensors` takes `&self`
|
/// takes `&self` and this is the one render state both backends
|
||||||
/// and this is the one render state both backends already share across
|
/// already share across frames.
|
||||||
/// frames.
|
|
||||||
last_input_at: Mutex<Option<Instant>>,
|
last_input_at: Mutex<Option<Instant>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +122,6 @@ impl UiRenderState {
|
|||||||
old_root: None,
|
old_root: None,
|
||||||
resized: false,
|
resized: false,
|
||||||
draw_started: Default::default(),
|
draw_started: Default::default(),
|
||||||
captured: Default::default(),
|
|
||||||
draw_count: 0,
|
draw_count: 0,
|
||||||
region_mut_count: 0,
|
region_mut_count: 0,
|
||||||
mov_count: 0,
|
mov_count: 0,
|
||||||
@@ -719,11 +702,12 @@ impl UiRenderState {
|
|||||||
if undraw {
|
if undraw {
|
||||||
// A captured widget that goes away mid-gesture (List's
|
// A captured widget that goes away mid-gesture (List's
|
||||||
// virtualisation retiring a row, a rebuild) must not leave
|
// virtualisation retiring a row, a rebuild) must not leave
|
||||||
// the pointer permanently captured by an id nothing will
|
// the pointer captured by an id nothing will ever draw
|
||||||
// ever draw again -- `captured`'s own path out.
|
// again. That path out is the sensor pass's, not this
|
||||||
if *self.captured.lock().unwrap() == Some(id) {
|
// one's: `iris::sense::SensorUi::run_sensors` releases a
|
||||||
*self.captured.lock().unwrap() = None;
|
// capture whose widget no longer resolves to a region,
|
||||||
}
|
// which covers this case and every other way an id can
|
||||||
|
// stop being drawn.
|
||||||
// Permanent removal: retire this widget's own move slot
|
// Permanent removal: retire this widget's own move slot
|
||||||
// (the self-ownership ref taken when it was allocated) and
|
// (the self-ownership ref taken when it was allocated) and
|
||||||
// the up-link ref it held on its parent's slot -- read from
|
// the up-link ref it held on its parent's slot -- read from
|
||||||
@@ -926,27 +910,6 @@ impl UiRenderState {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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> {
|
pub fn debug(&self, widgets: &Widgets, label: &str) -> impl Iterator<Item = &ActiveData> {
|
||||||
self.active.iter().filter_map(move |(&id, inst)| {
|
self.active.iter().filter_map(move |(&id, inst)| {
|
||||||
let l = widgets.label(id);
|
let l = widgets.label(id);
|
||||||
|
|||||||
+13
-1
@@ -52,8 +52,16 @@ pub fn recent_click(last_click: &mut Instant) -> bool {
|
|||||||
/// rather than reacting to `PressStart` alone the way `click_or_drag`'s
|
/// rather than reacting to `PressStart` alone the way `click_or_drag`'s
|
||||||
/// consumer used to (Iris, 2026-09-06: "if I swipe over the input bar it
|
/// consumer used to (Iris, 2026-09-06: "if I swipe over the input bar it
|
||||||
/// brings up the keyboard").
|
/// brings up the keyboard").
|
||||||
|
/// `CursorSense::Cancel` is in the set for the same reason `DragGesture`
|
||||||
|
/// registers it: if a scroll area or a list takes the pointer mid-gesture,
|
||||||
|
/// this field sees no `PressEnd`, and a `press_origin` left set is then
|
||||||
|
/// compared against the *next* press -- a stray selection, or a keyboard
|
||||||
|
/// summoned by a tap somewhere else entirely.
|
||||||
fn press_track() -> CursorSenses {
|
fn press_track() -> CursorSenses {
|
||||||
CursorSense::click() | CursorSense::Pressing(CursorButton::Left) | CursorSense::unclick()
|
CursorSense::click()
|
||||||
|
| CursorSense::Pressing(CursorButton::Left)
|
||||||
|
| CursorSense::unclick()
|
||||||
|
| CursorSense::Cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Selector;
|
pub struct Selector;
|
||||||
@@ -185,6 +193,7 @@ fn on_press(
|
|||||||
state.focus_gained(render.window_region(&id, &*rsc));
|
state.focus_gained(render.window_region(&id, &*rsc));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
CursorSense::Cancel => id.edit(rsc).text.press_origin = None,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -205,6 +214,9 @@ fn on_press(
|
|||||||
ctx.text.press_origin = None;
|
ctx.text.press_origin = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// The gesture was taken by somebody else, so it is not a tap and
|
||||||
|
// must not grant focus when it ends out of this widget's sight.
|
||||||
|
CursorSense::Cancel => id.edit(rsc).text.press_origin = None,
|
||||||
CursorSense::PressEnd(_) => {
|
CursorSense::PressEnd(_) => {
|
||||||
let was_tap = id.edit(rsc).text.press_origin.take().is_some();
|
let was_tap = id.edit(rsc).text.press_origin.take().is_some();
|
||||||
if was_tap {
|
if was_tap {
|
||||||
|
|||||||
+9
-3
@@ -44,13 +44,19 @@ impl<WL: WidgetLike<Rsc, Tag>, Rsc: HasEvents, Tag> Eventable<Rsc, Tag> for WL {
|
|||||||
|
|
||||||
widget_trait! {
|
widget_trait! {
|
||||||
pub trait TaskEventable<Rsc: HasEvents + HasTasks>;
|
pub trait TaskEventable<Rsc: HasEvents + HasTasks>;
|
||||||
fn task_on<'a, E: EventLike, F: AsyncWidgetEventFn<Rsc, WL::Widget>>(
|
/// No `Data: Send` bound, deliberately: the registered handler below
|
||||||
|
/// takes `|_, rsc|` and the event's data never crosses into the
|
||||||
|
/// spawned future -- `AsyncEventIdCtx` carries the widget id and the
|
||||||
|
/// task handle and nothing else. The bound used to be here anyway, and
|
||||||
|
/// it was the whole reason `CursorData`'s pointer state was behind a
|
||||||
|
/// `Mutex` rather than owned by the input handler (Iris, 2026-09-08:
|
||||||
|
/// never reach for a lock first).
|
||||||
|
fn task_on<E: EventLike, F: AsyncWidgetEventFn<Rsc, WL::Widget>>(
|
||||||
self,
|
self,
|
||||||
event: E,
|
event: E,
|
||||||
f: F,
|
f: F,
|
||||||
) -> impl WidgetIdFn<Rsc, WL::Widget>
|
) -> impl WidgetIdFn<Rsc, WL::Widget>
|
||||||
where <E::Event as Event>::Data<'a>: Send,
|
where for<'b> F::CallRefFuture<'b>: Send,
|
||||||
for<'b> F::CallRefFuture<'b>: Send,
|
|
||||||
{
|
{
|
||||||
let f = Arc::new(f);
|
let f = Arc::new(f);
|
||||||
move |rsc| {
|
move |rsc| {
|
||||||
|
|||||||
+298
-46
@@ -30,6 +30,20 @@ pub enum CursorSense {
|
|||||||
/// never call `capture_pointer` and have no use for it) to receive it
|
/// never call `capture_pointer` and have no use for it) to receive it
|
||||||
/// at all; ordinary hit-tested widgets keep seeing `PressEnd`.
|
/// at all; ordinary hit-tested widgets keep seeing `PressEnd`.
|
||||||
Drop,
|
Drop,
|
||||||
|
/// Delivered exactly once to a widget that was tracking this press
|
||||||
|
/// when **another** widget took pointer capture
|
||||||
|
/// (`UiRenderState::capture_pointer`): the gesture it was following
|
||||||
|
/// has been taken away and it will see no further frame of it, not
|
||||||
|
/// even a `PressEnd` or a `Drop`.
|
||||||
|
///
|
||||||
|
/// A separate sense rather than a second meaning for `Drop`, because
|
||||||
|
/// the two say opposite things to the widget reading them: `Drop` is
|
||||||
|
/// "your gesture finished", and a widget acts on it (a fling, a tap,
|
||||||
|
/// a link followed), while `Cancel` is "your gesture was never
|
||||||
|
/// yours", and acting on it is exactly the bug -- a horizontal pan of
|
||||||
|
/// a code fence would follow whatever markdown link the finger
|
||||||
|
/// happened to go down on. Registered explicitly, like `Drop`.
|
||||||
|
Cancel,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -38,6 +52,7 @@ pub struct CursorSenses(Vec<CursorSense>);
|
|||||||
impl Event for CursorSenses {
|
impl Event for CursorSenses {
|
||||||
type Data<'a> = CursorData<'a>;
|
type Data<'a> = CursorData<'a>;
|
||||||
type State = SensorState;
|
type State = SensorState;
|
||||||
|
type Global = PointerInput;
|
||||||
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
|
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
|
||||||
// `Drop` is never derived from raw cursor/hover state below (the
|
// `Drop` is never derived from raw cursor/hover state below (the
|
||||||
// free `should_run`'s own arm for it is only ever asked here,
|
// free `should_run`'s own arm for it is only ever asked here,
|
||||||
@@ -51,8 +66,13 @@ impl Event for CursorSenses {
|
|||||||
// `PressEnd` matches on, so falling through to the loop below
|
// `PressEnd` matches on, so falling through to the loop below
|
||||||
// would let whichever of the two happens to be registered first
|
// would let whichever of the two happens to be registered first
|
||||||
// win, silently swallowing the `Drop` a caller relied on.
|
// win, silently swallowing the `Drop` a caller relied on.
|
||||||
if data.sense == CursorSense::Drop {
|
// The same argument as `Drop` immediately above, for the same
|
||||||
return self.contains(&CursorSense::Drop).then(|| data.clone());
|
// reason: `run_sensors` has already decided this frame is a
|
||||||
|
// cancellation for this widget, and the registration list very
|
||||||
|
// likely also carries `Pressing`, which the loop below would
|
||||||
|
// match against a button that is still down.
|
||||||
|
if data.sense == CursorSense::Drop || data.sense == CursorSense::Cancel {
|
||||||
|
return self.contains(&data.sense).then(|| data.clone());
|
||||||
}
|
}
|
||||||
if let Some(sense) = should_run(self, &data.cursor, data.hover) {
|
if let Some(sense) = should_run(self, &data.cursor, data.hover) {
|
||||||
let mut data = data.clone();
|
let mut data = data.clone();
|
||||||
@@ -74,6 +94,21 @@ impl CursorSense {
|
|||||||
pub fn unclick() -> Self {
|
pub fn unclick() -> Self {
|
||||||
Self::PressEnd(CursorButton::Left)
|
Self::PressEnd(CursorButton::Left)
|
||||||
}
|
}
|
||||||
|
/// What a widget driving a [`DragGesture`] must register: the frames
|
||||||
|
/// of the gesture, plus **both** of the ways it can end for that
|
||||||
|
/// widget -- its own [`Self::Drop`] once it has captured the pointer,
|
||||||
|
/// and [`Self::Cancel`] if somebody else captured it first.
|
||||||
|
///
|
||||||
|
/// One function rather than a set spelled out per call site, because
|
||||||
|
/// the two terminal senses are exactly what gets forgotten: a `Scroll`
|
||||||
|
/// registered `click_or_drag | unclick` and so never saw the end of
|
||||||
|
/// any gesture it had captured, which left its arbiter panning from a
|
||||||
|
/// stale position and made the *next* drag jump by the distance
|
||||||
|
/// between them -- Iris's "it keeps snapping back to some position
|
||||||
|
/// when horizontally scrolling" (docs/RUST.md, 2026-09-08).
|
||||||
|
pub fn drag_senses() -> CursorSenses {
|
||||||
|
Self::click_or_drag() | Self::unclick() | Self::Drop | Self::Cancel
|
||||||
|
}
|
||||||
pub fn is_dragging(&self) -> bool {
|
pub fn is_dragging(&self) -> bool {
|
||||||
matches!(self, CursorSense::Pressing(CursorButton::Left))
|
matches!(self, CursorSense::Pressing(CursorButton::Left))
|
||||||
}
|
}
|
||||||
@@ -207,6 +242,99 @@ pub struct CursorData<'a> {
|
|||||||
/// the first sense that triggered this
|
/// the first sense that triggered this
|
||||||
pub sense: CursorSense,
|
pub sense: CursorSense,
|
||||||
pub render: &'a UiRenderState,
|
pub render: &'a UiRenderState,
|
||||||
|
/// The pointer itself, for the length of this dispatch -- who holds
|
||||||
|
/// exclusive input and how to ask for it. See [`PointerRequests`].
|
||||||
|
pub pointer: &'a PointerRequests,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the sensor pass knows about the pointer itself rather than about
|
||||||
|
/// any one listener: who has exclusive input, and who is tracking the
|
||||||
|
/// press in flight. [`Event::Global`] for [`CursorSenses`], so it is owned
|
||||||
|
/// by the event manager that runs the dispatch and reached by `&mut` --
|
||||||
|
/// there is no lock and no copy of it anywhere else.
|
||||||
|
///
|
||||||
|
/// **Capture** ([`PointerRequests::capture`]) gives one widget every later
|
||||||
|
/// sample of the gesture, so a pan or a selection keeps going once the
|
||||||
|
/// finger has moved off whatever hit region first noticed the press --
|
||||||
|
/// including right off the end of it, which is what used to leave a fling
|
||||||
|
/// never started because no widget saw the release.
|
||||||
|
///
|
||||||
|
/// **`pressed`** is capture's other half: every widget that has been
|
||||||
|
/// handed a frame of this press and not yet been told it ended. Taking the
|
||||||
|
/// pointer is a one-way door for all of them -- they see no `PressEnd` and
|
||||||
|
/// no `Drop` -- so each is sent one [`CursorSense::Cancel`], the way
|
||||||
|
/// Android sends `ACTION_CANCEL` and the web sends `pointercancel`.
|
||||||
|
/// Without it a gesture is left open forever with a stale origin, and the
|
||||||
|
/// *next* touch anywhere on screen is measured from it: Iris's 2026-09-08
|
||||||
|
/// phone report, where a horizontal pan inside a code fence made the
|
||||||
|
/// transcript jump on the following tap (docs/RUST.md).
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct PointerInput {
|
||||||
|
captured: Option<WidgetId>,
|
||||||
|
pressed: Vec<WidgetId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PointerInput {
|
||||||
|
/// Which widget holds exclusive pointer input between dispatches.
|
||||||
|
pub fn holder(&self) -> Option<WidgetId> {
|
||||||
|
self.captured
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hand the pointer to `id` from outside the sensor pass -- a test
|
||||||
|
/// setting a gesture up, or a backend tearing one down with `None`.
|
||||||
|
/// A handler *inside* the pass uses [`PointerRequests::capture`]
|
||||||
|
/// instead, which is the same state seen through the dispatch.
|
||||||
|
pub fn set_holder(&mut self, id: Option<WidgetId>) {
|
||||||
|
self.captured = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The pointer state of `rsc`'s cursor dispatch, for a caller outside the
|
||||||
|
/// sensor pass. Inside it, a handler has [`PointerRequests`] on its
|
||||||
|
/// [`CursorData`] and should use that.
|
||||||
|
pub fn pointer_input<Rsc: HasEvents>(rsc: &mut Rsc) -> &mut PointerInput {
|
||||||
|
&mut rsc.events_mut().get_type::<CursorSense>().global
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The pointer, as a handler sees it during one dispatch: what it may ask
|
||||||
|
/// of the capture, and who holds it. Owned by [`SensorUi::run_sensors`]
|
||||||
|
/// for the length of the dispatch and folded back into [`PointerInput`]
|
||||||
|
/// straight after, so a handler's request never races anything and nothing
|
||||||
|
/// global is reachable from a widget.
|
||||||
|
///
|
||||||
|
/// A `Cell`, not a lock: this is one frame of one thread's dispatch, and
|
||||||
|
/// the interior mutability is only here because a handler is handed
|
||||||
|
/// `CursorData` by shared reference.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct PointerRequests {
|
||||||
|
holder: std::cell::Cell<Option<WidgetId>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PointerRequests {
|
||||||
|
/// Give `id` exclusive pointer input from the next dispatch on. `id`
|
||||||
|
/// must be a widget that outlives the gesture -- a `List`'s own id,
|
||||||
|
/// not one of its virtualised rows, which can be retired mid-drag as
|
||||||
|
/// content scrolls. Overwrites any previous capture: a gesture that
|
||||||
|
/// starts a new one has already decided the old one is over, and the
|
||||||
|
/// old holder is told so with [`CursorSense::Cancel`].
|
||||||
|
pub fn capture(&self, id: WidgetId) {
|
||||||
|
self.holder.set(Some(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Give up exclusive pointer input. Called for the capturing widget by
|
||||||
|
/// `run_sensors` itself once it has delivered the terminal
|
||||||
|
/// [`CursorSense::Drop`], or by that widget if it decides the gesture
|
||||||
|
/// is over some other way.
|
||||||
|
pub fn release(&self) {
|
||||||
|
self.holder.set(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Which widget holds exclusive pointer input, as of this moment in
|
||||||
|
/// the dispatch. What a widget checks before releasing, so it cannot
|
||||||
|
/// drop a capture that is somebody else's.
|
||||||
|
pub fn holder(&self) -> Option<WidgetId> {
|
||||||
|
self.holder.get()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait SensorUi {
|
pub trait SensorUi {
|
||||||
@@ -232,27 +360,44 @@ impl SensorUi for UiRenderState {
|
|||||||
// so recording it once in the one place they share is what keeps
|
// so recording it once in the one place they share is what keeps
|
||||||
// it from needing a copy per backend.
|
// it from needing a copy per backend.
|
||||||
self.note_input(cursor.time);
|
self.note_input(cursor.time);
|
||||||
// Exclusive pointer capture (`UiRenderState::capture_pointer`,
|
|
||||||
// `DragGesture`): once some widget has committed to a drag, every
|
// The pointer's own state, taken out of the event manager for the
|
||||||
// other widget sees nothing from this pointer at all -- no hover,
|
// length of this dispatch and put back at the end -- the same
|
||||||
// no click, no press -- until it releases. This is what lets a
|
// `mem::take` the `active` map below uses, and for the same
|
||||||
// fast pan or a selection keep going once the finger has moved
|
// borrow reason. `PointerRequests` is what a handler sees of it.
|
||||||
// off whatever hit region first noticed the press (including
|
let mut pointer: PointerInput =
|
||||||
// right off the end of the gesture, at `PressEnd`/`Cancel`): a
|
std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().global);
|
||||||
// per-widget hit test would otherwise silently stop delivering to
|
let requests = PointerRequests {
|
||||||
// *anyone* the moment the pointer left every registered region,
|
holder: std::cell::Cell::new(pointer.captured),
|
||||||
// which is exactly what used to leave a fling never started (no
|
};
|
||||||
// widget ever saw the release). The captured widget keeps getting
|
let button_down = cursor.buttons.select(&CursorButton::Left).is_on();
|
||||||
// ordinary `Pressing` frames while the button is down and gets
|
|
||||||
// exactly one `Drop` -- not `PressEnd` -- the frame it lifts,
|
// Exclusive pointer capture (`PointerRequests::capture`): once
|
||||||
// which also releases the capture.
|
// some widget has committed to a drag, every other widget sees
|
||||||
if let Some(id) = self.captured_pointer() {
|
// nothing from this pointer at all -- no hover, no click, no press
|
||||||
|
// -- until it releases. That is what lets a fast pan or a
|
||||||
|
// selection keep going once the finger has moved off whatever hit
|
||||||
|
// region first noticed the press (including right off the end of
|
||||||
|
// the gesture, at `PressEnd`/`Cancel`): a per-widget hit test
|
||||||
|
// would otherwise silently stop delivering to *anyone* the moment
|
||||||
|
// the pointer left every registered region, which is exactly what
|
||||||
|
// used to leave a fling never started (no widget ever saw the
|
||||||
|
// release). The captured widget keeps getting ordinary `Pressing`
|
||||||
|
// frames while the button is down and gets exactly one `Drop` --
|
||||||
|
// not `PressEnd` -- the frame it lifts, which also releases the
|
||||||
|
// capture.
|
||||||
|
if let Some(id) = requests.holder() {
|
||||||
|
// The capture's path out for a widget that stopped being
|
||||||
|
// drawn mid-gesture -- a `List` row retired by virtualisation,
|
||||||
|
// a rebuilt subtree. Nothing can be delivered to an id with no
|
||||||
|
// region, so the gesture ends here for everyone.
|
||||||
let Some(shape) = self.resolved_region(&id, rsc) else {
|
let Some(shape) = self.resolved_region(&id, rsc) else {
|
||||||
self.release_pointer();
|
pointer.captured = None;
|
||||||
|
pointer.pressed.clear();
|
||||||
|
rsc.events_mut().get_type::<CursorSense>().global = pointer;
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let region = shape.to_px(window_size);
|
let region = shape.to_px(window_size);
|
||||||
let button_down = cursor.buttons.select(&CursorButton::Left).is_on();
|
|
||||||
let sense = if button_down {
|
let sense = if button_down {
|
||||||
CursorSense::Pressing(CursorButton::Left)
|
CursorSense::Pressing(CursorButton::Left)
|
||||||
} else {
|
} else {
|
||||||
@@ -266,11 +411,15 @@ impl SensorUi for UiRenderState {
|
|||||||
cursor: cursor.clone(),
|
cursor: cursor.clone(),
|
||||||
sense,
|
sense,
|
||||||
render: self,
|
render: self,
|
||||||
|
pointer: &requests,
|
||||||
};
|
};
|
||||||
rsc.run_event::<CursorSense>(id, data, state);
|
rsc.run_event::<CursorSense>(id, data, state);
|
||||||
if !button_down {
|
if !button_down {
|
||||||
self.release_pointer();
|
requests.release();
|
||||||
|
pointer.pressed.clear();
|
||||||
}
|
}
|
||||||
|
pointer.captured = requests.holder();
|
||||||
|
rsc.events_mut().get_type::<CursorSense>().global = pointer;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,17 +501,88 @@ impl SensorUi for UiRenderState {
|
|||||||
// might wanna set up Event to have a prepare stage
|
// might wanna set up Event to have a prepare stage
|
||||||
sense: CursorSense::Hovering,
|
sense: CursorSense::Hovering,
|
||||||
render: self,
|
render: self,
|
||||||
|
pointer: &requests,
|
||||||
};
|
};
|
||||||
rsc.run_event::<CursorSense>(*id, data, state);
|
rsc.run_event::<CursorSense>(*id, data, state);
|
||||||
|
// Anything handed a frame while the button is down may
|
||||||
|
// have opened a gesture on it, and is owed a `Cancel` if
|
||||||
|
// somebody else captures the pointer -- see
|
||||||
|
// `PointerInput`. Recorded for every such widget rather
|
||||||
|
// than only the ones known to drag, because this layer
|
||||||
|
// cannot see what a handler did with the frame.
|
||||||
|
if button_down && !pointer.pressed.contains(id) {
|
||||||
|
pointer.pressed.push(*id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if sensed {
|
if sensed {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rsc.events_mut().get_type::<CursorSense>().active = active;
|
rsc.events_mut().get_type::<CursorSense>().active = active;
|
||||||
|
|
||||||
|
pointer.captured = requests.holder();
|
||||||
|
match pointer.captured {
|
||||||
|
// A capture taken during this frame's dispatch: every other
|
||||||
|
// widget tracking the same press is told, once, that it is
|
||||||
|
// over for them. Delivered after `active` is restored, since
|
||||||
|
// these are ordinary registered handlers being run outside
|
||||||
|
// the loop.
|
||||||
|
Some(winner) => {
|
||||||
|
let losers: Vec<WidgetId> = pointer
|
||||||
|
.pressed
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|&id| id != winner)
|
||||||
|
.collect();
|
||||||
|
pointer.pressed.retain(|&id| id == winner);
|
||||||
|
for loser in losers {
|
||||||
|
deliver_cancel(self, rsc, state, loser, &cursor, window_size, &requests);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The press ended without anyone capturing: everybody who saw
|
||||||
|
// it got their own `PressEnd`, so there is nothing to cancel
|
||||||
|
// and nothing to remember.
|
||||||
|
None if !button_down => pointer.pressed.clear(),
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
// A cancel handler may itself have captured (a widget deciding
|
||||||
|
// the gesture is now its own); `requests` is still the truth.
|
||||||
|
pointer.captured = requests.holder();
|
||||||
|
rsc.events_mut().get_type::<CursorSense>().global = pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hand one widget a [`CursorSense::Cancel`] -- the gesture it was
|
||||||
|
/// tracking has been taken by whoever captured the pointer. Silent if
|
||||||
|
/// the widget has no resolved region any more (it was retired in the
|
||||||
|
/// same frame), which is the same "nothing to deliver to" case
|
||||||
|
/// `run_sensors`' capture branch handles by releasing.
|
||||||
|
fn deliver_cancel<Rsc: HasEvents>(
|
||||||
|
render: &UiRenderState,
|
||||||
|
rsc: &mut Rsc,
|
||||||
|
state: &mut Rsc::State,
|
||||||
|
id: WidgetId,
|
||||||
|
cursor: &CursorState,
|
||||||
|
window_size: Vec2,
|
||||||
|
pointer: &PointerRequests,
|
||||||
|
) {
|
||||||
|
let Some(shape) = render.resolved_region(&id, rsc) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let region = shape.to_px(window_size);
|
||||||
|
let data = CursorData {
|
||||||
|
pos: cursor.pos - region.top_left,
|
||||||
|
size: region.bot_right - region.top_left,
|
||||||
|
scroll_delta: cursor.scroll_delta,
|
||||||
|
hover: ActivationState::On,
|
||||||
|
cursor: cursor.clone(),
|
||||||
|
sense: CursorSense::Cancel,
|
||||||
|
render,
|
||||||
|
pointer,
|
||||||
|
};
|
||||||
|
rsc.run_event::<CursorSense>(id, data, state);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn should_run(
|
pub fn should_run(
|
||||||
senses: &CursorSenses,
|
senses: &CursorSenses,
|
||||||
cursor: &CursorState,
|
cursor: &CursorState,
|
||||||
@@ -385,7 +605,10 @@ pub fn should_run(
|
|||||||
// happened to register `Drop` (with no capture involved at
|
// happened to register `Drop` (with no capture involved at
|
||||||
// all) would see it fire on every plain button-up under the
|
// all) would see it fire on every plain button-up under the
|
||||||
// cursor.
|
// cursor.
|
||||||
CursorSense::Drop => false,
|
// Neither is ever derived from raw state -- see the `Drop`
|
||||||
|
// note above; both are set by `run_sensors` alone, for the one
|
||||||
|
// widget it is delivering to this frame.
|
||||||
|
CursorSense::Drop | CursorSense::Cancel => false,
|
||||||
} {
|
} {
|
||||||
return Some(*sense);
|
return Some(*sense);
|
||||||
}
|
}
|
||||||
@@ -879,6 +1102,12 @@ pub enum GestureOutcome {
|
|||||||
/// same units as `Pan`, so a caller hands it to `List::fling` with
|
/// same units as `Pan`, so a caller hands it to `List::fling` with
|
||||||
/// whatever sign flip it already applies to `Pan`.
|
/// whatever sign flip it already applies to `Pan`.
|
||||||
Released(Option<f32>),
|
Released(Option<f32>),
|
||||||
|
/// Another widget took the pointer (`CursorSense::Cancel`), so this
|
||||||
|
/// gesture is over and **nothing** should be acted on: not a tap, not
|
||||||
|
/// a fling, not a selection. Distinct from `Released(None)`, which is
|
||||||
|
/// a gesture of this widget's own that simply ended with nothing to
|
||||||
|
/// hand on.
|
||||||
|
Cancelled,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bundles a [`DragArbiter`] and a [`VelocityTracker`] into the one thing
|
/// Bundles a [`DragArbiter`] and a [`VelocityTracker`] into the one thing
|
||||||
@@ -960,7 +1189,7 @@ impl DragGesture {
|
|||||||
/// of a press already in flight, and this says so.
|
/// of a press already in flight, and this says so.
|
||||||
pub fn starts_press(&self, sense: CursorSense) -> bool {
|
pub fn starts_press(&self, sense: CursorSense) -> bool {
|
||||||
match sense {
|
match sense {
|
||||||
CursorSense::Drop | CursorSense::PressEnd(_) => false,
|
CursorSense::Drop | CursorSense::PressEnd(_) | CursorSense::Cancel => false,
|
||||||
_ => self.arbiter.is_idle(),
|
_ => self.arbiter.is_idle(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -969,13 +1198,13 @@ impl DragGesture {
|
|||||||
/// give exclusive pointer input to once this gesture commits to
|
/// give exclusive pointer input to once this gesture commits to
|
||||||
/// panning or selecting -- a stable widget that outlives the gesture
|
/// panning or selecting -- a stable widget that outlives the gesture
|
||||||
/// (a `List`'s own id, not one of its virtualised rows, which can be
|
/// (a `List`'s own id, not one of its virtualised rows, which can be
|
||||||
/// retired mid-drag as content scrolls). `render` is `CursorData`'s
|
/// retired mid-drag as content scrolls). `pointer` is `CursorData`'s
|
||||||
/// own field, already in hand at every call site. `press` only matters
|
/// own field, already in hand at every call site. `press` only matters
|
||||||
/// on the frames [`Self::starts_press`] answers true for -- see
|
/// on the frames [`Self::starts_press`] answers true for -- see
|
||||||
/// `DragArbiter::press_start`'s doc.
|
/// `DragArbiter::press_start`'s doc.
|
||||||
pub fn handle(
|
pub fn handle(
|
||||||
&mut self,
|
&mut self,
|
||||||
render: &UiRenderState,
|
pointer: &PointerRequests,
|
||||||
id: WidgetId,
|
id: WidgetId,
|
||||||
sense: CursorSense,
|
sense: CursorSense,
|
||||||
pos_window: Vec2,
|
pos_window: Vec2,
|
||||||
@@ -983,6 +1212,22 @@ impl DragGesture {
|
|||||||
press: PressState,
|
press: PressState,
|
||||||
) -> GestureOutcome {
|
) -> GestureOutcome {
|
||||||
match sense {
|
match sense {
|
||||||
|
// Somebody else won this gesture. Forget it completely --
|
||||||
|
// leaving the arbiter open is the fault this sense was added
|
||||||
|
// for, since its origin then measures the *next* touch and
|
||||||
|
// pans by the distance between two unrelated fingers.
|
||||||
|
CursorSense::Cancel => {
|
||||||
|
if crate::diagnostics::trace_enabled() {
|
||||||
|
log::debug!(
|
||||||
|
target: "iris::input",
|
||||||
|
"iris gesture: cancelled (pointer captured elsewhere)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.arbiter.release();
|
||||||
|
self.catch_unmoved = false;
|
||||||
|
self.velocity.reset();
|
||||||
|
GestureOutcome::Cancelled
|
||||||
|
}
|
||||||
CursorSense::Drop | CursorSense::PressEnd(_) => {
|
CursorSense::Drop | CursorSense::PressEnd(_) => {
|
||||||
// Once: a `velocity()` is a full Lsq2 fit, and the log
|
// Once: a `velocity()` is a full Lsq2 fit, and the log
|
||||||
// line below wants the same number the outcome carries.
|
// line below wants the same number the outcome carries.
|
||||||
@@ -1036,7 +1281,14 @@ impl DragGesture {
|
|||||||
}
|
}
|
||||||
self.arbiter.release();
|
self.arbiter.release();
|
||||||
self.catch_unmoved = false;
|
self.catch_unmoved = false;
|
||||||
render.release_pointer();
|
// Only if this gesture is the one holding it. A widget
|
||||||
|
// that never captured (it stayed `Undecided`, so this
|
||||||
|
// release is a tap) would otherwise drop somebody else's
|
||||||
|
// capture mid-drag, which is the same lost-gesture bug
|
||||||
|
// `CursorSense::Cancel` exists to prevent, in reverse.
|
||||||
|
if pointer.holder() == Some(id) {
|
||||||
|
pointer.release();
|
||||||
|
}
|
||||||
outcome
|
outcome
|
||||||
}
|
}
|
||||||
// A `Pressing` frame can arrive with no matching `PressStart`
|
// A `Pressing` frame can arrive with no matching `PressStart`
|
||||||
@@ -1070,15 +1322,15 @@ impl DragGesture {
|
|||||||
pos_window.x, pos_window.y, press.scrolling,
|
pos_window.x, pos_window.y, press.scrolling,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
self.dispatch(render, id, pos_window, now)
|
self.dispatch(pointer, id, pos_window, now)
|
||||||
}
|
}
|
||||||
_ => self.dispatch(render, id, pos_window, now),
|
_ => self.dispatch(pointer, id, pos_window, now),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch(
|
fn dispatch(
|
||||||
&mut self,
|
&mut self,
|
||||||
render: &UiRenderState,
|
pointer: &PointerRequests,
|
||||||
id: WidgetId,
|
id: WidgetId,
|
||||||
pos: Vec2,
|
pos: Vec2,
|
||||||
now: Instant,
|
now: Instant,
|
||||||
@@ -1086,7 +1338,7 @@ impl DragGesture {
|
|||||||
match self.arbiter.update(pos, now) {
|
match self.arbiter.update(pos, now) {
|
||||||
DragOutcome::Undecided => GestureOutcome::Undecided,
|
DragOutcome::Undecided => GestureOutcome::Undecided,
|
||||||
DragOutcome::Pan(dy) => {
|
DragOutcome::Pan(dy) => {
|
||||||
render.capture_pointer(id);
|
pointer.capture(id);
|
||||||
if dy != 0.0 {
|
if dy != 0.0 {
|
||||||
// The catch has moved something, so its release is an
|
// The catch has moved something, so its release is an
|
||||||
// ordinary pan release again -- see `catch_unmoved`.
|
// ordinary pan release again -- see `catch_unmoved`.
|
||||||
@@ -1100,11 +1352,11 @@ impl DragGesture {
|
|||||||
GestureOutcome::Pan(dy)
|
GestureOutcome::Pan(dy)
|
||||||
}
|
}
|
||||||
DragOutcome::SelectStart => {
|
DragOutcome::SelectStart => {
|
||||||
render.capture_pointer(id);
|
pointer.capture(id);
|
||||||
GestureOutcome::SelectStart
|
GestureOutcome::SelectStart
|
||||||
}
|
}
|
||||||
DragOutcome::SelectExtend => {
|
DragOutcome::SelectExtend => {
|
||||||
render.capture_pointer(id);
|
pointer.capture(id);
|
||||||
GestureOutcome::SelectExtend
|
GestureOutcome::SelectExtend
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2275,14 +2527,14 @@ mod drag_gesture_tests {
|
|||||||
*BASE + Duration::from_millis(ms)
|
*BASE + Duration::from_millis(ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A `UiRenderState` with nothing in it. `DragGesture` only ever calls
|
/// The pointer as a handler sees it, with nothing captured.
|
||||||
/// `capture_pointer`/`release_pointer` on it, which are bookkeeping on
|
/// `DragGesture` only ever reads and sets the holder, which needs no
|
||||||
/// a `Cell` and need no widget tree behind them.
|
/// widget tree behind it.
|
||||||
fn render() -> UiRenderState {
|
fn pointer() -> PointerRequests {
|
||||||
UiRenderState::new()
|
PointerRequests::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The id `capture_pointer` records. Any id will do -- nothing here
|
/// The id a capture records. Any id will do -- nothing here
|
||||||
/// resolves it -- so it comes from a real (empty) widget registry
|
/// resolves it -- so it comes from a real (empty) widget registry
|
||||||
/// rather than being fabricated.
|
/// rather than being fabricated.
|
||||||
fn some_id(ui: &mut UiData) -> WidgetId {
|
fn some_id(ui: &mut UiData) -> WidgetId {
|
||||||
@@ -2300,7 +2552,7 @@ mod drag_gesture_tests {
|
|||||||
fn a_flick_delivered_as_two_move_frames_releases_with_a_velocity() {
|
fn a_flick_delivered_as_two_move_frames_releases_with_a_velocity() {
|
||||||
let mut ui = UiData::default();
|
let mut ui = UiData::default();
|
||||||
let id = some_id(&mut ui);
|
let id = some_id(&mut ui);
|
||||||
let r = render();
|
let r = pointer();
|
||||||
let mut g = DragGesture::new();
|
let mut g = DragGesture::new();
|
||||||
|
|
||||||
g.handle(
|
g.handle(
|
||||||
@@ -2358,7 +2610,7 @@ mod drag_gesture_tests {
|
|||||||
fn a_flick_delivered_as_one_move_frame_carries_no_velocity_to_fit() {
|
fn a_flick_delivered_as_one_move_frame_carries_no_velocity_to_fit() {
|
||||||
let mut ui = UiData::default();
|
let mut ui = UiData::default();
|
||||||
let id = some_id(&mut ui);
|
let id = some_id(&mut ui);
|
||||||
let r = render();
|
let r = pointer();
|
||||||
let mut g = DragGesture::new();
|
let mut g = DragGesture::new();
|
||||||
|
|
||||||
g.handle(
|
g.handle(
|
||||||
@@ -2395,7 +2647,7 @@ mod drag_gesture_tests {
|
|||||||
fn a_tap_is_still_a_tap_and_flings_nothing() {
|
fn a_tap_is_still_a_tap_and_flings_nothing() {
|
||||||
let mut ui = UiData::default();
|
let mut ui = UiData::default();
|
||||||
let id = some_id(&mut ui);
|
let id = some_id(&mut ui);
|
||||||
let r = render();
|
let r = pointer();
|
||||||
let mut g = DragGesture::new();
|
let mut g = DragGesture::new();
|
||||||
|
|
||||||
g.handle(
|
g.handle(
|
||||||
@@ -2425,7 +2677,7 @@ mod drag_gesture_tests {
|
|||||||
fn a_selection_release_carries_no_velocity() {
|
fn a_selection_release_carries_no_velocity() {
|
||||||
let mut ui = UiData::default();
|
let mut ui = UiData::default();
|
||||||
let id = some_id(&mut ui);
|
let id = some_id(&mut ui);
|
||||||
let r = render();
|
let r = pointer();
|
||||||
let mut g = DragGesture::new();
|
let mut g = DragGesture::new();
|
||||||
|
|
||||||
g.handle(
|
g.handle(
|
||||||
@@ -2463,7 +2715,7 @@ mod drag_gesture_tests {
|
|||||||
fn a_press_on_moving_content_pans_from_the_first_sample() {
|
fn a_press_on_moving_content_pans_from_the_first_sample() {
|
||||||
let mut ui = UiData::default();
|
let mut ui = UiData::default();
|
||||||
let id = some_id(&mut ui);
|
let id = some_id(&mut ui);
|
||||||
let r = render();
|
let r = pointer();
|
||||||
let mut g = DragGesture::new();
|
let mut g = DragGesture::new();
|
||||||
let caught = PressState {
|
let caught = PressState {
|
||||||
scrolling: true,
|
scrolling: true,
|
||||||
@@ -2504,7 +2756,7 @@ mod drag_gesture_tests {
|
|||||||
fn the_same_press_on_settled_content_stays_undecided() {
|
fn the_same_press_on_settled_content_stays_undecided() {
|
||||||
let mut ui = UiData::default();
|
let mut ui = UiData::default();
|
||||||
let id = some_id(&mut ui);
|
let id = some_id(&mut ui);
|
||||||
let r = render();
|
let r = pointer();
|
||||||
let mut g = DragGesture::new();
|
let mut g = DragGesture::new();
|
||||||
|
|
||||||
g.handle(
|
g.handle(
|
||||||
@@ -2538,7 +2790,7 @@ mod drag_gesture_tests {
|
|||||||
fn a_catch_released_without_moving_is_neither_a_tap_nor_a_fling() {
|
fn a_catch_released_without_moving_is_neither_a_tap_nor_a_fling() {
|
||||||
let mut ui = UiData::default();
|
let mut ui = UiData::default();
|
||||||
let id = some_id(&mut ui);
|
let id = some_id(&mut ui);
|
||||||
let r = render();
|
let r = pointer();
|
||||||
let mut g = DragGesture::new();
|
let mut g = DragGesture::new();
|
||||||
let caught = PressState {
|
let caught = PressState {
|
||||||
scrolling: true,
|
scrolling: true,
|
||||||
@@ -2574,7 +2826,7 @@ mod drag_gesture_tests {
|
|||||||
fn a_catch_that_then_drags_still_flings() {
|
fn a_catch_that_then_drags_still_flings() {
|
||||||
let mut ui = UiData::default();
|
let mut ui = UiData::default();
|
||||||
let id = some_id(&mut ui);
|
let id = some_id(&mut ui);
|
||||||
let r = render();
|
let r = pointer();
|
||||||
let mut g = DragGesture::new();
|
let mut g = DragGesture::new();
|
||||||
let caught = PressState {
|
let caught = PressState {
|
||||||
scrolling: true,
|
scrolling: true,
|
||||||
@@ -2625,7 +2877,7 @@ mod drag_gesture_tests {
|
|||||||
fn a_second_delivery_of_one_press_start_does_not_restart_it() {
|
fn a_second_delivery_of_one_press_start_does_not_restart_it() {
|
||||||
let mut ui = UiData::default();
|
let mut ui = UiData::default();
|
||||||
let id = some_id(&mut ui);
|
let id = some_id(&mut ui);
|
||||||
let r = render();
|
let r = pointer();
|
||||||
let mut g = DragGesture::new();
|
let mut g = DragGesture::new();
|
||||||
|
|
||||||
let caught = PressState {
|
let caught = PressState {
|
||||||
|
|||||||
+178
-6
@@ -130,7 +130,7 @@ fn a_button_over_a_list_scrolls_the_list_and_still_clicks() {
|
|||||||
/// the press, in a gap, or off the loaded content entirely. Before pointer
|
/// the press, in a gap, or off the loaded content entirely. Before pointer
|
||||||
/// capture, `run_sensors`' hit test simply delivered nothing that frame,
|
/// capture, `run_sensors`' hit test simply delivered nothing that frame,
|
||||||
/// so a widget mid-drag never saw its release and never got a chance to
|
/// so a widget mid-drag never saw its release and never got a chance to
|
||||||
/// start a fling. `UiRenderState::capture_pointer`/`DragGesture` fix this
|
/// start a fling. `PointerRequests::capture`/`DragGesture` fix this
|
||||||
/// by giving the drag's widget every frame regardless of where the
|
/// by giving the drag's widget every frame regardless of where the
|
||||||
/// pointer is, including the terminal `Drop` in place of `PressEnd`.
|
/// pointer is, including the terminal `Drop` in place of `PressEnd`.
|
||||||
#[test]
|
#[test]
|
||||||
@@ -157,7 +157,7 @@ fn a_release_outside_every_hit_region_still_reaches_the_captured_widget() {
|
|||||||
// would gate this on a `DragArbiter`/`DragGesture`
|
// would gate this on a `DragArbiter`/`DragGesture`
|
||||||
// decision, but this test only needs to exercise the
|
// decision, but this test only needs to exercise the
|
||||||
// capture-and-release mechanics themselves.
|
// capture-and-release mechanics themselves.
|
||||||
ctx.data.render.capture_pointer(draggable_weak.id());
|
ctx.data.pointer.capture(draggable_weak.id());
|
||||||
let _ = rsc;
|
let _ = rsc;
|
||||||
}
|
}
|
||||||
CursorSense::Drop => dropped.set(true),
|
CursorSense::Drop => dropped.set(true),
|
||||||
@@ -175,7 +175,7 @@ fn a_release_outside_every_hit_region_still_reaches_the_captured_widget() {
|
|||||||
press.buttons.left = ActivationState::Start;
|
press.buttons.left = ActivationState::Start;
|
||||||
render.run_sensors(&mut rsc, &mut state, press, (100.0, 100.0).into());
|
render.run_sensors(&mut rsc, &mut state, press, (100.0, 100.0).into());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
render.captured_pointer(),
|
pointer_input(&mut rsc).holder(),
|
||||||
Some(draggable.id()),
|
Some(draggable.id()),
|
||||||
"the press should have taken capture"
|
"the press should have taken capture"
|
||||||
);
|
);
|
||||||
@@ -192,7 +192,7 @@ fn a_release_outside_every_hit_region_still_reaches_the_captured_widget() {
|
|||||||
the widget holding pointer capture"
|
the widget holding pointer capture"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
render.captured_pointer(),
|
pointer_input(&mut rsc).holder(),
|
||||||
None,
|
None,
|
||||||
"Drop must release the capture"
|
"Drop must release the capture"
|
||||||
);
|
);
|
||||||
@@ -236,7 +236,7 @@ fn capturing_one_widget_starves_every_other_widget_of_events() {
|
|||||||
let mut render = UiRenderState::new();
|
let mut render = UiRenderState::new();
|
||||||
render.resize((100.0, 100.0));
|
render.resize((100.0, 100.0));
|
||||||
render.update(&root, &mut rsc);
|
render.update(&root, &mut rsc);
|
||||||
render.capture_pointer(a_weak.id());
|
pointer_input(&mut rsc).set_holder(Some(a_weak.id()));
|
||||||
|
|
||||||
let mut state = ();
|
let mut state = ();
|
||||||
let cursor = cursor_at((50.0, 50.0).into());
|
let cursor = cursor_at((50.0, 50.0).into());
|
||||||
@@ -315,7 +315,7 @@ fn a_finger_drag_over_a_scroll_area_pans_it() {
|
|||||||
|
|
||||||
// And the gesture holds the pointer, so the rest of it reaches this
|
// And the gesture holds the pointer, so the rest of it reaches this
|
||||||
// widget even once the finger leaves its box.
|
// widget even once the finger leaves its box.
|
||||||
assert_eq!(render.captured_pointer(), Some(scroll.id()));
|
assert_eq!(pointer_input(&mut rsc).holder(), Some(scroll.id()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// docs/REVIEW-2026-09-07.md's D4. The first `MotionEvent` a view sees can
|
/// docs/REVIEW-2026-09-07.md's D4. The first `MotionEvent` a view sees can
|
||||||
@@ -364,3 +364,175 @@ fn the_clock_orders_samples_across_events() {
|
|||||||
std::time::Duration::from_millis(16)
|
std::time::Duration::from_millis(16)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Iris's 2026-09-08 phone report, first half: "it keeps snapping back to
|
||||||
|
/// some position when horizontally scrolling."
|
||||||
|
///
|
||||||
|
/// A `Scroll` that has committed to a pan holds the pointer, so the
|
||||||
|
/// gesture's end arrives as `CursorSense::Drop` -- and `scrollable_on`
|
||||||
|
/// used to register `click_or_drag | unclick` only, which `should_run`
|
||||||
|
/// never matches a `Drop` against. So the widget never learned its own
|
||||||
|
/// gesture had ended: its `DragArbiter` stayed `Panning` at the position
|
||||||
|
/// the finger left, and the *next* drag's first frame was measured from
|
||||||
|
/// there and applied in one step. The registration is
|
||||||
|
/// `CursorSense::drag_senses()` now, which is the rule for every widget
|
||||||
|
/// driving a `DragGesture` rather than a fact about this one.
|
||||||
|
#[test]
|
||||||
|
fn a_scroll_area_that_captured_the_pointer_learns_its_gesture_ended() {
|
||||||
|
let mut rsc = SenseRsc {
|
||||||
|
ui: UiData::default(),
|
||||||
|
events: EventManager::default(),
|
||||||
|
};
|
||||||
|
let scroll_strong = rect(UiColor::WHITE)
|
||||||
|
.height(Len::abs(1000.0))
|
||||||
|
.scrollable()
|
||||||
|
.add_strong(&mut rsc);
|
||||||
|
let scroll = scroll_strong.weak();
|
||||||
|
let root = scroll_strong.any();
|
||||||
|
|
||||||
|
let mut render = UiRenderState::new();
|
||||||
|
render.resize((100.0, 100.0));
|
||||||
|
render.update(&root, &mut rsc);
|
||||||
|
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(0.0);
|
||||||
|
render.update(&root, &mut rsc);
|
||||||
|
|
||||||
|
let mut state = ();
|
||||||
|
let win = Vec2::new(100.0, 100.0);
|
||||||
|
let mut send = |render: &UiRenderState, rsc: &mut SenseRsc, y: f32, button| {
|
||||||
|
let mut c = cursor_at((50.0, y).into());
|
||||||
|
c.buttons.left = button;
|
||||||
|
render.run_sensors(rsc, &mut state, c, win);
|
||||||
|
};
|
||||||
|
|
||||||
|
// One pan of 40px past the slop, then a release well outside the
|
||||||
|
// widget -- the ordinary shape of a flick.
|
||||||
|
send(&render, &mut rsc, 80.0, ActivationState::Start);
|
||||||
|
send(
|
||||||
|
&render,
|
||||||
|
&mut rsc,
|
||||||
|
80.0 - (DRAG_SLOP + 40.0),
|
||||||
|
ActivationState::On,
|
||||||
|
);
|
||||||
|
let after_first = rsc.ui.widgets.get(&scroll).unwrap().amt();
|
||||||
|
assert!((after_first - 40.0).abs() < 0.01, "amt={after_first}");
|
||||||
|
send(&render, &mut rsc, 400.0, ActivationState::End);
|
||||||
|
assert_eq!(
|
||||||
|
pointer_input(&mut rsc).holder(),
|
||||||
|
None,
|
||||||
|
"the release must give the pointer back"
|
||||||
|
);
|
||||||
|
|
||||||
|
// A second gesture, starting where the first one did. If the arbiter
|
||||||
|
// were still panning from the release position, this first frame
|
||||||
|
// would apply the whole distance between the two at once.
|
||||||
|
send(&render, &mut rsc, 80.0, ActivationState::Start);
|
||||||
|
let after_second = rsc.ui.widgets.get(&scroll).unwrap().amt();
|
||||||
|
assert!(
|
||||||
|
(after_second - after_first).abs() < 0.01,
|
||||||
|
"a fresh touch-down moved the content by {} -- the previous \
|
||||||
|
gesture was never closed",
|
||||||
|
after_second - after_first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The second half of the same report: "tapping sometimes seems to make
|
||||||
|
/// the scrolling jump, particularly when tapping on things that have
|
||||||
|
/// events like horizontal scrolling."
|
||||||
|
///
|
||||||
|
/// Two widgets see the same press -- a scroll area and, under it,
|
||||||
|
/// something tracking the gesture for a list. When the scroll area
|
||||||
|
/// captures, the other one is cut off completely: no `PressEnd`, no
|
||||||
|
/// `Drop`. It has to be told, or its gesture stays open at an origin
|
||||||
|
/// belonging to a finger that has long gone, and the next unrelated touch
|
||||||
|
/// is measured from it.
|
||||||
|
#[test]
|
||||||
|
fn taking_the_pointer_cancels_everyone_else_tracking_the_press() {
|
||||||
|
let mut rsc = SenseRsc {
|
||||||
|
ui: UiData::default(),
|
||||||
|
events: EventManager::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// The bystander *contains* the capturer, which is the real shape: a
|
||||||
|
// transcript's `List` and one row's own text both track the same
|
||||||
|
// press, and a `Stack`'s siblings would be on separate layers where
|
||||||
|
// only the topmost is dispatched to at all.
|
||||||
|
let capturer = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||||
|
let capturer_weak = capturer.weak();
|
||||||
|
let bystander = rsc.ui.widgets.add_strong(Stack {
|
||||||
|
children: vec![capturer.any()],
|
||||||
|
size: StackSize::default(),
|
||||||
|
});
|
||||||
|
let bystander_weak = bystander.weak();
|
||||||
|
|
||||||
|
let capturer_saw = Rc::new(Cell::new(0u32));
|
||||||
|
{
|
||||||
|
let capturer_saw = capturer_saw.clone();
|
||||||
|
rsc.register_event(
|
||||||
|
capturer_weak,
|
||||||
|
CursorSense::drag_senses(),
|
||||||
|
move |ctx, _rsc| {
|
||||||
|
capturer_saw.set(capturer_saw.get() + 1);
|
||||||
|
if matches!(ctx.data.sense, CursorSense::Pressing(_)) {
|
||||||
|
ctx.data.pointer.capture(capturer_weak.id());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let cancelled = Rc::new(Cell::new(0u32));
|
||||||
|
let ended = Rc::new(Cell::new(0u32));
|
||||||
|
{
|
||||||
|
let (cancelled, ended) = (cancelled.clone(), ended.clone());
|
||||||
|
rsc.register_event(
|
||||||
|
bystander_weak,
|
||||||
|
CursorSense::drag_senses(),
|
||||||
|
move |ctx, _rsc| match ctx.data.sense {
|
||||||
|
CursorSense::Cancel => cancelled.set(cancelled.get() + 1),
|
||||||
|
CursorSense::PressEnd(_) | CursorSense::Drop => ended.set(ended.get() + 1),
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let root = bystander.any();
|
||||||
|
let mut render = UiRenderState::new();
|
||||||
|
render.resize((100.0, 100.0));
|
||||||
|
render.update(&root, &mut rsc);
|
||||||
|
|
||||||
|
let mut state = ();
|
||||||
|
let win = Vec2::new(100.0, 100.0);
|
||||||
|
let mut down = cursor_at((50.0, 50.0).into());
|
||||||
|
down.buttons.left = ActivationState::Start;
|
||||||
|
render.run_sensors(&mut rsc, &mut state, down, win);
|
||||||
|
assert_eq!(cancelled.get(), 0, "nothing has captured yet");
|
||||||
|
|
||||||
|
let mut moved = cursor_at((50.0, 20.0).into());
|
||||||
|
moved.buttons.left = ActivationState::On;
|
||||||
|
render.run_sensors(&mut rsc, &mut state, moved, win);
|
||||||
|
assert!(capturer_saw.get() > 0, "the capturer never saw the press");
|
||||||
|
assert_eq!(
|
||||||
|
pointer_input(&mut rsc).holder(),
|
||||||
|
Some(capturer_weak.id()),
|
||||||
|
"the capture should have been taken on this frame"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cancelled.get(),
|
||||||
|
1,
|
||||||
|
"the widget that lost the gesture must be told exactly once"
|
||||||
|
);
|
||||||
|
|
||||||
|
// And exactly once: the frames after the capture reach the capturer
|
||||||
|
// alone, so there is nothing left to cancel.
|
||||||
|
let mut more = cursor_at((50.0, 10.0).into());
|
||||||
|
more.buttons.left = ActivationState::On;
|
||||||
|
render.run_sensors(&mut rsc, &mut state, more, win);
|
||||||
|
let mut up = cursor_at((50.0, 10.0).into());
|
||||||
|
up.buttons.left = ActivationState::End;
|
||||||
|
render.run_sensors(&mut rsc, &mut state, up, win);
|
||||||
|
assert_eq!(cancelled.get(), 1, "cancelled more than once");
|
||||||
|
assert_eq!(
|
||||||
|
ended.get(),
|
||||||
|
0,
|
||||||
|
"a cancelled widget must not also be told the gesture ended \
|
||||||
|
normally -- acting on that is the tap it never made"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::sense::{DragGesture, GestureOutcome, PressState};
|
use crate::sense::{DragGesture, GestureOutcome, PointerRequests, PressState};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
pub struct Scroll {
|
pub struct Scroll {
|
||||||
@@ -113,7 +113,7 @@ impl Scroll {
|
|||||||
/// released velocity is deliberately dropped rather than approximated.
|
/// released velocity is deliberately dropped rather than approximated.
|
||||||
pub fn drag(
|
pub fn drag(
|
||||||
&mut self,
|
&mut self,
|
||||||
render: &UiRenderState,
|
pointer: &PointerRequests,
|
||||||
id: WidgetId,
|
id: WidgetId,
|
||||||
sense: CursorSense,
|
sense: CursorSense,
|
||||||
pos_window: Vec2,
|
pos_window: Vec2,
|
||||||
@@ -128,7 +128,7 @@ impl Scroll {
|
|||||||
// `DragGesture` itself instead.
|
// `DragGesture` itself instead.
|
||||||
match self
|
match self
|
||||||
.gesture
|
.gesture
|
||||||
.handle(render, id, sense, pos_window, now, PressState::default())
|
.handle(pointer, id, sense, pos_window, now, PressState::default())
|
||||||
{
|
{
|
||||||
// `scroll(dy)`, not `scroll(-dy)` -- `Selection::drag` passes
|
// `scroll(dy)`, not `scroll(-dy)` -- `Selection::drag` passes
|
||||||
// `-dy` to `List::scroll` because a `List`'s anchor offset and
|
// `-dy` to `List::scroll` because a `List`'s anchor offset and
|
||||||
@@ -143,6 +143,7 @@ impl Scroll {
|
|||||||
| GestureOutcome::Tapped
|
| GestureOutcome::Tapped
|
||||||
| GestureOutcome::SelectStart
|
| GestureOutcome::SelectStart
|
||||||
| GestureOutcome::SelectExtend
|
| GestureOutcome::SelectExtend
|
||||||
|
| GestureOutcome::Cancelled
|
||||||
| GestureOutcome::Released(_) => {}
|
| GestureOutcome::Released(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,7 +192,7 @@ mod tests {
|
|||||||
|
|
||||||
fn press(
|
fn press(
|
||||||
s: &mut Scroll,
|
s: &mut Scroll,
|
||||||
render: &UiRenderState,
|
render: &PointerRequests,
|
||||||
id: WidgetId,
|
id: WidgetId,
|
||||||
sense: CursorSense,
|
sense: CursorSense,
|
||||||
y: f32,
|
y: f32,
|
||||||
@@ -203,7 +204,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn a_vertical_finger_drag_pans_the_content_with_the_finger() {
|
fn a_vertical_finger_drag_pans_the_content_with_the_finger() {
|
||||||
let (_ui, mut s, id) = area();
|
let (_ui, mut s, id) = area();
|
||||||
let render = UiRenderState::new();
|
let render = PointerRequests::default();
|
||||||
let t = Instant::now();
|
let t = Instant::now();
|
||||||
press(
|
press(
|
||||||
&mut s,
|
&mut s,
|
||||||
@@ -246,7 +247,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn a_press_that_stays_inside_the_slop_does_not_scroll() {
|
fn a_press_that_stays_inside_the_slop_does_not_scroll() {
|
||||||
let (_ui, mut s, id) = area();
|
let (_ui, mut s, id) = area();
|
||||||
let render = UiRenderState::new();
|
let render = PointerRequests::default();
|
||||||
let t = Instant::now();
|
let t = Instant::now();
|
||||||
press(
|
press(
|
||||||
&mut s,
|
&mut s,
|
||||||
@@ -286,7 +287,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn a_horizontal_drag_does_not_scroll() {
|
fn a_horizontal_drag_does_not_scroll() {
|
||||||
let (_ui, mut s, id) = area();
|
let (_ui, mut s, id) = area();
|
||||||
let render = UiRenderState::new();
|
let render = PointerRequests::default();
|
||||||
let t = Instant::now();
|
let t = Instant::now();
|
||||||
s.drag(
|
s.drag(
|
||||||
&render,
|
&render,
|
||||||
@@ -311,7 +312,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn a_pan_past_the_end_clamps_instead_of_running_off() {
|
fn a_pan_past_the_end_clamps_instead_of_running_off() {
|
||||||
let (_ui, mut s, id) = area();
|
let (_ui, mut s, id) = area();
|
||||||
let render = UiRenderState::new();
|
let render = PointerRequests::default();
|
||||||
let t = Instant::now();
|
let t = Instant::now();
|
||||||
s.drag(
|
s.drag(
|
||||||
&render,
|
&render,
|
||||||
|
|||||||
@@ -103,15 +103,12 @@ widget_trait! {
|
|||||||
// has the arbitration and why there is no fling. The wheel
|
// has the arbitration and why there is no fling. The wheel
|
||||||
// above and this are the two inputs of one scroll, so they
|
// above and this are the two inputs of one scroll, so they
|
||||||
// are registered together rather than left to each caller.
|
// are registered together rather than left to each caller.
|
||||||
.on(
|
.on(CursorSense::drag_senses(), |ctx, rsc| {
|
||||||
CursorSense::click_or_drag() | CursorSense::unclick(),
|
let id = ctx.widget.id();
|
||||||
|ctx, rsc| {
|
let (sense, pos) = (ctx.data.sense, ctx.data.cursor.pos);
|
||||||
let id = ctx.widget.id();
|
ctx.widget(rsc)
|
||||||
let (sense, pos) = (ctx.data.sense, ctx.data.cursor.pos);
|
.drag(ctx.data.pointer, id, sense, pos, ctx.data.cursor.time);
|
||||||
ctx.widget(rsc)
|
})
|
||||||
.drag(ctx.data.render, id, sense, pos, ctx.data.cursor.time);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.add(state)
|
.add(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -397,7 +397,7 @@ where
|
|||||||
{
|
{
|
||||||
let selection = selection.clone();
|
let selection = selection.clone();
|
||||||
list.on(
|
list.on(
|
||||||
CursorSense::Pressing(CursorButton::Left) | CursorSense::Drop,
|
CursorSense::Pressing(CursorButton::Left) | CursorSense::Drop | CursorSense::Cancel,
|
||||||
move |ctx, rsc| {
|
move |ctx, rsc| {
|
||||||
// Which *block* the finger is over, resolved from its
|
// Which *block* the finger is over, resolved from its
|
||||||
// drawn box rather than from the row's extent -- a row is
|
// drawn box rather than from the row's extent -- a row is
|
||||||
@@ -413,7 +413,7 @@ where
|
|||||||
ctx.data.cursor.pos,
|
ctx.data.cursor.pos,
|
||||||
ctx.data.sense,
|
ctx.data.sense,
|
||||||
ctx.data.cursor.time,
|
ctx.data.cursor.time,
|
||||||
ctx.data.render,
|
ctx.data.pointer,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ where
|
|||||||
cursor,
|
cursor,
|
||||||
ctx.data.sense,
|
ctx.data.sense,
|
||||||
ctx.data.cursor.time,
|
ctx.data.cursor.time,
|
||||||
ctx.data.render,
|
ctx.data.pointer,
|
||||||
);
|
);
|
||||||
// A *tap*, decided by the same `DragArbiter` the pan and
|
// A *tap*, decided by the same `DragArbiter` the pan and
|
||||||
// the selection are: a gesture that panned the list past
|
// the selection are: a gesture that panned the list past
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ impl Selection {
|
|||||||
/// selection, where it is rare and the frame is simply dropped.
|
/// selection, where it is rare and the frame is simply dropped.
|
||||||
/// `pos_window` is in window space, since a pan's delta has to stay
|
/// `pos_window` is in window space, since a pan's delta has to stay
|
||||||
/// meaningful even when this frame's event landed on a different row
|
/// meaningful even when this frame's event landed on a different row
|
||||||
/// than the last one. `render` is `CursorData`'s own field -- what
|
/// than the last one. `pointer` is `CursorData`'s own field -- what
|
||||||
/// `DragGesture` needs to take pointer capture.
|
/// `DragGesture` needs to take pointer capture.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
/// Returns what the gesture decided this frame, so a caller with its
|
/// Returns what the gesture decided this frame, so a caller with its
|
||||||
@@ -257,7 +257,7 @@ impl Selection {
|
|||||||
pos_window: Vec2,
|
pos_window: Vec2,
|
||||||
sense: CursorSense,
|
sense: CursorSense,
|
||||||
now: Instant,
|
now: Instant,
|
||||||
render: &UiRenderState,
|
pointer: &PointerRequests,
|
||||||
) -> GestureOutcome {
|
) -> GestureOutcome {
|
||||||
// A fresh touch-down cancels any fling still coasting from the
|
// A fresh touch-down cancels any fling still coasting from the
|
||||||
// previous gesture -- `List::fling`'s own doc, and Android's
|
// previous gesture -- `List::fling`'s own doc, and Android's
|
||||||
@@ -278,9 +278,14 @@ impl Selection {
|
|||||||
press.already_selected = self.has_selection(ui);
|
press.already_selected = self.has_selection(ui);
|
||||||
let outcome = self
|
let outcome = self
|
||||||
.gesture
|
.gesture
|
||||||
.handle(render, list.id(), sense, pos_window, now, press);
|
.handle(pointer, list.id(), sense, pos_window, now, press);
|
||||||
match outcome {
|
match outcome {
|
||||||
GestureOutcome::Undecided => {}
|
// Somebody else took the gesture (a code fence panning
|
||||||
|
// sideways under the finger). Nothing here acted on it, and
|
||||||
|
// `DragGesture` has already forgotten it, so there is nothing
|
||||||
|
// to undo either -- the point is that no tap, fling or
|
||||||
|
// selection follows from a gesture that was never ours.
|
||||||
|
GestureOutcome::Cancelled | GestureOutcome::Undecided => {}
|
||||||
GestureOutcome::Pan(dy) => list(ui).scroll(-dy),
|
GestureOutcome::Pan(dy) => list(ui).scroll(-dy),
|
||||||
GestureOutcome::SelectStart => {
|
GestureOutcome::SelectStart => {
|
||||||
if let Some((key, pos_row, size)) = row {
|
if let Some((key, pos_row, size)) = row {
|
||||||
@@ -428,7 +433,7 @@ mod tests {
|
|||||||
sel.register((1, 0), field);
|
sel.register((1, 0), field);
|
||||||
assert!(sel.gesture.is_idle());
|
assert!(sel.gesture.is_idle());
|
||||||
|
|
||||||
let render = UiRenderState::new();
|
let pointer = PointerRequests::default();
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let size = Vec2::new(100.0, 20.0);
|
let size = Vec2::new(100.0, 20.0);
|
||||||
// No `PressStart` is ever sent -- only the `Pressing` frames a
|
// No `PressStart` is ever sent -- only the `Pressing` frames a
|
||||||
@@ -440,7 +445,7 @@ mod tests {
|
|||||||
Vec2::new(540.0, 700.0),
|
Vec2::new(540.0, 700.0),
|
||||||
CursorSense::Pressing(CursorButton::Left),
|
CursorSense::Pressing(CursorButton::Left),
|
||||||
now,
|
now,
|
||||||
&render,
|
&pointer,
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!sel.gesture.is_idle(),
|
!sel.gesture.is_idle(),
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ fn on_tap<Rsc: HasEvents>(
|
|||||||
ctx.data.cursor.pos,
|
ctx.data.cursor.pos,
|
||||||
ctx.data.sense,
|
ctx.data.sense,
|
||||||
ctx.data.cursor.time,
|
ctx.data.cursor.time,
|
||||||
ctx.data.render,
|
ctx.data.pointer,
|
||||||
);
|
);
|
||||||
if outcome == GestureOutcome::Tapped {
|
if outcome == GestureOutcome::Tapped {
|
||||||
f(rsc);
|
f(rsc);
|
||||||
|
|||||||
Reference in new issue
Block a user