From 677241efbba5f2a725b247baf86a822127571225 Mon Sep 17 00:00:00 2001 From: Vlad Zahorodnii Date: Thu, 16 Feb 2023 10:16:17 +0200 Subject: [PATCH 1/2] wayland: Implement xx-pip-v1 The xx-pip-v1 protocol provides clients a way to create floating windows with miniature contents, for example a video (a movie or a video call), a map with directions, a timer countdown, etc. These windows are placed in the overlay layer above all windows, including fullscreen windows. This is an experimental version of the protocol. --- CMakeLists.txt | 4 + src/CMakeLists.txt | 2 + src/placement.cpp | 18 ++ src/placement.h | 1 + src/wayland/CMakeLists.txt | 2 + src/wayland/protocols/xx-pip-v1.xml | 305 +++++++++++++++++++++++++++ src/wayland/xdgshell.cpp | 2 +- src/wayland/xdgshell_p.h | 2 + src/wayland/xxpip_v1.cpp | 312 ++++++++++++++++++++++++++++ src/wayland/xxpip_v1.h | 159 ++++++++++++++ src/wayland_server.cpp | 7 + src/window.cpp | 3 + src/window.h | 6 + src/xxpipv1integration.cpp | 43 ++++ src/xxpipv1integration.h | 28 +++ src/xxpipv1window.cpp | 179 ++++++++++++++++ src/xxpipv1window.h | 48 +++++ tests/CMakeLists.txt | 4 + tests/pip/CMakeLists.txt | 22 ++ tests/pip/main.cpp | 19 ++ tests/pip/pip.cpp | 227 ++++++++++++++++++++ tests/pip/pip.h | 102 +++++++++ tests/pip/pipshellsurface.cpp | 156 ++++++++++++++ tests/pip/pipshellsurface.h | 70 +++++++ tests/pip/window.cpp | 19 ++ tests/pip/window.h | 22 ++ 26 files changed, 1761 insertions(+), 1 deletion(-) create mode 100644 src/wayland/protocols/xx-pip-v1.xml create mode 100644 src/wayland/xxpip_v1.cpp create mode 100644 src/wayland/xxpip_v1.h create mode 100644 src/xxpipv1integration.cpp create mode 100644 src/xxpipv1integration.h create mode 100644 src/xxpipv1window.cpp create mode 100644 src/xxpipv1window.h create mode 100644 tests/pip/CMakeLists.txt create mode 100644 tests/pip/main.cpp create mode 100644 tests/pip/pip.cpp create mode 100644 tests/pip/pip.h create mode 100644 tests/pip/pipshellsurface.cpp create mode 100644 tests/pip/pipshellsurface.h create mode 100644 tests/pip/window.cpp create mode 100644 tests/pip/window.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 8d4d03b8459..6ac395557c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -84,6 +84,10 @@ endif() if (BUILD_TESTING) find_package(KPipeWire) + + if (Qt6WaylandClient_VERSION VERSION_GREATER_EQUAL "6.10.0") + find_package(Qt6WaylandClientPrivate ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE) + endif() endif() # required frameworks by Core diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 48fb847ca7c..978c98f7ff3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -211,6 +211,8 @@ target_sources(kwin PRIVATE xdgshellintegration.cpp xdgshellwindow.cpp xkb.cpp + xxpipv1integration.cpp + xxpipv1window.cpp ) target_link_libraries(kwin diff --git a/src/placement.cpp b/src/placement.cpp index 6c984282e5e..8890da56159 100644 --- a/src/placement.cpp +++ b/src/placement.cpp @@ -48,6 +48,8 @@ std::optional Placement::place(const Window *c, const QRectF & return placeOnScreenDisplay(c, area.toRect()); } else if (c->isTransient() && c->surface()) { return placeDialog(c, area.toRect(), options->placement()); + } else if (c->isPictureInPicture()) { + return placePictureInPicture(c, area.toRect()); } else { return place(c, area, options->placement()); } @@ -403,6 +405,22 @@ std::optional Placement::placeDialog(const Window *c, const QR return placeOnMainWindow(c, area, nextPlacement); } +std::optional Placement::placePictureInPicture(const Window *c, const QRect &area) +{ + Q_ASSERT(area.isValid()); + + const QSizeF size = c->size(); + if (size.isEmpty()) { + return std::nullopt; + } + + const qreal x = area.x() + area.width() - size.width(); + const qreal y = area.y() + area.height() - size.height(); + + return QPointF(x, y); +} + + std::optional Placement::placeUnderMouse(const Window *c, const QRect &area, PlacementPolicy /*next*/) { const QSizeF size = c->size(); diff --git a/src/placement.h b/src/placement.h index 63be9df117f..e0da0f51ace 100644 --- a/src/placement.h +++ b/src/placement.h @@ -44,6 +44,7 @@ private: std::optional placeDialog(const Window *c, const QRect &area, PlacementPolicy next = PlacementUnknown); std::optional placeUtility(const Window *c, const QRect &area, PlacementPolicy next = PlacementUnknown); std::optional placeOnScreenDisplay(const Window *c, const QRect &area); + std::optional placePictureInPicture(const Window *c, const QRect &area); }; } // namespace diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index fde7fc50348..7261395ecf9 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -38,6 +38,7 @@ ecm_add_qtwayland_server_protocol_kde(WaylandProtocols_xml ${PROJECT_SOURCE_DIR}/src/wayland/protocols/drm.xml ${PROJECT_SOURCE_DIR}/src/wayland/protocols/frog-color-management-v1.xml ${PROJECT_SOURCE_DIR}/src/wayland/protocols/wlr-layer-shell-unstable-v1.xml + ${PROJECT_SOURCE_DIR}/src/wayland/protocols/xx-pip-v1.xml ${PROJECT_SOURCE_DIR}/src/wayland/protocols/xx-session-management-v1.xml ${WaylandProtocols_DATADIR}/stable/presentation-time/presentation-time.xml @@ -174,6 +175,7 @@ target_sources(kwin PRIVATE xdgtopleveltag_v1.cpp xwaylandkeyboardgrab_v1.cpp xwaylandshell_v1.cpp + xxpip_v1.cpp ) install(FILES diff --git a/src/wayland/protocols/xx-pip-v1.xml b/src/wayland/protocols/xx-pip-v1.xml new file mode 100644 index 00000000000..c5eb7636ee9 --- /dev/null +++ b/src/wayland/protocols/xx-pip-v1.xml @@ -0,0 +1,305 @@ + + + + Copyright © 2025 Vlad Zahorodnii + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + + The xx_pip_shell_v1 interface provides a way to create picture-in-picture + windows. + + Use cases are for example playing a video in a separate floating window. + + Warning! The protocol described in this file is currently in the testing + phase. Backward compatible changes may be added together with the + corresponding interface version bump. Backward incompatible changes can + only be done by creating a new major version of the extension. + + + + + + + + + + Destroy this xx_pip_shell_v1 object. Objects that have been created + through this instance are unaffected. + + + + + + This creates an xx_pip_v1 for the given xdg_surface and gives the + associated wl_surface the xx_pip_v1 role. + + If the wl_surface already has a role assigned, a role protocol error + will be raised. + + Creating a picture-in-picture surface from a wl_surface which has a + buffer attached or committed is a client error, and any attempts by + a client to attach or manipulate a buffer prior to the first + xx_pip_v1.configure event must also be treated as errors. + + After creating an xx_pip_v1 object and setting it up, the client + must perform an initial commit without any buffer attached. + The compositor will reply with a xx_pip_v1.configure event. + The client must acknowledge it and is then allowed to attach a buffer + to map the surface. + + The compositor may deny showing the picture-in-picture surface, in + which case it will send the closed event before the first configure + event. + + See the documentation of xdg_surface for more details about what an + xdg_surface is and how it is used. + + + + + + + + + This interface defines an xdg_surface role which represents a floating + window with some miniature contents, for example a video. + + The picture-in-picture window will be placed above all other windows. + Compositor-specific policies may override or customize the behavior + and the placement of the xx_pip_v1. For example, the compositor may + choose to put the xx_pip_v1 in a screen corner, etc. + + Unmapping an xx_pip_v1 means that the surface cannot be shown + by the compositor until it is explicitly mapped again. + All active operations (e.g., move, resize) are canceled and all + attributes (e.g. title, state, stacking, ...) are discarded for + an xx_pip_v1 surface when it is unmapped. The xx_pip_v1 returns to + the state it had right after xx_pip_shell_v1.get_pip. The client + can re-map the pip by perfoming a commit without any buffer + attached, waiting for a configure event and handling it as usual (see + xdg_surface description). + + Attaching a null buffer to a picture-in-picture unmaps the surface. + + + + + + + + + + + This request destroys the role surface and unmaps the surface. + + + + + + Set an application identifier for the surface. + + The app ID identifies the general class of applications to which + the surface belongs. The compositor can use this to group multiple + surfaces together, or to determine how to launch a new application. + + For D-Bus activatable applications, the app ID is used as the D-Bus + service name. + + The compositor shell will try to group application surfaces together + by their app ID. As a best practice, it is suggested to select app + ID's that match the basename of the application's .desktop file. + For example, "org.freedesktop.FooViewer" where the .desktop file is + "org.freedesktop.FooViewer.desktop". + + Like other properties, a set_app_id request can be sent after the + xx_pip_v1 has been mapped to update the property. + + See the desktop-entry specification [0] for more details on + application identifiers and how they relate to well-known D-Bus + names and .desktop files. + + [0] http://standards.freedesktop.org/desktop-entry-spec/ + + + + + + + Set the origin surface for the picture-in-picture surface. + + The origin surface is an optional property that specifies a surface + from which the picture-in-picture surface has been launched. If set, + the compositor may use this hint to play an animation when the + picture-in-picture surface is mapped or unmapped. For example, smoothly + move the surface from the origin to a screen corner. + + If the specified origin surface is the same as the picture-in-picture + surface, the invalid_origin protocol error will be posted. + + The origin surface is double-buffered state, see wl_surface.commit. + + + + + + + Set the origin rect within the origin surface for the picture-in-picture + surface. + + The origin rect is an optional property that specifies the launch + rectangle within the origin surface. The compositor may use this hint + to play an animation when the picture-in-picture surface is mapped or + unmapped. For example, smoothly move the surface from the origin rect + to a screen corner. + + The origin rect is specified in the surface-local coordinate space. + + The compositor ignores the parts of the origin rect that fall outside + of the origin surface. + + The origin rect is double-buffered state, see wl_surface.commit. + + + + + + + + + + Start an interactive, user-driven move of the surface. + + This request must be used in response to some sort of user action + like a button press, key press, or touch down event. The passed + serial is used to determine the type of interactive move (touch, + pointer, etc). + + The server may ignore move requests depending on the state of + the surface, or if the passed serial is no longer valid. + + If triggered, the surface will lose the focus of the device + (wl_pointer, wl_touch, etc) used for the move. It is up to the + compositor to visually indicate that the move is taking place, such as + updating a pointer cursor, during the move. There is no guarantee + that the device focus will return when the move is completed. + + + + + + + + These values are used to indicate which edge of a surface + is being dragged in a resize operation. + + + + + + + + + + + + + + + Start a user-driven, interactive resize of the surface. + + This request must be used in response to some sort of user action + like a button press, key press, or touch down event. The passed + serial is used to determine the type of interactive resize (touch, + pointer, etc). + + The server may ignore resize requests depending on the state of + the surface, or if the passed serial is no longer valid. + + If triggered, the surface also will lose the focus of the device + (wl_pointer, wl_touch, etc) used for the resize. It is up to the + compositor to visually indicate that the resize is taking place, + such as updating a pointer cursor, during the resize. There is no + guarantee that the device focus will return when the resize is + completed. + + The edges parameter specifies how the surface should be resized, + and is one of the values of the resize_edge enum. The compositor + may use this information to update the surface position for + example when dragging the top left corner. The compositor may also + use this information to adapt its behavior, e.g. choose an + appropriate cursor image. + + + + + + + + + The closed event is sent by the compositor when the surface will + no longer be shown. Further changes to the surface will be ignored. + The client should destroy the resource after receiving this event. + + + + + + The configure_bounds event may be sent prior to a xx_pip_v1.configure + event to communicate the bounds a surface size must be constrained to. + + The passed width and height are in surface coordinate space. + + If the surface width or the surface height is greater than the specified + surface size bounds, an invalid_size protocol error will be posted. + + The surface bounds subject to compositor policies. + + The bounds may change at any point, and in such a case, a new + xx_pip_v1.configure_bounds will be sent, followed by xx_pip_v1.configure and + xdg_surface.configure. + + + + + + + + This configure event asks the client to resize its pip surface. + The configured state should not be applied immediately. See + xdg_surface.configure for details. + + The width and height arguments specify a hint to the window + about how its surface should be resized in window geometry + coordinates. See set_window_geometry. + + If the width or height arguments are zero, it means the client + should decide its own window dimension. + + Clients must send an ack_configure in response to this event. See + xdg_surface.configure and xdg_surface.ack_configure for details. + + + + + + diff --git a/src/wayland/xdgshell.cpp b/src/wayland/xdgshell.cpp index 282678c0938..f1217e9d37a 100644 --- a/src/wayland/xdgshell.cpp +++ b/src/wayland/xdgshell.cpp @@ -186,7 +186,7 @@ void XdgSurfaceInterfacePrivate::xdg_surface_destroy_resource(Resource *resource void XdgSurfaceInterfacePrivate::xdg_surface_destroy(Resource *resource) { - if (toplevel || popup) { + if (!toplevel.isNull() || !popup.isNull() || !pip.isNull()) { qWarning() << "Tried to destroy xdg_surface before its role object"; } wl_resource_destroy(resource->handle); diff --git a/src/wayland/xdgshell_p.h b/src/wayland/xdgshell_p.h index 46244b23523..e48dfd8d78d 100644 --- a/src/wayland/xdgshell_p.h +++ b/src/wayland/xdgshell_p.h @@ -16,6 +16,7 @@ namespace KWin { class XdgToplevelDecorationV1Interface; +class XXPipV1Interface; class XdgShellInterfacePrivate : public QtWaylandServer::xdg_wm_base { @@ -118,6 +119,7 @@ public: XdgShellInterface *shell = nullptr; QPointer toplevel; QPointer popup; + QPointer pip; QPointer surface; QRect windowGeometry; bool firstBufferAttached = false; diff --git a/src/wayland/xxpip_v1.cpp b/src/wayland/xxpip_v1.cpp new file mode 100644 index 00000000000..e0b1f36134f --- /dev/null +++ b/src/wayland/xxpip_v1.cpp @@ -0,0 +1,312 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include "wayland/xxpip_v1.h" +#include "utils/resource.h" +#include "wayland/display.h" +#include "wayland/seat.h" +#include "wayland/surface.h" +#include "wayland/xdgshell_p.h" + +#include "qwayland-server-xx-pip-v1.h" + +namespace KWin +{ + +static const int s_version = 1; + +class XXPipShellV1InterfacePrivate : public QtWaylandServer::xx_pip_shell_v1 +{ +public: + explicit XXPipShellV1InterfacePrivate(XXPipShellV1Interface *q, Display *display); + + XXPipShellV1Interface *q; + Display *display; + +protected: + void xx_pip_shell_v1_destroy(Resource *resource) override; + void xx_pip_shell_v1_get_pip(Resource *resource, uint32_t id, struct ::wl_resource *xdg_surface) override; +}; + +XXPipShellV1InterfacePrivate::XXPipShellV1InterfacePrivate(XXPipShellV1Interface *q, Display *display) + : QtWaylandServer::xx_pip_shell_v1(*display, s_version) + , q(q) + , display(display) +{ +} + +void XXPipShellV1InterfacePrivate::xx_pip_shell_v1_destroy(Resource *resource) +{ + wl_resource_destroy(resource->handle); +} + +void XXPipShellV1InterfacePrivate::xx_pip_shell_v1_get_pip(Resource *resource, uint32_t id, struct ::wl_resource *xdg_surface) +{ + XdgSurfaceInterface *xdgSurface = XdgSurfaceInterface::get(xdg_surface); + + if (const SurfaceRole *role = xdgSurface->surface()->role()) { + if (role != XXPipV1Interface::role()) { + wl_resource_post_error(resource->handle, error_already_constructed, "the surface already has a role assigned %s", role->name().constData()); + return; + } + } else { + xdgSurface->surface()->setRole(XXPipV1Interface::role()); + } + + wl_resource *pipResource = wl_resource_create(resource->client(), &xx_pip_v1_interface, resource->version(), id); + auto pip = new XXPipV1Interface(q, xdgSurface, pipResource); + + Q_EMIT q->pipCreated(pip); +} + +XXPipShellV1Interface::XXPipShellV1Interface(Display *display, QObject *parent) + : QObject(parent) + , d(std::make_unique(this, display)) +{ +} + +XXPipShellV1Interface::~XXPipShellV1Interface() +{ +} + +Display *XXPipShellV1Interface::display() const +{ + return d->display; +} + +class XXPipV1Commit : public SurfaceAttachedState, public XdgSurfaceCommit +{ +public: + QPointer origin; + QRect originRect; +}; + +class XXPipV1InterfacePrivate : public SurfaceExtension, public QtWaylandServer::xx_pip_v1 +{ +public: + XXPipV1InterfacePrivate(XXPipV1Interface *q, XXPipShellV1Interface *shell, XdgSurfaceInterface *xdgSurface); + + void apply(XXPipV1Commit *comit); + void reset(); + + XXPipV1Interface *q; + XXPipShellV1Interface *shell; + XdgSurfaceInterface *xdgSurface; + QString applicationId; + QPointer origin; + QRect originRect; + +protected: + void xx_pip_v1_destroy_resource(Resource *resource) override; + void xx_pip_v1_destroy(Resource *resource) override; + void xx_pip_v1_set_app_id(Resource *resource, const QString &app_id) override; + void xx_pip_v1_set_origin(Resource *resource, struct ::wl_resource *origin) override; + void xx_pip_v1_set_origin_rect(Resource *resource, int32_t x, int32_t y, uint32_t width, uint32_t height) override; + void xx_pip_v1_move(Resource *resource, struct ::wl_resource *seat, uint32_t serial) override; + void xx_pip_v1_resize(Resource *resource, struct ::wl_resource *seat, uint32_t serial, uint32_t edges) override; +}; + +XXPipV1InterfacePrivate::XXPipV1InterfacePrivate(XXPipV1Interface *q, XXPipShellV1Interface *shell, XdgSurfaceInterface *xdgSurface) + : SurfaceExtension(xdgSurface->surface()) + , q(q) + , shell(shell) + , xdgSurface(xdgSurface) +{ +} + +void XXPipV1InterfacePrivate::apply(XXPipV1Commit *commit) +{ + auto xdgSurfacePrivate = XdgSurfaceInterfacePrivate::get(xdgSurface); + if (xdgSurfacePrivate->firstBufferAttached && !xdgSurfacePrivate->surface->buffer()) { + reset(); + return; + } + + if (!commit->origin.isNull()) { + origin = commit->origin; + } + if (!commit->originRect.isNull()) { + originRect = commit->originRect; + } + + xdgSurfacePrivate->apply(commit); + + if (!xdgSurfacePrivate->isConfigured) { + Q_EMIT q->initializeRequested(); + } +} + +void XXPipV1InterfacePrivate::reset() +{ + auto xdgSurfacePrivate = XdgSurfaceInterfacePrivate::get(xdgSurface); + xdgSurfacePrivate->reset(); + + Q_EMIT q->resetOccurred(); +} + +void XXPipV1InterfacePrivate::xx_pip_v1_destroy_resource(Resource *resource) +{ + Q_EMIT q->aboutToBeDestroyed(); + delete q; +} + +void XXPipV1InterfacePrivate::xx_pip_v1_destroy(Resource *resource) +{ + wl_resource_destroy(resource->handle); +} + +void XXPipV1InterfacePrivate::xx_pip_v1_set_app_id(Resource *resource, const QString &app_id) +{ + if (applicationId != app_id) { + applicationId = app_id; + Q_EMIT q->applicationIdChanged(); + } +} + +void XXPipV1InterfacePrivate::xx_pip_v1_set_origin(Resource *resource, struct ::wl_resource *origin_resource) +{ + SurfaceInterface *origin = SurfaceInterface::get(origin_resource); + if (origin == xdgSurface->surface()) { + wl_resource_post_error(resource->handle, error_invalid_origin, "pip surface cannot be its own origin"); + return; + } + + pending->origin = origin; +} + +void XXPipV1InterfacePrivate::xx_pip_v1_set_origin_rect(Resource *resource, int32_t x, int32_t y, uint32_t width, uint32_t height) +{ + pending->originRect = QRect(x, y, width, height); +} + +void XXPipV1InterfacePrivate::xx_pip_v1_move(Resource *resource, struct ::wl_resource *seat_resource, uint32_t serial) +{ + auto xdgSurfacePrivate = XdgSurfaceInterfacePrivate::get(xdgSurface); + if (!xdgSurfacePrivate->isConfigured) { + wl_resource_post_error(resource->handle, QtWaylandServer::xdg_surface::error_not_constructed, "surface has not been configured yet"); + return; + } + + Q_EMIT q->moveRequested(SeatInterface::get(seat_resource), serial); +} + +void XXPipV1InterfacePrivate::xx_pip_v1_resize(Resource *resource, struct ::wl_resource *seat_resource, uint32_t serial, uint32_t edges) +{ + auto xdgSurfacePrivate = XdgSurfaceInterfacePrivate::get(xdgSurface); + if (!xdgSurfacePrivate->isConfigured) { + wl_resource_post_error(resource->handle, QtWaylandServer::xdg_surface::error_not_constructed, "surface has not been configured yet"); + return; + } + + Gravity gravity; + switch (edges) { + case resize_edge_none: + gravity = Gravity::None; + break; + case resize_edge_top: + gravity = Gravity::Top; + break; + case resize_edge_bottom: + gravity = Gravity::Bottom; + break; + case resize_edge_left: + gravity = Gravity::Left; + break; + case resize_edge_top_left: + gravity = Gravity::TopLeft; + break; + case resize_edge_bottom_left: + gravity = Gravity::BottomLeft; + break; + case resize_edge_right: + gravity = Gravity::Right; + break; + case resize_edge_top_right: + gravity = Gravity::TopRight; + break; + case resize_edge_bottom_right: + gravity = Gravity::BottomRight; + break; + default: + wl_resource_post_error(resource->handle, error_invalid_resize_edge, "invalid resize edge"); + return; + } + + Q_EMIT q->resizeRequested(SeatInterface::get(seat_resource), gravity, serial); +} + +XXPipV1Interface::XXPipV1Interface(XXPipShellV1Interface *shell, XdgSurfaceInterface *xdgSurface, wl_resource *resource) + : d(std::make_unique(this, shell, xdgSurface)) +{ + XdgSurfaceInterfacePrivate *surfacePrivate = XdgSurfaceInterfacePrivate::get(xdgSurface); + surfacePrivate->pip = this; + surfacePrivate->pending = d->pending; + + d->init(resource); +} + +XXPipV1Interface::~XXPipV1Interface() +{ +} + +SurfaceRole *XXPipV1Interface::role() +{ + static SurfaceRole role(QByteArrayLiteral("xx_pip_v1")); + return &role; +} + +bool XXPipV1Interface::isConfigured() const +{ + return d->xdgSurface->isConfigured(); +} + +XdgSurfaceInterface *XXPipV1Interface::xdgSurface() const +{ + return d->xdgSurface; +} + +SurfaceInterface *XXPipV1Interface::surface() const +{ + return d->xdgSurface->surface(); +} + +QString XXPipV1Interface::applicationId() const +{ + return d->applicationId; +} + +quint32 XXPipV1Interface::sendConfigureSize(const QSizeF &size) +{ + const quint32 serial = d->shell->display()->nextSerial(); + + d->send_configure_size(size.width(), size.height()); + + auto xdgSurfacePrivate = XdgSurfaceInterfacePrivate::get(xdgSurface()); + xdgSurfacePrivate->send_configure(serial); + xdgSurfacePrivate->isConfigured = true; + + return serial; +} + +void XXPipV1Interface::sendClosed() +{ + d->send_closed(); +} + +void XXPipV1Interface::sendConfigureBounds(const QSizeF &size) +{ + d->send_configure_bounds(size.width(), size.height()); +} + +XXPipV1Interface *XXPipV1Interface::get(::wl_resource *resource) +{ + if (auto pipPrivate = resource_cast(resource)) { + return pipPrivate->q; + } + return nullptr; +} + +} // namespace KWin diff --git a/src/wayland/xxpip_v1.h b/src/wayland/xxpip_v1.h new file mode 100644 index 00000000000..c2737f2ec26 --- /dev/null +++ b/src/wayland/xxpip_v1.h @@ -0,0 +1,159 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#pragma once + +#include "kwin_export.h" + +#include + +#include + +struct wl_resource; + +namespace KWin +{ + +class Display; +class SeatInterface; +class SurfaceInterface; +class SurfaceRole; +class XXPipV1Interface; +class XXPipV1InterfacePrivate; +class XdgSurfaceInterface; +class XXPipShellV1InterfacePrivate; + +enum class Gravity; + +/** + * The XXPipShellV1Interface extension provides clients a way to create picture-in-picture + * surfaces. + */ +class KWIN_EXPORT XXPipShellV1Interface : public QObject +{ + Q_OBJECT + +public: + explicit XXPipShellV1Interface(Display *display, QObject *parent = nullptr); + ~XXPipShellV1Interface() override; + + Display *display() const; + +Q_SIGNALS: + void pipCreated(XXPipV1Interface *pip); + +private: + std::unique_ptr d; +}; + +/** + * The XXPipV1Interface class represents a picture-in-picture surface. + * + * XXPipV1Interface corresponds to the Wayland interface \c xx_pip_v1. + */ +class KWIN_EXPORT XXPipV1Interface : public QObject +{ + Q_OBJECT + +public: + XXPipV1Interface(XXPipShellV1Interface *shell, XdgSurfaceInterface *xdgSurface, wl_resource *resource); + ~XXPipV1Interface() override; + + static SurfaceRole *role(); + + /** + * Returns \c true if the popup has been configured; otherwise returns \c false. + */ + bool isConfigured() const; + + /** + * Returns the XdgSurfaceInterface associated with the XXPipV1Interface. + */ + XdgSurfaceInterface *xdgSurface() const; + + /** + * Returns the SurfaceInterface associated with the XXPipV1Interface. + */ + SurfaceInterface *surface() const; + + /** + * Returns the desktop file name of the pip surface. + */ + QString applicationId() const; + + /** + * Returns the surface from which the picture-in-picture surface has been launched, or \c null. + */ + SurfaceInterface *origin() const; + + /** + * Specifies the bounds within the origin surface from which the picture-in-picture surface has + * been launched. + */ + QRect originRect() const; + + /** + * Sends a configure event to the client. \a size specifies the new window geometry size. A size + * of zero means the client should decide its own window dimensions. + */ + quint32 sendConfigureSize(const QSizeF &size); + + /** + * Sends a close event to the client. The client may choose to ignore this request. + */ + void sendClosed(); + + /** + * Sends an event to the client specifying the maximum bounds for the surface size. Must be + * called before sendConfigure(). + */ + void sendConfigureBounds(const QSizeF &size); + + /** + * Returns the XXPipV1Interface for the specified wayland resource object \a resource. + */ + static XXPipV1Interface *get(::wl_resource *resource); + +Q_SIGNALS: + /** + * This signal is emitted when the xx-pip-v1 is about to be destroyed. + */ + void aboutToBeDestroyed(); + + /** + * This signal is emitted when the xx-pip-v1 has commited the initial state and wants to + * be configured. After initializing the pip surface, you must send a configure event. + */ + void initializeRequested(); + + /** + * This signal is emitted when the pip surface has been unmapped and its state has been reset. + */ + void resetOccurred(); + + /** + * This signal is emitted when the pip wants to be interactively moved. The \a seat and + * the \a serial indicate the user action in response to which this request has been issued. + */ + void moveRequested(SeatInterface *seat, quint32 serial); + + /** + * This signal is emitted when the pip wants to be interactively resized with + * the specified \a gravity. The \a seat and the \a serial indicate the user action + * in response to which this request has been issued. + */ + void resizeRequested(SeatInterface *seat, Gravity anchor, quint32 serial); + + /** + * This signal is emitted when the application id changes. + */ + void applicationIdChanged(); + +private: + std::unique_ptr d; +}; + +} // namespace KWin diff --git a/src/wayland_server.cpp b/src/wayland_server.cpp index 24f7fa71ebe..0d54680313a 100644 --- a/src/wayland_server.cpp +++ b/src/wayland_server.cpp @@ -91,6 +91,7 @@ #include "xdgactivationv1.h" #include "xdgshellintegration.h" #include "xdgshellwindow.h" +#include "xxpipv1integration.h" #if KWIN_BUILD_X11 #include "wayland/xwaylandkeyboardgrab_v1.h" #include "wayland/xwaylandshell_v1.h" @@ -581,6 +582,12 @@ void WaylandServer::initWorkspace() connect(layerShellV1Integration, &LayerShellV1Integration::windowCreated, this, &WaylandServer::registerWindow); + if (qEnvironmentVariableIntValue("KWIN_WAYLAND_SUPPORT_XX_PIP_V1") == 1) { + auto pipV1Integration = new XXPipV1Integration(this); + connect(pipV1Integration, &XXPipV1Integration::windowCreated, + this, &WaylandServer::registerWindow); + } + new KeyStateInterface(m_display, m_display); VirtualDesktopManager::self()->setVirtualDesktopManagement(m_virtualDesktopManagement); diff --git a/src/window.cpp b/src/window.cpp index 591950ab74a..f5a4be143b8 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -563,6 +563,9 @@ Layer Window::belongsToLayer() const if (isUnmanaged() || isInternal()) { return OverlayLayer; } + if (isPictureInPicture()) { + return OverlayLayer; + } if (isLockScreen() && !waylandServer()) { return OverlayLayer; } diff --git a/src/window.h b/src/window.h index 57be2c6891e..9ae5758de85 100644 --- a/src/window.h +++ b/src/window.h @@ -781,6 +781,7 @@ public: virtual bool isClient() const; bool isDeleted() const; virtual bool isUnmanaged() const; + virtual bool isPictureInPicture() const; bool isLockScreenOverlay() const; void setLockScreenOverlay(bool allowed); @@ -2086,6 +2087,11 @@ inline bool Window::isInternal() const return false; } +inline bool Window::isPictureInPicture() const +{ + return false; +} + inline WindowItem *Window::windowItem() const { return m_windowItem.get(); diff --git a/src/xxpipv1integration.cpp b/src/xxpipv1integration.cpp new file mode 100644 index 00000000000..b72f2cba140 --- /dev/null +++ b/src/xxpipv1integration.cpp @@ -0,0 +1,43 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "xxpipv1integration.h" +#include "wayland/xxpip_v1.h" +#include "wayland_server.h" +#include "workspace.h" +#include "xxpipv1window.h" + +namespace KWin +{ + +XXPipV1Integration::XXPipV1Integration(QObject *parent) + : WaylandShellIntegration(parent) +{ + XXPipShellV1Interface *shell = new XXPipShellV1Interface(waylandServer()->display(), this); + connect(shell, &XXPipShellV1Interface::pipCreated, + this, &XXPipV1Integration::registerPipV1Surface); +} + +void XXPipV1Integration::registerPipV1Surface(XXPipV1Interface *pip) +{ + createPipV1Window(pip); + connect(pip, &XXPipV1Interface::resetOccurred, this, [this, pip] { + createPipV1Window(pip); + }); +} + +void XXPipV1Integration::createPipV1Window(XXPipV1Interface *pip) +{ + if (!workspace()) { + qCWarning(KWIN_CORE, "An xx-pip-v1 surface has been created while the compositor " + "is still not fully initialized. That is a compositor bug!"); + return; + } + + Q_EMIT windowCreated(new XXPipV1Window(pip)); +} + +} // namespace KWin diff --git a/src/xxpipv1integration.h b/src/xxpipv1integration.h new file mode 100644 index 00000000000..8d2be310fe5 --- /dev/null +++ b/src/xxpipv1integration.h @@ -0,0 +1,28 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "waylandshellintegration.h" + +namespace KWin +{ + +class XXPipV1Interface; + +class XXPipV1Integration : public WaylandShellIntegration +{ + Q_OBJECT + +public: + explicit XXPipV1Integration(QObject *parent = nullptr); + +private: + void registerPipV1Surface(XXPipV1Interface *pip); + void createPipV1Window(XXPipV1Interface *pip); +}; + +} // namespace KWin diff --git a/src/xxpipv1window.cpp b/src/xxpipv1window.cpp new file mode 100644 index 00000000000..0d6b6bafc8b --- /dev/null +++ b/src/xxpipv1window.cpp @@ -0,0 +1,179 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "xxpipv1window.h" +#include "input.h" +#include "wayland/seat.h" +#include "wayland/surface.h" +#include "wayland/tablet_v2.h" +#include "wayland_server.h" +#include "workspace.h" + +namespace KWin +{ + +XXPipV1Window::XXPipV1Window(XXPipV1Interface *shellSurface) + : XdgSurfaceWindow(shellSurface->xdgSurface()) + , m_shellSurface(shellSurface) +{ + setOutput(workspace()->activeOutput()); + setMoveResizeOutput(workspace()->activeOutput()); + setOnAllDesktops(true); + setOnAllActivities(true); + + connect(shellSurface, &XXPipV1Interface::initializeRequested, + this, &XXPipV1Window::initialize); + connect(shellSurface, &XXPipV1Interface::aboutToBeDestroyed, + this, &XXPipV1Window::destroyWindow); + connect(shellSurface, &XXPipV1Interface::moveRequested, + this, &XXPipV1Window::handleMoveRequested); + connect(shellSurface, &XXPipV1Interface::resizeRequested, + this, &XXPipV1Window::handleResizeRequested); + connect(shellSurface, &XXPipV1Interface::applicationIdChanged, + this, &XXPipV1Window::handleApplicationIdChanged); +} + +void XXPipV1Window::initialize() +{ + scheduleConfigure(); +} + +bool XXPipV1Window::isPictureInPicture() const +{ + return true; +} + +bool XXPipV1Window::isResizable() const +{ + return true; +} + +bool XXPipV1Window::isMovable() const +{ + return true; +} + +bool XXPipV1Window::isMovableAcrossScreens() const +{ + return true; +} + +bool XXPipV1Window::isCloseable() const +{ + return true; +} + +void XXPipV1Window::closeWindow() +{ + m_shellSurface->sendClosed(); +} + +bool XXPipV1Window::wantsInput() const +{ + return false; +} + +bool XXPipV1Window::takeFocus() +{ + return false; +} + +bool XXPipV1Window::acceptsFocus() const +{ + return false; +} + +XdgSurfaceConfigure *XXPipV1Window::sendRoleConfigure() const +{ + surface()->setPreferredBufferScale(nextTargetScale()); + surface()->setPreferredBufferTransform(preferredBufferTransform()); + surface()->setPreferredColorDescription(preferredColorDescription()); + + const QRectF geometry = moveResizeGeometry(); + if (geometry.isEmpty()) { + const QRectF workArea = workspace()->clientArea(PlacementArea, this, moveResizeOutput()); + m_shellSurface->sendConfigureBounds(workArea.size() * 0.25); + } + + XdgSurfaceConfigure *configureEvent = new XdgSurfaceConfigure(); + configureEvent->bounds = moveResizeGeometry(); + configureEvent->serial = m_shellSurface->sendConfigureSize(geometry.size()); + + return configureEvent; +} + +void XXPipV1Window::handleRoleDestroyed() +{ + m_shellSurface->disconnect(this); + + XdgSurfaceWindow::handleRoleDestroyed(); +} + +void XXPipV1Window::handleApplicationIdChanged() +{ + setResourceClass(resourceName(), m_shellSurface->applicationId()); + setDesktopFileName(m_shellSurface->applicationId()); +} + +void XXPipV1Window::handleMoveRequested(SeatInterface *seat, quint32 serial) +{ + if (const auto anchor = input()->implicitGrabPositionBySerial(seat, serial)) { + performMousePressCommand(Options::MouseMove, *anchor); + } +} + +void XXPipV1Window::handleResizeRequested(SeatInterface *seat, Gravity gravity, quint32 serial) +{ + const auto anchor = input()->implicitGrabPositionBySerial(seat, serial); + if (!anchor) { + return; + } + if (isInteractiveMoveResize()) { + finishInteractiveMoveResize(false); + } + setInteractiveMoveResizePointerButtonDown(true); + setInteractiveMoveResizeAnchor(*anchor); + setInteractiveMoveResizeModifiers(Qt::KeyboardModifiers()); + setInteractiveMoveOffset(QPointF((anchor->x() - x()) / width(), (anchor->y() - y()) / height())); + setUnrestrictedInteractiveMoveResize(false); + setInteractiveMoveResizeGravity(gravity); + if (!startInteractiveMoveResize()) { + setInteractiveMoveResizePointerButtonDown(false); + } + updateCursor(); +} + +void XXPipV1Window::doSetNextTargetScale() +{ + if (isDeleted()) { + return; + } + if (m_shellSurface->isConfigured()) { + scheduleConfigure(); + } +} + +void XXPipV1Window::doSetPreferredBufferTransform() +{ + if (isDeleted()) { + return; + } + if (m_shellSurface->isConfigured()) { + scheduleConfigure(); + } +} + +void XXPipV1Window::doSetPreferredColorDescription() +{ + if (isDeleted()) { + return; + } + if (m_shellSurface->isConfigured()) { + scheduleConfigure(); + } +} + +} // namespace KWin diff --git a/src/xxpipv1window.h b/src/xxpipv1window.h new file mode 100644 index 00000000000..15873e3b25f --- /dev/null +++ b/src/xxpipv1window.h @@ -0,0 +1,48 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "wayland/xxpip_v1.h" +#include "xdgshellwindow.h" + +namespace KWin +{ + +class XXPipV1Window final : public XdgSurfaceWindow +{ + Q_OBJECT + +public: + explicit XXPipV1Window(XXPipV1Interface *shellSurface); + + bool isPictureInPicture() const override; + bool isResizable() const override; + bool isMovable() const override; + bool isMovableAcrossScreens() const override; + bool isCloseable() const override; + void closeWindow() override; + bool wantsInput() const override; + bool takeFocus() override; + +protected: + bool acceptsFocus() const override; + XdgSurfaceConfigure *sendRoleConfigure() const override; + void handleRoleDestroyed() override; + void doSetNextTargetScale() override; + void doSetPreferredBufferTransform() override; + void doSetPreferredColorDescription() override; + +private: + void initialize(); + void handleApplicationIdChanged(); + void handleMoveRequested(SeatInterface *seat, quint32 serial); + void handleResizeRequested(SeatInterface *seat, Gravity gravity, quint32 serial); + + XXPipV1Interface *m_shellSurface; +}; + +} // namespace KWin diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 134f5416975..824cd67083c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,3 +1,7 @@ +if (Qt6_VERSION VERSION_GREATER_EQUAL "6.9.0") + add_subdirectory(pip) +endif() + if(KWIN_BUILD_X11) set(normalhintsbasesizetest_SRCS normalhintsbasesizetest.cpp) add_executable(normalhintsbasesizetest ${normalhintsbasesizetest_SRCS}) diff --git a/tests/pip/CMakeLists.txt b/tests/pip/CMakeLists.txt new file mode 100644 index 00000000000..9135a5ab072 --- /dev/null +++ b/tests/pip/CMakeLists.txt @@ -0,0 +1,22 @@ +add_executable(piptest) + +target_sources(piptest PRIVATE + main.cpp + pipshellsurface.cpp + pip.cpp + window.cpp +) + +qt6_generate_wayland_protocol_client_sources(piptest + PRIVATE_CODE + FILES + ${PROJECT_SOURCE_DIR}/src/wayland/protocols/xx-pip-v1.xml + ${WaylandProtocols_DATADIR}/stable/xdg-shell/xdg-shell.xml + ${Wayland_DATADIR}/wayland.xml +) + +target_link_libraries(piptest PRIVATE + Qt::Gui + Qt::WaylandClientPrivate + Qt::Widgets +) diff --git a/tests/pip/main.cpp b/tests/pip/main.cpp new file mode 100644 index 00000000000..6d7c4f5b3b4 --- /dev/null +++ b/tests/pip/main.cpp @@ -0,0 +1,19 @@ +/* + SPDX-FileCopyrightText: 2025 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include "window.h" + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + + Window w; + w.show(); + + return app.exec(); +} diff --git a/tests/pip/pip.cpp b/tests/pip/pip.cpp new file mode 100644 index 00000000000..ed14f5f98d4 --- /dev/null +++ b/tests/pip/pip.cpp @@ -0,0 +1,227 @@ +/* + SPDX-FileCopyrightText: 2025 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "pip.h" +#include "pipshellsurface.h" + +#include +#include + +PipPin::PipPin(QWidget *parent) + : QWidget(parent) +{ + resize(100, 50); +} + +bool PipPin::isPinned() const +{ + return m_pinned; +} + +void PipPin::setPinned(bool pinned) +{ + if (m_pinned != pinned) { + m_pinned = pinned; + update(); + } +} + +void PipPin::paintEvent(QPaintEvent *event) +{ + QPainter painter(this); + painter.setClipRegion(event->region()); + + if (m_hovered) { + painter.setOpacity(1.0); + } else { + painter.setOpacity(0.5); + } + + painter.fillRect(rect(), Qt::black); + painter.setPen(Qt::white); + painter.drawText(rect(), Qt::AlignCenter, m_pinned ? QStringLiteral("Unpin") : QStringLiteral("Pin")); +} + +void PipPin::mousePressEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { + event->accept(); + Q_EMIT clicked(); + } +} + +void PipPin::enterEvent(QEnterEvent *event) +{ + m_hovered = true; + update(); +} + +void PipPin::leaveEvent(QEvent *event) +{ + m_hovered = false; + update(); +} + +Media::Media(QWidget *parent) + : QWidget(parent) +{ + m_pip = std::make_unique(); + connect(m_pip.get(), &Pip::pinned, this, [this]() { + m_pin->setPinned(true); + }); + connect(m_pip.get(), &Pip::unpinned, this, [this]() { + m_pin->setPinned(false); + }); + + m_pin = new PipPin(this); + connect(m_pin, &PipPin::clicked, this, [this]() { + if (m_pin->isPinned()) { + m_pip->show(); + } else { + m_pip->hide(); + } + }); +} + +void Media::paintEvent(QPaintEvent *event) +{ + QPainter painter(this); + painter.setClipRegion(event->region()); + painter.fillRect(rect(), QColor(0, 0, 0, 128)); +} + +void Media::resizeEvent(QResizeEvent *event) +{ + m_pin->move(width() - m_pin->width() - 50, height() - m_pin->height() - 50); + m_pip->resize(width(), height()); +} + +PipResizeHandle::PipResizeHandle(Qt::Edges edges, QWidget *parent) + : QWidget(parent) + , m_edges(edges) +{ + switch (edges) { + case Qt::LeftEdge: + case Qt::RightEdge: + setCursor(Qt::SizeHorCursor); + break; + case Qt::TopEdge: + case Qt::BottomEdge: + setCursor(Qt::SizeVerCursor); + break; + case Qt::TopEdge | Qt::LeftEdge: + case Qt::BottomEdge | Qt::RightEdge: + setCursor(Qt::SizeFDiagCursor); + break; + case Qt::TopEdge | Qt::RightEdge: + case Qt::BottomEdge | Qt::LeftEdge: + setCursor(Qt::SizeBDiagCursor); + break; + default: + Q_UNREACHABLE(); + } +} + +void PipResizeHandle::enterEvent(QEnterEvent *event) +{ + m_hovered = true; + update(); +} + +void PipResizeHandle::leaveEvent(QEvent *event) +{ + m_hovered = false; + update(); +} + +void PipResizeHandle::mousePressEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { + event->accept(); + window()->windowHandle()->startSystemResize(m_edges); + } +} + +void PipResizeHandle::paintEvent(QPaintEvent *event) +{ + QPainter painter(this); + painter.setClipRegion(event->region()); + painter.fillRect(rect(), QColor(222, 137, 190, m_hovered ? 128 : 0)); +} + +Pip::Pip(QWidget *parent) + : QWidget(parent) +{ + m_closeButton = new QPushButton(this); + m_closeButton->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close"))); + m_closeButton->setText(QStringLiteral("Close")); + connect(m_closeButton, &QPushButton::clicked, this, &Pip::hide); + + m_topLeftResizeHandle = new PipResizeHandle(Qt::TopEdge | Qt::LeftEdge, this); + m_topResizeHandle = new PipResizeHandle(Qt::TopEdge, this); + m_topRightResizeHandle = new PipResizeHandle(Qt::TopEdge | Qt::RightEdge, this); + m_rightResizeHandle = new PipResizeHandle(Qt::RightEdge, this); + m_bottomRightResizeHandle = new PipResizeHandle(Qt::BottomEdge | Qt::BottomEdge, this); + m_bottomResizeHandle = new PipResizeHandle(Qt::BottomEdge, this); + m_bottomLeftResizeHandle = new PipResizeHandle(Qt::BottomEdge | Qt::LeftEdge, this); + m_leftResizeHandle = new PipResizeHandle(Qt::LeftEdge, this); + + winId(); + PipShellIntegration::assignPipRole(windowHandle()); +} + +void Pip::layout() +{ + const int gridUnit = 5; + const int resizeZone = 2 * gridUnit; + + m_topLeftResizeHandle->setGeometry(0, 0, resizeZone, resizeZone); + m_topResizeHandle->setGeometry(resizeZone, 0, width() - 2 * resizeZone, resizeZone); + m_topRightResizeHandle->setGeometry(width() - resizeZone, 0, resizeZone, resizeZone); + m_rightResizeHandle->setGeometry(width() - resizeZone, resizeZone, resizeZone, height() - 2 * resizeZone); + m_bottomRightResizeHandle->setGeometry(width() - resizeZone, height() - resizeZone, resizeZone, resizeZone); + m_bottomResizeHandle->setGeometry(resizeZone, height() - resizeZone, width() - 2 * resizeZone, resizeZone); + m_bottomLeftResizeHandle->setGeometry(0, height() - resizeZone, resizeZone, resizeZone); + m_leftResizeHandle->setGeometry(0, resizeZone, resizeZone, height() - 2 * resizeZone); + + m_closeButton->move(width() - resizeZone - gridUnit - m_closeButton->width(), resizeZone + gridUnit); +} + +void Pip::paintEvent(QPaintEvent *event) +{ + QPainter painter(this); + painter.setClipRegion(event->region()); + painter.fillRect(rect(), QColor(64, 67, 78)); +} + +void Pip::resizeEvent(QResizeEvent *event) +{ + layout(); +} + +void Pip::mousePressEvent(QMouseEvent *event) +{ + switch (event->button()) { + case Qt::LeftButton: + event->accept(); + windowHandle()->startSystemMove(); + break; + default: + break; + } +} + +void Pip::showEvent(QShowEvent *event) +{ + Q_EMIT unpinned(); +} + +void Pip::hideEvent(QHideEvent *event) +{ + Q_EMIT pinned(); +} + +#include "moc_pip.cpp" diff --git a/tests/pip/pip.h b/tests/pip/pip.h new file mode 100644 index 00000000000..2f6e4a87d86 --- /dev/null +++ b/tests/pip/pip.h @@ -0,0 +1,102 @@ +/* + SPDX-FileCopyrightText: 2025 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +class PipResizeHandle : public QWidget +{ + Q_OBJECT + +public: + explicit PipResizeHandle(Qt::Edges edges, QWidget *parent = nullptr); + +protected: + void enterEvent(QEnterEvent *event) override; + void leaveEvent(QEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void paintEvent(QPaintEvent *event) override; + +private: + Qt::Edges m_edges; + bool m_hovered = false; +}; + +class Pip : public QWidget +{ + Q_OBJECT + +public: + explicit Pip(QWidget *parent = nullptr); + +Q_SIGNALS: + void pinned(); + void unpinned(); + +protected: + void paintEvent(QPaintEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void showEvent(QShowEvent *event) override; + void hideEvent(QHideEvent *event) override; + +private: + void layout(); + + QPushButton *m_closeButton = nullptr; + PipResizeHandle *m_topLeftResizeHandle = nullptr; + PipResizeHandle *m_topResizeHandle = nullptr; + PipResizeHandle *m_topRightResizeHandle = nullptr; + PipResizeHandle *m_rightResizeHandle = nullptr; + PipResizeHandle *m_bottomRightResizeHandle = nullptr; + PipResizeHandle *m_bottomResizeHandle = nullptr; + PipResizeHandle *m_bottomLeftResizeHandle = nullptr; + PipResizeHandle *m_leftResizeHandle = nullptr; +}; + +class PipPin : public QWidget +{ + Q_OBJECT + +public: + explicit PipPin(QWidget *parent = nullptr); + + bool isPinned() const; + void setPinned(bool pinned); + +Q_SIGNALS: + void clicked(); + +protected: + void paintEvent(QPaintEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void enterEvent(QEnterEvent *event) override; + void leaveEvent(QEvent *event) override; + +private: + bool m_pinned = true; + bool m_hovered = false; +}; + +class Media : public QWidget +{ + Q_OBJECT + +public: + explicit Media(QWidget *parent = nullptr); + +protected: + void paintEvent(QPaintEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + +private: + void layout(); + + std::unique_ptr m_pip; + PipPin *m_pin = nullptr; +}; diff --git a/tests/pip/pipshellsurface.cpp b/tests/pip/pipshellsurface.cpp new file mode 100644 index 00000000000..082a51a3c24 --- /dev/null +++ b/tests/pip/pipshellsurface.cpp @@ -0,0 +1,156 @@ +/* + SPDX-FileCopyrightText: 2025 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "pipshellsurface.h" + +#include +#include + +XdgWmBase::XdgWmBase() + : QWaylandClientExtensionTemplate(6) +{ + initialize(); + if (!isActive()) { + qFatal("The xdg-shell protocol is unsupported by the compositor"); + } +} + +XXPipShell::XXPipShell() + : QWaylandClientExtensionTemplate(1) +{ + initialize(); + if (!isActive()) { + qFatal("The xx-pip-v1 protocol is unsupported by the compositor"); + } +} + +void PipShellIntegration::assignPipRole(QWindow *window) +{ + window->create(); + + auto waylandWindow = dynamic_cast(window->handle()); + if (!waylandWindow) { + return; + } + + static PipShellIntegration *shellIntegration = nullptr; + if (!shellIntegration) { + shellIntegration = new PipShellIntegration(); + } + + waylandWindow->setShellIntegration(shellIntegration); +} + +PipShellIntegration::PipShellIntegration() + : m_xdgWmBase(std::make_unique()) + , m_xxPipShell(std::make_unique()) +{ +} + +bool PipShellIntegration::initialize(QtWaylandClient::QWaylandDisplay *display) +{ + return m_xdgWmBase->isInitialized() && m_xxPipShell->isInitialized(); +} + +QtWaylandClient::QWaylandShellSurface *PipShellIntegration::createShellSurface(QtWaylandClient::QWaylandWindow *window) +{ + ::xdg_surface *xdgSurface = m_xdgWmBase->get_xdg_surface(window->wlSurface()); + ::xx_pip_v1 *xxPip = m_xxPipShell->get_pip(xdgSurface); + return new PipShellSurface(xdgSurface, xxPip, window); +} + +PipShellSurface::PipShellSurface(::xdg_surface *xdgSurface, ::xx_pip_v1 *xxPip, QtWaylandClient::QWaylandWindow *window) + : QWaylandShellSurface(window) + , QtWayland::xdg_surface(xdgSurface) + , QtWayland::xx_pip_v1(xxPip) +{ +} + +PipShellSurface::~PipShellSurface() +{ + xx_pip_v1::destroy(); + xdg_surface::destroy(); +} + +bool PipShellSurface::isExposed() const +{ + return m_configured; +} + +void PipShellSurface::applyConfigure() +{ + QSize size = window()->windowContentGeometry().size(); + if (m_pendingSize.width() > 0) { + size.setWidth(m_pendingSize.width()); + } + if (m_pendingSize.height() > 0) { + size.setHeight(m_pendingSize.height()); + } + + window()->resizeFromApplyConfigure(size); +} + +void PipShellSurface::setWindowGeometry(const QRect &rect) +{ + if (window()->isExposed()) { + xdg_surface::set_window_geometry(rect.x(), rect.y(), rect.width(), rect.height()); + } +} + +bool PipShellSurface::move(QtWaylandClient::QWaylandInputDevice *inputDevice) +{ + if (!m_configured) { + return false; + } + xx_pip_v1::move(inputDevice->wl_seat(), inputDevice->serial()); + return true; +} + +bool PipShellSurface::resize(QtWaylandClient::QWaylandInputDevice *inputDevice, Qt::Edges edges) +{ + if (!m_configured) { + return false; + } + + const resize_edge edge = static_cast( + ((edges & Qt::TopEdge) ? resize_edge_top : 0) + | ((edges & Qt::BottomEdge) ? resize_edge_bottom : 0) + | ((edges & Qt::LeftEdge) ? resize_edge_left : 0) + | ((edges & Qt::RightEdge) ? resize_edge_right : 0)); + + xx_pip_v1::resize(inputDevice->wl_seat(), inputDevice->serial(), edge); + return true; +} + +void PipShellSurface::xdg_surface_configure(uint32_t serial) +{ + xdg_surface::ack_configure(serial); + + if (!m_configured) { + m_configured = true; + applyConfigure(); + } else { + window()->applyConfigureWhenPossible(); + } + + window()->updateExposure(); +} + +void PipShellSurface::xx_pip_v1_configure_bounds(int32_t width, int32_t height) +{ +} + +void PipShellSurface::xx_pip_v1_configure_size(int32_t width, int32_t height) +{ + m_pendingSize = QSize(width, height); +} + +void PipShellSurface::xx_pip_v1_closed() +{ + QWindowSystemInterface::handleCloseEvent(window()->window()); +} + +#include "moc_pipshellsurface.cpp" diff --git a/tests/pip/pipshellsurface.h b/tests/pip/pipshellsurface.h new file mode 100644 index 00000000000..b02e83b3fc9 --- /dev/null +++ b/tests/pip/pipshellsurface.h @@ -0,0 +1,70 @@ +/* + SPDX-FileCopyrightText: 2025 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include + +#include "qwayland-xdg-shell.h" +#include "qwayland-xx-pip-v1.h" + +class XdgWmBase : public QWaylandClientExtensionTemplate, public QtWayland::xdg_wm_base +{ + Q_OBJECT + +public: + XdgWmBase(); +}; + +class XXPipShell : public QWaylandClientExtensionTemplate, public QtWayland::xx_pip_shell_v1 +{ + Q_OBJECT + +public: + XXPipShell(); +}; + +class PipShellIntegration : public QtWaylandClient::QWaylandShellIntegration +{ +public: + PipShellIntegration(); + + bool initialize(QtWaylandClient::QWaylandDisplay *display) override; + QtWaylandClient::QWaylandShellSurface *createShellSurface(QtWaylandClient::QWaylandWindow *window) override; + + static void assignPipRole(QWindow *window); + +private: + std::unique_ptr m_xdgWmBase; + std::unique_ptr m_xxPipShell; +}; + +class PipShellSurface : public QtWaylandClient::QWaylandShellSurface, public QtWayland::xdg_surface, public QtWayland::xx_pip_v1 +{ + Q_OBJECT + +public: + PipShellSurface(::xdg_surface *xdgSurface, ::xx_pip_v1 *xxPip, QtWaylandClient::QWaylandWindow *window); + ~PipShellSurface() override; + + bool isExposed() const override; + void applyConfigure() override; + void setWindowGeometry(const QRect &rect) override; + bool move(QtWaylandClient::QWaylandInputDevice *inputDevice) override; + bool resize(QtWaylandClient::QWaylandInputDevice *inputDevice, Qt::Edges edges) override; + +private: + void xdg_surface_configure(uint32_t serial) override; + void xx_pip_v1_closed() override; + void xx_pip_v1_configure_bounds(int32_t width, int32_t height) override; + void xx_pip_v1_configure_size(int32_t width, int32_t height) override; + + QSize m_pendingSize; + bool m_configured = false; +}; diff --git a/tests/pip/window.cpp b/tests/pip/window.cpp new file mode 100644 index 00000000000..2ac98bde845 --- /dev/null +++ b/tests/pip/window.cpp @@ -0,0 +1,19 @@ +/* + SPDX-FileCopyrightText: 2025 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "window.h" +#include "pip.h" + +Window::Window(QWidget *parent) + : QWidget(parent) +{ + resize(800, 600); + + m_media = new Media(this); + m_media->setGeometry(100, 100, 400, 300); +} + +#include "moc_window.cpp" diff --git a/tests/pip/window.h b/tests/pip/window.h new file mode 100644 index 00000000000..bfd83e9d467 --- /dev/null +++ b/tests/pip/window.h @@ -0,0 +1,22 @@ +/* + SPDX-FileCopyrightText: 2025 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +class Media; + +class Window : public QWidget +{ + Q_OBJECT + +public: + explicit Window(QWidget *parent = nullptr); + +private: + Media *m_media = nullptr; +}; -- GitLab From cab39d37c8cb7d1c37d04a76cc7fa7af39fb7908 Mon Sep 17 00:00:00 2001 From: Vlad Zahorodnii Date: Thu, 12 Jun 2025 14:22:55 +0000 Subject: [PATCH 2/2] Apply 2 suggestion(s) to 1 file(s) Co-authored-by: Xaver Hugl --- src/wayland/xxpip_v1.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wayland/xxpip_v1.cpp b/src/wayland/xxpip_v1.cpp index e0b1f36134f..bc1b65d0212 100644 --- a/src/wayland/xxpip_v1.cpp +++ b/src/wayland/xxpip_v1.cpp @@ -282,7 +282,7 @@ quint32 XXPipV1Interface::sendConfigureSize(const QSizeF &size) { const quint32 serial = d->shell->display()->nextSerial(); - d->send_configure_size(size.width(), size.height()); + d->send_configure_size(std::round(size.width()), std::round(size.height())); auto xdgSurfacePrivate = XdgSurfaceInterfacePrivate::get(xdgSurface()); xdgSurfacePrivate->send_configure(serial); @@ -298,7 +298,7 @@ void XXPipV1Interface::sendClosed() void XXPipV1Interface::sendConfigureBounds(const QSizeF &size) { - d->send_configure_bounds(size.width(), size.height()); + d->send_configure_bounds(std::round(size.width()), std::round(size.height())); } XXPipV1Interface *XXPipV1Interface::get(::wl_resource *resource) -- GitLab