Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions src/seat/helper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
// 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"

Check warning on line 7 in src/seat/helper.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "qwxdgshell.h" not found.
#include "seatsmanager.h"

#include <QFile>
Expand Down Expand Up @@ -72,12 +73,14 @@
#include <WOutput>
#include <WServer>
#include <WSurfaceItem>
#include <WXdgOutput>

Check warning on line 76 in src/seat/helper.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <WXdgOutput> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <wayland-util.h>

Check warning on line 77 in src/seat/helper.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <wayland-util.h> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <wcursorshapemanagerv1.h>

Check warning on line 78 in src/seat/helper.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <wcursorshapemanagerv1.h> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <wpointerconstraintsv1.h>

Check warning on line 79 in src/seat/helper.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <wpointerconstraintsv1.h> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <wrelativepointerv1.h>

Check warning on line 80 in src/seat/helper.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <wrelativepointerv1.h> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <wextimagecapturesourcev1impl.h>

Check warning on line 81 in src/seat/helper.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <wextimagecapturesourcev1impl.h> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <wlayersurface.h>

Check warning on line 82 in src/seat/helper.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <wlayersurface.h> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <woutputhelper.h>

Check warning on line 83 in src/seat/helper.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <woutputhelper.h> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <woutputitem.h>
#include <woutputlayout.h>
#include <woutputmanagerv1.h>
Expand Down Expand Up @@ -2074,6 +2077,9 @@
&Helper::onOutputTestOrApply);

m_server->attach<WCursorShapeManagerV1>();
m_pointerConstraintsV1 = m_server->attach<WPointerConstraintsV1>();
m_relativePointerManagerV1 = m_server->attach<WRelativePointerManagerV1>();
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);
Expand Down Expand Up @@ -3309,9 +3315,21 @@

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) {
Expand Down
9 changes: 9 additions & 0 deletions src/seat/helper.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ class WToplevelSurface;
class WXdgDecorationManager;
class WXdgOutputManager;
class WXWayland;
class WPointerConstraintsV1;
class WRelativePointerManagerV1;

class WForeignToplevel;
class WExtForeignToplevelListV1;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
};
175 changes: 175 additions & 0 deletions src/seat/pointerconstraintsmanager.cpp
Original file line number Diff line number Diff line change
@@ -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 <wcursor.h>

Check warning on line 7 in src/seat/pointerconstraintsmanager.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <wcursor.h> not found. Please note: Cppcheck does not need standard library headers to get proper results.

#include <qwpointerconstraintsv1.h>
#include <qwseat.h>

#include <wlr/types/wlr_pointer_constraints_v1.h>
#include <wlr/types/wlr_seat.h>

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);
}
44 changes: 44 additions & 0 deletions src/seat/pointerconstraintsmanager.h
Original file line number Diff line number Diff line change
@@ -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 <wpointerconstraintsv1.h>
#include <wseat.h>

#include <QHash>
#include <QObject>
#include <QPointer>
#include <QSet>

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<wlr_pointer_constraint_v1 *> m_constraints;
QSet<wlr_pointer_constraint_v1 *> m_active;
QHash<wlr_pointer_constraint_v1 *, QPointer<WAYLIB_SERVER_NAMESPACE::WCursor>> m_cursors;
QSet<QW_NAMESPACE::qw_seat *> m_connectedSeats;
};
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
19 changes: 19 additions & 0 deletions tests/test_protocol_pointerconstraints/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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
)
60 changes: 60 additions & 0 deletions tests/test_protocol_pointerconstraints/main.cpp
Original file line number Diff line number Diff line change
@@ -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 <wpointerconstraintsv1.h>
#include <wrelativepointerv1.h>
#include <wserver.h>

#include <QObject>
#include <QTest>

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<WPointerConstraintsV1>();
auto *relative = m_server->attach<WRelativePointerManagerV1>();
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<WPointerConstraintsV1>() == constraints);
QVERIFY(m_server->findInterface<WRelativePointerManagerV1>() == relative);
}

// Without any surface/seat there is nothing to constrain.
void testConstraintForSurfaceReturnsNullWithoutSeat()
{
auto *constraints = m_server->findInterface<WPointerConstraintsV1>();
QVERIFY(constraints != nullptr);
QVERIFY(constraints->constraintForSurface(nullptr, nullptr) == nullptr);
}
};

QTEST_MAIN(PointerConstraintsTest)
#include "main.moc"
Loading
Loading