[plasma/kwin] /: pointer_input: rework pointer constraints

Xaver Hugl <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 8f0882b2e0af66c0a04f54b823bc4aed10b49da7 by Xaver Hugl.
Committed on 21/07/2026 at 19:14.
Pushed by zamundaaa into branch 'master'.

pointer_input: rework pointer constraints

On the Wayland side, pointer constraints were implemented in a problematic way:
While position hints and regions were applied together with the Wayland commit
(and possibly delayed), the destruction of pointer constraints was handled
immediately. This could lead to the pointer warping to the wrong location when
the pointer constraint object gets destroyed by the client.

So, instead of applying pointer constraint creation and destruction immediately,
this delays all the pointer constraint and lock properties with Wayland commits.

Changed constraint regions were also applied before the rest of each Wayland
commit, which caused issues when the geometry of the window changed along with
the constraint region (like windowed -> fullscreen).

Note that this also requires committing the surface after installing a pointer
constraint for it to be actually applied. This is unlikely to cause issues with
real-world applications, but it's underspecified in the protocol and KWin's
autotest for it needed adjusting.

BUG: 520910
BUG: 487189

M  +7    -2    autotests/integration/pointer_constraints_test.cpp
M  +50   -109  src/pointer_input.cpp
M  +2    -4    src/pointer_input.h
M  +58   -79   src/wayland/pointerconstraints_v1.cpp
M  +5    -81   src/wayland/pointerconstraints_v1.h
M  +11   -28   src/wayland/pointerconstraints_v1_p.h
M  +79   -57   src/wayland/surface.cpp
M  +13   -10   src/wayland/surface.h
M  +9    -2    src/wayland/surface_p.h

https://invent.kde.org/plasma/kwin/-/commit/8f0882b2e0af66c0a04f54b823bc4aed10b49da7

diff --git a/autotests/integration/pointer_constraints_test.cpp b/autotests/integration/pointer_constraints_test.cpp
index f7a43e30328..cffd0a6d030 100644
--- a/autotests/integration/pointer_constraints_test.cpp
+++ b/autotests/integration/pointer_constraints_test.cpp
@@ -201,6 +201,7 @@ void TestPointerConstraints::testConfinedPointer()
 
     // reconfine pointer (this time with persistent life time)
     confinedPointer.reset(Test::waylandPointerConstraints()->confinePointer(surface.get(), pointer.get(), nullptr, KWayland::Client::PointerConstraints::LifeTime::Persistent));
+    surface->commit(KWayland::Client::Surface::CommitFlag::None);
     QSignalSpy confinedSpy2(confinedPointer.get(), &KWayland::Client::ConfinedPointer::confined);
     QSignalSpy unconfinedSpy2(confinedPointer.get(), &KWayland::Client::ConfinedPointer::unconfined);
 
@@ -243,9 +244,10 @@ void TestPointerConstraints::testConfinedPointer()
 
     // delete pointer confine
     confinedPointer.reset(nullptr);
+    surface->commit(KWayland::Client::Surface::CommitFlag::None);
     Test::flushWaylandConnection();
 
-    QSignalSpy constraintsChangedSpy(input()->pointer()->focus()->surface(), &SurfaceInterface::pointerConstraintsChanged);
+    QSignalSpy constraintsChangedSpy(input()->pointer()->focus()->surface(), &SurfaceInterface::confinedPointerRegionChanged);
     QVERIFY(constraintsChangedSpy.wait());
 
     // should be unconfined
@@ -253,6 +255,7 @@ void TestPointerConstraints::testConfinedPointer()
 
     // confine again
     confinedPointer.reset(Test::waylandPointerConstraints()->confinePointer(surface.get(), pointer.get(), nullptr, KWayland::Client::PointerConstraints::LifeTime::Persistent));
+    surface->commit(KWayland::Client::Surface::CommitFlag::None);
     QSignalSpy confinedSpy3(confinedPointer.get(), &KWayland::Client::ConfinedPointer::confined);
     QVERIFY(confinedSpy3.wait());
     QCOMPARE(input()->pointer()->isConstrained(), true);
@@ -303,6 +306,7 @@ void TestPointerConstraints::testLockedPointer()
     QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center() + QPoint(1, 1));
 
     lockedPointer.reset(Test::waylandPointerConstraints()->lockPointer(surface.get(), pointer.get(), nullptr, KWayland::Client::PointerConstraints::LifeTime::Persistent));
+    surface->commit(KWayland::Client::Surface::CommitFlag::None);
     QSignalSpy lockedSpy2(lockedPointer.get(), &KWayland::Client::LockedPointer::locked);
 
     // activate the window again, this should lock again
@@ -317,9 +321,10 @@ void TestPointerConstraints::testLockedPointer()
 
     // delete pointer lock
     lockedPointer.reset(nullptr);
+    surface->commit(KWayland::Client::Surface::CommitFlag::None);
     Test::flushWaylandConnection();
 
-    QSignalSpy constraintsChangedSpy(input()->pointer()->focus()->surface(), &SurfaceInterface::pointerConstraintsChanged);
+    QSignalSpy constraintsChangedSpy(input()->pointer()->focus()->surface(), &SurfaceInterface::lockedPointerRegionChanged);
     QVERIFY(constraintsChangedSpy.wait());
 
     // moving cursor should be allowed again
diff --git a/src/pointer_input.cpp b/src/pointer_input.cpp
index 012e10384a4..90be8bf3e16 100644
--- a/src/pointer_input.cpp
+++ b/src/pointer_input.cpp
@@ -650,8 +650,6 @@ void PointerInputRedirection::focusUpdate(Window *focusOld, Window *focusNow)
         waylandServer()->seat()->setFocusedPointerSurfaceTransformation(focus()->inputTransformation());
     });
 
-    m_constraintsConnection = connect(focusNow->surface(), &SurfaceInterface::pointerConstraintsChanged,
-                                      this, &PointerInputRedirection::updatePointerConstraints);
     m_constraintsActivatedConnection = connect(workspace(), &Workspace::windowActivated,
                                                this, &PointerInputRedirection::updatePointerConstraints);
     updatePointerConstraints();
