[plasma/kwin] /: Improve picture-in-picture positioning
Vlad Zahorodnii <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit c28e4338ee1b8694dec595a01b7c52720c9c8c7a by Vlad Zahorodnii.
Committed on 03/08/2026 at 13:24.
Pushed by vladz into branch 'master'.
Improve picture-in-picture positioning
This makes picture-in-picture windows grow or shrink relative to the
geometry center.
If a picture-in-picture window gets offscreen, it will be moved so it is
still inside the work area. If the picture-in-picture had already been
partially outside the work area (for example because the user moved it
partially offscreen), it won't be moved back inside the work area.
The generic geometry code path in the XdgSurfaceWindow is no longer
suitable for constraining picture-in-picture windows so the geometry
calculation step has been moved to subtypes, i.e. XdgToplevelWindow,
XdgPopupWindow, and XXPipV1Window.
The picture-in-picture windows use the centered gravity. The
xdg-toplevel windows should also use centered gravity. That didn't go
as planned though, some tests started failing and I'm not sure what
specifically needs to be done fix them. The xdg-toplevel windows should
be tackled in a second attempt.
M +1 -1 src/window.cpp
M +1 -0 src/window.h
M +56 -69 src/xdgshellwindow.cpp
M +5 -11 src/xdgshellwindow.h
M +97 -0 src/xxpipv1window.cpp
M +3 -0 src/xxpipv1window.h
M +6 -0 tests/pip/pip.cpp
https://invent.kde.org/plasma/kwin/-/commit/c28e4338ee1b8694dec595a01b7c52720c9c8c7a
diff --git a/src/window.cpp b/src/window.cpp
index f551106130d..3083db7740b 100644
--- a/src/window.cpp
+++ b/src/window.cpp
@@ -1370,7 +1370,7 @@ static RegionF interactiveMoveResizeVisibleSubrectRegion(const RectF &geometry,
*
* See doc/moveresizerestriction for more details on algorithm.
*/
-static std::optional<QPointF> confineInteractiveMove(const RectF &geometry, const QSizeF &minVisibleArea)
+std::optional<QPointF> Window::confineInteractiveMove(const RectF &geometry, const QSizeF &minVisibleArea) const
{
std::optional<QPointF> candidate;
qreal bestScore;
diff --git a/src/window.h b/src/window.h
index fc419ba3777..d767a69fe85 100644
--- a/src/window.h
+++ b/src/window.h
@@ -1745,6 +1745,7 @@ protected:
QSizeF minVisibleArea() const;
RectF nextInteractiveMoveGeometry(const RectF &rect) const;
RectF nextInteractiveResizeGeometry(const QPointF &global) const;
+ std::optional<QPointF> confineInteractiveMove(const RectF &geometry, const QSizeF &minVisibleArea) const;
std::optional<QPointF> confineInteractiveResize(const RectF &geometry, Gravity gravity, const QSizeF &minVisibleArea) const;
void dontInteractiveMoveResize();
diff --git a/src/xdgshellwindow.cpp b/src/xdgshellwindow.cpp
index d6c39cca58e..60c789462ca 100644
--- a/src/xdgshellwindow.cpp
+++ b/src/xdgshellwindow.cpp
@@ -57,8 +57,6 @@ XdgSurfaceWindow::XdgSurfaceWindow(XdgSurfaceInterface *shellSurface)
this, &XdgSurfaceWindow::destroyWindow);
connect(shellSurface->surface(), &SurfaceInterface::aboutToBeDestroyed,
this, &XdgSurfaceWindow::destroyWindow);
- connect(shellSurface, &XdgSurfaceInterface::windowGeometryChanged,
- this, &XdgSurfaceWindow::setHaveNextWindowGeometry);
// Configure events are not sent immediately, but rather scheduled to be sent when the event
// loop is about to be idle. By doing this, we can avoid sending configure events that do
@@ -91,16 +89,7 @@ void XdgSurfaceWindow::scheduleConfigure()
void XdgSurfaceWindow::sendConfigure()
{
- XdgSurfaceConfigure *configureEvent = sendRoleConfigure();
-
- configureEvent->gravity = m_nextGravity;
- configureEvent->scale = m_nextTargetScale;
-
- if (!isInteractiveMoveResize()) {
- m_nextGravity = Gravity::BottomRight;
- }
-
- m_configureEvents.append(configureEvent);
+ m_configureEvents.append(sendRoleConfigure());
}
void XdgSurfaceWindow::handleConfigureAcknowledged(quint32 serial)
@@ -128,10 +117,8 @@ void XdgSurfaceWindow::handleCommit()
}
}
- handleRolePrecommit();
- if (haveNextWindowGeometry()) {
- handleNextWindowGeometry();
- resetHaveNextWindowGeometry();
+ if (const XdgSurfaceConfigure *configureEvent = lastAcknowledgedConfigure()) {
+ setTargetScale(configureEvent->scale);
}
handleRoleCommit();
@@ -141,59 +128,10 @@ void XdgSurfaceWindow::handleCommit()
markAsMapped();
}
-void XdgSurfaceWindow::handleRolePrecommit()
-{
-}
-
void XdgSurfaceWindow::handleRoleCommit()
{
}
-void XdgSurfaceWindow::handleNextWindowGeometry()
-{
- if (const XdgSurfaceConfigure *configureEvent = lastAcknowledgedConfigure()) {
- setTargetScale(configureEvent->scale);
- }
-
- m_windowGeometry = snapToPixels(m_shellSurface->windowGeometry(), targetScale());
-
- RectF frameGeometry(pos(), clientSizeToFrameSize(m_windowGeometry.size()));
- if (const XdgSurfaceConfigure *configureEvent = lastAcknowledgedConfigure()) {
- frameGeometry = configureEvent->gravity.apply(frameGeometry, configureEvent->bounds);
- }
-
- if (isInteractiveMove()) {
- bool fullscreen = isFullScreen();
- if (const auto configureEvent = static_cast<XdgToplevelConfigure *>(lastAcknowledgedConfigure())) {
- fullscreen = configureEvent->states & XdgToplevelInterface::State::FullScreen;
- }
- if (!fullscreen) {
- frameGeometry = nextInteractiveMoveGeometry(frameGeometry);
- }
- }
-
- if (!m_configureTimer->isActive() && m_configureEvents.isEmpty()) {
- setMoveResizeGeometry(frameGeometry);
- }
-
- updateGeometry(frameGeometry);
-}
-
-bool XdgSurfaceWindow::haveNextWindowGeometry() const
-{
- return m_haveNextWindowGeometry || m_lastAcknowledgedConfigure;
-}
-
-void XdgSurfaceWindow::setHaveNextWindowGeometry()
-{
- m_haveNextWindowGeometry = true;
-}
-
-void XdgSurfaceWindow::resetHaveNextWindowGeometry()
-{
- m_haveNextWindowGeometry = false;
-}
-
void XdgSurfaceWindow::moveResizeInternal(const RectF &rect, MoveResizeMode mode)
{
Q_EMIT frameGeometryAboutToChange();
@@ -761,11 +699,17 @@ XdgSurfaceConfigure *XdgToplevelWindow::sendRoleConfigure()
configureEvent->decorationState = m_nextDecorationState;
configureEvent->serial = serial;
configureEvent->tile = m_requestedTile;
+ configureEvent->gravity = m_nextGravity;
+ configureEvent->scale = m_nextTargetScale;
+
+ if (!isInteractiveMoveResize()) {
+ m_nextGravity = Gravity::BottomRight;
+ }
return configureEvent;
}
-void XdgToplevelWindow::handleRolePrecommit()
+void XdgToplevelWindow::handleRoleCommit()
{
if (auto configureEvent = static_cast<XdgToplevelConfigure *>(lastAcknowledgedConfigure())) {
if (configureEvent->decoration) {
@@ -777,14 +721,43 @@ void XdgToplevelWindow::handleRolePrecommit()
updateShadow();
}
}
-}
-void XdgToplevelWindow::handleRoleCommit()
-{
+ const auto oldWindowGeometry = m_windowGeometry;
+ m_windowGeometry = snapToPixels(m_shellSurface->xdgSurface()->windowGeometry(), targetScale());
+
+ RectF frameGeometry(pos(), clientSizeToFrameSize(m_windowGeometry.size()));
+ if (isInteractiveMove()) {
+ bool fullscreen = isFullScreen();
+ if (const auto configureEvent = static_cast<XdgToplevelConfigure *>(lastAcknowledgedConfigure())) {
+ fullscreen = configureEvent->states & XdgToplevelInterface::State::FullScreen;
+ }
+ if (!fullscreen) {
+ frameGeometry = nextInteractiveMoveGeometry(frameGeometry);
+ }
+ } else {
+ if (const XdgSurfaceConfigure *configureEvent = lastAcknowledgedConfigure()) {
+ frameGeometry = configureEvent->gravity.apply(frameGeometry, configureEvent->bounds);
+ } else if (oldWindowGeometry != m_windowGeometry) {
+ frameGeometry = m_gravity.apply(frameGeometry, m_frameGeometry);
+ }
+ }
+
+ if (!m_configureTimer->isActive() && m_configureEvents.isEmpty()) {
+ setMoveResizeGeometry(frameGeometry);
+ }
+
+ updateGeometry(frameGeometry);
+
auto configureEvent = static_cast<XdgToplevelConfigure *>(lastAcknowledgedConfigure());
if (configureEvent) {
handleStatesAcknowledged(configureEvent->states);
commitTile(configureEvent->tile);
+
+ if (!m_configureEvents.isEmpty()) {
+ m_gravity = configureEvent->gravity;
+ } else if (!isInteractiveResize()) {
+ m_gravity = Gravity::BottomRight;
+ }
}
}
@@ -1844,6 +1817,18 @@ XdgPopupWindow::XdgPopupWindow(XdgPopupInterface *shellSurface)
this, &XdgPopupWindow::destroyWindow);
}
+void XdgPopupWindow::handleRoleCommit()
+{
+ m_windowGeometry = snapToPixels(m_shellSurface->xdgSurface()->windowGeometry(), targetScale());
+
+ RectF frameGeometry(pos(), clientSizeToFrameSize(m_windowGeometry.size()));
+ if (const XdgSurfaceConfigure *configureEvent = lastAcknowledgedConfigure()) {
+ frameGeometry.moveTopLeft(configureEvent->bounds.topLeft());
+ }
+
+ updateGeometry(frameGeometry);
+}
+
void XdgPopupWindow::handleRoleDestroyed()
{
if (transientFor()) {
@@ -1976,6 +1961,8 @@ XdgSurfaceConfigure *XdgPopupWindow::sendRoleConfigure()
XdgSurfaceConfigure *configureEvent = new XdgSurfaceConfigure();
configureEvent->bounds = moveResizeGeometry();
configureEvent->serial = serial;
+ configureEvent->gravity = m_nextGravity;
+ configureEvent->scale = m_nextTargetScale;
return configureEvent;
}
diff --git a/src/xdgshellwindow.h b/src/xdgshellwindow.h
index 233e986e7f9..ac2c1e3d468 100644
--- a/src/xdgshellwindow.h
+++ b/src/xdgshellwindow.h
@@ -71,33 +71,27 @@ protected:
virtual XdgSurfaceConfigure *sendRoleConfigure() = 0;
virtual void handleRoleCommit();
- virtual void handleRolePrecommit();
virtual void handleRoleDestroyed();
XdgSurfaceConfigure *lastAcknowledgedConfigure() const;
void scheduleConfigure();
void sendConfigure();
+ void handleConfigureAcknowledged(quint32 serial);
+ void handleCommit();
+
QPointer<PlasmaShellSurfaceInterface> m_plasmaShellSurface;
WindowType m_windowType = WindowType::Normal;
+ Gravity m_gravity = Gravity::BottomRight;
Gravity m_nextGravity = Gravity::BottomRight;
-private:
- void handleConfigureAcknowledged(quint32 serial);
- void handleCommit();
- void handleNextWindowGeometry();
- bool haveNextWindowGeometry() const;
- void setHaveNextWindowGeometry();
- void resetHaveNextWindowGeometry();
-
XdgSurfaceInterface *m_shellSurface;
QTimer *m_configureTimer;
QQueue<XdgSurfaceConfigure *> m_configureEvents;
std::unique_ptr<XdgSurfaceConfigure> m_lastAcknowledgedConfigure;
std::optional<quint32> m_lastAcknowledgedConfigureSerial;
RectF m_windowGeometry;
- bool m_haveNextWindowGeometry = false;
};
class XdgToplevelConfigure final : public XdgSurfaceConfigure
@@ -185,7 +179,6 @@ public:
protected:
XdgSurfaceConfigure *sendRoleConfigure() override;
void handleRoleCommit() override;
- void handleRolePrecommit() override;
void handleRoleDestroyed() override;
void doMinimize() override;
void doSetActive() override;
@@ -296,6 +289,7 @@ public:
protected:
bool acceptsFocus() const override;
XdgSurfaceConfigure *sendRoleConfigure() override;
+ void handleRoleCommit() override;
void handleRoleDestroyed() override;
void doSetNextTargetScale() override;
void doSetPreferredBufferTransform() override;
diff --git a/src/xxpipv1window.cpp b/src/xxpipv1window.cpp
index f42e23da6d1..a25061b0785 100644
--- a/src/xxpipv1window.cpp
+++ b/src/xxpipv1window.cpp
@@ -5,6 +5,7 @@
*/
#include "xxpipv1window.h"
+#include "core/pixelgrid.h"
#include "input.h"
#include "wayland/seat.h"
#include "wayland/surface.h"
@@ -24,6 +25,9 @@ XXPipV1Window::XXPipV1Window(XXPipV1Interface *shellSurface)
setOnAllDesktops(true);
setOnAllActivities(true);
+ m_gravity = Gravity::Center;
+ m_nextGravity = Gravity::Center;
+
connect(shellSurface, &XXPipV1Interface::initializeRequested,
this, &XXPipV1Window::initialize);
connect(shellSurface, &XXPipV1Interface::aboutToBeDestroyed,
@@ -96,10 +100,88 @@ XdgSurfaceConfigure *XXPipV1Window::sendRoleConfigure()
XdgSurfaceConfigure *configureEvent = new XdgSurfaceConfigure();
configureEvent->bounds = moveResizeGeometry();
configureEvent->serial = m_shellSurface->sendConfigureSize(geometry.size());
+ configureEvent->gravity = m_nextGravity;
+ configureEvent->scale = m_nextTargetScale;
+
+ if (!isInteractiveMoveResize()) {
+ m_nextGravity = Gravity::Center;
+ }
return configureEvent;
}
+void XXPipV1Window::handleRoleCommit()
+{
+ const RectF oldWindowGeometry = m_windowGeometry;
+ m_windowGeometry = snapToPixels(m_shellSurface->xdgSurface()->windowGeometry(), targetScale());
+
+ RectF frameGeometry(pos(), clientSizeToFrameSize(m_windowGeometry.size()));
+ if (isInteractiveMove()) {
+ frameGeometry = nextInteractiveMoveGeometry(frameGeometry);
+ } else {
+ if (const XdgSurfaceConfigure *configureEvent = lastAcknowledgedConfigure()) {
+ frameGeometry = configureEvent->gravity.apply(frameGeometry, configureEvent->bounds);
+ if (const auto anchor = confineInteractiveMove(frameGeometry, minVisibleArea())) {
+ frameGeometry.moveTopLeft(*anchor);
+ }
+ } else if (oldWindowGeometry != m_windowGeometry) {
+ frameGeometry = m_gravity.apply(frameGeometry, m_frameGeometry);
+ if (const auto anchor = confineInteractiveMove(frameGeometry, minVisibleArea())) {
+ frameGeometry.moveTopLeft(*anchor);
+ }
+ }
+
+ const RectF safeArea = workspace()->clientArea(PlacementArea, this)
+ .shrunkBy(QMarginsF(options->pictureInPictureMargin(),
+ options->pictureInPictureMargin(),
+ options->pictureInPictureMargin(),
+ options->pictureInPictureMargin()));
+ const qreal snapDistance = options->pictureInPictureMargin() / 2.0;
+
+ if (m_frameGeometry.right() - safeArea.right() < snapDistance) {
+ if (frameGeometry.right() - safeArea.right() > -snapDistance
+ || m_frameGeometry.right() - safeArea.right() > -snapDistance) {
+ frameGeometry.moveRight(safeArea.right());
+ }
+ }
+
+ if (m_frameGeometry.left() - safeArea.left() > -snapDistance) {
+ if (frameGeometry.left() - safeArea.left() < snapDistance
+ || m_frameGeometry.left() - safeArea.left() < snapDistance) {
+ frameGeometry.moveLeft(safeArea.left());
+ }
+ }
+
+ if (m_frameGeometry.bottom() - safeArea.bottom() < snapDistance) {
+ if (frameGeometry.bottom() - safeArea.bottom() > -snapDistance
+ || m_frameGeometry.bottom() - safeArea.bottom() > -snapDistance) {
+ frameGeometry.moveBottom(safeArea.bottom());
+ }
+ }
+
+ if (m_frameGeometry.top() - safeArea.top() > -snapDistance) {
+ if (frameGeometry.top() - safeArea.top() < snapDistance
+ || m_frameGeometry.top() - safeArea.top() < snapDistance) {
+ frameGeometry.moveTop(safeArea.top());
+ }
+ }
+ }
+
+ if (!m_configureTimer->isActive() && m_configureEvents.isEmpty()) {
+ setMoveResizeGeometry(frameGeometry);
+ }
+
+ updateGeometry(frameGeometry);
+
+ if (const auto configureEvent = lastAcknowledgedConfigure()) {
+ if (!m_configureEvents.isEmpty()) {
+ m_gravity = configureEvent->gravity;
+ } else if (!isInteractiveResize()) {
+ m_gravity = Gravity::Center;
+ }
+ }
+}
+
void XXPipV1Window::handleRoleDestroyed()
{
m_shellSurface->disconnect(this);
@@ -171,4 +253,19 @@ void XXPipV1Window::doSetPreferredColorDescription()
}
}
+bool XXPipV1Window::doStartInteractiveMoveResize()
+{
+ if (interactiveMoveResizeGravity() != Gravity::Center) {
+ m_nextGravity = interactiveMoveResizeGravity();
+ scheduleConfigure();
+ }
+
+ return true;
+}
+
+void XXPipV1Window::doFinishInteractiveMoveResize()
+{
+ scheduleConfigure();
+}
+
} // namespace KWin
diff --git a/src/xxpipv1window.h b/src/xxpipv1window.h
index 09a9f2252a1..af695051309 100644
--- a/src/xxpipv1window.h
+++ b/src/xxpipv1window.h
@@ -30,10 +30,13 @@ public:
protected:
bool acceptsFocus() const override;
XdgSurfaceConfigure *sendRoleConfigure() override;
+ void handleRoleCommit() override;
void handleRoleDestroyed() override;
void doSetNextTargetScale() override;
void doSetPreferredBufferTransform() override;
void doSetPreferredColorDescription() override;
+ bool doStartInteractiveMoveResize() override;
+ void doFinishInteractiveMoveResize() override;
private:
void initialize();
diff --git a/tests/pip/pip.cpp b/tests/pip/pip.cpp
index ed14f5f98d4..6863a6c4d3d 100644
--- a/tests/pip/pip.cpp
+++ b/tests/pip/pip.cpp
@@ -209,6 +209,12 @@ void Pip::mousePressEvent(QMouseEvent *event)
event->accept();
windowHandle()->startSystemMove();
break;
+ case Qt::MiddleButton:
+ resize(width() * 1.5, height() * 1.5);
+ break;
+ case Qt::RightButton:
+ resize(width() * 0.5, height() * 0.5);
+ break;
default:
break;
}