use std::collections::HashMap;
use teksilo_canvas::{Point, Vec2};
use crate::WidgetId;
use crate::event::{Modifiers, ScrollDelta, WidgetEvent};
use crate::gesture::{GestureEvent, PalmWatch, PanRecognizer, TouchPinchRecognizer};
use crate::kinetic::FlingDriver;
use crate::overscroll::OverscrollBehavior;
use crate::pointer::touch_action::PanClaim;
use crate::pointer::{CancelReason, PointerId, ScrollPhase, ScrollSample, ScrollSource};
use super::WidgetTree;
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum ScrollDelivery {
Bubble,
ClaimantChain,
}
impl ScrollDelivery {
pub fn for_source(source: ScrollSource) -> Self {
match source {
ScrollSource::TouchPan => Self::ClaimantChain,
_ => Self::Bubble,
}
}
}
#[derive(Debug)]
struct PanSession {
recognizer: PanRecognizer,
candidates: Vec<(WidgetId, PanClaim)>,
owner: Option<WidgetId>,
began: bool,
modifiers: Modifiers,
}
impl PanSession {
fn chain(&self) -> &[(WidgetId, PanClaim)] {
match self
.owner
.and_then(|owner| self.candidates.iter().position(|(id, _)| *id == owner))
{
Some(index) => &self.candidates[index..],
None => &self.candidates,
}
}
}
#[derive(Debug)]
pub(crate) struct TouchMotion {
pans: HashMap<PointerId, PanSession>,
driver: FlingDriver,
fling_chains: HashMap<WidgetId, Vec<(WidgetId, PanClaim)>>,
last_chain_absorbed: bool,
pinch: TouchPinchRecognizer,
pinch_target: Option<WidgetId>,
palms: HashMap<PointerId, PalmWatch>,
backend_reports_palm: bool,
}
impl TouchMotion {
pub(crate) fn new(scheduler: crate::frame_tick_scheduler::FrameTickScheduler) -> Self {
Self {
pans: HashMap::new(),
driver: FlingDriver::with_scheduler(scheduler),
fling_chains: HashMap::new(),
last_chain_absorbed: false,
pinch: TouchPinchRecognizer::new(),
pinch_target: None,
palms: HashMap::new(),
backend_reports_palm: false,
}
}
}
impl WidgetTree {
pub(super) fn begin_pan(&mut self, target: WidgetId, position: Point, modifiers: Modifiers) {
let pointer = self.current_input.pointer;
let action = self.effective_touch_action(target);
let candidates = self.pan_candidates(target, action);
for (id, _) in &candidates {
self.stop_fling(*id);
}
if !pointer.kind.is_direct() {
return;
}
let profile = self.current_profile();
if profile.pan_slop.is_none() {
return;
}
let Some((_, claim)) = candidates
.iter()
.copied()
.find(|(_, claim)| claim.devices.contains(pointer.kind))
else {
return;
};
let mut recognizer = PanRecognizer::new(claim);
recognizer.press(position, self.sequence_now());
self.touch_motion.pans.insert(
pointer.id,
PanSession {
recognizer,
candidates,
owner: None,
began: false,
modifiers,
},
);
}
pub(crate) fn pan_session_open(&self, pointer: PointerId) -> bool {
self.touch_motion.pans.contains_key(&pointer)
}
pub(super) fn note_pan_claimed(&mut self, pointer: PointerId, owner: WidgetId) {
if let Some(session) = self.touch_motion.pans.get_mut(&pointer) {
session.owner = Some(owner);
}
}
pub(super) fn advance_pan(&mut self, position: Point, ops: &mut dyn crate::window::WindowOps) {
let pointer = self.current_pointer_id();
let now = self.sequence_now();
let coalesced = self.current_input.coalesced.clone();
let Some(session) = self.touch_motion.pans.get_mut(&pointer) else {
return;
};
let delta = session.recognizer.feed_coalesced(&coalesced, position, now);
if session.owner.is_none() {
return;
}
if delta.x == 0.0 && delta.y == 0.0 {
return;
}
let phase = if session.began {
ScrollPhase::Changed
} else {
session.began = true;
ScrollPhase::Began
};
self.deliver_pan(pointer, delta, phase, position, ops);
}
pub(super) fn end_pan(&mut self, position: Point, ops: &mut dyn crate::window::WindowOps) {
let pointer = self.current_pointer_id();
let now = self.sequence_now();
let profile = self.current_profile();
let Some(mut session) = self.touch_motion.pans.remove(&pointer) else {
return;
};
let (Some(owner), true) = (session.owner, session.began) else {
return;
};
session.recognizer.feed(position, now);
let velocity = session.recognizer.velocity(&profile);
let chain: Vec<(WidgetId, PanClaim)> = session.chain().to_vec();
self.dispatch_chained_scroll(
chain.clone(),
Vec2::ZERO,
ScrollPhase::Ended,
Some(position),
session.modifiers,
ops,
);
if !session.recognizer.should_fling(velocity, &profile) {
return;
}
self.start_fling(owner, Vec2::new(-velocity.x, -velocity.y), chain);
}
pub(super) fn pan_chain_ids(&self, pointer: PointerId) -> Vec<WidgetId> {
self.touch_motion
.pans
.get(&pointer)
.map(|session| session.chain().iter().map(|(id, _)| *id).collect())
.unwrap_or_default()
}
pub(super) fn abandon_pan(&mut self, pointer: PointerId) {
self.touch_motion.pans.remove(&pointer);
}
pub(super) fn deliver_pan(
&mut self,
pointer: PointerId,
delta: Vec2,
phase: ScrollPhase,
position: Point,
ops: &mut dyn crate::window::WindowOps,
) {
let Some(session) = self.touch_motion.pans.get(&pointer) else {
return;
};
let chain: Vec<(WidgetId, PanClaim)> = session.chain().to_vec();
let modifiers = session.modifiers;
self.dispatch_chained_scroll(
chain,
Vec2::new(-delta.x, -delta.y),
phase,
Some(position),
modifiers,
ops,
);
}
fn dispatch_chained_scroll(
&mut self,
chain: Vec<(WidgetId, PanClaim)>,
offset_delta: Vec2,
phase: ScrollPhase,
position: Option<Point>,
modifiers: Modifiers,
ops: &mut dyn crate::window::WindowOps,
) {
let pointer = self
.pointers
.get(self.current_pointer_id())
.map(|e| e.info)
.unwrap_or(self.current_input.pointer);
let sample = ScrollSample {
delta: ScrollDelta::Pixels {
x: offset_delta.x,
y: offset_delta.y,
},
position,
phase,
source: ScrollSource::TouchPan,
pointer,
modifiers,
};
self.armed_chain = Some(chain);
self.dispatch_scroll_with_ops(sample, ops);
}
pub(super) fn route_scroll_along_chain(
&mut self,
event: &WidgetEvent,
position: Option<Point>,
ops: &mut dyn crate::window::WindowOps,
) {
let chain = match self.armed_chain.take() {
Some(chain) => chain,
None => {
let pointer = self.current_input.pointer;
let Some(target) = position.and_then(|p| self.hit_test_for(p, &pointer)) else {
return;
};
let action = self.effective_touch_action(target);
self.pan_candidates(target, action)
}
};
let absorbed = self.walk_claimant_chain(&chain, event, ops);
self.touch_motion.last_chain_absorbed = absorbed;
}
fn walk_claimant_chain(
&mut self,
chain: &[(WidgetId, PanClaim)],
event: &WidgetEvent,
ops: &mut dyn crate::window::WindowOps,
) -> bool {
for &(id, _) in chain {
if !self.arena.is_active(id) {
continue;
}
if self.dispatch_to_widget_direct_returning_handled(id, event, &mut *ops) {
crate::trace_input!(Samples, "pan absorbed by {id:?}");
return true;
}
if self.overscroll_behavior_of(id) == OverscrollBehavior::Contain {
crate::trace_input!(Samples, "pan contained at {id:?}: the chain stops here");
return true;
}
}
false
}
fn overscroll_behavior_of(&self, id: WidgetId) -> OverscrollBehavior {
self.arena
.get(id)
.map(|n| n.overscroll_behavior)
.unwrap_or(OverscrollBehavior::Chain)
}
pub fn start_fling(
&mut self,
target: WidgetId,
velocity: Vec2,
chain: Vec<(WidgetId, PanClaim)>,
) {
let physics = self.effective_theme.input.scroll_physics.physics;
self.touch_motion
.driver
.set_tokens(&self.effective_theme.input.scroll_physics);
self.touch_motion
.driver
.set_reduced_motion(self.prefers_reduced_motion);
self.touch_motion
.driver
.start(target, velocity, physics, self.input_now());
if self.touch_motion.driver.is_flinging(target) {
self.touch_motion.fling_chains.insert(target, chain);
} else {
self.touch_motion.fling_chains.remove(&target);
}
}
pub fn stop_fling(&mut self, target: WidgetId) {
self.touch_motion.driver.stop(target);
self.touch_motion.fling_chains.remove(&target);
}
pub fn is_flinging(&self, target: WidgetId) -> bool {
self.touch_motion.driver.is_flinging(target)
}
pub fn tick_flings(&mut self, now: std::time::Instant) {
let mut noop = crate::window::NoopWindowOps;
self.tick_flings_with_ops(now, &mut noop);
}
pub fn tick_flings_with_ops(
&mut self,
now: std::time::Instant,
ops: &mut dyn crate::window::WindowOps,
) {
if self.touch_motion.driver.is_empty() {
return;
}
let now = self.event_time_for(now);
let steps = self.touch_motion.driver.tick(now);
for (target, delta) in steps {
let Some(chain) = self.touch_motion.fling_chains.get(&target).cloned() else {
continue;
};
let position = self
.arena
.is_active(target)
.then(|| self.bounds(target).center());
self.dispatch_chained_scroll(
chain,
delta,
ScrollPhase::Fling,
position,
Modifiers::NONE,
&mut *ops,
);
if !self.touch_motion.last_chain_absorbed {
self.stop_fling(target);
}
}
}
pub fn dispatch_os_gesture(
&mut self,
gesture: GestureEvent,
at: Option<Point>,
ops: &mut dyn crate::window::WindowOps,
) {
let Some(target) = self.os_gesture_target(at) else {
return;
};
self.dispatch_to_widget(target, &WidgetEvent::Gesture { gesture }, ops);
}
fn os_gesture_target(&self, at: Option<Point>) -> Option<WidgetId> {
let hover_position = self.pointers.hover_owner().map(|e| e.position);
let pointer = self.current_input.pointer;
at.or(hover_position)
.and_then(|p| self.hit_test_for(p, &pointer))
.or_else(|| self.hovered_id())
.or(self.focused)
.or_else(|| self.roots().first().copied())
}
pub(super) fn feed_pinch(
&mut self,
phase: PinchFeed,
position: Point,
ops: &mut dyn crate::window::WindowOps,
) {
let pointer = self.current_input.pointer;
if !pointer.kind.is_direct() {
return;
}
let recognized = match phase {
PinchFeed::Down => {
let permitted = self
.hit_test_for(position, &pointer)
.is_some_and(|target| self.effective_touch_action(target).allows_pinch());
permitted
.then(|| self.touch_motion.pinch.contact_down(pointer.id, position))
.flatten()
}
PinchFeed::Move => self.touch_motion.pinch.contact_moved(pointer.id, position),
PinchFeed::Up => self.touch_motion.pinch.contact_up(pointer.id),
};
if let Some(gesture) = recognized {
self.emit_pinch(gesture, ops);
}
}
pub(super) fn cancel_pinch(
&mut self,
pointer: PointerId,
reason: CancelReason,
ops: &mut dyn crate::window::WindowOps,
) {
if !self.touch_motion.pinch.contact_ids().contains(&pointer) {
return;
}
if let Some(gesture) = self.touch_motion.pinch.cancel(reason) {
self.emit_pinch(gesture, ops);
}
}
fn emit_pinch(&mut self, gesture: GestureEvent, ops: &mut dyn crate::window::WindowOps) {
if let GestureEvent::PinchStarted { center } = gesture {
self.touch_motion.pinch_target = self.os_gesture_target(Some(center));
}
let Some(target) = self.touch_motion.pinch_target else {
return;
};
self.dispatch_to_widget(target, &WidgetEvent::Gesture { gesture }, ops);
if matches!(
gesture,
GestureEvent::PinchEnded | GestureEvent::PinchCancelled { .. }
) {
self.touch_motion.pinch_target = None;
}
}
pub fn touch_pinch_active(&self) -> bool {
self.touch_motion.pinch.is_active()
}
pub fn set_backend_reports_palm(&mut self, reports: bool) {
self.touch_motion.backend_reports_palm = reports;
if reports {
self.touch_motion.palms.clear();
}
}
pub fn palm_fallback_active(&self) -> bool {
!self.touch_motion.backend_reports_palm
}
pub(super) fn begin_palm_watch(&mut self, position: Point) {
let pointer = self.current_input.pointer;
if self.touch_motion.backend_reports_palm || !pointer.kind.is_direct() {
return;
}
self.touch_motion
.palms
.insert(pointer.id, PalmWatch::press(position, &pointer.axes));
}
pub(super) fn note_palm_sample(&mut self, position: Point) {
let pointer = self.current_input.pointer;
let profile = self.current_profile();
if let Some(watch) = self.touch_motion.palms.get_mut(&pointer.id) {
watch.sample(position, &pointer.axes, &profile);
}
}
pub(super) fn take_palm_verdict(&mut self, pointer: PointerId) -> bool {
self.touch_motion
.palms
.remove(&pointer)
.is_some_and(|watch| watch.is_palm())
}
pub(super) fn forget_palm_watch(&mut self, pointer: PointerId) {
self.touch_motion.palms.remove(&pointer);
}
pub fn next_input_deadline(&self) -> Option<std::time::Instant> {
let fling = self
.touch_motion
.driver
.next_deadline()
.map(|t| self.instant_for(t));
let hold = self
.next_sequence_hold_deadline()
.map(|t| self.instant_for(t));
let touch_route = self
.next_touch_route_deadline()
.map(|t| self.instant_for(t));
[
self.next_gesture_deadline(),
fling,
hold,
touch_route,
self.next_press_deadline(),
]
.into_iter()
.flatten()
.min()
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub(super) enum PinchFeed {
Down,
Move,
Up,
}
#[cfg(test)]
mod tests;