@@ -677,21 +675,12 @@ void PointerInputRedirection::breakPointerConstraints(SurfaceInterface *surface)
 
 void PointerInputRedirection::disconnectConfinedPointerRegionConnection()
 {
-    disconnect(m_confinedPointerRegionConnection);
-    m_confinedPointerRegionConnection = QMetaObject::Connection();
-}
-
-void PointerInputRedirection::disconnectLockedPointerAboutToBeUnboundConnection()
-{
-    disconnect(m_lockedPointerAboutToBeUnboundConnection);
-    m_lockedPointerAboutToBeUnboundConnection = QMetaObject::Connection();
+    disconnect(m_surfaceCommittedConnection);
+    m_surfaceCommittedConnection = QMetaObject::Connection();
 }
 
 void PointerInputRedirection::disconnectPointerConstraintsConnection()
 {
-    disconnect(m_constraintsConnection);
-    m_constraintsConnection = QMetaObject::Connection();
-
     disconnect(m_constraintsActivatedConnection);
     m_constraintsActivatedConnection = QMetaObject::Connection();
 }
@@ -707,118 +696,70 @@ void PointerInputRedirection::setEnableConstraints(bool set)
 
 void PointerInputRedirection::updatePointerConstraints()
 {
-    if (!focus()) {
+    if (!supportsWarping()) {
         return;
     }
-    const auto s = focus()->surface();
-    if (!s) {
-        return;
+    const bool canConstrain = m_enableConstraints
+        && focus()
+        && focus()->surface()
+        && focus() == workspace()->activeWindow()
+        && focus()->surface() == waylandServer()->seat()->focusedPointerSurface();
+    if (!canConstrain || focus()->surface() != m_constrainedSurface) {
+        disconnectConfinedPointerRegionConnection();
+        m_confined = false;
+        m_locked = false;
+        if (m_constrainedSurface) {
+            m_constrainedSurface->setPointerConfined(false);
+            m_constrainedSurface->setPointerLocked(false);
+            m_constrainedSurface = nullptr;
+        }
     }
-    if (s != waylandServer()->seat()->focusedPointerSurface()) {
+    if (!canConstrain) {
         return;
     }
-    if (!supportsWarping()) {
-        return;
+
+    SurfaceInterface *s = focus()->surface();
+    m_constrainedSurface = s;
+    if (!m_surfaceCommittedConnection) {
+        m_surfaceCommittedConnection = connect(s, &SurfaceInterface::committed,
+                                               this, &PointerInputRedirection::updatePointerConstraints);
     }
-    const bool canConstrain = m_enableConstraints && focus() == workspace()->activeWindow();
-    const auto cf = s->confinedPointer();
-    if (cf) {
-        if (cf->isConfined()) {
-            if (!canConstrain) {
-                cf->setConfined(false);
-                m_confined = false;
-                disconnectConfinedPointerRegionConnection();
-            }
-            return;
-        }
-        if (canConstrain && cf->region().contains(focus()->mapToLocal(m_pos))) {
-            cf->setConfined(true);
-            m_confined = true;
-            m_confinedPointerRegionConnection = connect(cf, &ConfinedPointerV1Interface::regionChanged, this, [this]() {
-                if (!focus()) {
-                    return;
-                }
-                const auto s = focus()->surface();
-                if (!s) {
-                    return;
-                }
-                const auto cf = s->confinedPointer();
-                if (!cf->region().contains(focus()->mapToLocal(m_pos))) {
-                    // pointer no longer in confined region, break the confinement
-                    cf->setConfined(false);
-                    m_confined = false;
-                } else {
-                    if (!cf->isConfined()) {
-                        cf->setConfined(true);
-                        m_confined = true;
-                    }
-                }
-            });
-            return;
-        }
-    } else {
-        m_confined = false;
-        disconnectConfinedPointerRegionConnection();
+
+    // NOTE the lifetime of the confined pointer interfaces
+    // don't necessarily match the confinement region,
+    // since Wayland commits are applied asynchronously
+
+    const bool shouldConfine = s->confinedPointerRegion()
+        && s->confinedPointerRegion()->contains(focus()->mapToLocal(m_pos));
+    if (m_confined != shouldConfine) {
+        m_confined = shouldConfine;
+        s->setPointerConfined(m_confined);
     }
-    const auto lock = s->lockedPointer();
-    if (lock) {
-        if (lock->isLocked()) {
-            if (!canConstrain) {
-                const auto hint = lock->cursorPositionHint();
-                lock->setLocked(false);
-                m_locked = false;
-                disconnectLockedPointerAboutToBeUnboundConnection();
-                if (hint.x() >= 0 && hint.y() >= 0 && focus() && hint.x() < focus()->width() && hint.y() < focus()->height()) {
-                    processWarp(focus()->mapFromLocal(hint), waylandServer()->seat()->timestamp());
-                }
+
+    const bool shouldLock = s->lockedPointerRegion()
+        && s->lockedPointerRegion()->contains(focus()->mapToLocal(m_pos));
+    if (m_locked != shouldLock) {
+        m_locked = shouldLock;
+        s->setPointerLocked(m_locked);
+        if (!shouldLock) {
+            const auto hint = s->lockedPointerHint();
+            if (hint && hint->x() >= 0 && hint->y() >= 0 && focus() && hint->x() < focus()->width() && hint->y() < focus()->height()) {
+                processWarp(focus()->mapFromLocal(*hint), waylandServer()->seat()->timestamp());
             }
-            return;
-        }
-        if (canConstrain && lock->region().contains(focus()->mapToLocal(m_pos))) {
-            lock->setLocked(true);
-            m_locked = true;
-
-            // The client might cancel pointer locking from its side by unbinding the LockedPointerInterface.
-            // In this case the cached cursor position hint must be fetched before the resource goes away
-            m_lockedPointerAboutToBeUnboundConnection = connect(lock, &LockedPointerV1Interface::aboutToBeDestroyed, this, [this, lock]() {
-                const auto hint = lock->cursorPositionHint();
-                if (hint.x() < 0 || hint.y() < 0 || !focus() || hint.x() >= focus()->width() || hint.y() >= focus()->height()) {
-                    return;
-                }
-                auto globalHint = focus()->mapFromLocal(hint);
-
-                // When the resource finally goes away, reposition the cursor according to the hint
-                connect(lock, &LockedPointerV1Interface::destroyed, this, [this, globalHint]() {
-                    processWarp(globalHint, waylandServer()->seat()->timestamp());
-                });
-            });
-            // TODO: connect to region change - is it needed at all? If the pointer is locked it's always in the region
         }
-    } else {
-        m_locked = false;
-        disconnectLockedPointerAboutToBeUnboundConnection();
     }
 }
 
 QPointF PointerInputRedirection::applyPointerConfinement(const QPointF &pos) const
 {
-    if (!focus()) {
-        return pos;
-    }
-    auto s = focus()->surface();
-    if (!s) {
-        return pos;
-    }
-    auto cf = s->confinedPointer();
-    if (!cf) {
-        return pos;
-    }
-    if (!cf->isConfined()) {
+    if (!m_confined || !focus() || !focus()->surface() || !focus()->surface()->confinedPointerRegion()) {
         return pos;
     }
+    const auto s = focus()->surface();
+    const RegionF region = *s->confinedPointerRegion();
 
     const QPointF localPos = focus()->mapToLocal(pos);
-    if (cf->region().contains(localPos)) {
+    if (region.contains(localPos)) {
         return pos;
     }
 
@@ -826,12 +767,12 @@ QPointF PointerInputRedirection::applyPointerConfinement(const QPointF &pos) con
 
     // allow either x or y to pass
     QPointF p(currentPos.x(), localPos.y());
-    if (cf->region().contains(p)) {
+    if (region.contains(p)) {
         return focus()->mapFromLocal(p);
     }
 
     p = QPointF(localPos.x(), currentPos.y());
-    if (cf->region().contains(p)) {
+    if (region.contains(p)) {
         return focus()->mapFromLocal(p);
     }
 
diff --git a/src/pointer_input.h b/src/pointer_input.h
index 8d4320c766a..fe5dad0be9c 100644
--- a/src/pointer_input.h
+++ b/src/pointer_input.h
@@ -178,7 +178,6 @@ private:
     qreal edgeBarrier(EdgeBarrierType type) const;
     QPointF applyPointerConfinement(const QPointF &pos) const;
     void disconnectConfinedPointerRegionConnection();
-    void disconnectLockedPointerAboutToBeUnboundConnection();
     void disconnectPointerConstraintsConnection();
     void breakPointerConstraints(SurfaceInterface *surface);
     CursorImage *m_cursor;
@@ -186,14 +185,13 @@ private:
     QHash<uint32_t, PointerButtonState> m_buttons;
     Qt::MouseButtons m_qtButtons;
     QMetaObject::Connection m_focusGeometryConnection;
-    QMetaObject::Connection m_constraintsConnection;
     QMetaObject::Connection m_constraintsActivatedConnection;
-    QMetaObject::Connection m_confinedPointerRegionConnection;
-    QMetaObject::Connection m_lockedPointerAboutToBeUnboundConnection;
+    QMetaObject::Connection m_surfaceCommittedConnection;
     QMetaObject::Connection m_decorationGeometryConnection;
     QMetaObject::Connection m_decorationDestroyedConnection;
     QMetaObject::Connection m_decorationClosedConnection;
     bool m_confined = false;
+    QPointer<SurfaceInterface> m_constrainedSurface;
     bool m_locked = false;
     bool m_enableConstraints = true;
     bool m_lastOutputWasPlaceholder = true;
diff --git a/src/wayland/pointerconstraints_v1.cpp b/src/wayland/pointerconstraints_v1.cpp
index 25fabf56e5a..780e27b919a 100644
--- a/src/wayland/pointerconstraints_v1.cpp
+++ b/src/wayland/pointerconstraints_v1.cpp
@@ -63,7 +63,8 @@ void PointerConstraintsV1InterfacePrivate::zwp_pointer_constraints_v1_lock_point
         return;
     }
 
-    new LockedPointerV1Interface(surface, LockedPointerV1Interface::LifeTime(lifetime), regionFromResource(region_resource).scaled(1.0 / surface->clientToCompositorScale()), lockedPointerResource);
+    RegionF region = regionFromResource(region_resource).scaled(1.0 / surface->clientToCompositorScale() / surface->serverScale());
+    new LockedPointerV1Interface(surface, PointerConstraintLifetime(lifetime), std::move(region), lockedPointerResource);
 }
 
 void PointerConstraintsV1InterfacePrivate::zwp_pointer_constraints_v1_confine_pointer(Resource *resource,
@@ -101,7 +102,8 @@ void PointerConstraintsV1InterfacePrivate::zwp_pointer_constraints_v1_confine_po
         return;
     }
 
-    new ConfinedPointerV1Interface(surface, ConfinedPointerV1Interface::LifeTime(lifetime), regionFromResource(region_resource).scaled(1.0 / surface->clientToCompositorScale()), confinedPointerResource);
+    RegionF region = regionFromResource(region_resource).scaled(1.0 / surface->clientToCompositorScale() / surface->serverScale());
+    new ConfinedPointerV1Interface(surface, PointerConstraintLifetime(lifetime), std::move(region), confinedPointerResource);
 }
 
 void PointerConstraintsV1InterfacePrivate::zwp_pointer_constraints_v1_destroy(Resource *resource)
@@ -126,49 +128,36 @@ LockedPointerV1InterfacePrivate *LockedPointerV1InterfacePrivate::get(LockedPoin
 
 LockedPointerV1InterfacePrivate::LockedPointerV1InterfacePrivate(LockedPointerV1Interface *q,
                                                                  SurfaceInterface *surface,
-                                                                 LockedPointerV1Interface::LifeTime lifeTime,
+                                                                 PointerConstraintLifetime lifeTime,
                                                                  const RegionF &region,
                                                                  ::wl_resource *resource)
     : QtWaylandServer::zwp_locked_pointer_v1(resource)
-    , SurfaceExtension(surface)
     , q(q)
     , lifeTime(lifeTime)
+    , m_surface(surface)
 {
-    pending->region = region;
-
-    apply(pending);
-
-    *pending = LockedPointerV1Commit{};
+    auto priv = SurfaceInterfacePrivate::get(surface);
+    priv->pending->pointerLockHint.reset();
+    priv->pending->pointerLockRegion = region;
+    priv->pending->confinementLifetime = lifeTime;
+    priv->pending->committed |= SurfaceState::Field::PointerLockHint | SurfaceState::Field::PointerLockRegion;
+    priv->lockedPointer = q;
 }
 
-void LockedPointerV1InterfacePrivate::apply(LockedPointerV1Commit *commit)
+LockedPointerV1InterfacePrivate::~LockedPointerV1InterfacePrivate()
 {
-    const RegionF oldRegion = effectiveRegion;
-    const QPointF oldHint = hint;
-
-    if (commit->region.has_value()) {
-        region = commit->region->scaled(1.0 / surface->serverScale());
-    }
-    if (commit->hint.has_value()) {
-        hint = commit->hint.value() / surface->serverScale();
-    }
-
-    effectiveRegion = surface->input();
-    if (!region.isEmpty()) {
-        effectiveRegion &= region;
-    }
-
-    if (oldRegion != effectiveRegion) {
-        Q_EMIT q->regionChanged();
-    }
-    if (oldHint != hint) {
-        Q_EMIT q->cursorPositionHintChanged();
+    if (m_surface) {
+        // NOTE the position hint is intentionally not reset,
+        // since it will be used when the next commit is applied.
+        auto priv = SurfaceInterfacePrivate::get(m_surface);
+        priv->pending->pointerLockRegion.reset();
+        priv->pending->committed |= SurfaceState::Field::PointerLockRegion;
+        priv->lockedPointer = nullptr;
     }
 }
 
 void LockedPointerV1InterfacePrivate::zwp_locked_pointer_v1_destroy_resource(Resource *resource)
 {
-    Q_EMIT q->aboutToBeDestroyed();
     delete q;
 }
 
@@ -179,45 +168,41 @@ void LockedPointerV1InterfacePrivate::zwp_locked_pointer_v1_destroy(Resource *re
 
 void LockedPointerV1InterfacePrivate::zwp_locked_pointer_v1_set_cursor_position_hint(Resource *resource, wl_fixed_t surface_x, wl_fixed_t surface_y)
 {
-    pending->hint = QPointF(wl_fixed_to_double(surface_x), wl_fixed_to_double(surface_y));
+    if (Q_UNLIKELY(!m_surface)) {
+        return;
+    }
+    auto priv = SurfaceInterfacePrivate::get(m_surface);
+    priv->pending->pointerLockHint = QPointF(wl_fixed_to_double(surface_x), wl_fixed_to_double(surface_y)) / m_surface->clientToCompositorScale() / m_surface->serverScale();
+    priv->pending->committed |= SurfaceState::Field::PointerLockHint;
 }
 
 void LockedPointerV1InterfacePrivate::zwp_locked_pointer_v1_set_region(Resource *resource, ::wl_resource *region_resource)
 {
-    if (Q_UNLIKELY(!surface)) {
+    if (Q_UNLIKELY(!m_surface)) {
         return;
     }
-    pending->region = regionFromResource(region_resource).scaled(1.0 / surface->clientToCompositorScale());
+    auto priv = SurfaceInterfacePrivate::get(m_surface);
+    priv->pending->pointerLockRegion = regionFromResource(region_resource).scaled(1.0 / m_surface->clientToCompositorScale() / m_surface->serverScale());
+    priv->pending->committed |= SurfaceState::Field::PointerLockRegion;
 }
 
 LockedPointerV1Interface::LockedPointerV1Interface(SurfaceInterface *surface,
-                                                   LifeTime lifeTime,
+                                                   PointerConstraintLifetime lifeTime,
                                                    const RegionF &region,
                                                    ::wl_resource *resource)
     : d(new LockedPointerV1InterfacePrivate(this, surface, lifeTime, region, resource))
 {
-    SurfaceInterfacePrivate::get(surface)->installPointerConstraint(this);
 }
 
 LockedPointerV1Interface::~LockedPointerV1Interface()
 {
 }
 
-LockedPointerV1Interface::LifeTime LockedPointerV1Interface::lifeTime() const
+PointerConstraintLifetime LockedPointerV1Interface::lifeTime() const
 {
     return d->lifeTime;
 }
 
-RegionF LockedPointerV1Interface::region() const
-{
-    return d->effectiveRegion;
-}
-
-QPointF LockedPointerV1Interface::cursorPositionHint() const
-{
-    return d->hint;
-}
-
 bool LockedPointerV1Interface::isLocked() const
 {
     return d->isLocked;
@@ -229,7 +214,7 @@ void LockedPointerV1Interface::setLocked(bool locked)
         return;
     }
     if (!locked) {
-        d->hint = QPointF(-1, -1);
+        SurfaceInterfacePrivate::get(d->m_surface)->pending->pointerLockHint.reset();
     }
     d->isLocked = locked;
     if (d->isLocked) {
@@ -238,6 +223,9 @@ void LockedPointerV1Interface::setLocked(bool locked)
         d->send_unlocked();
     }
     Q_EMIT lockedChanged();
+    if (!locked && d->lifeTime == PointerConstraintLifetime::OneShot) {
+        delete this;
+    }
 }
 
 ConfinedPointerV1InterfacePrivate *ConfinedPointerV1InterfacePrivate::get(ConfinedPointerV1Interface *q)
@@ -247,36 +235,28 @@ ConfinedPointerV1InterfacePrivate *ConfinedPointerV1InterfacePrivate::get(Confin
 
 ConfinedPointerV1InterfacePrivate::ConfinedPointerV1InterfacePrivate(ConfinedPointerV1Interface *q,
                                                                      SurfaceInterface *surface,
-                                                                     ConfinedPointerV1Interface::LifeTime lifeTime,
+                                                                     PointerConstraintLifetime lifeTime,
                                                                      const RegionF &region,
                                                                      ::wl_resource *resource)
     : QtWaylandServer::zwp_confined_pointer_v1(resource)
-    , SurfaceExtension(surface)
     , q(q)
     , lifeTime(lifeTime)
+    , m_surface(surface)
 {
-    pending->region = region;
-
-    apply(pending);
-
-    *pending = ConfinedPointerV1Commit{};
+    auto priv = SurfaceInterfacePrivate::get(surface);
+    priv->pending->pointerConfinementRegion = region;
+    priv->pending->confinementLifetime = lifeTime;
+    priv->pending->committed |= SurfaceState::Field::PointerConfinementRegion;
+    priv->confinedPointer = q;
 }
 
-void ConfinedPointerV1InterfacePrivate::apply(ConfinedPointerV1Commit *commit)
+ConfinedPointerV1InterfacePrivate::~ConfinedPointerV1InterfacePrivate()
 {
-    const RegionF oldRegion = effectiveRegion;
-
-    if (commit->region.has_value()) {
-        region = commit->region->scaled(1.0 / surface->serverScale());
-    }
-
-    effectiveRegion = surface->input();
-    if (!region.isEmpty()) {
-        effectiveRegion &= region;
-    }
-
-    if (oldRegion != effectiveRegion) {
-        Q_EMIT q->regionChanged();
+    if (m_surface) {
+        auto priv = SurfaceInterfacePrivate::get(m_surface);
+        priv->pending->pointerConfinementRegion.reset();
+        priv->pending->committed |= SurfaceState::Field::PointerConfinementRegion;
+        priv->confinedPointer = nullptr;
     }
 }
 
@@ -292,35 +272,31 @@ void ConfinedPointerV1InterfacePrivate::zwp_confined_pointer_v1_destroy(Resource
 
 void ConfinedPointerV1InterfacePrivate::zwp_confined_pointer_v1_set_region(Resource *resource, ::wl_resource *region_resource)
 {
-    if (Q_UNLIKELY(!surface)) {
+    if (Q_UNLIKELY(!m_surface)) {
         return;
     }
-    pending->region = regionFromResource(region_resource).scaled(1.0 / surface->clientToCompositorScale());
+    auto priv = SurfaceInterfacePrivate::get(m_surface);
+    priv->pending->pointerConfinementRegion = regionFromResource(region_resource).scaled(1.0 / m_surface->clientToCompositorScale() / m_surface->serverScale());
+    priv->pending->committed |= SurfaceState::Field::PointerConfinementRegion;
 }
 
 ConfinedPointerV1Interface::ConfinedPointerV1Interface(SurfaceInterface *surface,
-                                                       LifeTime lifeTime,
+                                                       PointerConstraintLifetime lifeTime,
                                                        const RegionF &region,
                                                        ::wl_resource *resource)
     : d(new ConfinedPointerV1InterfacePrivate(this, surface, lifeTime, region, resource))
 {
-    SurfaceInterfacePrivate::get(surface)->installPointerConstraint(this);
 }
 
 ConfinedPointerV1Interface::~ConfinedPointerV1Interface()
 {
 }
 
-ConfinedPointerV1Interface::LifeTime ConfinedPointerV1Interface::lifeTime() const
+PointerConstraintLifetime ConfinedPointerV1Interface::lifeTime() const
 {
     return d->lifeTime;
 }
 
-RegionF ConfinedPointerV1Interface::region() const
-{
-    return d->effectiveRegion;
-}
-
 bool ConfinedPointerV1Interface::isConfined() const
 {
     return d->isConfined;
@@ -338,6 +314,9 @@ void ConfinedPointerV1Interface::setConfined(bool confined)
         d->send_unconfined();
     }
     Q_EMIT confinedChanged();
+    if (!confined && d->lifeTime == PointerConstraintLifetime::OneShot) {
+        delete this;
+    }
 }
 
 } // namespace KWin
diff --git a/src/wayland/pointerconstraints_v1.h b/src/wayland/pointerconstraints_v1.h
index f9873a6c3f0..94df8c52664 100644
--- a/src/wayland/pointerconstraints_v1.h
+++ b/src/wayland/pointerconstraints_v1.h
@@ -9,6 +9,7 @@
 
 #include "core/region.h"
 #include "kwin_export.h"
+#include "wayland/surface.h"
 
 #include <QObject>
 #include <memory>
@@ -65,42 +66,7 @@ class KWIN_EXPORT LockedPointerV1Interface : public QObject
 public:
     ~LockedPointerV1Interface() override;
 
-    enum class LifeTime : uint {
-        OneShot = 1,
-        Persistent = 2,
-    };
-
-    LifeTime lifeTime() const;
-
-    /**
-     * The intersection of this region and the input region of the SurfaceInterface is used
-     * to determine where the pointer must be in order for the lock to activate.
-     * It is up to the compositor whether to warp the pointer or require some kind of
-     * user interaction for the lock to activate.
-     *
-     * If the region is empty the SurfaceInterface input region is used.
-     *
-     * @see regionChanged
-     * @see SurfaceInterface::input
-     */
-    RegionF region() const;
-
-    /**
-     * Indicates where the mouse cursor should be positioned after it has been unlocked again.
-     * The compositor can warp the cursor at this moment to the position. For that it
-     * will not Q_EMIT any relative motion events. The hint is relative to the top-left
-     * corner of the surface the lock was applied to. Only non-negative x and y values
-     * are allowed. Otherwise the hint is invalid and should be ignored by the compositor.
-     *
-     * In case the client never set the hint, an invalid one will be returned.
-     *
-     * This function should be called when the compositor decides to break the lock or the
-     * client unbinds the resource. To set the position in this case the compositor should
-     * call this function when the aboutToBeUnbound signal has been emitted.
-     *
-     * @see cursorPositionHintChanged
-     */
-    QPointF cursorPositionHint() const;
+    PointerConstraintLifetime lifeTime() const;
 
     /**
      * Whether the Compositor set this pointer lock to be active.
@@ -125,24 +91,6 @@ public:
     void setLocked(bool locked);
 
 Q_SIGNALS:
-    /**
-     * This is signal is emitted when the locked pointer is about to be destroyed.
-     */
-    void aboutToBeDestroyed();
-
-    /**
-     * Emitted whenever the region changes.
-     * This happens when the parent SurfaceInterface gets committed
-     * @see region
-     */
-    void regionChanged();
-
-    /**
-     * Emitted whenever the cursor position hint changes.
-     * This happens when the parent SurfaceInterface gets committed
-     * @see cursorPositionHint
-     */
-    void cursorPositionHintChanged();
 
     /**
      * Emitted whenever the {@link isLocked} state changes.
@@ -152,7 +100,7 @@ Q_SIGNALS:
     void lockedChanged();
 
 private:
-    LockedPointerV1Interface(SurfaceInterface *surface, LifeTime lifeTime, const RegionF &region, ::wl_resource *resource);
+    LockedPointerV1Interface(SurfaceInterface *surface, PointerConstraintLifetime lifeTime, const RegionF &region, ::wl_resource *resource);
     std::unique_ptr<LockedPointerV1InterfacePrivate> d;
     friend class LockedPointerV1InterfacePrivate;
     friend class PointerConstraintsV1InterfacePrivate;
@@ -181,25 +129,7 @@ class KWIN_EXPORT ConfinedPointerV1Interface : public QObject
 public:
     ~ConfinedPointerV1Interface() override;
 
-    enum class LifeTime : uint {
-        OneShot = 1,
-        Persistent = 2,
-    };
-
-    LifeTime lifeTime() const;
-
-    /**
-     * The intersection of this region and the input region of the SurfaceInterface is used
-     * to determine where the pointer must be in order for the confinement to activate.
-     * It is up to the compositor whether to warp the pointer or require some kind of
-     * user interaction for the confinement to activate.
-     *
-     * If the region is empty the SurfaceInterface input region is used.
-     *
-     * @see regionChanged
-     * @see SurfaceInterface::input
-     */
-    RegionF region() const;
+    PointerConstraintLifetime lifeTime() const;
 
     /**
      * Whether the Compositor set this pointer confinement to be active.
@@ -222,12 +152,6 @@ public:
     void setConfined(bool confined);
 
 Q_SIGNALS:
-    /**
-     * Emitted whenever the region changes.
-     * This happens when the parent SurfaceInterface gets committed
-     * @see region
-     */
-    void regionChanged();
 
     /**
      * Emitted whenever the {@link isConfined} state changes.
@@ -237,7 +161,7 @@ Q_SIGNALS:
     void confinedChanged();
 
 private:
-    ConfinedPointerV1Interface(SurfaceInterface *surface, LifeTime lifeTime, const RegionF &region, ::wl_resource *resource);
+    ConfinedPointerV1Interface(SurfaceInterface *surface, PointerConstraintLifetime lifeTime, const RegionF &region, ::wl_resource *resource);
     std::unique_ptr<ConfinedPointerV1InterfacePrivate> d;
     friend class ConfinedPointerV1InterfacePrivate;
     friend class PointerConstraintsV1InterfacePrivate;
diff --git a/src/wayland/pointerconstraints_v1_p.h b/src/wayland/pointerconstraints_v1_p.h
index 1aeefffd05c..fb160aa5d8d 100644
--- a/src/wayland/pointerconstraints_v1_p.h
+++ b/src/wayland/pointerconstraints_v1_p.h
@@ -9,6 +9,7 @@
 
 #include "pointerconstraints_v1.h"
 #include "surface.h"
+#include "surface_p.h"
 
 #include <QPointer>
 
@@ -38,28 +39,18 @@ protected:
     void zwp_pointer_constraints_v1_destroy(Resource *resource) override;
 };
 
-class LockedPointerV1Commit : public SurfaceAttachedState<LockedPointerV1Commit>
-{
-public:
-    std::optional<RegionF> region;
-    std::optional<QPointF> hint;
-};
-
-class LockedPointerV1InterfacePrivate final : public QtWaylandServer::zwp_locked_pointer_v1, public SurfaceExtension<LockedPointerV1InterfacePrivate, LockedPointerV1Commit>
+class LockedPointerV1InterfacePrivate : public QtWaylandServer::zwp_locked_pointer_v1
 {
 public:
     static LockedPointerV1InterfacePrivate *get(LockedPointerV1Interface *pointer);
 
-    LockedPointerV1InterfacePrivate(LockedPointerV1Interface *q, SurfaceInterface *surface, LockedPointerV1Interface::LifeTime lifeTime, const RegionF &region, ::wl_resource *resource);
-
-    void apply(LockedPointerV1Commit *commit);
+    LockedPointerV1InterfacePrivate(LockedPointerV1Interface *q, SurfaceInterface *surface, PointerConstraintLifetime lifeTime, const RegionF &region, ::wl_resource *resource);
+    ~LockedPointerV1InterfacePrivate() override;
 
     LockedPointerV1Interface *q;
-    LockedPointerV1Interface::LifeTime lifeTime;
-    RegionF effectiveRegion;
-    RegionF region;
-    QPointF hint = QPointF(-1, -1);
+    PointerConstraintLifetime lifeTime;
     bool isLocked = false;
+    QPointer<SurfaceInterface> m_surface;
 
 protected:
     void zwp_locked_pointer_v1_destroy_resource(Resource *resource) override;
@@ -68,30 +59,22 @@ protected:
     void zwp_locked_pointer_v1_set_region(Resource *resource, struct ::wl_resource *region_resource) override;
 };
 
-class ConfinedPointerV1Commit : public SurfaceAttachedState<ConfinedPointerV1Commit>
-{
-public:
-    std::optional<RegionF> region;
-};
-
-class ConfinedPointerV1InterfacePrivate final : public QtWaylandServer::zwp_confined_pointer_v1, public SurfaceExtension<ConfinedPointerV1InterfacePrivate, ConfinedPointerV1Commit>
+class ConfinedPointerV1InterfacePrivate : public QtWaylandServer::zwp_confined_pointer_v1
 {
 public:
     static ConfinedPointerV1InterfacePrivate *get(ConfinedPointerV1Interface *pointer);
 
     ConfinedPointerV1InterfacePrivate(ConfinedPointerV1Interface *q,
                                       SurfaceInterface *surface,
-                                      ConfinedPointerV1Interface::LifeTime lifeTime,
+                                      PointerConstraintLifetime lifeTime,
                                       const RegionF &region,
                                       ::wl_resource *resource);
-
-    void apply(ConfinedPointerV1Commit *commit);
+    ~ConfinedPointerV1InterfacePrivate() override;
 
     ConfinedPointerV1Interface *q;
-    ConfinedPointerV1Interface::LifeTime lifeTime;
-    RegionF effectiveRegion;
-    RegionF region;
+    PointerConstraintLifetime lifeTime;
     bool isConfined = false;
+    QPointer<SurfaceInterface> m_surface;
 
 protected:
     void zwp_confined_pointer_v1_destroy_resource(Resource *resource) override;
diff --git a/src/wayland/surface.cpp b/src/wayland/surface.cpp
index 4642266d305..ec97522753f 100644
--- a/src/wayland/surface.cpp
+++ b/src/wayland/surface.cpp
@@ -185,62 +185,6 @@ void SurfaceInterfacePrivate::setSlide(const QPointer<SlideInterface> &slide)
     pending->committed |= SurfaceState::Field::Slide;
 }
 
-void SurfaceInterfacePrivate::installPointerConstraint(LockedPointerV1Interface *lock)
-{
-    Q_ASSERT(!lockedPointer);
-    Q_ASSERT(!confinedPointer);
-
-    lockedPointer = lock;
-
-    auto cleanUp = [this]() {
-        lockedPointer = nullptr;
-        QObject::disconnect(constrainsOneShotConnection);
-        constrainsOneShotConnection = QMetaObject::Connection();
-        QObject::disconnect(constrainsUnboundConnection);
-        constrainsUnboundConnection = QMetaObject::Connection();
-        Q_EMIT q->pointerConstraintsChanged();
-    };
-
-    if (lock->lifeTime() == LockedPointerV1Interface::LifeTime::OneShot) {
-        constrainsOneShotConnection = QObject::connect(lock, &LockedPointerV1Interface::lockedChanged, q, [this, cleanUp] {
-            if (lockedPointer->isLocked()) {
-                return;
-            }
-            cleanUp();
-        });
-    }
-    constrainsUnboundConnection = QObject::connect(lock, &LockedPointerV1Interface::destroyed, q, cleanUp);
-    Q_EMIT q->pointerConstraintsChanged();
-}
-
-void SurfaceInterfacePrivate::installPointerConstraint(ConfinedPointerV1Interface *confinement)
-{
-    Q_ASSERT(!lockedPointer);
-    Q_ASSERT(!confinedPointer);
-
-    confinedPointer = confinement;
-
-    auto cleanUp = [this]() {
-        confinedPointer = nullptr;
-        QObject::disconnect(constrainsOneShotConnection);
-        constrainsOneShotConnection = QMetaObject::Connection();
-        QObject::disconnect(constrainsUnboundConnection);
-        constrainsUnboundConnection = QMetaObject::Connection();
-        Q_EMIT q->pointerConstraintsChanged();
-    };
-
-    if (confinement->lifeTime() == ConfinedPointerV1Interface::LifeTime::OneShot) {
-        constrainsOneShotConnection = QObject::connect(confinement, &ConfinedPointerV1Interface::confinedChanged, q, [this, cleanUp] {
-            if (confinedPointer->isConfined()) {
-                return;
-            }
-            cleanUp();
-        });
-    }
-    constrainsUnboundConnection = QObject::connect(confinement, &ConfinedPointerV1Interface::destroyed, q, cleanUp);
-    Q_EMIT q->pointerConstraintsChanged();
-}
-
 void SurfaceInterfacePrivate::recursivelyEmitIdleInhibitChanged()
 {
     Q_EMIT q->inhibitsIdleChanged();
@@ -647,6 +591,15 @@ void SurfaceState::mergeInto(SurfaceState *target)
     target->range = range;
     target->presentationFeedback = std::move(presentationFeedback);
     target->blurRegion = blurRegion;
+    if (committed & SurfaceState::Field::PointerConfinementRegion) {
+        target->pointerConfinementRegion = pointerConfinementRegion;
+        target->confinementLifetime = confinementLifetime;
+    }
+    if (committed & SurfaceState::Field::PointerLockRegion) {
+        target->pointerLockRegion = pointerLockRegion;
+        target->confinementLifetime = confinementLifetime;
+    }
+    target->pointerLockHint = pointerLockHint;
 
     auto previousExtensions = std::exchange(target->extensions, {});
     for (const auto &[extension, sourceState] : extensions) {
@@ -681,10 +634,14 @@ void SurfaceInterfacePrivate::applyState(SurfaceState *next)
     const bool bufferReleasePointChanged = (next->committed & SurfaceState::Field::Buffer) && current->releasePoint != next->releasePoint;
     const bool alphaMultiplierChanged = (next->committed & SurfaceState::Field::AlphaMultiplier);
     const bool yuvCoefficientsChanged = (next->committed & SurfaceState::Field::YuvCoefficients) && (current->yuvCoefficients != next->yuvCoefficients);
+    const bool pointerLockRegionChanged = (next->committed & SurfaceState::Field::PointerLockRegion) && (current->pointerLockRegion != next->pointerLockRegion);
+    const bool pointerConfinementRegionChanged = (next->committed & SurfaceState::Field::PointerConfinementRegion) && (current->pointerConfinementRegion != next->pointerConfinementRegion);
 
     const QSizeF oldSurfaceSize = surfaceSize;
     const RectF oldBufferSourceBox = bufferSourceBox;
     const RegionF oldInputRegion = inputRegion;
+    const std::optional<RegionF> oldEffectivePointerLock = effectivePointerLock;
+    const std::optional<RegionF> oldEffectivePointerConfinement = effectivePointerConfinement;
 
     next->mergeInto(current.get());
     bufferRef = current->buffer;
@@ -738,10 +695,30 @@ void SurfaceInterfacePrivate::applyState(SurfaceState *next)
         fifoFallbackTimer.start();
     }
 
+    const bool inputRegionChanged = oldInputRegion != inputRegion;
+    if (inputRegionChanged || pointerLockRegionChanged) {
+        effectivePointerLock = current->pointerLockRegion.transform([this](const RegionF &region) {
+            if (region.isEmpty()) {
+                return inputRegion;
+            } else {
+                return region & inputRegion;
+            }
+        });
+    }
+    if (inputRegionChanged || pointerConfinementRegionChanged) {
+        effectivePointerConfinement = current->pointerConfinementRegion.transform([this](const RegionF &region) {
+            if (region.isEmpty()) {
+                return inputRegion;
+            } else {
+                return region & inputRegion;
+            }
+        });
+    }
+
     if (opaqueRegionChanged) {
         Q_EMIT q->opaqueChanged(opaqueRegion);
     }
-    if (oldInputRegion != inputRegion) {
+    if (inputRegionChanged) {
         Q_EMIT q->inputChanged(inputRegion);
     }
     if (transformChanged) {
@@ -784,6 +761,12 @@ void SurfaceInterfacePrivate::applyState(SurfaceState *next)
     if (!bufferDamage.isEmpty()) {
         Q_EMIT q->damaged(bufferDamage);
     }
+    if (oldEffectivePointerLock != effectivePointerLock) {
+        Q_EMIT q->lockedPointerRegionChanged();
+    }
+    if (oldEffectivePointerConfinement != effectivePointerConfinement) {
+        Q_EMIT q->confinedPointerRegionChanged();
+    }
 
     // The position of a sub-surface is applied when its parent is committed.
     for (SubSurfaceInterface *subsurface : std::as_const(current->subsurface.below)) {
@@ -1339,6 +1322,45 @@ bool SurfaceInterface::hasFifoBarrier() const
     return d->current->fifoBarrier;
 }
 
+std::optional<RegionF> SurfaceInterface::confinedPointerRegion() const
+{
+    return d->effectivePointerConfinement;
+}
+
+std::optional<RegionF> SurfaceInterface::lockedPointerRegion() const
+{
+    return d->effectivePointerLock;
+}
+
+std::optional<QPointF> SurfaceInterface::lockedPointerHint() const
+{
+    return d->current->pointerLockHint;
+}
+
+void SurfaceInterface::setPointerConfined(bool confined)
+{
+    if (!confined && d->current->confinementLifetime == PointerConstraintLifetime::OneShot) {
+        // oneshot confinement has to be reset even if the confinement object is already deleted
+        d->current->pointerConfinementRegion.reset();
+        d->effectivePointerConfinement.reset();
+    }
+    if (d->confinedPointer) {
+        d->confinedPointer->setConfined(confined);
+    }
+}
+
+void SurfaceInterface::setPointerLocked(bool locked)
+{
+    if (!locked && d->current->confinementLifetime == PointerConstraintLifetime::OneShot) {
+        // oneshot confinement has to be reset even if the confinement object is already deleted
+        d->current->pointerLockRegion.reset();
+        d->effectivePointerLock.reset();
+    }
+    if (d->lockedPointer) {
+        d->lockedPointer->setLocked(locked);
+    }
+}
+
 } // namespace KWin
 
 #include "moc_surface.cpp"
diff --git a/src/wayland/surface.h b/src/wayland/surface.h
index 86e8ca1c0b7..481d7f05c9c 100644
--- a/src/wayland/surface.h
+++ b/src/wayland/surface.h
@@ -38,6 +38,11 @@ enum class ColorDescriptionType {
     Windows,
 };
 
+enum class PointerConstraintLifetime : uint {
+    OneShot = 1,
+    Persistent = 2,
+};
+
 /**
  * The SurfaceRole class represents a role assigned to a wayland surface.
  */
@@ -385,6 +390,12 @@ public:
 
     QPointF mapToMainSurface(const QPointF &localPoint) const;
 
+    std::optional<RegionF> confinedPointerRegion() const;
+    std::optional<RegionF> lockedPointerRegion() const;
+    std::optional<QPointF> lockedPointerHint() const;
+    void setPointerConfined(bool confined);
+    void setPointerLocked(bool locked);
+
 Q_SIGNALS:
     /**
      * This signal is emitted when the underlying wl_surface resource is about to be freed.
@@ -441,16 +452,8 @@ Q_SIGNALS:
      */
     void childSubSurfacesChanged();
 
-    /**
-     * Emitted whenever a pointer constraint get (un)installed on this SurfaceInterface.
-     *
-     * The pointer constraint does not get activated, the compositor needs to activate
-     * the lock/confinement.
-     *
-     * @see confinedPointer
-     * @see lockedPointer
-     */
-    void pointerConstraintsChanged();
+    void confinedPointerRegionChanged();
+    void lockedPointerRegionChanged();
 
     /**
      * Emitted whenever the SurfaceInterface starts/ends to inhibit idle.
diff --git a/src/wayland/surface_p.h b/src/wayland/surface_p.h
index 818122e727e..c1208969a87 100644
--- a/src/wayland/surface_p.h
+++ b/src/wayland/surface_p.h
@@ -66,6 +66,9 @@ struct SurfaceState
         YuvCoefficients = 1 << 15,
         SourceGeometry = 1 << 16,
         DestinationSize = 1 << 17,
+        PointerLockHint = 1 << 18,
+        PointerLockRegion = 1 << 19,
+        PointerConfinementRegion = 1 << 20,
     };
     Q_DECLARE_FLAGS(Fields, Field)
 
@@ -101,6 +104,10 @@ struct SurfaceState
     bool fifoBarrier = false;
     bool hasFifoWaitCondition = false;
     RegionF blurRegion;
+    std::optional<QPointF> pointerLockHint;
+    std::optional<RegionF> pointerLockRegion;
+    std::optional<RegionF> pointerConfinementRegion;
+    PointerConstraintLifetime confinementLifetime = PointerConstraintLifetime::Persistent;
 
     struct
     {
@@ -139,8 +146,6 @@ public:
     bool lowerChild(SubSurfaceInterface *subsurface, SurfaceInterface *anchor);
     void setShadow(const QPointer<ShadowInterface> &shadow);
     void setSlide(const QPointer<SlideInterface> &slide);
-    void installPointerConstraint(LockedPointerV1Interface *lock);
-    void installPointerConstraint(ConfinedPointerV1Interface *confinement);
     void installIdleInhibitor(IdleInhibitorV1Interface *inhibitor);
     void removeIdleInhibitor(IdleInhibitorV1Interface *inhibitor);
     void recursivelyEmitIdleInhibitChanged();
@@ -168,6 +173,8 @@ public:
     QSizeF surfaceSize = QSizeF(0, 0);
 
     RegionF inputRegion;
+    std::optional<RegionF> effectivePointerLock;
+    std::optional<RegionF> effectivePointerConfinement;
     RegionF opaqueRegion;
     GraphicsBufferRef bufferRef;
     Region bufferDamage;
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.