Touch handler: Fix race condition and rate-limit move events (#35537)

* TouchSequenceInfo is added to store information about a touch sequence.
  For details about TouchSequenceInfo, see the code comments.
The handling_touch_move attribute is added to the TouchHandler, indicating that the script is processing the touch move event.
  When handling_touch_move is set to true, the touch move event does not need to be sent to the script thread.

Signed-off-by: kongbai1996 <1782765876@qq.com>

* move touch state, active_touch_point and handling_touch_move to TouchSequenceInfo form TouchHandler.
remove TouchSequenceInfo end_sequence property, add Finished state mark sequence end.
if preventDefault on touchup, do not prevent Fling.

Signed-off-by: kongbai1996 <1782765876@qq.com>

* Refactor Touchhandler

- Add a newtype wrapper for the TouchSequenceId
- Move more state back into the TouchSequenceState
- Rename TouchAction to TouchMoveAction,
  since it only covers immediate actions now.
  Everything else is handled via state, since
  it needs to wait on the handler.

Signed-off-by: Jonathan Schwender <schwenderjonathan@gmail.com>

* Fix test-tidy

Signed-off-by: Jonathan Schwender <schwenderjonathan@gmail.com>

* Fix clippy missing-default lint

Signed-off-by: Jonathan Schwender <schwenderjonathan@gmail.com>

* Fix remaining clippy lints

Signed-off-by: Jonathan Schwender <schwenderjonathan@gmail.com>

* Remove accidental committed test file

Signed-off-by: Jonathan Schwender <schwenderjonathan@gmail.com>

* Remove wrong todo comment

(move events that are sent to script are just raw  touchpoints,
 no merging needed)

Signed-off-by: Jonathan Schwender <schwenderjonathan@gmail.com>

* Fix preventdefault after long touch_down handler

Signed-off-by: Jonathan Schwender <schwenderjonathan@gmail.com>

---------

Signed-off-by: kongbai1996 <1782765876@qq.com>
Signed-off-by: Jonathan Schwender <schwenderjonathan@gmail.com>
Co-authored-by: Jonathan Schwender <schwenderjonathan@gmail.com>
This commit is contained in:
Bi Fuguo 2025-02-25 15:13:16 +08:00 committed by GitHub
parent 374bfc6983
commit f3a8bf8ca2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 746 additions and 278 deletions

View file

@ -22,7 +22,7 @@ use compositing_traits::{
use crossbeam_channel::Sender;
use embedder_traits::{
Cursor, InputEvent, MouseButton, MouseButtonAction, MouseButtonEvent, MouseMoveEvent,
ShutdownState, TouchAction, TouchEvent, TouchEventType, TouchId,
ShutdownState, TouchEvent, TouchEventType, TouchId,
};
use euclid::{Point2D, Rect, Scale, Size2D, Transform3D, Vector2D};
use fnv::{FnvHashMap, FnvHashSet};
@ -33,7 +33,7 @@ use pixels::{CorsStatus, Image, PixelFormat};
use profile_traits::time::{self as profile_time, ProfilerCategory};
use profile_traits::time_profile;
use script_traits::{
AnimationState, AnimationTickType, EventResult, ScriptThreadMessage, ScrollState,
AnimationState, AnimationTickType, ScriptThreadMessage, ScrollState, TouchEventResult,
WindowSizeData, WindowSizeType,
};
use servo_config::opts;
@ -57,7 +57,7 @@ use webrender_traits::{
CompositorHitTestResult, CrossProcessCompositorMessage, ImageUpdate, UntrustedNodeAddress,
};
use crate::touch::TouchHandler;
use crate::touch::{TouchHandler, TouchMoveAction, TouchMoveAllowed, TouchSequenceState};
use crate::webview::{UnknownWebView, WebView, WebViewAlreadyExists, WebViewManager};
use crate::windowing::{self, EmbedderCoordinates, WebRenderDebugOption, WindowMethods};
use crate::InitialCompositorState;
@ -1303,28 +1303,25 @@ impl IOCompositor {
InputEvent::MouseButton(event) => {
match event.action {
MouseButtonAction::Click => {},
MouseButtonAction::Down => self.on_touch_down(TouchEvent {
event_type: TouchEventType::Down,
id: TouchId(0),
point: event.point,
action: TouchAction::NoAction,
}),
MouseButtonAction::Up => self.on_touch_up(TouchEvent {
event_type: TouchEventType::Up,
id: TouchId(0),
point: event.point,
action: TouchAction::NoAction,
}),
MouseButtonAction::Down => self.on_touch_down(TouchEvent::new(
TouchEventType::Down,
TouchId(0),
event.point,
)),
MouseButtonAction::Up => self.on_touch_up(TouchEvent::new(
TouchEventType::Up,
TouchId(0),
event.point,
)),
}
return;
},
InputEvent::MouseMove(event) => {
self.on_touch_move(TouchEvent {
event_type: TouchEventType::Move,
id: TouchId(0),
point: event.point,
action: TouchAction::NoAction,
});
self.on_touch_move(TouchEvent::new(
TouchEventType::Move,
TouchId(0),
event.point,
));
return;
},
_ => {},
@ -1386,11 +1383,12 @@ impl IOCompositor {
.collect()
}
fn send_touch_event(&self, event: TouchEvent) {
fn send_touch_event(&self, mut event: TouchEvent) -> bool {
let Some(result) = self.hit_test_at_point(event.point) else {
return;
return false;
};
event.init_sequence_id(self.touch_handler.current_sequence_id);
let event = InputEvent::Touch(event);
if let Err(e) = self
.global
@ -1398,6 +1396,9 @@ impl IOCompositor {
.send(ConstellationMsg::ForwardInputEvent(event, Some(result)))
{
warn!("Sending event to constellation failed ({:?}).", e);
false
} else {
true
}
}
@ -1419,16 +1420,21 @@ impl IOCompositor {
self.send_touch_event(event);
}
fn on_touch_move(&mut self, mut event: TouchEvent) {
let action: TouchAction = self.touch_handler.on_touch_move(event.id, event.point);
if TouchAction::NoAction != action {
if !self.touch_handler.prevent_move {
fn on_touch_move(&mut self, event: TouchEvent) {
let action: TouchMoveAction = self.touch_handler.on_touch_move(event.id, event.point);
if TouchMoveAction::NoAction != action {
// if first move processed and allowed, we directly process the move event,
// without waiting for the script handler.
if self
.touch_handler
.move_allowed(self.touch_handler.current_sequence_id)
{
match action {
TouchAction::Scroll(delta, point) => self.on_scroll_window_event(
TouchMoveAction::Scroll(delta, point) => self.on_scroll_window_event(
ScrollLocation::Delta(LayoutVector2D::from_untyped(delta.to_untyped())),
point.cast(),
),
TouchAction::Zoom(magnification, scroll_delta) => {
TouchMoveAction::Zoom(magnification, scroll_delta) => {
let cursor = Point2D::new(-1, -1); // Make sure this hits the base layer.
// The order of these events doesn't matter, because zoom is handled by
@ -1447,69 +1453,203 @@ impl IOCompositor {
},
_ => {},
}
} else {
event.action = action;
}
self.send_touch_event(event);
// When the event is touchmove, if the script thread is processing the touch
// move event, we skip sending the event to the script thread.
// This prevents the script thread from stacking up for a large amount of time.
if !self
.touch_handler
.is_handling_touch_move(self.touch_handler.current_sequence_id) &&
self.send_touch_event(event)
{
self.touch_handler
.set_handling_touch_move(self.touch_handler.current_sequence_id, true);
}
}
}
fn on_touch_up(&mut self, mut event: TouchEvent) {
let action = self.touch_handler.on_touch_up(event.id, event.point);
event.action = action;
fn on_touch_up(&mut self, event: TouchEvent) {
self.touch_handler.on_touch_up(event.id, event.point);
self.send_touch_event(event);
}
fn on_touch_cancel(&mut self, event: TouchEvent) {
// Send the event to script.
self.touch_handler.on_touch_cancel(event.id, event.point);
self.send_touch_event(event)
self.send_touch_event(event);
}
fn on_touch_event_processed(&mut self, result: EventResult) {
fn on_touch_event_processed(&mut self, result: TouchEventResult) {
match result {
EventResult::DefaultPrevented(event_type) => {
TouchEventResult::DefaultPrevented(sequence_id, event_type) => {
debug!(
"Touch event {:?} in sequence {:?} prevented!",
event_type, sequence_id
);
match event_type {
TouchEventType::Down | TouchEventType::Move => {
self.touch_handler.prevent_move = true;
TouchEventType::Down => {
// prevents both click and move
self.touch_handler.prevent_click(sequence_id);
self.touch_handler.prevent_move(sequence_id);
self.touch_handler
.remove_pending_touch_move_action(sequence_id);
},
_ => {},
}
self.touch_handler.prevent_click = true;
},
EventResult::DefaultAllowed(action) => {
self.touch_handler.prevent_move = false;
match action {
TouchAction::Click(point) => {
if !self.touch_handler.prevent_click {
self.simulate_mouse_click(point);
TouchEventType::Move => {
// script thread processed the touch move event, mark this false.
let info = self.touch_handler.get_touch_sequence_mut(sequence_id);
info.prevent_move = TouchMoveAllowed::Prevented;
if let TouchSequenceState::PendingFling { .. } = info.state {
info.state = TouchSequenceState::Finished;
}
self.touch_handler.prevent_move(sequence_id);
self.touch_handler
.set_handling_touch_move(self.touch_handler.current_sequence_id, false);
self.touch_handler
.remove_pending_touch_move_action(sequence_id);
},
TouchEventType::Up => {
// Note: We don't have to consider PendingFling here, since we handle that
// in the DefaultAllowed case of the touch_move event.
// Note: Removing can and should fail, if we still have an active Fling,
let Some(info) =
&mut self.touch_handler.touch_sequence_map.get_mut(&sequence_id)
else {
// The sequence ID could already be removed, e.g. if Fling finished,
// before the touch_up event was handled (since fling can start
// immediately if move was previously allowed, and clicks are anyway not
// happening from fling).
return;
};
match info.state {
TouchSequenceState::PendingClick(_) => {
info.state = TouchSequenceState::Finished;
self.touch_handler.remove_touch_sequence(sequence_id);
},
TouchSequenceState::Flinging { .. } => {
// We can't remove the touch sequence yet
},
TouchSequenceState::Finished => {
self.touch_handler.remove_touch_sequence(sequence_id);
},
TouchSequenceState::Touching |
TouchSequenceState::Panning { .. } |
TouchSequenceState::Pinching |
TouchSequenceState::MultiTouch |
TouchSequenceState::PendingFling { .. } => {
// It's possible to transition from Pinch to pan, Which means that
// a touch_up event for a pinch might have arrived here, but we
// already transitioned to pan or even PendingFling.
// We don't need to do anything in these cases though.
},
}
},
TouchAction::Flinging(velocity, point) => {
self.touch_handler.on_fling(velocity, point);
TouchEventType::Cancel => {
// We could still have pending event handlers, so we remove the pending
// actions, and try to remove the touch sequence.
self.touch_handler
.remove_pending_touch_move_action(sequence_id);
// Todo: Perhaps we need to check how many fingers are still active.
self.touch_handler.get_touch_sequence_mut(sequence_id).state =
TouchSequenceState::Finished;
// Cancel should be the last event for a given sequence_id.
self.touch_handler.try_remove_touch_sequence(sequence_id);
},
TouchAction::Scroll(delta, point) => self.on_scroll_window_event(
ScrollLocation::Delta(LayoutVector2D::from_untyped(delta.to_untyped())),
point.cast(),
),
TouchAction::Zoom(magnification, scroll_delta) => {
let cursor = Point2D::new(-1, -1); // Make sure this hits the base layer.
// The order of these events doesn't matter, because zoom is handled by
// a root display list and the scroll event here is handled by the scroll
// applied to the content display list.
self.pending_scroll_zoom_events
.push(ScrollZoomEvent::PinchZoom(magnification));
self.pending_scroll_zoom_events
.push(ScrollZoomEvent::Scroll(ScrollEvent {
scroll_location: ScrollLocation::Delta(
LayoutVector2D::from_untyped(scroll_delta.to_untyped()),
),
cursor,
event_count: 1,
}));
}
},
TouchEventResult::DefaultAllowed(sequence_id, event_type) => {
debug!(
"Touch event {:?} in sequence {:?} allowed",
event_type, sequence_id
);
match event_type {
TouchEventType::Down => {},
TouchEventType::Move => {
if let Some(action) =
self.touch_handler.pending_touch_move_action(sequence_id)
{
match action {
TouchMoveAction::Scroll(delta, point) => self
.on_scroll_window_event(
ScrollLocation::Delta(LayoutVector2D::from_untyped(
delta.to_untyped(),
)),
point.cast(),
),
TouchMoveAction::Zoom(magnification, scroll_delta) => {
let cursor = Point2D::new(-1, -1);
// Make sure this hits the base layer.
// The order of these events doesn't matter, because zoom is handled by
// a root display list and the scroll event here is handled by the scroll
// applied to the content display list.
self.pending_scroll_zoom_events
.push(ScrollZoomEvent::PinchZoom(magnification));
self.pending_scroll_zoom_events
.push(ScrollZoomEvent::Scroll(ScrollEvent {
scroll_location: ScrollLocation::Delta(
LayoutVector2D::from_untyped(
scroll_delta.to_untyped(),
),
),
cursor,
event_count: 1,
}));
},
TouchMoveAction::NoAction => {
// This shouldn't happen, but we can also just ignore it.
},
}
self.touch_handler
.remove_pending_touch_move_action(sequence_id);
}
self.touch_handler
.set_handling_touch_move(self.touch_handler.current_sequence_id, false);
let info = self.touch_handler.get_touch_sequence_mut(sequence_id);
info.prevent_move = TouchMoveAllowed::Allowed;
if let TouchSequenceState::PendingFling { velocity, cursor } = info.state {
info.state = TouchSequenceState::Flinging { velocity, cursor }
}
},
TouchEventType::Up => {
let Some(info) =
self.touch_handler.touch_sequence_map.get_mut(&sequence_id)
else {
// The sequence was already removed because there is no default action.
return;
};
match info.state {
TouchSequenceState::PendingClick(point) => {
info.state = TouchSequenceState::Finished;
// PreventDefault from touch_down may have been processed after
// touch_up already occurred.
if !info.prevent_click {
self.simulate_mouse_click(point);
}
self.touch_handler.remove_touch_sequence(sequence_id);
},
TouchSequenceState::Flinging { .. } => {
// We can't remove the touch sequence yet
},
TouchSequenceState::Finished => {
self.touch_handler.remove_touch_sequence(sequence_id);
},
TouchSequenceState::Panning { .. } |
TouchSequenceState::Pinching |
TouchSequenceState::PendingFling { .. } => {
// It's possible to transition from Pinch to pan, Which means that
// a touch_up event for a pinch might have arrived here, but we
// already transitioned to pan or even PendingFling.
// We don't need to do anything in these cases though.
},
TouchSequenceState::MultiTouch | TouchSequenceState::Touching => {
// We transitioned to touching from multi-touch or pinching.
},
}
},
TouchEventType::Cancel => {
self.touch_handler
.remove_pending_touch_move_action(sequence_id);
self.touch_handler.remove_touch_sequence(sequence_id);
},
_ => {},
}
},
}