From 3f24dceabe26acb416ca81addb29187f134b8d65 Mon Sep 17 00:00:00 2001 From: luhongxu Date: Wed, 12 Aug 2026 00:25:27 +0800 Subject: [PATCH] feat: support pointer-constraints protocol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Add PointerConstraintsManager for activation/deactivation policy 2. Mount WPointerConstraintsV1 and WRelativePointerManagerV1 in Helper 3. Add isInMoveResize accessor and grab exclusion in canActivate 4. Deactivate all constraints on mode switch (lock screen/multitask/view) 5. Add in-tree waylib wrappers and WCursor enforcement path 6. Inline pixman-based regionConfine to avoid wlroots linking dependency 7. Add test_protocol_pointerconstraints test case Log: Added pointer lock and confinement support for wayland clients Influence: 1. Test locked pointer with a 3D mouselook client 2. Test confined pointer with a region-limited client 3. Verify constraints deactivate on lock screen/multitask/view switch 4. Verify constraints do not activate during drag, popup grab, or move/resize 5. Verify oneshot constraints are destroyed after deactivation feat: 支持 pointer-constraints 协议 1. 新增 PointerConstraintsManager 管理激活/失活策略 2. 在 Helper 中挂载 WPointerConstraintsV1 和 WRelativePointerManagerV1 3. 新增 isInMoveResize 访问器并在 canActivate 中排除 grab/moveResize 4. 模式切换(锁屏/多任务/视图)时全量失活约束 5. 内嵌 waylib 包装器及 WCursor 约束强制路径 6. 内联基于 pixman 的 regionConfine 避免对 wlroots 的链接依赖 7. 新增 test_protocol_pointerconstraints 测试用例 Log: 新增 Wayland 客户端指针锁定和限制功能 Influence: 1. 使用 3D 视角客户端测试锁定指针 2. 使用区域限制客户端测试 confined 指针 3. 验证锁屏/多任务/视图切换时约束失活 4. 验证拖拽/popup grab/move-resize 期间约束不激活 5. 验证 oneshot 约束失活后自动销毁 --- src/CMakeLists.txt | 2 + src/seat/helper.cpp | 18 ++ src/seat/helper.h | 9 + src/seat/pointerconstraintsmanager.cpp | 175 ++++++++++++++++++ src/seat/pointerconstraintsmanager.h | 44 +++++ tests/CMakeLists.txt | 1 + .../CMakeLists.txt | 19 ++ .../test_protocol_pointerconstraints/main.cpp | 60 ++++++ waylib/src/server/CMakeLists.txt | 4 + waylib/src/server/kernel/private/wcursor_p.h | 7 + waylib/src/server/kernel/wcursor.cpp | 170 ++++++++++++++++- waylib/src/server/kernel/wcursor.h | 3 + .../protocols/wpointerconstraintsv1.cpp | 79 ++++++++ .../server/protocols/wpointerconstraintsv1.h | 43 +++++ .../server/protocols/wrelativepointerv1.cpp | 78 ++++++++ .../src/server/protocols/wrelativepointerv1.h | 41 ++++ 16 files changed, 751 insertions(+), 2 deletions(-) create mode 100644 src/seat/pointerconstraintsmanager.cpp create mode 100644 src/seat/pointerconstraintsmanager.h create mode 100644 tests/test_protocol_pointerconstraints/CMakeLists.txt create mode 100644 tests/test_protocol_pointerconstraints/main.cpp create mode 100644 waylib/src/server/protocols/wpointerconstraintsv1.cpp create mode 100644 waylib/src/server/protocols/wpointerconstraintsv1.h create mode 100644 waylib/src/server/protocols/wrelativepointerv1.cpp create mode 100644 waylib/src/server/protocols/wrelativepointerv1.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index edfcc420b9..83071d6135 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -183,6 +183,8 @@ qt_add_qml_module(libtreeland seat/helper.h seat/seatsmanager.cpp seat/seatsmanager.h + seat/pointerconstraintsmanager.cpp + seat/pointerconstraintsmanager.h session/session.cpp session/session.h surface/surfacecontainer.cpp diff --git a/src/seat/helper.cpp b/src/seat/helper.cpp index 2116148a58..d914710687 100644 --- a/src/seat/helper.cpp +++ b/src/seat/helper.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "helper.h" +#include "pointerconstraintsmanager.h" #include "qwxdgshell.h" #include "seatsmanager.h" @@ -75,6 +76,8 @@ #include #include #include +#include +#include #include #include #include @@ -2074,6 +2077,9 @@ void Helper::init(Treeland::Treeland *treeland) &Helper::onOutputTestOrApply); m_server->attach(); + m_pointerConstraintsV1 = m_server->attach(); + m_relativePointerManagerV1 = m_server->attach(); + m_pointerConstraintsManager = new PointerConstraintsManager(m_pointerConstraintsV1, this, this); qw_fractional_scale_manager_v1::create(*m_server->handle(), WLR_FRACTIONAL_SCALE_V1_VERSION); qw_data_control_manager_v1::create(*m_server->handle()); qw_ext_data_control_manager_v1::create(*m_server->handle(), EXT_DATA_CONTROL_MANAGER_V1_VERSION); @@ -3309,9 +3315,21 @@ void Helper::setCurrentMode(CurrentMode mode) m_currentMode = mode; + if (m_currentMode != CurrentMode::Normal && m_pointerConstraintsManager) + m_pointerConstraintsManager->deactivateAll(); + Q_EMIT currentModeChanged(); } +bool Helper::isInMoveResize(WAYLIB_SERVER_NAMESPACE::WSeat *seat) const +{ + if (!m_rootSurfaceContainer || !seat) + return false; + + auto *container = m_rootSurfaceContainer->getSeatContainer(seat); + return container && container->moveResizeState().surface; +} + void Helper::prepareLockScreenTransition() { if (m_multitaskView) { diff --git a/src/seat/helper.h b/src/seat/helper.h index 52cdb49405..c63d5ec247 100644 --- a/src/seat/helper.h +++ b/src/seat/helper.h @@ -75,6 +75,8 @@ class WToplevelSurface; class WXdgDecorationManager; class WXdgOutputManager; class WXWayland; +class WPointerConstraintsV1; +class WRelativePointerManagerV1; class WForeignToplevel; class WExtForeignToplevelListV1; @@ -139,6 +141,7 @@ class WallpaperManager; class WallpaperItem; class TreelandInputManagerInterfaceV1; class InputManager; +class PointerConstraintsManager; struct wlr_ext_foreign_toplevel_image_capture_source_manager_v1_request; struct wlr_idle_inhibitor_v1; @@ -239,6 +242,8 @@ class Helper : public WSeatEventFilter void setCurrentMode(CurrentMode mode); + bool isInMoveResize(WAYLIB_SERVER_NAMESPACE::WSeat *seat) const; + bool isNormalOrMultitaskview() const { return m_currentMode == CurrentMode::Normal || m_currentMode == CurrentMode::Multitaskview; @@ -499,4 +504,8 @@ private Q_SLOTS: SeatsManager *m_seatManager = nullptr; InputManager *m_inputManager = nullptr; TreelandInputManagerInterfaceV1 *m_inputManagerInterfaceV1 = nullptr; + + WPointerConstraintsV1 *m_pointerConstraintsV1 = nullptr; + WRelativePointerManagerV1 *m_relativePointerManagerV1 = nullptr; + PointerConstraintsManager *m_pointerConstraintsManager = nullptr; }; diff --git a/src/seat/pointerconstraintsmanager.cpp b/src/seat/pointerconstraintsmanager.cpp new file mode 100644 index 0000000000..8616f675eb --- /dev/null +++ b/src/seat/pointerconstraintsmanager.cpp @@ -0,0 +1,175 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "pointerconstraintsmanager.h" +#include "helper.h" + +#include + +#include +#include + +#include +#include + +PointerConstraintsManager::PointerConstraintsManager(WAYLIB_SERVER_NAMESPACE::WPointerConstraintsV1 *constraints, + Helper *helper, QObject *parent) + : QObject(parent) + , m_interface(constraints) + , m_helper(helper) +{ + if (m_interface) { + connect(m_interface, &WAYLIB_SERVER_NAMESPACE::WPointerConstraintsV1::newConstraint, + this, &PointerConstraintsManager::onNewConstraint); + } +} + +PointerConstraintsManager::~PointerConstraintsManager() = default; + +void PointerConstraintsManager::onNewConstraint(wlr_pointer_constraint_v1 *constraint) +{ + if (!constraint) + return; + + m_constraints.insert(constraint); + + auto *qc = QW_NAMESPACE::qw_pointer_constraint_v1::from(constraint); + + connect(qc, &QW_NAMESPACE::qw_pointer_constraint_v1::notify_set_region, this, [this, constraint]() { + // wlroots updates the effective region automatically; re-evaluate activation. + if (!m_active.contains(constraint)) + activateConstraint(constraint); + }); + + connect(qc, &QW_NAMESPACE::qw_object_basic::before_destroy, this, [this, constraint]() { + onConstraintDestroyed(constraint); + }); + + // Track grab state on the constraint's seat so we can deactivate during drag/popup. + if (constraint->seat) { + auto *qwSeat = QW_NAMESPACE::qw_seat::from(constraint->seat); + if (qwSeat && !m_connectedSeats.contains(qwSeat)) { + m_connectedSeats.insert(qwSeat); + connect(qwSeat, &QW_NAMESPACE::qw_seat::notify_pointer_grab_begin, this, [this, seat = constraint->seat]() { + onSeatGrabChanged(seat); + }); + connect(qwSeat, &QW_NAMESPACE::qw_seat::notify_pointer_grab_end, this, [this, seat = constraint->seat]() { + onSeatGrabChanged(seat); + }); + connect(qwSeat, &QW_NAMESPACE::qw_object_basic::before_destroy, this, [this, qwSeat]() { + m_connectedSeats.remove(qwSeat); + }); + } + } + + activateConstraint(constraint); +} + +bool PointerConstraintsManager::canActivate(wlr_pointer_constraint_v1 *constraint) const +{ + if (!constraint || !constraint->seat || !constraint->surface) + return false; + + // Never activate while the compositor is in a special mode. + if (m_helper->currentMode() != Helper::CurrentMode::Normal) + return false; + + // Don't activate during an active drag or any other (non-default) pointer + // grab (popup grab, implicit move/resize grab, etc.). pointer_state.grab is + // always non-null (it defaults to default_grab), so use wlr_seat_pointer_has_grab + // which compares the grab interface against the default. + if (constraint->seat->drag) + return false; + if (wlr_seat_pointer_has_grab(constraint->seat)) + return false; + + // treeland move/resize is Qt-level and does not raise a seat grab, so it + // must be excluded explicitly via the Helper accessor. + auto *seat = WAYLIB_SERVER_NAMESPACE::WSeat::fromHandle(QW_NAMESPACE::qw_seat::from(constraint->seat)); + if (seat && m_helper->isInMoveResize(seat)) + return false; + + // The constraint's surface must currently hold pointer focus. + if (constraint->seat->pointer_state.focused_surface != constraint->surface) + return false; + + return true; +} + +void PointerConstraintsManager::activateConstraint(wlr_pointer_constraint_v1 *constraint) +{ + if (m_active.contains(constraint)) + return; + + if (!canActivate(constraint)) + return; + + auto *seat = WAYLIB_SERVER_NAMESPACE::WSeat::fromHandle(QW_NAMESPACE::qw_seat::from(constraint->seat)); + auto *cursor = seat ? seat->cursor() : nullptr; + if (!cursor) + return; + + m_cursors[constraint] = cursor; + cursor->setActivePointerConstraint(constraint); + + auto *qc = QW_NAMESPACE::qw_pointer_constraint_v1::from(constraint); + qc->send_activated(); + + m_active.insert(constraint); +} + +void PointerConstraintsManager::deactivateConstraint(wlr_pointer_constraint_v1 *constraint) +{ + if (!m_active.remove(constraint)) + return; + + auto it = m_cursors.find(constraint); + if (it != m_cursors.end() && it.value()) + it.value()->setActivePointerConstraint(nullptr); + + // send_deactivated may destroy the constraint for oneshot lifetime, so do + // not touch the constraint object after this call. + auto *qc = QW_NAMESPACE::qw_pointer_constraint_v1::from(constraint); + qc->send_deactivated(); +} + +void PointerConstraintsManager::onConstraintDestroyed(wlr_pointer_constraint_v1 *constraint) +{ + m_constraints.remove(constraint); + bool wasActive = m_active.remove(constraint); + auto it = m_cursors.find(constraint); + if (it != m_cursors.end()) { + if (wasActive && it.value()) + it.value()->setActivePointerConstraint(nullptr); + m_cursors.erase(it); + } +} + +void PointerConstraintsManager::onSeatGrabChanged(wlr_seat *seat) +{ + // A grab started or ended on this seat: always deactivate its constraints. + const auto active = m_active; + for (auto *constraint : active) { + if (constraint->seat == seat) + deactivateConstraint(constraint); + } + + // Only re-evaluate (potentially re-activate) after the grab has ended. + // During an active grab canActivate's grab check would reject anyway, and + // re-evaluating on grab_begin would immediately undo the deactivation above. + if (wlr_seat_pointer_has_grab(seat)) + return; + + const auto tracked = m_constraints; + for (auto *constraint : tracked) { + if (constraint->seat == seat) + activateConstraint(constraint); + } +} + +void PointerConstraintsManager::deactivateAll() +{ + const auto active = m_active; + for (auto *constraint : active) + deactivateConstraint(constraint); +} diff --git a/src/seat/pointerconstraintsmanager.h b/src/seat/pointerconstraintsmanager.h new file mode 100644 index 0000000000..535248bb5d --- /dev/null +++ b/src/seat/pointerconstraintsmanager.h @@ -0,0 +1,44 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include +#include + +#include +#include +#include +#include + +struct wlr_pointer_constraint_v1; + +class Helper; + +class PointerConstraintsManager : public QObject +{ + Q_OBJECT + +public: + explicit PointerConstraintsManager(WAYLIB_SERVER_NAMESPACE::WPointerConstraintsV1 *constraints, + Helper *helper, QObject *parent = nullptr); + ~PointerConstraintsManager() override; + + void deactivateAll(); + +private: + void onNewConstraint(wlr_pointer_constraint_v1 *constraint); + bool canActivate(wlr_pointer_constraint_v1 *constraint) const; + void activateConstraint(wlr_pointer_constraint_v1 *constraint); + void deactivateConstraint(wlr_pointer_constraint_v1 *constraint); + void onConstraintDestroyed(wlr_pointer_constraint_v1 *constraint); + void onSeatGrabChanged(struct wlr_seat *seat); + + WAYLIB_SERVER_NAMESPACE::WPointerConstraintsV1 *m_interface = nullptr; + Helper *m_helper = nullptr; + + QSet m_constraints; + QSet m_active; + QHash> m_cursors; + QSet m_connectedSeats; +}; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a9edf9590b..b37de6660c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -9,3 +9,4 @@ add_subdirectory(test_protocol_wallpaper-color) add_subdirectory(test_protocol_window-management) add_subdirectory(test_protocol_prelaunch-splash) add_subdirectory(test_effect_glass) +add_subdirectory(test_protocol_pointerconstraints) diff --git a/tests/test_protocol_pointerconstraints/CMakeLists.txt b/tests/test_protocol_pointerconstraints/CMakeLists.txt new file mode 100644 index 0000000000..da85e8cde9 --- /dev/null +++ b/tests/test_protocol_pointerconstraints/CMakeLists.txt @@ -0,0 +1,19 @@ +find_package(Qt6 REQUIRED COMPONENTS Test) + +add_executable(test_protocol_pointerconstraints main.cpp) + +target_link_libraries(test_protocol_pointerconstraints + PRIVATE + libtreeland + Qt::Test +) + +add_test(NAME test_protocol_pointerconstraints COMMAND test_protocol_pointerconstraints) + +set_property(TEST test_protocol_pointerconstraints PROPERTY + ENVIRONMENT "QT_QPA_PLATFORM=offscreen" +) + +set_property(TEST test_protocol_pointerconstraints PROPERTY + TIMEOUT 3 +) diff --git a/tests/test_protocol_pointerconstraints/main.cpp b/tests/test_protocol_pointerconstraints/main.cpp new file mode 100644 index 0000000000..3b291476f7 --- /dev/null +++ b/tests/test_protocol_pointerconstraints/main.cpp @@ -0,0 +1,60 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include +#include +#include + +#include +#include + +using namespace WAYLIB_SERVER_NAMESPACE; + +class PointerConstraintsTest : public QObject +{ + Q_OBJECT + + WServer *m_server = nullptr; + +public: + PointerConstraintsTest(QObject *parent = nullptr) + : QObject(parent) + { + } + +private Q_SLOTS: + void initTestCase() + { + m_server = new WServer(); + } + + void cleanupTestCase() + { + delete m_server; + m_server = nullptr; + } + + // Both WServerInterface wrappers should attach and expose their globals. + void testCreateInterfaces() + { + auto *constraints = m_server->attach(); + auto *relative = m_server->attach(); + QVERIFY(constraints != nullptr); + QVERIFY(relative != nullptr); + QCOMPARE(constraints->interfaceName(), QByteArrayView("zwp_pointer_constraints_v1")); + QCOMPARE(relative->interfaceName(), QByteArrayView("zwp_relative_pointer_manager_v1")); + QVERIFY(m_server->findInterface() == constraints); + QVERIFY(m_server->findInterface() == relative); + } + + // Without any surface/seat there is nothing to constrain. + void testConstraintForSurfaceReturnsNullWithoutSeat() + { + auto *constraints = m_server->findInterface(); + QVERIFY(constraints != nullptr); + QVERIFY(constraints->constraintForSurface(nullptr, nullptr) == nullptr); + } +}; + +QTEST_MAIN(PointerConstraintsTest) +#include "main.moc" diff --git a/waylib/src/server/CMakeLists.txt b/waylib/src/server/CMakeLists.txt index c52280aa9b..0a7e54b9c1 100644 --- a/waylib/src/server/CMakeLists.txt +++ b/waylib/src/server/CMakeLists.txt @@ -217,6 +217,8 @@ set(SOURCES protocols/private/wvirtualkeyboardv1.cpp protocols/ext_foreign_toplevel_image_capture_source.c #TODO: Remove after wlroots 0.20 protocols/wcursorshapemanagerv1.cpp + protocols/wpointerconstraintsv1.cpp + protocols/wrelativepointerv1.cpp protocols/woutputmanagerv1.cpp protocols/wextforeigntoplevellistv1.cpp protocols/wxdgdialogmanagerv1.cpp @@ -314,6 +316,8 @@ set(HEADERS protocols/winputpopupsurface.h protocols/WInputPopupSurface protocols/wcursorshapemanagerv1.h + protocols/wpointerconstraintsv1.h + protocols/wrelativepointerv1.h protocols/WCursorShapeManagerV1 protocols/woutputmanagerv1.h protocols/WOutputManagerV1 diff --git a/waylib/src/server/kernel/private/wcursor_p.h b/waylib/src/server/kernel/private/wcursor_p.h index b1df78c1c3..ce1bd843b9 100644 --- a/waylib/src/server/kernel/private/wcursor_p.h +++ b/waylib/src/server/kernel/private/wcursor_p.h @@ -29,6 +29,7 @@ struct wlr_pointer_pinch_end_event; struct wlr_pointer_hold_begin_event; struct wlr_pointer_hold_end_event; struct wlr_cursor; +struct wlr_pointer_constraint_v1; struct wlr_touch_down_event; struct wlr_touch_up_event; struct wlr_touch_motion_event; @@ -72,6 +73,9 @@ class Q_DECL_HIDDEN WCursorPrivate : public WWrapObjectPrivate void connect(); void processCursorMotion(QW_NAMESPACE::qw_pointer *device, uint32_t time); + bool applyPointerConstraint(QW_NAMESPACE::qw_pointer *device, uint32_t timeMsec, + double dx, double dy, double dxUnaccel, double dyUnaccel, + const QPointF &oldPos); W_DECLARE_PUBLIC(WCursor) @@ -89,6 +93,9 @@ class Q_DECL_HIDDEN WCursorPrivate : public WWrapObjectPrivate Qt::MouseButton button = Qt::NoButton; QPointF lastPressedOrTouchDownPosition; bool visible = true; + wlr_pointer_constraint_v1 *activeConstraint = nullptr; + QPointF lockedWarpTarget; + bool cursorVisibleBeforeLock = true; double scrollFactor = 1.0; }; diff --git a/waylib/src/server/kernel/wcursor.cpp b/waylib/src/server/kernel/wcursor.cpp index 84f074c02b..40bd97c390 100644 --- a/waylib/src/server/kernel/wcursor.cpp +++ b/waylib/src/server/kernel/wcursor.cpp @@ -10,6 +10,8 @@ #include "winputdevice.h" #include "wimagebuffer.h" #include "wseat.h" +#include "wrelativepointerv1.h" +#include "wserver.h" #include "woutput.h" #include "woutputlayout.h" #include "wayliblogging.h" @@ -25,11 +27,87 @@ #include #include +#include +#include +#include +#include + #include #include #include #include +// Inline replacement for wlr_region_confine() to avoid linking the wlroots +// util library directly from libwaylibserver (it is only available indirectly +// via QWlroots). The implementation mirrors wlr_region_confine from +// wlroots/util/region.c, using only the pixman API that waylibserver already +// links against. +static void regionConfine(const pixman_region32_t *region, double x1, double y1, + double x2, double y2, double *x2_out, double *y2_out, + pixman_box32_t box) +{ + double x_clamped = std::fmax(std::fmin(x2, box.x2 - 1), box.x1); + double y_clamped = std::fmax(std::fmin(y2, box.y2 - 1), box.y1); + + if (std::floor(x_clamped) == std::floor(x2) && std::floor(y_clamped) == std::floor(y2)) { + *x2_out = x2; + *y2_out = y2; + return; + } + + double dx = x2 - x1; + double dy = y2 - y1; + + double delta = std::fmin(std::fabs(x_clamped - x1) / std::fabs(dx), + std::fabs(y_clamped - y1) / std::fabs(dy)); + + double x = std::fmax(std::fmin(delta * dx + x1, box.x2 - 1), box.x1); + double y = std::fmax(std::fmin(delta * dy + y1, box.y2 - 1), box.y1); + + int x_ext = static_cast(std::floor(x)) + (dx == 0 ? 0 : dx > 0 ? 1 : -1); + int y_ext = static_cast(std::floor(y)) + (dy == 0 ? 0 : dy > 0 ? 1 : -1); + + if (pixman_region32_contains_point(region, x_ext, y_ext, &box)) { + return regionConfine(region, x, y, x2, y2, x2_out, y2_out, box); + } else if (dx == 0 || dy == 0) { + *x2_out = x; + *y2_out = y; + } else { + bool bordering_x = x == box.x1 || x == box.x2 - 1; + bool bordering_y = y == box.y1 || y == box.y2 - 1; + + if (bordering_x == bordering_y) { + double x2_potential, y2_potential; + double tmp1, tmp2; + regionConfine(region, x, y, x, y2, &tmp1, &y2_potential, box); + regionConfine(region, x, y, x2, y, &x2_potential, &tmp2, box); + if (std::fabs(x2_potential - x) > std::fabs(y2_potential - y)) { + *x2_out = x2_potential; + *y2_out = y; + } else { + *x2_out = x; + *y2_out = y2_potential; + } + } else if (bordering_x) { + return regionConfine(region, x, y, x, y2, x2_out, y2_out, box); + } else if (bordering_y) { + return regionConfine(region, x, y, x2, y, x2_out, y2_out, box); + } + } +} + +static bool regionConfineWrapper(const pixman_region32_t *region, double x1, double y1, + double x2, double y2, double *x2_out, double *y2_out) +{ + pixman_box32_t box; + if (pixman_region32_contains_point(region, static_cast(std::floor(x1)), + static_cast(std::floor(y1)), &box)) { + regionConfine(region, x1, y1, x2, y2, x2_out, y2_out, box); + return true; + } + return false; +} + W_DECLARE_PRIVATE_MEMBER(QCursor_d_tag, QCursor, d, QCursorData*); QW_USE_NAMESPACE @@ -86,15 +164,22 @@ void WCursorPrivate::sendLeaveEvent(WInputDevice *device) void WCursorPrivate::on_motion(wlr_pointer_motion_event *event) { auto device = qw_pointer::from(event->pointer); + const QPointF oldPos = q_func()->position(); q_func()->move(device, QPointF(event->delta_x, event->delta_y)); - processCursorMotion(device, event->time_msec); + if (!applyPointerConstraint(device, event->time_msec, event->delta_x, event->delta_y, + event->unaccel_dx, event->unaccel_dy, oldPos)) + processCursorMotion(device, event->time_msec); } void WCursorPrivate::on_motion_absolute(wlr_pointer_motion_absolute_event *event) { auto device = qw_pointer::from(event->pointer); + const QPointF oldPos = q_func()->position(); q_func()->setScalePosition(device, QPointF(event->x, event->y)); - processCursorMotion(device, event->time_msec); + const QPointF delta = q_func()->position() - oldPos; + if (!applyPointerConstraint(device, event->time_msec, delta.x(), delta.y(), + delta.x(), delta.y(), oldPos)) + processCursorMotion(device, event->time_msec); } void WCursorPrivate::on_button(wlr_pointer_button_event *event) @@ -340,6 +425,50 @@ void WCursorPrivate::processCursorMotion(qw_pointer *device, uint32_t time) } } +bool WCursorPrivate::applyPointerConstraint(qw_pointer *device, uint32_t timeMsec, + double dx, double dy, + double dxUnaccel, double dyUnaccel, + const QPointF &oldPos) +{ + W_Q(WCursor); + if (!activeConstraint) + return false; + + wlr_seat *wlrSeat = seat ? seat->handle()->handle() : nullptr; + if (!wlrSeat) + return false; + + if (activeConstraint->type == WLR_POINTER_CONSTRAINT_V1_LOCKED) { + if (auto *server = seat->server()) { + if (auto *relative = server->findInterface()) { + relative->sendRelativeMotion(wlrSeat, static_cast(timeMsec) * 1000, + dx, dy, dxUnaccel, dyUnaccel); + } + } + // Warp back to the anchor so physical movement does not accumulate. + q->setPosition(device, lockedWarpTarget); + return true; + } + + if (activeConstraint->type == WLR_POINTER_CONSTRAINT_V1_CONFINED) { + // The constraint region and the seat's last surface-local position are + // both in logical coordinates, so the output-layout delta maps directly. + const double sx = wlrSeat->pointer_state.sx; + const double sy = wlrSeat->pointer_state.sy; + double confinedSx = sx + dx; + double confinedSy = sy + dy; + if (regionConfineWrapper(&activeConstraint->region, sx, sy, + confinedSx, confinedSy, &confinedSx, &confinedSy)) { + q->setPosition(device, oldPos + QPointF(confinedSx - sx, confinedSy - sy)); + } else { + q->setPosition(device, oldPos); + } + return false; + } + + return false; +} + WCursor::WCursor(WCursorPrivate &dd, QObject *parent) : WWrapObject(dd, parent) { @@ -716,6 +845,43 @@ void WCursor::setVisible(bool visible) Q_EMIT visibleChanged(); } +void WCursor::setActivePointerConstraint(wlr_pointer_constraint_v1 *constraint) +{ + W_D(WCursor); + if (d->activeConstraint == constraint) + return; + + if (d->activeConstraint + && d->activeConstraint->type == WLR_POINTER_CONSTRAINT_V1_LOCKED) + setVisible(d->cursorVisibleBeforeLock); + + d->activeConstraint = constraint; + + if (!constraint) + return; + + if (constraint->type == WLR_POINTER_CONSTRAINT_V1_LOCKED) { + d->cursorVisibleBeforeLock = isVisible(); + setVisible(false); + wlr_seat *wlrSeat = d->seat ? d->seat->handle()->handle() : nullptr; + if (wlrSeat && constraint->current.cursor_hint.enabled) { + const double sx = wlrSeat->pointer_state.sx; + const double sy = wlrSeat->pointer_state.sy; + d->lockedWarpTarget = position() + + QPointF(constraint->current.cursor_hint.x - sx, + constraint->current.cursor_hint.y - sy); + } else { + d->lockedWarpTarget = position(); + } + } +} + +wlr_pointer_constraint_v1 *WCursor::activePointerConstraint() const +{ + W_DC(WCursor); + return d->activeConstraint; +} + QPointF WCursor::position() const { W_DC(WCursor); diff --git a/waylib/src/server/kernel/wcursor.h b/waylib/src/server/kernel/wcursor.h index 01189bc724..e986b254ec 100644 --- a/waylib/src/server/kernel/wcursor.h +++ b/waylib/src/server/kernel/wcursor.h @@ -22,6 +22,7 @@ class qw_output_cursor; class qw_surface; QW_END_NAMESPACE +struct wlr_pointer_constraint_v1; WAYLIB_SERVER_BEGIN_NAMESPACE class WSeat; @@ -80,6 +81,8 @@ class WAYLIB_SERVER_EXPORT WCursor : public WWrapObject bool isVisible() const; void setVisible(bool visible); + void setActivePointerConstraint(wlr_pointer_constraint_v1 *constraint); + wlr_pointer_constraint_v1 *activePointerConstraint() const; QPointF position() const; QPointF lastPressedOrTouchDownPosition() const; diff --git a/waylib/src/server/protocols/wpointerconstraintsv1.cpp b/waylib/src/server/protocols/wpointerconstraintsv1.cpp new file mode 100644 index 0000000000..8529f7205c --- /dev/null +++ b/waylib/src/server/protocols/wpointerconstraintsv1.cpp @@ -0,0 +1,79 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "wpointerconstraintsv1.h" +#include "private/wglobal_p.h" + +#include +#include + +#include + +WAYLIB_SERVER_BEGIN_NAMESPACE + +using QW_NAMESPACE::qw_pointer_constraints_v1; + +class Q_DECL_HIDDEN WPointerConstraintsV1Private : public WObjectPrivate +{ +public: + WPointerConstraintsV1Private(WPointerConstraintsV1 *qq) + : WObjectPrivate(qq) + { + } + + inline qw_pointer_constraints_v1 *handle() const { + return q_func()->nativeInterface(); + } + + inline wlr_pointer_constraints_v1 *nativeHandle() const { + Q_ASSERT(handle()); + return handle()->handle(); + } + + W_DECLARE_PUBLIC(WPointerConstraintsV1) +}; + +WPointerConstraintsV1::WPointerConstraintsV1() + : WObject(*new WPointerConstraintsV1Private(this)) +{ + +} + +qw_pointer_constraints_v1 *WPointerConstraintsV1::handle() const +{ + return nativeInterface(); +} + +QByteArrayView WPointerConstraintsV1::interfaceName() const +{ + return "zwp_pointer_constraints_v1"; +} + +wlr_pointer_constraint_v1 *WPointerConstraintsV1::constraintForSurface(wlr_surface *surface, wlr_seat *seat) const +{ + if (!m_handle) + return nullptr; + + return handle()->constraint_for_surface(surface, seat); +} + +void WPointerConstraintsV1::create(WServer *server) +{ + if (!m_handle) { + m_handle = qw_pointer_constraints_v1::create(*server->handle()); + QObject::connect(handle(), &qw_pointer_constraints_v1::notify_new_constraint, this, [this] (wlr_pointer_constraint_v1 *constraint) { + Q_EMIT newConstraint(constraint); + }); + } +} + +wl_global *WPointerConstraintsV1::global() const +{ + W_D(const WPointerConstraintsV1); + if (m_handle) + return d->nativeHandle()->global; + + return nullptr; +} + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/protocols/wpointerconstraintsv1.h b/waylib/src/server/protocols/wpointerconstraintsv1.h new file mode 100644 index 0000000000..54150f79c4 --- /dev/null +++ b/waylib/src/server/protocols/wpointerconstraintsv1.h @@ -0,0 +1,43 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include + +#include + +QW_BEGIN_NAMESPACE +class qw_pointer_constraints_v1; +QW_END_NAMESPACE + +struct wlr_pointer_constraint_v1; +struct wlr_seat; +struct wlr_surface; + +WAYLIB_SERVER_BEGIN_NAMESPACE + +class WPointerConstraintsV1Private; +class WAYLIB_SERVER_EXPORT WPointerConstraintsV1 : public QObject, public WObject, public WServerInterface +{ + Q_OBJECT + W_DECLARE_PRIVATE(WPointerConstraintsV1) + +public: + explicit WPointerConstraintsV1(); + + QW_NAMESPACE::qw_pointer_constraints_v1 *handle() const; + + QByteArrayView interfaceName() const override; + + wlr_pointer_constraint_v1 *constraintForSurface(wlr_surface *surface, wlr_seat *seat) const; + +Q_SIGNALS: + void newConstraint(wlr_pointer_constraint_v1 *constraint); + +protected: + void create(WServer *server) override; + wl_global *global() const override; +}; + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/protocols/wrelativepointerv1.cpp b/waylib/src/server/protocols/wrelativepointerv1.cpp new file mode 100644 index 0000000000..7a9f40d485 --- /dev/null +++ b/waylib/src/server/protocols/wrelativepointerv1.cpp @@ -0,0 +1,78 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "wrelativepointerv1.h" +#include "private/wglobal_p.h" + +#include +#include + +#include + +WAYLIB_SERVER_BEGIN_NAMESPACE + +using QW_NAMESPACE::qw_relative_pointer_manager_v1; + +class Q_DECL_HIDDEN WRelativePointerManagerV1Private : public WObjectPrivate +{ +public: + WRelativePointerManagerV1Private(WRelativePointerManagerV1 *qq) + : WObjectPrivate(qq) + { + } + + inline qw_relative_pointer_manager_v1 *handle() const { + return q_func()->nativeInterface(); + } + + inline wlr_relative_pointer_manager_v1 *nativeHandle() const { + Q_ASSERT(handle()); + return handle()->handle(); + } + + W_DECLARE_PUBLIC(WRelativePointerManagerV1) +}; + +WRelativePointerManagerV1::WRelativePointerManagerV1() + : WObject(*new WRelativePointerManagerV1Private(this)) +{ + +} + +qw_relative_pointer_manager_v1 *WRelativePointerManagerV1::handle() const +{ + return nativeInterface(); +} + +QByteArrayView WRelativePointerManagerV1::interfaceName() const +{ + return "zwp_relative_pointer_manager_v1"; +} + +void WRelativePointerManagerV1::sendRelativeMotion(wlr_seat *seat, uint64_t timeUsec, + double dx, double dy, + double dxUnaccel, double dyUnaccel) const +{ + if (!m_handle || !seat) + return; + + handle()->send_relative_motion(seat, timeUsec, dx, dy, dxUnaccel, dyUnaccel); +} + +void WRelativePointerManagerV1::create(WServer *server) +{ + if (!m_handle) { + m_handle = qw_relative_pointer_manager_v1::create(*server->handle()); + } +} + +wl_global *WRelativePointerManagerV1::global() const +{ + W_D(const WRelativePointerManagerV1); + if (m_handle) + return d->nativeHandle()->global; + + return nullptr; +} + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/protocols/wrelativepointerv1.h b/waylib/src/server/protocols/wrelativepointerv1.h new file mode 100644 index 0000000000..68ea489f21 --- /dev/null +++ b/waylib/src/server/protocols/wrelativepointerv1.h @@ -0,0 +1,41 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include + +#include + +#include + +QW_BEGIN_NAMESPACE +class qw_relative_pointer_manager_v1; +QW_END_NAMESPACE + +struct wlr_seat; + +WAYLIB_SERVER_BEGIN_NAMESPACE + +class WRelativePointerManagerV1Private; +class WAYLIB_SERVER_EXPORT WRelativePointerManagerV1 : public QObject, public WObject, public WServerInterface +{ + Q_OBJECT + W_DECLARE_PRIVATE(WRelativePointerManagerV1) + +public: + explicit WRelativePointerManagerV1(); + + QW_NAMESPACE::qw_relative_pointer_manager_v1 *handle() const; + + QByteArrayView interfaceName() const override; + + void sendRelativeMotion(wlr_seat *seat, uint64_t timeUsec, + double dx, double dy, double dxUnaccel, double dyUnaccel) const; + +protected: + void create(WServer *server) override; + wl_global *global() const override; +}; + +WAYLIB_SERVER_END_NAMESPACE