| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/ozone/platform/wayland/host/wayland_event_source.h" |
| |
| #include <cstdint> |
| #include <functional> |
| #include <memory> |
| #include <optional> |
| #include <tuple> |
| |
| #include "base/check.h" |
| #include "base/containers/cxx20_erase.h" |
| #include "base/functional/bind.h" |
| #include "base/logging.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/ranges/algorithm.h" |
| #include "base/time/time.h" |
| #include "build/chromeos_buildflags.h" |
| #include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h" |
| #include "ui/events/base_event_utils.h" |
| #include "ui/events/event_utils.h" |
| #include "ui/events/keycodes/dom/dom_code.h" |
| #include "ui/events/keycodes/dom/dom_key.h" |
| #include "ui/events/keycodes/dom/keycode_converter.h" |
| #include "ui/events/keycodes/keyboard_code_conversion.h" |
| #include "ui/events/keycodes/keyboard_codes.h" |
| #include "ui/events/ozone/events_ozone.h" |
| #include "ui/events/ozone/layout/keyboard_layout_engine.h" |
| #include "ui/events/ozone/layout/keyboard_layout_engine_manager.h" |
| #include "ui/events/platform/wayland/wayland_event_watcher.h" |
| #include "ui/events/pointer_details.h" |
| #include "ui/events/types/event_type.h" |
| #include "ui/gfx/geometry/point_f.h" |
| #include "ui/gfx/geometry/vector2d_f.h" |
| #include "ui/ozone/platform/wayland/host/dump_util.h" |
| #include "ui/ozone/platform/wayland/host/wayland_connection.h" |
| #include "ui/ozone/platform/wayland/host/wayland_cursor_position.h" |
| #include "ui/ozone/platform/wayland/host/wayland_keyboard.h" |
| #include "ui/ozone/platform/wayland/host/wayland_window.h" |
| #include "ui/ozone/platform/wayland/host/wayland_window_drag_controller.h" |
| #include "ui/ozone/platform/wayland/host/wayland_window_manager.h" |
| |
| namespace ui { |
| |
| namespace { |
| |
| constexpr auto kPointerToStringMap = base::MakeFixedFlatMap<int, const char*>({ |
| {EF_LEFT_MOUSE_BUTTON, "Left"}, |
| {EF_MIDDLE_MOUSE_BUTTON, "Middle"}, |
| {EF_RIGHT_MOUSE_BUTTON, "Right"}, |
| {EF_BACK_MOUSE_BUTTON, "Back"}, |
| {EF_FORWARD_MOUSE_BUTTON, "Forward"}, |
| }); |
| |
| constexpr auto kModifierToStringMap = base::MakeFixedFlatMap<int, const char*>({ |
| {ui::EF_SHIFT_DOWN, "Shift"}, |
| {ui::EF_CONTROL_DOWN, "Control"}, |
| {ui::EF_ALT_DOWN, "Alt"}, |
| {ui::EF_COMMAND_DOWN, "Command"}, |
| {ui::EF_ALTGR_DOWN, "AltGr"}, |
| {ui::EF_MOD3_DOWN, "Mod3"}, |
| {ui::EF_CAPS_LOCK_ON, "CapsLock"}, |
| {ui::EF_NUM_LOCK_ON, "NumLock"}, |
| }); |
| |
| std::string ToPointerFlagsString(int flags) { |
| return ToMatchingKeyMaskString(flags, kPointerToStringMap); |
| } |
| |
| std::string ToKeyboardModifierStrings(int modifiers) { |
| return ToMatchingKeyMaskString(modifiers, kModifierToStringMap); |
| } |
| |
| bool HasAnyPointerButtonFlag(int flags) { |
| return (flags & (EF_LEFT_MOUSE_BUTTON | EF_MIDDLE_MOUSE_BUTTON | |
| EF_RIGHT_MOUSE_BUTTON | EF_BACK_MOUSE_BUTTON | |
| EF_FORWARD_MOUSE_BUTTON)) != 0; |
| } |
| |
| std::vector<uint8_t> ToLittleEndianByteVector(uint32_t value) { |
| return {static_cast<uint8_t>(value), static_cast<uint8_t>(value >> 8), |
| static_cast<uint8_t>(value >> 16), static_cast<uint8_t>(value >> 24)}; |
| } |
| |
| EventTarget* GetRootTarget(EventTarget* target) { |
| EventTarget* parent = target->GetParentTarget(); |
| return parent ? GetRootTarget(parent) : target; |
| } |
| |
| gfx::Point GetOriginInScreen(WaylandWindow* target) { |
| gfx::Point origin = target->GetBoundsInDIP().origin(); |
| auto* parent = static_cast<WaylandWindow*>(target->GetParentTarget()); |
| while (parent) { |
| origin += parent->GetBoundsInDIP().origin().OffsetFromOrigin(); |
| parent = static_cast<WaylandWindow*>(parent->GetParentTarget()); |
| } |
| return origin; |
| } |
| |
| gfx::Point GetLocationInScreen(LocatedEvent* event) { |
| auto* root_window = |
| static_cast<WaylandWindow*>(GetRootTarget(event->target())); |
| return event->root_location() + |
| root_window->GetBoundsInDIP().origin().OffsetFromOrigin(); |
| } |
| |
| void SetRootLocation(LocatedEvent* event) { |
| gfx::PointF location = event->location_f(); |
| auto* target = static_cast<WaylandWindow*>(event->target()); |
| |
| while (target->GetParentTarget()) { |
| location += target->GetBoundsInDIP().origin().OffsetFromOrigin(); |
| target = static_cast<WaylandWindow*>(target->GetParentTarget()); |
| } |
| event->set_root_location_f(location); |
| } |
| |
| // Number of fingers for scroll gestures. |
| constexpr int kGestureScrollFingerCount = 2; |
| |
| // Maximum size of the latest pointer scroll data set to be stored. |
| constexpr int kPointerScrollDataSetMaxSize = 3; |
| |
| // Maximum time delta between last scroll event and lifting of fingers. |
| constexpr int kFlingStartTimeoutMs = 200; |
| |
| } // namespace |
| |
| struct WaylandEventSource::TouchPoint { |
| TouchPoint(gfx::PointF location, WaylandWindow* current_window); |
| ~TouchPoint() = default; |
| |
| raw_ptr<WaylandWindow, DanglingUntriaged> window; |
| gfx::PointF last_known_location; |
| }; |
| |
| WaylandEventSource::TouchPoint::TouchPoint(gfx::PointF location, |
| WaylandWindow* current_window) |
| : window(current_window), last_known_location(location) { |
| DCHECK(window); |
| } |
| |
| // WaylandEventSource::PointerScrollData implementation |
| WaylandEventSource::PointerScrollData::PointerScrollData() = default; |
| WaylandEventSource::PointerScrollData::PointerScrollData( |
| const PointerScrollData&) = default; |
| WaylandEventSource::PointerScrollData::PointerScrollData(PointerScrollData&&) = |
| default; |
| WaylandEventSource::PointerScrollData::~PointerScrollData() = default; |
| |
| WaylandEventSource::PointerScrollData& |
| WaylandEventSource::PointerScrollData::operator=(const PointerScrollData&) = |
| default; |
| WaylandEventSource::PointerScrollData& |
| WaylandEventSource::PointerScrollData::operator=(PointerScrollData&&) = default; |
| |
| void WaylandEventSource::PointerScrollData::DumpState(std::ostream& out) const { |
| if (axis_source) { |
| out << "axis_source=" << *axis_source; |
| } |
| if (timestamp) { |
| out << ", timestamp=" << *timestamp; |
| } else { |
| out << ", no timestamp"; |
| } |
| out << ", d=(" << dx << ", " << dy << "), dt=" << dt |
| << ", is_axis_stop=" << ToBoolString(is_axis_stop); |
| } |
| |
| // WaylandEventSource::FrameData implementation |
| WaylandEventSource::FrameData::FrameData(const Event& e, |
| base::OnceCallback<void()> cb) |
| : event(e.Clone()), completion_cb(std::move(cb)) {} |
| |
| WaylandEventSource::FrameData::~FrameData() = default; |
| |
| void WaylandEventSource::FrameData::DumpState(std::ostream& out) const { |
| out << "event=" << (event ? event->ToString() : "none") |
| << ", callback=" << !!completion_cb; |
| } |
| |
| // WaylandEventSource implementation |
| |
| // static |
| void WaylandEventSource::ConvertEventToTarget(const EventTarget* new_target, |
| LocatedEvent* event) { |
| auto* current_target = static_cast<WaylandWindow*>(event->target()); |
| gfx::Vector2d diff = GetOriginInScreen(current_target) - |
| GetOriginInScreen(static_cast<WaylandWindow*>( |
| const_cast<EventTarget*>(new_target))); |
| event->set_location_f(event->location_f() + diff); |
| } |
| |
| WaylandEventSource::WaylandEventSource(wl_display* display, |
| wl_event_queue* event_queue, |
| WaylandWindowManager* window_manager, |
| WaylandConnection* connection, |
| bool use_threaded_polling) |
| : window_manager_(window_manager), |
| connection_(connection), |
| event_watcher_(WaylandEventWatcher::CreateWaylandEventWatcher( |
| display, |
| event_queue, |
| use_threaded_polling)) { |
| DCHECK(window_manager_); |
| |
| // Observes remove changes to know when touch points can be removed. |
| window_manager_->AddObserver(this); |
| } |
| |
| WaylandEventSource::~WaylandEventSource() = default; |
| |
| void WaylandEventSource::SetShutdownCb(base::OnceCallback<void()> shutdown_cb) { |
| event_watcher_->SetShutdownCb(std::move(shutdown_cb)); |
| } |
| |
| void WaylandEventSource::StartProcessingEvents() { |
| event_watcher_->StartProcessingEvents(); |
| } |
| |
| void WaylandEventSource::OnKeyboardFocusChanged(WaylandWindow* window, |
| bool focused) { |
| DCHECK(window); |
| #if DCHECK_IS_ON() |
| if (!focused) |
| DCHECK_EQ(window, window_manager_->GetCurrentKeyboardFocusedWindow()); |
| #endif |
| window_manager_->SetKeyboardFocusedWindow(focused ? window : nullptr); |
| } |
| |
| void WaylandEventSource::OnKeyboardModifiersChanged(int modifiers) { |
| keyboard_modifiers_ = modifiers; |
| } |
| |
| uint32_t WaylandEventSource::OnKeyboardKeyEvent( |
| EventType type, |
| DomCode dom_code, |
| bool repeat, |
| std::optional<uint32_t> serial, |
| base::TimeTicks timestamp, |
| int device_id, |
| WaylandKeyboard::KeyEventKind kind) { |
| DCHECK(type == ET_KEY_PRESSED || type == ET_KEY_RELEASED); |
| |
| DomKey dom_key; |
| KeyboardCode key_code; |
| auto* layout_engine = KeyboardLayoutEngineManager::GetKeyboardLayoutEngine(); |
| if (!layout_engine || !layout_engine->Lookup(dom_code, keyboard_modifiers_, |
| &dom_key, &key_code)) { |
| LOG(ERROR) << "Failed to decode key event."; |
| return POST_DISPATCH_NONE; |
| } |
| |
| #if BUILDFLAG(USE_GTK) |
| // GTK expects the state of a key event to be the mask of modifier keys |
| // _prior_ to this event. Some IMEs rely on this behavior. See |
| // https://crbug.com/1086946#c11. |
| int state_before_event = keyboard_modifiers_; |
| #endif |
| |
| KeyEvent event(type, key_code, dom_code, |
| keyboard_modifiers_ | (repeat ? EF_IS_REPEAT : 0), dom_key, |
| timestamp); |
| event.set_source_device_id(device_id); |
| |
| auto* focus = window_manager_->GetCurrentKeyboardFocusedWindow(); |
| if (!focus) |
| return POST_DISPATCH_STOP_PROPAGATION; |
| |
| Event::DispatcherApi(&event).set_target(focus); |
| |
| Event::Properties properties; |
| #if BUILDFLAG(USE_GTK) |
| // GTK uses XKB keycodes. |
| uint32_t converted_key_code = |
| ui::KeycodeConverter::DomCodeToXkbKeycode(dom_code); |
| properties.emplace( |
| kPropertyKeyboardHwKeyCode, |
| std::vector<uint8_t>{static_cast<unsigned char>(converted_key_code)}); |
| // Save state before event. The flags have different values than what GTK |
| // expects, but GtkUiPlatformWayland::GetGdkKeyEventState() takes care of the |
| // conversion. |
| properties.emplace(kPropertyKeyboardState, |
| ToLittleEndianByteVector(state_before_event)); |
| #endif |
| |
| if (serial.has_value()) { |
| properties.emplace(WaylandKeyboard::kPropertyWaylandSerial, |
| ToLittleEndianByteVector(serial.value())); |
| } |
| |
| if (kind == WaylandKeyboard::KeyEventKind::kKey) { |
| // Mark that this is the key event which IME did not consume. |
| SetKeyboardImeFlagProperty(&properties, kPropertyKeyboardImeIgnoredFlag); |
| } |
| event.SetProperties(properties); |
| return DispatchEvent(&event); |
| } |
| |
| void WaylandEventSource::OnSynthesizedKeyPressEvent(DomCode dom_code, |
| base::TimeTicks timestamp) { |
| std::ignore = |
| OnKeyboardKeyEvent(ET_KEY_PRESSED, dom_code, /*repeat=*/false, |
| /*serial=*/std::nullopt, timestamp, |
| /*device_id=*/0, WaylandKeyboard::KeyEventKind::kKey); |
| } |
| |
| void WaylandEventSource::OnPointerFocusChanged( |
| WaylandWindow* window, |
| const gfx::PointF& location, |
| base::TimeTicks timestamp, |
| wl::EventDispatchPolicy dispatch_policy) { |
| bool focused = !!window; |
| if (focused) { |
| // Save new pointer location. |
| pointer_location_ = location; |
| window_manager_->SetPointerFocusedWindow(window); |
| } |
| |
| auto closure = focused ? base::NullCallback() |
| : base::BindOnce( |
| [](WaylandWindowManager* wwm) { |
| wwm->SetPointerFocusedWindow(nullptr); |
| }, |
| window_manager_); |
| |
| auto* target = window_manager_->GetCurrentPointerFocusedWindow(); |
| if (target) { |
| EventType type = focused ? ET_MOUSE_ENTERED : ET_MOUSE_EXITED; |
| MouseEvent event(type, pointer_location_, pointer_location_, timestamp, |
| pointer_flags_, 0); |
| if (dispatch_policy == wl::EventDispatchPolicy::kImmediate) { |
| SetTargetAndDispatchEvent(&event, target); |
| } else { |
| pointer_frames_.push_back( |
| std::make_unique<FrameData>(event, std::move(closure))); |
| return; |
| } |
| } |
| |
| if (!closure.is_null()) |
| std::move(closure).Run(); |
| } |
| |
| void WaylandEventSource::OnPointerButtonEvent( |
| EventType type, |
| int changed_button, |
| base::TimeTicks timestamp, |
| WaylandWindow* window, |
| wl::EventDispatchPolicy dispatch_policy) { |
| OnPointerButtonEvent(type, changed_button, timestamp, window, dispatch_policy, |
| false); |
| } |
| |
| void WaylandEventSource::OnPointerButtonEvent( |
| EventType type, |
| int changed_button, |
| base::TimeTicks timestamp, |
| WaylandWindow* window, |
| wl::EventDispatchPolicy dispatch_policy, |
| bool allow_release_of_unpressed_button) { |
| DCHECK(type == ET_MOUSE_PRESSED || type == ET_MOUSE_RELEASED); |
| DCHECK(HasAnyPointerButtonFlag(changed_button)); |
| |
| // Ignore release events for buttons that aren't currently pressed. Such |
| // events should never happen, but there have been compositor bugs before |
| // (e.g. crbug.com/1376393). |
| if (!allow_release_of_unpressed_button && type == ET_MOUSE_RELEASED && |
| (pointer_flags_ & changed_button) == 0) |
| return; |
| |
| WaylandWindow* prev_focused_window = |
| window_manager_->GetCurrentPointerFocusedWindow(); |
| if (window) |
| window_manager_->SetPointerFocusedWindow(window); |
| |
| auto closure = base::BindOnce( |
| &WaylandEventSource::OnPointerButtonEventInternal, base::Unretained(this), |
| (window ? prev_focused_window : nullptr), type); |
| |
| pointer_flags_ = type == ET_MOUSE_PRESSED |
| ? (pointer_flags_ | changed_button) |
| : (pointer_flags_ & ~changed_button); |
| last_pointer_button_pressed_ = changed_button; |
| |
| auto* target = window_manager_->GetCurrentPointerFocusedWindow(); |
| // A window may be deleted when the event arrived from the server. |
| if (target) { |
| // MouseEvent's flags should contain the button that was released too. |
| int flags = pointer_flags_ | keyboard_modifiers_ | changed_button; |
| MouseEvent event(type, pointer_location_, pointer_location_, timestamp, |
| flags, changed_button); |
| if (dispatch_policy == wl::EventDispatchPolicy::kImmediate) { |
| SetTargetAndDispatchEvent(&event, target); |
| } else { |
| pointer_frames_.push_back( |
| std::make_unique<FrameData>(event, std::move(closure))); |
| return; |
| } |
| } |
| |
| if (!closure.is_null()) |
| std::move(closure).Run(); |
| } |
| |
| void WaylandEventSource::OnPointerButtonEventInternal(WaylandWindow* window, |
| EventType type) { |
| if (window) |
| window_manager_->SetPointerFocusedWindow(window); |
| |
| if (type == ET_MOUSE_RELEASED) |
| last_pointer_stylus_data_.reset(); |
| } |
| |
| void WaylandEventSource::OnPointerMotionEvent( |
| const gfx::PointF& location, |
| base::TimeTicks timestamp, |
| wl::EventDispatchPolicy dispatch_policy) { |
| pointer_location_ = location; |
| |
| int flags = pointer_flags_ | keyboard_modifiers_; |
| MouseEvent event(ET_MOUSE_MOVED, pointer_location_, pointer_location_, |
| timestamp, flags, 0); |
| auto* target = window_manager_->GetCurrentPointerFocusedWindow(); |
| |
| // A window may be deleted when the event arrived from the server. |
| if (!target) |
| return; |
| |
| if (dispatch_policy == wl::EventDispatchPolicy::kImmediate) { |
| SetTargetAndDispatchEvent(&event, target); |
| } else { |
| pointer_frames_.push_back( |
| std::make_unique<FrameData>(event, base::NullCallback())); |
| } |
| } |
| |
| void WaylandEventSource::OnPointerAxisEvent(const gfx::Vector2dF& offset, |
| base::TimeTicks timestamp) { |
| EnsurePointerScrollData(timestamp); |
| pointer_scroll_data_->dx += offset.x(); |
| pointer_scroll_data_->dy += offset.y(); |
| } |
| |
| void WaylandEventSource::OnResetPointerFlags() { |
| ResetPointerFlags(); |
| } |
| |
| void WaylandEventSource::RoundTripQueue() { |
| event_watcher_->RoundTripQueue(); |
| } |
| |
| void WaylandEventSource::DumpState(std::ostream& out) const { |
| out << "WaylandEventSource: " << std::endl; |
| out << " pointer_location=" << pointer_location_.ToString() |
| << ", flags=" << ToPointerFlagsString(pointer_flags_) |
| << ", last button pressed=" << last_pointer_button_pressed_ |
| << ", keyboard modifiers=" |
| << ToKeyboardModifierStrings(keyboard_modifiers_) << std::endl; |
| if (relative_pointer_location_) { |
| out << " relative_poniter_location=" |
| << relative_pointer_location_->ToString() << std::endl; |
| } |
| |
| size_t i = 0; |
| for (const auto& frame_data : pointer_frames_) { |
| out << " pointer_frame[" << i++ << "]="; |
| frame_data->DumpState(out); |
| out << std::endl; |
| } |
| i = 0; |
| for (const auto& frame_data : touch_frames_) { |
| out << " touch_frame[" << i++ << "]="; |
| frame_data->DumpState(out); |
| out << std::endl; |
| } |
| i = 0; |
| for (const auto& scroll_data : pointer_scroll_data_set_) { |
| out << " point_scroll_data[" << i++ << "]="; |
| scroll_data.DumpState(out); |
| out << std::endl; |
| } |
| } |
| |
| const gfx::PointF& WaylandEventSource::GetPointerLocation() const { |
| return pointer_location_; |
| } |
| |
| void WaylandEventSource::OnPointerFrameEvent() { |
| base::TimeTicks now = EventTimeForNow(); |
| if (pointer_scroll_data_) { |
| pointer_scroll_data_->dt = now - last_pointer_frame_time_; |
| ProcessPointerScrollData(); |
| } |
| |
| last_pointer_frame_time_ = now; |
| |
| auto* target = window_manager_->GetCurrentPointerFocusedWindow(); |
| if (!target) |
| return; |
| |
| while (!pointer_frames_.empty()) { |
| // It is safe to pop the first queued event for processing. |
| auto pointer_frame = std::move(pointer_frames_.front()); |
| pointer_frames_.pop_front(); |
| |
| // In case there are pointer stylus information, override the current |
| // 'event' instance, given that PointerDetails is 'const'. |
| auto pointer_details_with_stylus_data = AmendStylusData(); |
| if (pointer_details_with_stylus_data && |
| pointer_frame->event->IsMouseEvent() && |
| pointer_frame->event->AsMouseEvent()->IsOnlyLeftMouseButton()) { |
| auto old_event = std::move(pointer_frame->event); |
| pointer_frame->event = std::make_unique<MouseEvent>( |
| old_event->type(), old_event->AsMouseEvent()->location(), |
| old_event->AsMouseEvent()->root_location(), old_event->time_stamp(), |
| old_event->flags(), old_event->AsMouseEvent()->changed_button_flags(), |
| pointer_details_with_stylus_data.value()); |
| } |
| |
| SetTargetAndDispatchEvent(pointer_frame->event.get(), target); |
| if (!pointer_frame->completion_cb.is_null()) |
| std::move(pointer_frame->completion_cb).Run(); |
| } |
| } |
| |
| void WaylandEventSource::OnPointerAxisSourceEvent(uint32_t axis_source) { |
| EnsurePointerScrollData(/*timestamp*/ std::nullopt); |
| pointer_scroll_data_->axis_source = axis_source; |
| } |
| |
| void WaylandEventSource::OnPointerAxisStopEvent(uint32_t axis, |
| base::TimeTicks timestamp) { |
| EnsurePointerScrollData(timestamp); |
| if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL) { |
| pointer_scroll_data_->dy = 0; |
| } else if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL) { |
| pointer_scroll_data_->dx = 0; |
| } |
| pointer_scroll_data_->is_axis_stop = true; |
| } |
| |
| void WaylandEventSource::OnTouchPressEvent( |
| WaylandWindow* window, |
| const gfx::PointF& location, |
| base::TimeTicks timestamp, |
| PointerId id, |
| wl::EventDispatchPolicy dispatch_policy) { |
| DCHECK(window); |
| HandleTouchFocusChange(window, true); |
| |
| // Make sure this touch point wasn't present before. |
| auto success = touch_points_.try_emplace( |
| id, std::make_unique<TouchPoint>(location, window)); |
| if (!success.second) { |
| LOG(WARNING) << "Touch down fired with wrong id"; |
| return; |
| } |
| |
| PointerDetails details(EventPointerType::kTouch, id); |
| TouchEvent event(ET_TOUCH_PRESSED, location, location, timestamp, details, |
| keyboard_modifiers_); |
| touch_frames_.push_back( |
| std::make_unique<FrameData>(event, base::NullCallback())); |
| } |
| |
| void WaylandEventSource::OnTouchReleaseEvent( |
| base::TimeTicks timestamp, |
| PointerId id, |
| wl::EventDispatchPolicy dispatch_policy) { |
| // Make sure this touch point was present before. |
| const auto it = touch_points_.find(id); |
| if (it == touch_points_.end()) { |
| LOG(WARNING) << "Touch up fired with no matching touch down"; |
| return; |
| } |
| |
| TouchPoint* touch_point = it->second.get(); |
| gfx::PointF location = touch_point->last_known_location; |
| PointerDetails details(EventPointerType::kTouch, id); |
| |
| TouchEvent event(ET_TOUCH_RELEASED, location, location, timestamp, details, |
| keyboard_modifiers_); |
| if (dispatch_policy == wl::EventDispatchPolicy::kImmediate) { |
| SetTouchTargetAndDispatchTouchEvent(&event); |
| OnTouchReleaseInternal(id); |
| } else { |
| touch_frames_.push_back(std::make_unique<FrameData>( |
| event, base::BindOnce(&WaylandEventSource::OnTouchReleaseInternal, |
| base::Unretained(this), id))); |
| } |
| } |
| |
| void WaylandEventSource::OnTouchReleaseInternal(PointerId id) { |
| // It is possible that an user interaction triggers nested loops |
| // in higher levels of the application stack in order to process a |
| // given touch down/up action. |
| // For instance, a modal dialog might block this execution point, |
| // and trigger thread to continue to process events. |
| // The auxiliary flow might clear entries in touch_points_. |
| // |
| // Hence, we check whether the TouchId is still being held. |
| const auto it = touch_points_.find(id); |
| if (it == touch_points_.end()) { |
| LOG(WARNING) << "Touch has been released during processing."; |
| return; |
| } |
| |
| TouchPoint* touch_point = it->second.get(); |
| HandleTouchFocusChange(touch_point->window, false, id); |
| touch_points_.erase(it); |
| |
| // Clean up stylus touch tracking, if any. |
| const auto stylus_data_it = last_touch_stylus_data_.find(id); |
| if (stylus_data_it != last_touch_stylus_data_.end()) |
| last_touch_stylus_data_.erase(stylus_data_it); |
| } |
| |
| void WaylandEventSource::SetTargetAndDispatchEvent(Event* event, |
| EventTarget* target) { |
| Event::DispatcherApi(event).set_target(target); |
| if (event->IsLocatedEvent()) { |
| SetRootLocation(event->AsLocatedEvent()); |
| auto* cursor_position = connection_->wayland_cursor_position(); |
| #if BUILDFLAG(IS_CHROMEOS_LACROS) |
| bool update_cursor_position = cursor_position && event->IsMouseEvent(); |
| #else |
| // TODO(crbug.com/1488644): Touch event should not update the cursor |
| // position. |
| bool update_cursor_position = cursor_position; |
| #endif |
| if (update_cursor_position) { |
| cursor_position->OnCursorPositionChanged( |
| GetLocationInScreen(event->AsLocatedEvent())); |
| } |
| } |
| DispatchEvent(event); |
| } |
| |
| void WaylandEventSource::SetTouchTargetAndDispatchTouchEvent( |
| TouchEvent* event) { |
| auto iter = touch_points_.find(event->pointer_details().id); |
| auto target = iter != touch_points_.end() ? iter->second->window : nullptr; |
| // Skip if the touch target has alrady been removed. |
| if (!target.get()) |
| return; |
| SetTargetAndDispatchEvent(event, target.get()); |
| } |
| |
| void WaylandEventSource::OnTouchMotionEvent( |
| const gfx::PointF& location, |
| base::TimeTicks timestamp, |
| PointerId id, |
| wl::EventDispatchPolicy dispatch_policy) { |
| const auto it = touch_points_.find(id); |
| // Make sure this touch point was present before. |
| if (it == touch_points_.end()) { |
| LOG(WARNING) << "Touch event fired with wrong id"; |
| return; |
| } |
| it->second->last_known_location = location; |
| PointerDetails details(EventPointerType::kTouch, id); |
| TouchEvent event(ET_TOUCH_MOVED, location, location, timestamp, details, |
| keyboard_modifiers_); |
| if (dispatch_policy == wl::EventDispatchPolicy::kImmediate) { |
| SetTouchTargetAndDispatchTouchEvent(&event); |
| } else { |
| touch_frames_.push_back( |
| std::make_unique<FrameData>(event, base::NullCallback())); |
| } |
| } |
| |
| void WaylandEventSource::OnTouchCancelEvent() { |
| // Some compositors emit a TouchCancel event when a drag'n drop |
| // session is started on the server, eg Exo. |
| // On Chrome, this event would actually abort the whole drag'n drop |
| // session on the client side. |
| if (connection_->IsDragInProgress()) |
| return; |
| |
| gfx::PointF location; |
| base::TimeTicks timestamp = base::TimeTicks::Now(); |
| for (auto& touch_point : touch_points_) { |
| PointerId id = touch_point.first; |
| TouchEvent event(ET_TOUCH_CANCELLED, location, location, timestamp, |
| PointerDetails(EventPointerType::kTouch, id)); |
| SetTouchTargetAndDispatchTouchEvent(&event); |
| HandleTouchFocusChange(touch_point.second->window, false); |
| } |
| touch_points_.clear(); |
| last_touch_stylus_data_.clear(); |
| } |
| |
| void WaylandEventSource::OnTouchFrame() { |
| while (!touch_frames_.empty()) { |
| // It is safe to pop the first queued event for processing. |
| auto touch_frame = std::move(touch_frames_.front()); |
| touch_frames_.pop_front(); |
| |
| // In case there are touch stylus information, override the current 'event' |
| // instance, given that PointerDetails is 'const'. |
| auto pointer_details_with_stylus_data = AmendStylusData( |
| touch_frame->event->AsTouchEvent()->pointer_details().id); |
| if (pointer_details_with_stylus_data) { |
| auto old_event = std::move(touch_frame->event); |
| touch_frame->event = std::make_unique<TouchEvent>( |
| old_event->type(), old_event->AsTouchEvent()->location_f(), |
| old_event->AsTouchEvent()->root_location_f(), old_event->time_stamp(), |
| pointer_details_with_stylus_data.value(), old_event->flags()); |
| } |
| SetTouchTargetAndDispatchTouchEvent(touch_frame->event->AsTouchEvent()); |
| if (!touch_frame->completion_cb.is_null()) |
| std::move(touch_frame->completion_cb).Run(); |
| } |
| } |
| |
| void WaylandEventSource::OnTouchFocusChanged(WaylandWindow* window) { |
| // If a window dragging session is active (and touch-based), transfer the |
| // touch points to it. |
| auto drag_source = connection_->window_drag_controller()->drag_source(); |
| if (drag_source && window) { |
| DCHECK_EQ(*drag_source, mojom::DragEventSource::kTouch); |
| for (auto& touch_point : touch_points_) { |
| touch_point.second->window = window; |
| } |
| } |
| |
| window_manager_->SetTouchFocusedWindow(window); |
| } |
| |
| std::vector<PointerId> WaylandEventSource::GetActiveTouchPointIds() { |
| std::vector<PointerId> pointer_ids; |
| for (auto& touch_point : touch_points_) |
| pointer_ids.push_back(touch_point.first); |
| return pointer_ids; |
| } |
| |
| void WaylandEventSource::OnTouchStylusToolChanged( |
| PointerId pointer_id, |
| EventPointerType pointer_type) { |
| StylusData stylus_data = {.type = pointer_type, |
| .tilt = gfx::Vector2dF(), |
| .force = std::numeric_limits<float>::quiet_NaN()}; |
| bool inserted = |
| last_touch_stylus_data_.try_emplace(pointer_id, stylus_data).second; |
| DCHECK(inserted); |
| } |
| |
| void WaylandEventSource::OnTouchStylusForceChanged(PointerId pointer_id, |
| float force) { |
| DCHECK(last_touch_stylus_data_[pointer_id].has_value()); |
| last_touch_stylus_data_[pointer_id]->force = force; |
| } |
| |
| void WaylandEventSource::OnTouchStylusTiltChanged(PointerId pointer_id, |
| const gfx::Vector2dF& tilt) { |
| DCHECK(last_touch_stylus_data_[pointer_id].has_value()); |
| last_touch_stylus_data_[pointer_id]->tilt = tilt; |
| } |
| |
| const WaylandWindow* WaylandEventSource::GetTouchTarget(PointerId id) const { |
| const auto it = touch_points_.find(id); |
| return it == touch_points_.end() ? nullptr : it->second->window.get(); |
| } |
| |
| void WaylandEventSource::OnPinchEvent(EventType event_type, |
| const gfx::Vector2dF& delta, |
| base::TimeTicks timestamp, |
| int device_id, |
| std::optional<float> scale_delta) { |
| GestureEventDetails details(event_type); |
| details.set_device_type(GestureDeviceType::DEVICE_TOUCHPAD); |
| if (scale_delta) |
| details.set_scale(*scale_delta); |
| |
| auto location = pointer_location_ + delta; |
| GestureEvent event(location.x(), location.y(), 0 /* flags */, timestamp, |
| details); |
| event.set_source_device_id(device_id); |
| |
| auto* target = window_manager_->GetCurrentPointerFocusedWindow(); |
| // A window may be deleted when the event arrived from the server. |
| if (!target) |
| return; |
| |
| SetTargetAndDispatchEvent(&event, target); |
| } |
| |
| void WaylandEventSource::OnHoldEvent(EventType event_type, |
| uint32_t finger_count, |
| base::TimeTicks timestamp, |
| int device_id, |
| wl::EventDispatchPolicy dispatch_policy) { |
| // Lifting the finger from the touchpad will be ignored. |
| if (event_type != ET_TOUCH_PRESSED) { |
| return; |
| } |
| |
| #if !BUILDFLAG(IS_CHROMEOS_LACROS) |
| // Prevent generating any scroll events if pointer has just been moved. |
| if (!is_fling_active_) { |
| return; |
| } |
| is_fling_active_ = false; |
| #endif |
| |
| // Prevent fling start if axis stop arrives after hold gesture. |
| if (pointer_scroll_data_) { |
| pointer_scroll_data_->dx = 0; |
| pointer_scroll_data_->dy = 0; |
| } |
| |
| pointer_scroll_data_set_.clear(); |
| |
| ScrollEvent event(ET_SCROLL_FLING_CANCEL, pointer_location_, |
| pointer_location_, timestamp, pointer_flags_, 0, 0, 0, 0, |
| finger_count); |
| |
| auto* target = window_manager_->GetCurrentPointerFocusedWindow(); |
| |
| if (dispatch_policy == wl::EventDispatchPolicy::kImmediate) { |
| SetTargetAndDispatchEvent(&event, target); |
| } else { |
| pointer_frames_.push_back( |
| std::make_unique<FrameData>(event, base::NullCallback())); |
| } |
| } |
| |
| void WaylandEventSource::SetRelativePointerMotionEnabled(bool enabled) { |
| if (enabled) |
| relative_pointer_location_ = pointer_location_; |
| else |
| relative_pointer_location_.reset(); |
| } |
| |
| void WaylandEventSource::OnRelativePointerMotion(const gfx::Vector2dF& delta, |
| base::TimeTicks timestamp) { |
| DCHECK(relative_pointer_location_.has_value()); |
| // TODO(oshima): Investigate if we need to scale the delta |
| // when surface_submission_in_pixel_coordinates is on. |
| relative_pointer_location_ = *relative_pointer_location_ + delta; |
| OnPointerMotionEvent(*relative_pointer_location_, timestamp, |
| wl::EventDispatchPolicy::kImmediate); |
| } |
| |
| bool WaylandEventSource::IsPointerButtonPressed(EventFlags button) const { |
| DCHECK(HasAnyPointerButtonFlag(button)); |
| return pointer_flags_ & button; |
| } |
| |
| void WaylandEventSource::OnPointerStylusToolChanged( |
| EventPointerType pointer_type) { |
| // When the reported pointer stylus type is `mouse`, handle it as a regular |
| // pointer event. |
| // |
| // TODO(https://crbug.com/1298504): Better handle the `touch` type, which |
| // seems mis-specified in |
| // //t_p/wayland-protocols/unstable/stylus/stylus-unstable-v2.xml. |
| if (pointer_type == ui::EventPointerType::kMouse) { |
| last_pointer_stylus_data_.reset(); |
| return; |
| } |
| |
| last_pointer_stylus_data_ = { |
| .type = pointer_type, |
| .tilt = gfx::Vector2dF(), |
| .force = std::numeric_limits<float>::quiet_NaN()}; |
| } |
| |
| void WaylandEventSource::OnPointerStylusForceChanged(float force) { |
| if (!last_pointer_stylus_data_.has_value()) { |
| // This is a stray force event that the default tool cannot accept. |
| LOG(WARNING) << "Cannot handle force for the default tool! (the value is " |
| << force << ")"; |
| return; |
| } |
| |
| last_pointer_stylus_data_->force = force; |
| } |
| |
| void WaylandEventSource::OnPointerStylusTiltChanged( |
| const gfx::Vector2dF& tilt) { |
| if (!last_pointer_stylus_data_.has_value()) { |
| // This is a stray tilt event that the default tool cannot accept. |
| LOG(WARNING) << "Cannot handle tilt for the default tool! (the value is [" |
| << tilt.x() << "," << tilt.y() << "])"; |
| return; |
| } |
| |
| last_pointer_stylus_data_->tilt = tilt; |
| } |
| |
| const WaylandWindow* WaylandEventSource::GetPointerTarget() const { |
| return window_manager_->GetCurrentPointerFocusedWindow(); |
| } |
| |
| void WaylandEventSource::ResetPointerFlags() { |
| pointer_flags_ = 0; |
| } |
| |
| void WaylandEventSource::OnDispatcherListChanged() { |
| StartProcessingEvents(); |
| } |
| |
| void WaylandEventSource::OnWindowRemoved(WaylandWindow* window) { |
| // A window can be `swallowed` by another window during tab-dragging, which |
| // results in OnWindowRemoved() being called. |
| // |
| // If a window dragging session is active and is touch-based, verify if there |
| // is a valid target window to transfer the touch points to. |
| if (auto* target_window = window_manager_->GetCurrentTouchFocusedWindow()) { |
| auto drag_source = connection_->window_drag_controller()->drag_source(); |
| if (drag_source && *drag_source == mojom::DragEventSource::kTouch) { |
| for (auto& touch_point : touch_points_) |
| touch_point.second->window = target_window; |
| return; |
| } |
| } |
| |
| // Clear touch-related data. |
| base::EraseIf(touch_points_, [window](const auto& point) { |
| return point.second->window == window; |
| }); |
| } |
| |
| void WaylandEventSource::HandleTouchFocusChange(WaylandWindow* window, |
| bool focused, |
| std::optional<PointerId> id) { |
| DCHECK(window); |
| bool actual_focus = id ? !ShouldUnsetTouchFocus(window, id.value()) : focused; |
| window->set_touch_focus(actual_focus); |
| } |
| |
| // Focus must not be unset if there is another touch point within |window|. |
| bool WaylandEventSource::ShouldUnsetTouchFocus(WaylandWindow* win, |
| PointerId id) { |
| return base::ranges::none_of(touch_points_, [win, id](auto& p) { |
| return p.second->window == win && p.first != id; |
| }); |
| } |
| |
| gfx::Vector2dF WaylandEventSource::ComputeFlingVelocity() { |
| struct RegressionSums { |
| float tt_; // Cumulative sum of t^2. |
| float t_; // Cumulative sum of t. |
| float tx_; // Cumulative sum of t * x. |
| float ty_; // Cumulative sum of t * y. |
| float x_; // Cumulative sum of x. |
| float y_; // Cumulative sum of y. |
| }; |
| |
| const size_t count = pointer_scroll_data_set_.size(); |
| |
| if (count == 0) { |
| return gfx::Vector2dF(); |
| } |
| |
| // Prevents small jumps if someone scrolls fast, immediately stops scrolling |
| // and then waits a little before lifting fingers from touchpad. |
| if (pointer_scroll_data_->dt > base::Milliseconds(kFlingStartTimeoutMs)) { |
| return gfx::Vector2dF(); |
| } |
| |
| if (count == 1) { |
| const auto& pointer_frame = pointer_scroll_data_set_.front(); |
| const float dt = |
| pointer_frame.dt.InSecondsF() + pointer_scroll_data_->dt.InSecondsF(); |
| return gfx::Vector2dF(pointer_frame.dx * dt, pointer_frame.dy * dt); |
| } |
| |
| RegressionSums sums = {0, 0, 0, 0, 0, 0}; |
| |
| float time = pointer_scroll_data_->dt.InSecondsF(); |
| float x_coord = 0; |
| float y_coord = 0; |
| |
| // Formula matches libgestures's RegressScrollVelocity() |
| // from src/platform/gestures/src/immediate_interpreter.cc |
| for (const auto& frame : pointer_scroll_data_set_) { |
| if (frame.axis_source && |
| *frame.axis_source != WL_POINTER_AXIS_SOURCE_FINGER) { |
| break; |
| } |
| time += frame.dt.InSecondsF(); |
| x_coord += frame.dx; |
| y_coord += frame.dy; |
| |
| sums.tt_ += time * time; |
| sums.t_ += time; |
| sums.tx_ += time * x_coord; |
| sums.ty_ += time * y_coord; |
| sums.x_ += x_coord; |
| sums.y_ += y_coord; |
| } |
| pointer_scroll_data_set_.clear(); |
| |
| // Note the regression determinant only depends on the values of t, and should |
| // never be zero so long as (1) count > 1, and (2) dt[0] != d[1]. The |
| // condition of (1) was already caught at the beginning of the method. |
| const float det = count * sums.tt_ - sums.t_ * sums.t_; |
| if (!det) { |
| // This will return the average scroll value if dt values are |
| // non-zero. |
| if (sums.t_) { |
| return gfx::Vector2dF(x_coord / sums.t_, y_coord / sums.t_); |
| } |
| return gfx::Vector2dF(); |
| } |
| |
| const float det_inv = 1.0 / det; |
| return gfx::Vector2dF((count * sums.tx_ - sums.t_ * sums.x_) * det_inv, |
| (count * sums.ty_ - sums.t_ * sums.y_) * det_inv); |
| } |
| |
| std::optional<PointerDetails> WaylandEventSource::AmendStylusData() const { |
| if (!last_pointer_stylus_data_) |
| return std::nullopt; |
| |
| DCHECK_NE(last_pointer_stylus_data_->type, EventPointerType::kUnknown); |
| return PointerDetails(last_pointer_stylus_data_->type, /*pointer_id=*/0, |
| /*radius_x=*/1.0f, |
| /*radius_y=*/1.0f, last_pointer_stylus_data_->force, |
| /*twist=*/0.0f, last_pointer_stylus_data_->tilt.x(), |
| last_pointer_stylus_data_->tilt.y()); |
| } |
| |
| std::optional<PointerDetails> WaylandEventSource::AmendStylusData( |
| PointerId pointer_id) const { |
| const auto it = last_touch_stylus_data_.find(pointer_id); |
| if (it == last_touch_stylus_data_.end() || !it->second || |
| it->second->type == EventPointerType::kTouch) { |
| return std::nullopt; |
| } |
| |
| // The values below come from the default values in pointer_details.cc|h. |
| return PointerDetails(it->second->type, pointer_id, |
| /*radius_x=*/1.0f, |
| /*radius_y=*/1.0f, it->second->force, |
| /*twist=*/0.0f, it->second->tilt.x(), |
| it->second->tilt.y()); |
| } |
| |
| void WaylandEventSource::EnsurePointerScrollData( |
| const std::optional<base::TimeTicks>& timestamp) { |
| if (!pointer_scroll_data_) |
| pointer_scroll_data_ = PointerScrollData(); |
| if (!pointer_scroll_data_->timestamp && timestamp) { |
| pointer_scroll_data_->timestamp = *timestamp; |
| } |
| } |
| |
| // This method behaves differently in Exo than in other window managers. |
| // If you place a finger on the touchpad, Exo dispatches axis events with an |
| // offset of 0 to indicate that a fling should be aborted. This event does not |
| // exist in Linux window managers. Instead, some window managers implement |
| // zwp_pointer_gesture_hold_v1 for this. However, for those who don't implement |
| // that, it needs to be ensured that flings are aborted when new axis events |
| // arrive. |
| void WaylandEventSource::ProcessPointerScrollData() { |
| DCHECK(pointer_scroll_data_); |
| // While it does not make sense for a server to send axis source only, |
| // the protocol does not explicitly specify it's illegal. Just skip if |
| // that happens. |
| if (!pointer_scroll_data_->timestamp) { |
| pointer_scroll_data_.reset(); |
| return; |
| } |
| base::TimeTicks& timestamp = *pointer_scroll_data_->timestamp; |
| |
| int flags = pointer_flags_ | keyboard_modifiers_; |
| // Dispatch Fling event if pointer.axis_stop is notified and the recent |
| // pointer.axis events meets the criteria to start fling scroll. |
| if (pointer_scroll_data_->dx == 0 && pointer_scroll_data_->dy == 0 |
| #if !BUILDFLAG(IS_CHROMEOS_LACROS) |
| && pointer_scroll_data_->is_axis_stop |
| #endif |
| ) { |
| gfx::Vector2dF initial_velocity = ComputeFlingVelocity(); |
| float vx = initial_velocity.x(); |
| float vy = initial_velocity.y(); |
| #if BUILDFLAG(IS_CHROMEOS_LACROS) |
| ScrollEvent event(pointer_scroll_data_->is_axis_stop |
| ? ET_SCROLL_FLING_START |
| : ET_SCROLL_FLING_CANCEL, |
| pointer_location_, pointer_location_, timestamp, flags, |
| vx, vy, vx, vy, kGestureScrollFingerCount); |
| #else |
| // In Linux there is no axis event with 0 delta when start scrolling. |
| // A fling is therefore always started at this point. |
| ScrollEvent event(ET_SCROLL_FLING_START, pointer_location_, |
| pointer_location_, timestamp, flags, vx, vy, vx, vy, |
| kGestureScrollFingerCount); |
| is_fling_active_ = true; |
| #endif |
| pointer_frames_.push_back( |
| std::make_unique<FrameData>(event, base::NullCallback())); |
| } else if (pointer_scroll_data_->axis_source) { |
| if (*pointer_scroll_data_->axis_source == WL_POINTER_AXIS_SOURCE_WHEEL || |
| *pointer_scroll_data_->axis_source == |
| WL_POINTER_AXIS_SOURCE_WHEEL_TILT) { |
| MouseWheelEvent event( |
| gfx::Vector2d(pointer_scroll_data_->dx, pointer_scroll_data_->dy), |
| pointer_location_, pointer_location_, timestamp, flags, 0); |
| pointer_frames_.push_back( |
| std::make_unique<FrameData>(event, base::NullCallback())); |
| } else if (*pointer_scroll_data_->axis_source == |
| WL_POINTER_AXIS_SOURCE_FINGER || |
| *pointer_scroll_data_->axis_source == |
| WL_POINTER_AXIS_SOURCE_CONTINUOUS) { |
| #if !BUILDFLAG(IS_CHROMEOS_LACROS) |
| // Fling has to be stopped if a new scroll event is received. |
| // From Wayland 1.23 this will be done through hold event. |
| if (is_fling_active_) { |
| is_fling_active_ = false; |
| ScrollEvent stop_fling_event(ET_SCROLL_FLING_CANCEL, pointer_location_, |
| pointer_location_, timestamp, flags, 0, 0, |
| 0, 0, kGestureScrollFingerCount); |
| pointer_frames_.push_back(std::make_unique<FrameData>( |
| stop_fling_event, base::NullCallback())); |
| } |
| #endif |
| ScrollEvent event(ET_SCROLL, pointer_location_, pointer_location_, |
| timestamp, flags, pointer_scroll_data_->dx, |
| pointer_scroll_data_->dy, pointer_scroll_data_->dx, |
| pointer_scroll_data_->dy, kGestureScrollFingerCount); |
| pointer_frames_.push_back( |
| std::make_unique<FrameData>(event, base::NullCallback())); |
| } |
| |
| if (pointer_scroll_data_set_.size() + 1 > kPointerScrollDataSetMaxSize) |
| pointer_scroll_data_set_.pop_back(); |
| pointer_scroll_data_set_.push_front(*pointer_scroll_data_); |
| } |
| |
| pointer_scroll_data_.reset(); |
| } |
| |
| } // namespace ui |