| // Copyright 2013 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/input/touch_action_filter.h" |
| |
| #include <math.h> |
| |
| #include "base/check_op.h" |
| #include "base/notreached.h" |
| #include "base/trace_event/trace_event.h" |
| #include "third_party/blink/public/common/input/web_gesture_event.h" |
| #include "ui/base/ui_base_features.h" |
| #include "ui/events/blink/blink_features.h" |
| |
| using blink::WebInputEvent; |
| using blink::WebGestureEvent; |
| |
| namespace input { |
| namespace { |
| |
| // Actions on an axis are disallowed if the perpendicular axis has a filter set |
| // and no filter is set for the queried axis. |
| bool IsYAxisActionDisallowed(cc::TouchAction action) { |
| return ((action & cc::TouchAction::kPanX) != cc::TouchAction::kNone) && |
| ((action & cc::TouchAction::kPanY) == cc::TouchAction::kNone); |
| } |
| |
| bool IsXAxisActionDisallowed(cc::TouchAction action) { |
| return ((action & cc::TouchAction::kPanY) != cc::TouchAction::kNone) && |
| ((action & cc::TouchAction::kPanX) == cc::TouchAction::kNone); |
| } |
| |
| void SetCursorControlIfNecessary(WebGestureEvent* event, |
| cc::TouchAction action) { |
| if (event->data.scroll_begin.pointer_count != 1) |
| return; |
| const float abs_delta_x = fabs(event->data.scroll_begin.delta_x_hint); |
| const float abs_delta_y = fabs(event->data.scroll_begin.delta_y_hint); |
| if (abs_delta_x <= abs_delta_y) |
| return; |
| |
| // We shouldn't reach here if kPanX is not allowed for horizontal scroll. |
| DCHECK_NE(action & cc::TouchAction::kPanX, cc::TouchAction::kNone); |
| if ((action & cc::TouchAction::kInternalPanXScrolls) == |
| cc::TouchAction::kInternalPanXScrolls) |
| return; |
| |
| event->data.scroll_begin.cursor_control = true; |
| } |
| |
| } // namespace |
| |
| TouchActionFilter::TouchActionFilter() { |
| ResetTouchAction(); |
| } |
| |
| TouchActionFilter::~TouchActionFilter() = default; |
| |
| FilterGestureEventResult TouchActionFilter::FilterGestureEvent( |
| WebGestureEvent* gesture_event) { |
| TRACE_EVENT0("input", "TouchActionFilter::FilterGestureEvent"); |
| if (gesture_event->SourceDevice() != blink::WebGestureDevice::kTouchscreen) |
| return FilterGestureEventResult::kAllowed; |
| |
| if (has_deferred_events_) { |
| TRACE_EVENT_INSTANT0("input", "Has Deferred", TRACE_EVENT_SCOPE_THREAD); |
| return FilterGestureEventResult::kDelayed; |
| } |
| |
| TRACE_EVENT_INSTANT1( |
| "input", "active_action", TRACE_EVENT_SCOPE_THREAD, "action", |
| (active_touch_action_.has_value() |
| ? cc::TouchActionToString(active_touch_action_.value()) |
| : "n/a")); |
| TRACE_EVENT_INSTANT1( |
| "input", "allowed_action", TRACE_EVENT_SCOPE_THREAD, "action", |
| (allowed_touch_action_.has_value() |
| ? cc::TouchActionToString(allowed_touch_action_.value()) |
| : "n/a")); |
| TRACE_EVENT_INSTANT1( |
| "input", "compositor_allowed_action", TRACE_EVENT_SCOPE_THREAD, "action", |
| cc::TouchActionToString(compositor_allowed_touch_action_)); |
| |
| cc::TouchAction touch_action = active_touch_action_.has_value() |
| ? active_touch_action_.value() |
| : compositor_allowed_touch_action_; |
| |
| // Filter for allowable touch actions first (eg. before the TouchEventQueue |
| // can decide to send a touch cancel event). |
| switch (gesture_event->GetType()) { |
| case WebInputEvent::Type::kGestureScrollBegin: { |
| // In VR or virtual keyboard (https://crbug.com/880701), |
| // GestureScrollBegin could come without GestureTapDown. |
| // TODO(bokan): This can also happen due to the fling controller |
| // filtering out the GestureTapDown due to tap suppression (i.e. tapping |
| // during a fling should stop the fling, not be sent to the page). We |
| // should not reset the touch action in this case! We currently work |
| // around this by resetting the compositor allowed touch action in this |
| // case as well but we should investigate not filtering the TapDown. |
| if (!gesture_sequence_in_progress_) { |
| TRACE_EVENT_INSTANT0("input", "No Sequence at GSB!", |
| TRACE_EVENT_SCOPE_THREAD); |
| gesture_sequence_in_progress_ = true; |
| if (allowed_touch_action_.has_value()) { |
| active_touch_action_ = allowed_touch_action_; |
| touch_action = allowed_touch_action_.value(); |
| } else { |
| touch_action = compositor_allowed_touch_action_; |
| } |
| } |
| drop_scroll_events_ = ShouldSuppressScrolling( |
| *gesture_event, touch_action, active_touch_action_.has_value()); |
| FilterGestureEventResult res; |
| if (!drop_scroll_events_) { |
| if (allow_cursor_control_) { |
| SetCursorControlIfNecessary(gesture_event, touch_action); |
| } |
| res = FilterGestureEventResult::kAllowed; |
| } else if (active_touch_action_.has_value()) { |
| res = FilterGestureEventResult::kFiltered; |
| } else { |
| TRACE_EVENT_INSTANT0("input", "Deferring Events", |
| TRACE_EVENT_SCOPE_THREAD); |
| has_deferred_events_ = true; |
| res = FilterGestureEventResult::kDelayed; |
| } |
| return res; |
| } |
| |
| case WebInputEvent::Type::kGestureScrollUpdate: { |
| if (drop_scroll_events_) { |
| TRACE_EVENT_INSTANT0("input", "Drop Events", TRACE_EVENT_SCOPE_THREAD); |
| return FilterGestureEventResult::kFiltered; |
| } |
| |
| // Scrolls restricted to a specific axis shouldn't permit movement |
| // in the perpendicular axis. |
| // |
| // Note the direction suppression with pinch-zoom here, which matches |
| // Edge: a "touch-action: pan-y pinch-zoom" region allows vertical |
| // two-finger scrolling but a "touch-action: pan-x pinch-zoom" region |
| // doesn't. |
| // TODO(mustaq): Add it to spec? |
| if (IsYAxisActionDisallowed(touch_action)) { |
| if (!active_touch_action_.has_value() && |
| gesture_event->data.scroll_update.delta_y != 0) { |
| TRACE_EVENT_INSTANT0("input", "Defer Due to YAxis", |
| TRACE_EVENT_SCOPE_THREAD); |
| has_deferred_events_ = true; |
| return FilterGestureEventResult::kDelayed; |
| } |
| gesture_event->data.scroll_update.delta_y = 0; |
| } else if (IsXAxisActionDisallowed(touch_action)) { |
| if (!active_touch_action_.has_value() && |
| gesture_event->data.scroll_update.delta_x != 0) { |
| TRACE_EVENT_INSTANT0("input", "Defer Due to XAxis", |
| TRACE_EVENT_SCOPE_THREAD); |
| has_deferred_events_ = true; |
| return FilterGestureEventResult::kDelayed; |
| } |
| gesture_event->data.scroll_update.delta_x = 0; |
| } |
| break; |
| } |
| |
| case WebInputEvent::Type::kGestureFlingStart: |
| // Fling controller processes FlingStart event, and we should never get |
| // it here. |
| NOTREACHED(); |
| |
| case WebInputEvent::Type::kGestureScrollEnd: |
| // Do not reset |compositor_allowed_touch_action_|. In the fling cancel |
| // case, the ack for the second touch sequence start, which sets the |
| // compositor allowed touch action, could arrive before the GSE of the |
| // first fling sequence, we do not want to reset the compositor allowed |
| // touch action. |
| gesture_sequence_in_progress_ = false; |
| return FilterScrollEventAndResetState(); |
| |
| // Evaluate the |drop_pinch_events_| here instead of GSB because pinch |
| // events could arrive without GSB, e.g. double-tap-drag. |
| case WebInputEvent::Type::kGesturePinchBegin: |
| drop_pinch_events_ = (touch_action & cc::TouchAction::kPinchZoom) == |
| cc::TouchAction::kNone; |
| [[fallthrough]]; |
| case WebInputEvent::Type::kGesturePinchUpdate: |
| if (!drop_pinch_events_) |
| return FilterGestureEventResult::kAllowed; |
| if (!active_touch_action_.has_value()) { |
| has_deferred_events_ = true; |
| return FilterGestureEventResult::kDelayed; |
| } |
| return FilterGestureEventResult::kFiltered; |
| case WebInputEvent::Type::kGesturePinchEnd: |
| return FilterPinchEventAndResetState(); |
| |
| // The double tap gesture is a tap ending event. If a double-tap gesture is |
| // filtered out, replace it with a tap event but preserve the tap-count to |
| // allow firing dblclick event in Blink. |
| // |
| // TODO(mustaq): This replacement of a double-tap gesture with a tap seems |
| // buggy, it produces an inconsistent gesture event stream: GestureTapCancel |
| // followed by GestureTap. See crbug.com/874474#c47 for a repro. We don't |
| // know of any bug resulting from it, but it's better to fix the broken |
| // assumption here at least to avoid introducing new bugs in future. |
| case WebInputEvent::Type::kGestureDoubleTap: |
| gesture_sequence_in_progress_ = false; |
| DCHECK_EQ(1, gesture_event->data.tap.tap_count); |
| if (!allow_current_double_tap_event_) { |
| gesture_event->SetType(WebInputEvent::Type::kGestureTap); |
| gesture_event->data.tap.tap_count = 2; |
| } |
| allow_current_double_tap_event_ = true; |
| break; |
| |
| // If double tap is disabled, there's no reason for the tap delay. |
| case WebInputEvent::Type::kGestureTapUnconfirmed: { |
| DCHECK_EQ(1, gesture_event->data.tap.tap_count); |
| allow_current_double_tap_event_ = |
| (touch_action & cc::TouchAction::kDoubleTapZoom) != |
| cc::TouchAction::kNone; |
| if (!allow_current_double_tap_event_) { |
| gesture_event->SetType(WebInputEvent::Type::kGestureTap); |
| drop_current_tap_ending_event_ = true; |
| } |
| break; |
| } |
| |
| case WebInputEvent::Type::kGestureTap: |
| gesture_sequence_in_progress_ = false; |
| if (drop_current_tap_ending_event_) { |
| drop_current_tap_ending_event_ = false; |
| return FilterGestureEventResult::kFiltered; |
| } |
| break; |
| |
| case WebInputEvent::Type::kGestureTapCancel: |
| if (drop_current_tap_ending_event_) { |
| drop_current_tap_ending_event_ = false; |
| return FilterGestureEventResult::kFiltered; |
| } |
| break; |
| |
| case WebInputEvent::Type::kGestureTapDown: |
| gesture_sequence_in_progress_ = true; |
| allow_cursor_control_ = |
| !::features::IsTouchTextEditingRedesignEnabled() || |
| gesture_event->data.tap_down.tap_down_count <= 1; |
| // In theory, the num_of_active_touches_ should be > 0 at this point. But |
| // crash reports suggest otherwise. |
| if (num_of_active_touches_ <= 0) |
| SetTouchAction(cc::TouchAction::kAuto); |
| active_touch_action_ = allowed_touch_action_; |
| DCHECK(!drop_current_tap_ending_event_); |
| break; |
| |
| case WebInputEvent::Type::kGestureLongPress: |
| allow_cursor_control_ = false; |
| break; |
| |
| case WebInputEvent::Type::kGestureLongTap: |
| case WebInputEvent::Type::kGestureTwoFingerTap: |
| gesture_sequence_in_progress_ = false; |
| break; |
| |
| default: |
| // Gesture events unrelated to touch actions (panning/zooming) are left |
| // alone. |
| break; |
| } |
| |
| return FilterGestureEventResult::kAllowed; |
| } |
| |
| void TouchActionFilter::SetTouchAction(cc::TouchAction touch_action) { |
| TRACE_EVENT1("input", "TouchActionFilter::SetTouchAction", "action", |
| cc::TouchActionToString(touch_action)); |
| allowed_touch_action_ = touch_action; |
| active_touch_action_ = allowed_touch_action_; |
| compositor_allowed_touch_action_ = touch_action; |
| } |
| |
| FilterGestureEventResult TouchActionFilter::FilterPinchEventAndResetState() { |
| if (drop_pinch_events_) { |
| drop_pinch_events_ = false; |
| return FilterGestureEventResult::kFiltered; |
| } |
| return FilterGestureEventResult::kAllowed; |
| } |
| |
| FilterGestureEventResult TouchActionFilter::FilterScrollEventAndResetState() { |
| if (drop_scroll_events_) { |
| drop_scroll_events_ = false; |
| return FilterGestureEventResult::kFiltered; |
| } |
| return FilterGestureEventResult::kAllowed; |
| } |
| |
| void TouchActionFilter::ForceResetTouchActionForTest() { |
| allowed_touch_action_.reset(); |
| active_touch_action_.reset(); |
| } |
| |
| void TouchActionFilter::OnSetTouchAction(cc::TouchAction touch_action) { |
| TRACE_EVENT2("input", "TouchActionFilter::OnSetTouchAction", "action", |
| cc::TouchActionToString(touch_action), "allowed", |
| (allowed_touch_action_.has_value() |
| ? cc::TouchActionToString(allowed_touch_action_.value()) |
| : "n/a")); |
| // TODO(crbug.com/40579429): add a DCHECK for |
| // |has_touch_event_handler_|. |
| // For multiple fingers, we take the intersection of the touch actions for |
| // all fingers that have gone down during this action. In the majority of |
| // real-world scenarios the touch action for all fingers will be the same. |
| // This is left as implementation-defined in the pointer events |
| // specification because of the relationship to gestures (which are off |
| // limits for the spec). I believe the following are desirable properties |
| // of this choice: |
| // 1. Not sensitive to finger touch order. Behavior of putting two fingers |
| // down "at once" will be deterministic. |
| // 2. Only subtractive - eg. can't trigger scrolling on a element that |
| // otherwise has scrolling disabling by the addition of a finger. |
| allowed_touch_action_ = |
| allowed_touch_action_.value_or(cc::TouchAction::kAuto) & touch_action; |
| |
| // When user enabled force enable zoom, we should always allow pinch-zoom |
| // except for touch-action:none. |
| if (force_enable_zoom_ && allowed_touch_action_ != cc::TouchAction::kNone) { |
| allowed_touch_action_ = |
| allowed_touch_action_.value() | cc::TouchAction::kPinchZoom; |
| } |
| active_touch_action_ = allowed_touch_action_; |
| has_deferred_events_ = false; |
| } |
| |
| void TouchActionFilter::IncreaseActiveTouches() { |
| TRACE_EVENT1("input", "TouchActionFilter::IncreaseActiveTouches", "num", |
| num_of_active_touches_); |
| num_of_active_touches_++; |
| } |
| |
| void TouchActionFilter::DecreaseActiveTouches() { |
| TRACE_EVENT1("input", "TouchActionFilter::DecreaseActiveTouches", "num", |
| num_of_active_touches_); |
| num_of_active_touches_--; |
| } |
| |
| void TouchActionFilter::ReportAndResetTouchAction() { |
| if (num_of_active_touches_ <= 0) { |
| ResetTouchAction(); |
| allow_cursor_control_ = true; |
| } |
| } |
| |
| void TouchActionFilter::ResetTouchAction() { |
| TRACE_EVENT0("input", "TouchActionFilter::ResetTouchAction"); |
| // Note that resetting the action mid-sequence is tolerated. Gestures that had |
| // their begin event(s) suppressed will be suppressed until the next sequence. |
| if (has_touch_event_handler_) { |
| allowed_touch_action_.reset(); |
| compositor_allowed_touch_action_ = cc::TouchAction::kAuto; |
| } else { |
| // Lack of a touch handler indicates that the page either has no |
| // touch-action modifiers or that all its touch-action modifiers are auto. |
| // Resetting the touch-action here allows forwarding of subsequent gestures |
| // even if the underlying touches never reach the router. |
| SetTouchAction(cc::TouchAction::kAuto); |
| } |
| } |
| |
| void TouchActionFilter::OnSetCompositorAllowedTouchAction( |
| cc::TouchAction allowed_touch_action) { |
| TRACE_EVENT2("input", "TouchActionFilter::OnSetCompositorAllowedTouchAction", |
| "action", cc::TouchActionToString(allowed_touch_action), |
| "current", cc::TouchActionToString(allowed_touch_action)); |
| // We use '&' here to account for the multiple-finger case, which is the same |
| // as OnSetTouchAction. |
| compositor_allowed_touch_action_ = |
| compositor_allowed_touch_action_ & allowed_touch_action; |
| } |
| |
| bool TouchActionFilter::ShouldSuppressScrolling( |
| const blink::WebGestureEvent& gesture_event, |
| cc::TouchAction touch_action, |
| bool is_active_touch_action) { |
| DCHECK(gesture_event.GetType() == WebInputEvent::Type::kGestureScrollBegin); |
| // If kInternalPanXScrolls is true, kPanX must be true; |
| DCHECK((touch_action & cc::TouchAction::kInternalPanXScrolls) == |
| cc::TouchAction::kNone || |
| (touch_action & cc::TouchAction::kPanX) != cc::TouchAction::kNone); |
| |
| if (gesture_event.data.scroll_begin.pointer_count >= 2) { |
| // Any GestureScrollBegin with more than one fingers is like a pinch-zoom |
| // for touch-actions, see crbug.com/632525. Therefore, we switch to |
| // blocked-manipulation mode iff pinch-zoom is disallowed. |
| return (touch_action & cc::TouchAction::kPinchZoom) == |
| cc::TouchAction::kNone; |
| } |
| |
| const float& deltaXHint = gesture_event.data.scroll_begin.delta_x_hint; |
| const float& deltaYHint = gesture_event.data.scroll_begin.delta_y_hint; |
| |
| if (deltaXHint == 0.0 && deltaYHint == 0.0) |
| return false; |
| |
| const float absDeltaXHint = fabs(deltaXHint); |
| const float absDeltaYHint = fabs(deltaYHint); |
| |
| // We need to wait for main-thread touch action to see if touch region is |
| // writable for stylus handwriting, and accumulate scroll events until then. |
| if ((gesture_event.primary_pointer_type == |
| blink::WebPointerProperties::PointerType::kPen || |
| gesture_event.primary_pointer_type == |
| blink::WebPointerProperties::PointerType::kEraser) && |
| !is_active_touch_action && |
| (touch_action & cc::TouchAction::kInternalNotWritable) != |
| cc::TouchAction::kInternalNotWritable) |
| return true; |
| |
| cc::TouchAction minimal_conforming_touch_action = cc::TouchAction::kNone; |
| if (absDeltaXHint > absDeltaYHint) { |
| // If we're performing a horizontal gesture over a region that could |
| // potentially activate cursor control, we need to wait for the real |
| // main-thread touch action before making a decision since we'll need to set |
| // the cursor control bit correctly. |
| if (!is_active_touch_action && |
| (touch_action & cc::TouchAction::kInternalPanXScrolls) != |
| cc::TouchAction::kInternalPanXScrolls) |
| return true; |
| |
| if (deltaXHint > 0) |
| minimal_conforming_touch_action |= cc::TouchAction::kPanLeft; |
| else if (deltaXHint < 0) |
| minimal_conforming_touch_action |= cc::TouchAction::kPanRight; |
| } else { |
| if (deltaYHint > 0) |
| minimal_conforming_touch_action |= cc::TouchAction::kPanUp; |
| else if (deltaYHint < 0) |
| minimal_conforming_touch_action |= cc::TouchAction::kPanDown; |
| } |
| DCHECK(minimal_conforming_touch_action != cc::TouchAction::kNone); |
| |
| return (touch_action & minimal_conforming_touch_action) == |
| cc::TouchAction::kNone; |
| } |
| |
| void TouchActionFilter::OnHasTouchEventHandlers(bool has_handlers) { |
| TRACE_EVENT1("input", "TouchActionFilter::OnHasTouchEventHandlers", |
| "has handlers", has_handlers); |
| // The has_touch_event_handler_ is default to false which is why we have the |
| // "&&" condition here, to ensure that touch actions will be set if there is |
| // no touch event consumers. |
| if (has_handlers && has_touch_event_handler_ == has_handlers) |
| return; |
| has_touch_event_handler_ = has_handlers; |
| // We have set the associated touch action if the touch start already happened |
| // or there is a gesture in progress. In these cases, we should not reset the |
| // associated touch action. |
| if (!gesture_sequence_in_progress_ && num_of_active_touches_ <= 0) { |
| ResetTouchAction(); |
| if (has_touch_event_handler_) { |
| active_touch_action_.reset(); |
| } |
| } |
| } |
| |
| } // namespace input |