diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index df9803fc92..b95a045e38 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -28,3 +28,4 @@ add_subdirectory(test_im_candidate_panel_xprop) add_subdirectory(test_input_manager) add_subdirectory(test_keyboard_state_notify) add_subdirectory(test_glass) +add_subdirectory(test-window-transition) diff --git a/examples/test-window-transition/CMakeLists.txt b/examples/test-window-transition/CMakeLists.txt new file mode 100644 index 0000000000..39b51fbbbd --- /dev/null +++ b/examples/test-window-transition/CMakeLists.txt @@ -0,0 +1,30 @@ +find_package(Qt6 REQUIRED COMPONENTS Gui WaylandClient Widgets) +if(Qt6_VERSION VERSION_GREATER_EQUAL 6.10) + find_package(Qt6 REQUIRED COMPONENTS GuiPrivate WaylandClientPrivate) +endif() +find_package(PkgConfig REQUIRED) +pkg_get_variable(WAYLAND_PROTOCOLS_DATADIR wayland-protocols pkgdatadir) +find_package(TreelandProtocols REQUIRED) + +set(BIN_NAME test-window-transition) + +qt_add_executable(${BIN_NAME} + main.cpp +) + +qt_generate_wayland_protocol_client_sources(${BIN_NAME} + FILES + ${WAYLAND_PROTOCOLS_DATADIR}/staging/xdg-activation/xdg-activation-v1.xml + ${TREELAND_PROTOCOLS_DATA_DIR}/treeland-window-transition-unstable-v1.xml +) + +target_link_libraries(${BIN_NAME} + PRIVATE + Qt6::Gui + Qt6::Widgets + Qt6::WaylandClient + Qt6::GuiPrivate + Qt6::WaylandClientPrivate +) + +install(TARGETS ${BIN_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") diff --git a/examples/test-window-transition/main.cpp b/examples/test-window-transition/main.cpp new file mode 100644 index 0000000000..42cdcea5e6 --- /dev/null +++ b/examples/test-window-transition/main.cpp @@ -0,0 +1,568 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qwayland-treeland-window-transition-unstable-v1.h" +#include "qwayland-xdg-activation-v1.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Protocol client wrappers — managers (globals) +// --------------------------------------------------------------------------- + +class XdgActivationManager + : public QWaylandClientExtensionTemplate + , public QtWayland::xdg_activation_v1 +{ + Q_OBJECT +public: + explicit XdgActivationManager() + : QWaylandClientExtensionTemplate(1) + { + } + + ~XdgActivationManager() override + { + if (isInitialized()) + destroy(); + } +}; + +class WindowTransitionManager + : public QWaylandClientExtensionTemplate + , public QtWayland::treeland_window_transition_manager_v1 +{ + Q_OBJECT +public: + explicit WindowTransitionManager() + : QWaylandClientExtensionTemplate(1) + { + } + + ~WindowTransitionManager() override + { + if (isInitialized()) + destroy(); + } +}; + +// --------------------------------------------------------------------------- +// Per-object wrappers +// --------------------------------------------------------------------------- + +class ActivationToken + : public QObject + , public QtWayland::xdg_activation_token_v1 +{ + Q_OBJECT +public: + explicit ActivationToken(::xdg_activation_token_v1 *obj, QObject *parent = nullptr) + : QObject(parent) + , QtWayland::xdg_activation_token_v1(obj) + { + } + + QString tokenString() const + { + return m_token; + } + +protected: + void xdg_activation_token_v1_done(const QString &token) override + { + m_token = token; + Q_EMIT done(token); + } + +Q_SIGNALS: + void done(const QString &token); + +private: + QString m_token; +}; + +class WindowTransitionRect + : public QObject + , public QtWayland::treeland_window_transition_rect_v1 +{ + Q_OBJECT +public: + explicit WindowTransitionRect(::treeland_window_transition_rect_v1 *obj, + QObject *parent = nullptr) + : QObject(parent) + , QtWayland::treeland_window_transition_rect_v1(obj) + { + } + + void setSourceImage(QtWaylandClient::QWaylandDisplay *display, const QPixmap &pixmap) + { + delete m_shmBuffer; + m_shmBuffer = new QtWaylandClient::QWaylandShmBuffer(display, + pixmap.size(), + QImage::Format_ARGB32_Premultiplied); + QImage *image = m_shmBuffer->image(); + image->fill(Qt::transparent); + QPainter p(image); + p.drawPixmap(0, 0, pixmap); + p.end(); + set_source_buffer(m_shmBuffer->buffer()); + } + + void clearSourceImage() + { + delete m_shmBuffer; + m_shmBuffer = nullptr; + set_source_buffer(nullptr); + } + + ~WindowTransitionRect() override + { + delete m_shmBuffer; + } + +protected: + void treeland_window_transition_rect_v1_closed() override + { + Q_EMIT closed(); + } + +Q_SIGNALS: + void closed(); + +private: + QtWaylandClient::QWaylandShmBuffer *m_shmBuffer = nullptr; +}; + +// --------------------------------------------------------------------------- +// Test image +// --------------------------------------------------------------------------- + +static QPixmap createTestImage(int w, int h) +{ + QPixmap pm(w, h); + QPainter p(&pm); + QLinearGradient grad(0, 0, w, h); + grad.setColorAt(0, QColor(63, 81, 181)); + grad.setColorAt(1, QColor(33, 150, 243)); + p.fillRect(0, 0, w, h, grad); + p.setPen(QPen(QColor(255, 255, 255, 200), 4)); + p.drawRoundedRect(10, 10, w - 20, h - 20, 20, 20); + p.setPen(Qt::white); + QFont f = p.font(); + f.setPointSize(28); + f.setBold(true); + p.setFont(f); + p.drawText(QRect(0, 0, w, h), Qt::AlignCenter, "Window\nTransition"); + return pm; +} + +// --------------------------------------------------------------------------- +// Image widget +// --------------------------------------------------------------------------- + +class ImageWidget : public QWidget +{ +public: + explicit ImageWidget(const QPixmap &pixmap, QWidget *parent = nullptr) + : QWidget(parent) + , m_pixmap(pixmap) + { + } + + void paintEvent(QPaintEvent *) override + { + QPainter p(this); + p.drawPixmap( + 0, + 0, + m_pixmap.scaled(size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); + } + +private: + QPixmap m_pixmap; +}; + +// --------------------------------------------------------------------------- +// Helper: get wl_surface from a widget +// --------------------------------------------------------------------------- + +static ::wl_surface *getWlSurface(QWidget *widget) +{ + QWindow *handle = widget->windowHandle(); + if (!handle || !handle->handle()) + return nullptr; + auto *waylandWindow = static_cast(handle->handle()); + return waylandWindow ? waylandWindow->surface() : nullptr; +} + +// --------------------------------------------------------------------------- +// Helper: get the wayland display behind a widget +// --------------------------------------------------------------------------- + +static QtWaylandClient::QWaylandDisplay *getWlDisplay(QWidget *widget) +{ + QWindow *handle = widget->windowHandle(); + if (!handle || !handle->handle()) + return nullptr; + auto *waylandWindow = static_cast(handle->handle()); + return waylandWindow ? waylandWindow->display() : nullptr; +} + +// --------------------------------------------------------------------------- +// Sender (Application A) +// --------------------------------------------------------------------------- + +class SenderWindow : public QMainWindow +{ + Q_OBJECT +public: + SenderWindow(XdgActivationManager *actMgr, WindowTransitionManager *transitionMgr) + : m_actMgr(actMgr) + , m_transitionMgr(transitionMgr) + , m_image(createTestImage(200, 200)) + { + setWindowTitle("Window Transition - Sender (press Space)"); + + auto *central = new QWidget(this); + auto *layout = new QVBoxLayout(central); + + m_imageWidget = new ImageWidget(m_image, central); + m_imageWidget->setFixedSize(200, 200); + layout->addWidget(m_imageWidget, 0, Qt::AlignCenter); + + auto *label = new QLabel("SPACE: launch receiver (new process)\n" + "ENTER: launch modal (same process)\n" + "0: toggle disable animation (hide image)\n" + "B: toggle commit source buffer", + central); + label->setAlignment(Qt::AlignCenter); + layout->addWidget(label); + + m_sourceBufferIndicator = new QLabel("Commit source buffer: ON", central); + m_sourceBufferIndicator->setAlignment(Qt::AlignCenter); + layout->addWidget(m_sourceBufferIndicator); + + setCentralWidget(central); + resize(400, 300); + } + + ~SenderWindow() override + { + // Destroy all rect objects so the compositor falls back + // to the default close animation once sender is gone. + for (auto *rect : std::as_const(m_rects)) { + rect->destroy(); + } + m_rects.clear(); + } + +protected: + void keyPressEvent(QKeyEvent *event) override + { + if (event->key() == Qt::Key_Space) + launchReceiver(); + else if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) + launchModal(); + else if (event->key() == Qt::Key_0) + toggleAnimation(); + else if (event->key() == Qt::Key_B) + toggleSourceBuffer(); + QMainWindow::keyPressEvent(event); + } + + void resizeEvent(QResizeEvent *event) override + { + // The image widget is centered in the layout, so resizing the main + // window moves it. Re-set the persistent rect(s) with the updated + // geometry so the open/close animation follows the new position. + updateRectGeometries(); + QMainWindow::resizeEvent(event); + } + +private: + // Geometry (relative to this sender surface) of the image widget that is + // the source of the window transition. + QRect animationRect() const + { + const QPoint imagePos = m_imageWidget->mapTo(this, QPoint(0, 0)); + return QRect(imagePos, m_imageWidget->size()); + } + + void toggleAnimation() + { + m_animationDisabled = !m_animationDisabled; + m_imageWidget->setVisible(!m_animationDisabled); + updateRectGeometries(); + } + + void toggleSourceBuffer() + { + m_commitSourceBuffer = !m_commitSourceBuffer; + if (m_sourceBufferIndicator) + m_sourceBufferIndicator->setText(m_commitSourceBuffer ? "Commit source buffer: ON" + : "Commit source buffer: OFF"); + + auto *display = getWlDisplay(this); + for (auto *rect : std::as_const(m_rects)) { + if (m_commitSourceBuffer && display) + rect->setSourceImage(display, m_image); + else + rect->clearSourceImage(); + } + } + + void updateRectGeometries() + { + if (m_rects.isEmpty()) + return; + // When disabled, keep setting a 0x0 rect so the compositor stays in + // the disabled state; a resize must not re-enable it. + const QRect rect = m_animationDisabled ? QRect() : animationRect(); + for (auto *r : std::as_const(m_rects)) { + r->set_geometry(rect.x(), rect.y(), rect.width(), rect.height()); + } + } + + ActivationToken *createLaunchToken() + { + if (!m_actMgr || !m_actMgr->isInitialized()) { + qWarning() << "xdg_activation_v1 not available"; + return nullptr; + } + + ::wl_surface *surface = getWlSurface(this); + if (!surface) { + qWarning() << "Cannot get wl_surface"; + return nullptr; + } + + // 1. Create activation token + auto *rawToken = m_actMgr->get_activation_token(); + if (!rawToken) { + qWarning() << "Failed to create activation token"; + return nullptr; + } + auto *token = new ActivationToken(rawToken, this); + + // 2. Set the originating surface on the token + token->set_surface(surface); + + // 3. Attach window transition rect (relative to this surface). + // The rect is persistent: it stays alive after set_geometry and is + // used for both the open and close animations. We keep a reference + // so the compositor can read the latest geometry when the target + // window closes. Destroying the rect (or disconnecting) makes the + // compositor fall back to the default close animation. + if (m_transitionMgr && m_transitionMgr->isInitialized()) { + const QRect rectGeo = m_animationDisabled ? QRect() : animationRect(); + auto *rawRect = m_transitionMgr->get_window_transition_rect(rawToken); + if (rawRect) { + // Each launched window gets its own persistent rect. The rect + // stays alive (associated with that window) so it can drive + // that window's close animation. We must NOT destroy a + // previous rect here: doing so clears the close animation of + // a still-open earlier window. + auto *rect = new WindowTransitionRect(rawRect, this); + // When the compositor reports that the rect's target window + // has closed, the rect is useless: destroy it and drop it + // from our list. + connect(rect, &WindowTransitionRect::closed, this, [this, rect] { + const int idx = m_rects.indexOf(rect); + if (idx >= 0) + m_rects.removeAt(idx); + rect->destroy(); + }); + rect->set_geometry(rectGeo.x(), rectGeo.y(), rectGeo.width(), rectGeo.height()); + // Attach the launch-icon image as the animation source. The + // compositor samples it immediately and uses it as the + // starting visual of the open animation (and the ending + // visual of the close animation). + if (auto *display = getWlDisplay(this)) { + if (m_commitSourceBuffer) + rect->setSourceImage(display, m_image); + else + rect->clearSourceImage(); + } + m_rects.append(rect); + } + } + + // 4. Commit the token to receive the token string + token->commit(); + return token; + } + + void launchReceiver() + { + auto *token = createLaunchToken(); + if (!token) + return; + connect(token, &ActivationToken::done, this, &SenderWindow::onReceiverToken); + } + + void launchModal() + { + auto *token = createLaunchToken(); + if (!token) + return; + connect(token, &ActivationToken::done, this, &SenderWindow::onModalToken); + } + + void onReceiverToken(const QString &tokenStr) + { + qInfo() << "Got activation token, launching receiver..."; + + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert("XDG_ACTIVATION_TOKEN", tokenStr); + + // Do NOT parent the QProcess to this window: QProcess kills its + // child on destruction, so closing the sender would tear down the + // launched receiver along with it. Keep it parentless and let it + // self-clean once the receiver exits. + auto *proc = new QProcess; + proc->setProcessEnvironment(env); + connect(proc, &QProcess::finished, proc, &QProcess::deleteLater); + proc->start(QCoreApplication::applicationFilePath(), { "--receiver" }); + } + + void onModalToken(const QString &tokenStr) + { + qInfo() << "Got activation token, showing modal..."; + + if (!m_actMgr || !m_actMgr->isInitialized()) { + qWarning() << "xdg_activation_v1 not available"; + return; + } + + // A modal QDialog maps to an xdg_toplevel with set_parent + xdg_dialog + // set_modal. WA_ShowWithoutActivating makes Qt skip its own + // requestActivateOnShow, so we drive activation (and thus the window + // transition) with the token we just created, mirroring the receiver. + auto *dlg = new QDialog(this); + dlg->setWindowTitle("Modal (Enter)"); + dlg->setModal(true); + dlg->setAttribute(Qt::WA_ShowWithoutActivating, true); + dlg->setAttribute(Qt::WA_DeleteOnClose, true); + + auto *layout = new QVBoxLayout(dlg); + auto *label = new QLabel("Modal launched via xdg_activation + window transition rect", dlg); + label->setAlignment(Qt::AlignCenter); + layout->addWidget(label); + dlg->resize(400, 300); + + dlg->show(); + + ::wl_surface *surface = getWlSurface(dlg); + if (!surface) { + qWarning() << "Cannot get wl_surface for modal"; + return; + } + m_actMgr->activate(tokenStr, surface); + } + + XdgActivationManager *m_actMgr; + WindowTransitionManager *m_transitionMgr; + QList m_rects; + QPixmap m_image; + QWidget *m_imageWidget = nullptr; + QLabel *m_sourceBufferIndicator = nullptr; + bool m_commitSourceBuffer = true; + bool m_animationDisabled = false; +}; + +// --------------------------------------------------------------------------- +// Receiver (Application B) +// --------------------------------------------------------------------------- + +class ReceiverWindow : public QMainWindow +{ + Q_OBJECT +public: + explicit ReceiverWindow(XdgActivationManager *actMgr) + : m_actMgr(actMgr) + , m_image(createTestImage(800, 600)) + { + setWindowTitle("Window Transition - Receiver"); + resize(800, 600); + + auto *central = new ImageWidget(m_image, this); + setCentralWidget(central); + } + + void activateWithToken() + { + QByteArray tokenEnv = qgetenv("XDG_ACTIVATION_TOKEN"); + if (tokenEnv.isEmpty()) { + qWarning() << "No XDG_ACTIVATION_TOKEN set"; + return; + } + qputenv("XDG_ACTIVATION_TOKEN", ""); + + if (!m_actMgr || !m_actMgr->isInitialized()) { + qWarning() << "xdg_activation_v1 not available"; + return; + } + + ::wl_surface *surface = getWlSurface(this); + if (!surface) { + qWarning() << "Cannot get wl_surface for receiver"; + return; + } + + qInfo() << "Activating with token..."; + m_actMgr->activate(QString::fromUtf8(tokenEnv), surface); + } + +private: + XdgActivationManager *m_actMgr; + QPixmap m_image; +}; + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + + bool receiverMode = false; + for (int i = 1; i < argc; ++i) { + if (qstrcmp(argv[i], "--receiver") == 0) + receiverMode = true; + } + + XdgActivationManager actMgr; + WindowTransitionManager transitionMgr; + + if (receiverMode) { + ReceiverWindow window(&actMgr); + window.show(); + window.activateWithToken(); + return app.exec(); + } else { + SenderWindow window(&actMgr, &transitionMgr); + window.show(); + return app.exec(); + } +} + +#include "main.moc" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1eaf0d2c23..8822a4d1c2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -255,6 +255,7 @@ qt_add_qml_module(libtreeland core/qml/WorkspaceProxy.qml core/qml/FpsDisplay.qml core/qml/Animations/GeometryAnimation.qml + core/qml/Animations/WindowTransition.qml core/qml/Animations/NewAnimation.qml core/qml/Animations/MinimizeAnimation.qml core/qml/Animations/ShowDesktopAnimation.qml diff --git a/src/common/treelandlogging.cpp b/src/common/treelandlogging.cpp index c2a2f795a1..d6eba3844a 100644 --- a/src/common/treelandlogging.cpp +++ b/src/common/treelandlogging.cpp @@ -74,6 +74,9 @@ Q_LOGGING_CATEGORY(lcTlXsettings, "treeland.xsettings") // Activation module Q_LOGGING_CATEGORY(lcTlActivation, "treeland.activation") +// Window transition module +Q_LOGGING_CATEGORY(lcTlWindowTransition, "treeland.window.transition") + // App ID resolver Q_LOGGING_CATEGORY(lcTlAppIdResolver, "treeland.appid.resolver", QtInfoMsg) diff --git a/src/common/treelandlogging.h b/src/common/treelandlogging.h index f8a0d9df55..14d96e8abe 100644 --- a/src/common/treelandlogging.h +++ b/src/common/treelandlogging.h @@ -77,6 +77,9 @@ Q_DECLARE_LOGGING_CATEGORY(lcTlXsettings) // Activation module Q_DECLARE_LOGGING_CATEGORY(lcTlActivation) +// Window transition module +Q_DECLARE_LOGGING_CATEGORY(lcTlWindowTransition) + // App ID resolver Q_DECLARE_LOGGING_CATEGORY(lcTlAppIdResolver) diff --git a/src/core/qml/Animations/WindowTransition.qml b/src/core/qml/Animations/WindowTransition.qml new file mode 100644 index 0000000000..9100aee997 --- /dev/null +++ b/src/core/qml/Animations/WindowTransition.qml @@ -0,0 +1,136 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import QtQuick +import Treeland +import Waylib.Server +import QtQuick.Effects + +Item { + id: root + + required property SurfaceWrapper surface + required property rect fromGeometry + required property rect toGeometry + property int duration: 400 * Helper.animationSpeed + property real fadeDurationRatio: 0.5 + property var enableBlur: false + property var sourceBuffer: null + property int direction: 0 + + signal ready + signal finished + + x: fromGeometry.x + y: fromGeometry.y + width: fromGeometry.width + height: fromGeometry.height + + function start() { + animation.start(); + } + + XdgShadow { + anchors.fill: parent + visible: surface.visibleDecoration && !surface.noDecoration + cornerRadius: surface.radius + } + + Loader { + active: root.enableBlur + anchors.fill: parent + sourceComponent: Blur { + anchors.fill: parent + radius: surface.radius + } + } + + ShaderEffectSource { + id: backgroundEffect + + readonly property real xScale: root.width / surface.width + readonly property real yScale: root.height / surface.height + + live: true + sourceItem: surface + hideSource: true + sourceRect: surface.boundingRect + width: sourceRect.width * xScale + height: sourceRect.height * yScale + x: sourceRect.x * xScale + y: sourceRect.y * yScale + } + + BufferItem { + id: sourceImage + + anchors.fill: parent + visible: !!root.sourceBuffer + buffer: root.sourceBuffer + smooth: true + opacity: root.direction === 1 ? 1 : 0 + } + + ParallelAnimation { + id: animation + + XAnimator { + target: root + duration: root.duration + easing.type: Easing.OutExpo + from: root.fromGeometry.x + to: root.toGeometry.x + } + + YAnimator { + target: root + duration: root.duration + easing.type: Easing.OutExpo + from: root.fromGeometry.y + to: root.toGeometry.y + } + + PropertyAnimation { + target: root + property: "width" + duration: root.duration + easing.type: Easing.OutExpo + from: root.fromGeometry.width + to: root.toGeometry.width + } + + PropertyAnimation { + target: root + property: "height" + duration: root.duration + easing.type: Easing.OutExpo + from: root.fromGeometry.height + to: root.toGeometry.height + } + + SequentialAnimation { + PauseAnimation { + duration: root.direction === 1 ? 0 : root.duration * (1 - root.fadeDurationRatio) + } + PropertyAnimation { + target: sourceImage + property: "opacity" + duration: root.duration * root.fadeDurationRatio + from: root.direction === 1 ? 1 : 0 + to: root.direction === 1 ? 0 : 1 + easing.type: Easing.Linear + } + } + + onFinished: { + // Defer the signal emission to avoid deleting animation objects in the same callback stack. + Qt.callLater(function() { + root.finished(); + }) + } + } + + Component.onCompleted: { + root.ready(); + } +} diff --git a/src/core/qmlengine.cpp b/src/core/qmlengine.cpp index 3ddf1dbd93..18aa7df90a 100644 --- a/src/core/qmlengine.cpp +++ b/src/core/qmlengine.cpp @@ -43,6 +43,7 @@ QmlEngine::QmlEngine(QObject *parent) , xdgShadowComponent(this, "Treeland", "XdgShadow") , taskSwitchComponent(this, "Treeland", "TaskSwitcher") , geometryAnimationComponent(this, "Treeland", "GeometryAnimation") + , windowTransitionComponent(this, "Treeland", "WindowTransition") , menuBarComponent(this, "Treeland", "OutputMenuBar") , workspaceSwitcher(this, "Treeland", "WorkspaceSwitcher") , newAnimationComponent(this, "Treeland", "NewAnimation") @@ -150,6 +151,24 @@ QQuickItem *QmlEngine::createGeometryAnimation(SurfaceWrapper *surface, }); } +QQuickItem *QmlEngine::createWindowTransition(SurfaceWrapper *surface, + const QRectF &startGeo, + const QRectF &endGeo, + QQuickItem *parent, + wlr_buffer *sourceBuffer, + int direction) +{ + return createComponent(windowTransitionComponent, + parent, + { + { "surface", QVariant::fromValue(surface) }, + { "fromGeometry", QVariant::fromValue(startGeo) }, + { "toGeometry", QVariant::fromValue(endGeo) }, + { "sourceBuffer", QVariant::fromValue(sourceBuffer) }, + { "direction", QVariant::fromValue(direction) }, + }); +} + QQuickItem *QmlEngine::createMenuBar(WOutputItem *output, QQuickItem *parent) { return createComponent(menuBarComponent, parent, { { "output", QVariant::fromValue(output) } }); diff --git a/src/core/qmlengine.h b/src/core/qmlengine.h index 3492c14660..3f0bb0fcee 100644 --- a/src/core/qmlengine.h +++ b/src/core/qmlengine.h @@ -45,6 +45,12 @@ class QmlEngine : public QQmlApplicationEngine const QRectF &startGeo, const QRectF &endGeo, QQuickItem *parent); + QQuickItem *createWindowTransition(SurfaceWrapper *surface, + const QRectF &startGeo, + const QRectF &endGeo, + QQuickItem *parent, + wlr_buffer *sourceBuffer = nullptr, + int direction = 0); QQuickItem *createMenuBar(WOutputItem *output, QQuickItem *parent); QQuickItem *createWorkspaceSwitcher(Workspace *parent); QQuickItem *createNewAnimation(SurfaceWrapper *surface, QQuickItem *parent, uint direction); @@ -89,6 +95,7 @@ class QmlEngine : public QQmlApplicationEngine QQmlComponent xdgShadowComponent; QQmlComponent taskSwitchComponent; QQmlComponent geometryAnimationComponent; + QQmlComponent windowTransitionComponent; QQmlComponent menuBarComponent; QQmlComponent workspaceSwitcher; QQmlComponent newAnimationComponent; diff --git a/src/modules/CMakeLists.txt b/src/modules/CMakeLists.txt index 8b97e59fcd..cefee7025f 100644 --- a/src/modules/CMakeLists.txt +++ b/src/modules/CMakeLists.txt @@ -42,6 +42,7 @@ add_subdirectory(wine-window-state) add_subdirectory(wine-window-management) add_subdirectory(wallpaper) add_subdirectory(activation) +add_subdirectory(window-transition) add_subdirectory(input-manager) add_subdirectory(keyboard-state-notify) add_subdirectory(keyboard-shortcuts-inhibit) diff --git a/src/modules/activation/activationmanagerinterfacev1.cpp b/src/modules/activation/activationmanagerinterfacev1.cpp index fa7a335ac0..5fa242c693 100644 --- a/src/modules/activation/activationmanagerinterfacev1.cpp +++ b/src/modules/activation/activationmanagerinterfacev1.cpp @@ -46,6 +46,11 @@ class TokenContext : public QtWaylandServer::xdg_activation_token_v1 { } + struct ::wl_resource *tokenResourceHandle() const + { + return resource()->handle; + } + protected: void destroy(Resource *resource) override { @@ -118,6 +123,7 @@ struct TokenInfo bool fromTrustedSurface = false; // set_surface called and surface was active at commit time QDeadlineTimer expiry; // invalidated 60 s after registration QPointer seat; // seat associated with the token via set_serial + QPointer originatingSurface; // surface set via set_surface }; class ActivationManagerInterfaceV1Private @@ -145,11 +151,18 @@ class ActivationManagerInterfaceV1Private wl_client *client, std::optional serial, bool fromTrustedSurface, - WSeat *seat) + WSeat *seat, + WSurface *originatingSurface) { const QString token = QUuid::createUuid().toString(QUuid::WithoutBraces); - m_tokens.append(TokenInfo{ token, appId, client, serial, fromTrustedSurface, - QDeadlineTimer(TokenLifetimeMs), seat }); + m_tokens.append(TokenInfo{ token, + appId, + client, + serial, + fromTrustedSurface, + QDeadlineTimer(TokenLifetimeMs), + seat, + originatingSurface }); qCDebug(lcTlActivation) << "Registered activation token" << token.left(8) + u"..."_s << "for app" << appId << (fromTrustedSurface ? "" : "(inactive-surface-token-request)"); @@ -163,6 +176,8 @@ class ActivationManagerInterfaceV1Private return false; // conservative: no checker → treat as inactive } + ActivationManagerInterfaceV1::WindowTransitionRectConsumer windowTransitionRectConsumer; + protected: void destroy_global() override { @@ -177,9 +192,9 @@ class ActivationManagerInterfaceV1Private void get_activation_token(Resource *resource, uint32_t id) override { auto *tokenResource = wl_resource_create(resource->client(), - &xdg_activation_token_v1_interface, - wl_resource_get_version(resource->handle), - id); + &xdg_activation_token_v1_interface, + resource->version(), + id); if (!tokenResource) { wl_resource_post_no_memory(resource->handle); return; @@ -206,41 +221,37 @@ class ActivationManagerInterfaceV1Private return; } - auto disposition = dispositionForToken(token); - // Retrieve the seat associated with the token before consuming it. + // Single lookup: resolve disposition, seat and originating surface from + // the same token iterator (avoids a second linear scan). + ActivationManagerInterfaceV1::TokenDisposition disposition = + ActivationManagerInterfaceV1::TokenDisposition::Invalid; WSeat *tokenSeat = nullptr; + WSurface *originatingSurface = nullptr; + auto it = std::find_if(m_tokens.begin(), m_tokens.end(), [&token](const TokenInfo &t) { return t.token == token; }); if (it != m_tokens.end()) { tokenSeat = it->seat.data(); - m_tokens.erase(it); + originatingSurface = it->originatingSurface; + disposition = it->fromTrustedSurface + ? (it->serial.has_value() + ? ActivationManagerInterfaceV1::TokenDisposition::Active + : ActivationManagerInterfaceV1::TokenDisposition::Attention) + : ActivationManagerInterfaceV1::TokenDisposition::Attention; } - qCInfo(lcTlActivation) << "activate: emitting activateRequested for token" << token.left(8) + u"..."_s - << "with disposition" << disposition; - Q_EMIT q->activateRequested(disposition, wsurface, tokenSeat); - } + qCInfo(lcTlActivation) << "activate: emitting activateRequested for token" + << token.left(8) + u"..."_s << "with disposition" << disposition; -private: - ActivationManagerInterfaceV1::TokenDisposition dispositionForToken(const QString &token) const - { - auto it = std::find_if(m_tokens.cbegin(), m_tokens.cend(), - [&token](const TokenInfo &t) { return t.token == token; }); - if (it == m_tokens.cend()) { - return ActivationManagerInterfaceV1::TokenDisposition::Invalid; - } - if (it->expiry.hasExpired()) { - return ActivationManagerInterfaceV1::TokenDisposition::Invalid; - } - // inactive-surface-token-request: set_surface not called or surface was not active - // → treat as Attention - if (!it->fromTrustedSurface) { - return ActivationManagerInterfaceV1::TokenDisposition::Attention; + // Keep token one-shot semantics + if (it != m_tokens.end()) { + m_tokens.erase(it); } - return it->serial.has_value() ? ActivationManagerInterfaceV1::TokenDisposition::Active - : ActivationManagerInterfaceV1::TokenDisposition::Attention; + + Q_EMIT q->activateRequested(token, disposition, wsurface, tokenSeat, originatingSurface); } +private: void sweepExpiredTokens() { auto it = m_tokens.begin(); @@ -285,7 +296,17 @@ void TokenContext::commit(Resource *resource) // fromTrustedSurface: set_surface was called, surface still alive, and currently active const bool fromTrustedSurface = m_surface && m_manager->isTrustedSurface(m_surface, m_seat); - const QString token = m_manager->registerToken(m_appId, resource->client(), m_serial, fromTrustedSurface, m_seat); + + const QString token = m_manager->registerToken(m_appId, + resource->client(), + m_serial, + fromTrustedSurface, + m_seat, + m_surface); + + if (m_surface && m_manager->windowTransitionRectConsumer) + m_manager->windowTransitionRectConsumer(token, tokenResourceHandle()); + send_done(token); } @@ -309,6 +330,12 @@ QByteArrayView ActivationManagerInterfaceV1::interfaceName() const return d->interfaceName(); } +void ActivationManagerInterfaceV1::setWindowTransitionRectConsumer( + WindowTransitionRectConsumer consumer) +{ + d->windowTransitionRectConsumer = std::move(consumer); +} + void ActivationManagerInterfaceV1::create(WServer *server) { d->init(server->handle(), InterfaceVersion); diff --git a/src/modules/activation/activationmanagerinterfacev1.h b/src/modules/activation/activationmanagerinterfacev1.h index d37519bdf2..40429cda55 100644 --- a/src/modules/activation/activationmanagerinterfacev1.h +++ b/src/modules/activation/activationmanagerinterfacev1.h @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -29,6 +30,9 @@ class ActivationManagerInterfaceV1 : public QObject, public WAYLIB_SERVER_NAMESP }; Q_ENUM(TokenDisposition) + using WindowTransitionRectConsumer = + std::function; + explicit ActivationManagerInterfaceV1( std::function trustedSurfaceChecker, QObject *parent = nullptr); @@ -38,11 +42,19 @@ class ActivationManagerInterfaceV1 : public QObject, public WAYLIB_SERVER_NAMESP static constexpr int InterfaceVersion = 1; + void setWindowTransitionRectConsumer(WindowTransitionRectConsumer consumer); + Q_SIGNALS: // The server emits one signal with precomputed disposition for policy handling. - void activateRequested(TokenDisposition disposition, + // token is the committed activation token string. + // seat is the WSeat associated with the token via set_serial, or null. + // originatingSurface is the WSurface that was set via set_surface on the + // token (the surface that initiated the launch), or null. + void activateRequested(const QString &token, + TokenDisposition disposition, WAYLIB_SERVER_NAMESPACE::WSurface *surface, - WAYLIB_SERVER_NAMESPACE::WSeat *seat); + WAYLIB_SERVER_NAMESPACE::WSeat *seat, + WAYLIB_SERVER_NAMESPACE::WSurface *originatingSurface); protected: void create(WAYLIB_SERVER_NAMESPACE::WServer *server) override; diff --git a/src/modules/window-transition/CMakeLists.txt b/src/modules/window-transition/CMakeLists.txt new file mode 100644 index 0000000000..aed55f7a01 --- /dev/null +++ b/src/modules/window-transition/CMakeLists.txt @@ -0,0 +1,18 @@ +find_package(TreelandProtocols 0.5.10 REQUIRED) + +waylib_generate_qtwayland_server_protocol(libtreeland + PROTOCOL ${TREELAND_PROTOCOLS_DATA_DIR}/treeland-window-transition-unstable-v1.xml + BASENAME treeland-window-transition-unstable-v1 +) + +impl_treeland( + NAME + module_window_transition + SOURCE + ${CMAKE_SOURCE_DIR}/src/modules/window-transition/windowtransitionmanagerinterfacev1.h + ${CMAKE_SOURCE_DIR}/src/modules/window-transition/windowtransitionmanagerinterfacev1.cpp + INCLUDE + $ + LINK + Qt6::Core +) diff --git a/src/modules/window-transition/windowtransitionmanagerinterfacev1.cpp b/src/modules/window-transition/windowtransitionmanagerinterfacev1.cpp new file mode 100644 index 0000000000..b462f923e7 --- /dev/null +++ b/src/modules/window-transition/windowtransitionmanagerinterfacev1.cpp @@ -0,0 +1,397 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "windowtransitionmanagerinterfacev1.h" + +#include "common/treelandlogging.h" +#include "modules/activation/wayland-xdg-activation-v1-server-protocol.h" +#include "qwayland-server-treeland-window-transition-unstable-v1.h" +#include "surface/surfacewrapper.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +WAYLIB_SERVER_USE_NAMESPACE +using namespace Qt::StringLiterals; + +namespace { +// A consumed transition rect stays pending for the same lifetime as an +// activation token; sweep it once that lifetime elapses so a token that is +// committed but never activated does not leak the rect object. +constexpr int kPendingRectLifetimeMs = 60'000; +} + +class WindowTransitionManagerInterfaceV1Private; + +class WindowTransitionRectV1 : public QtWaylandServer::treeland_window_transition_rect_v1 +{ + friend class WindowTransitionManagerInterfaceV1; + +public: + WindowTransitionRectV1(WindowTransitionManagerInterfaceV1Private *manager, + wl_resource *tokenResource, + wl_resource *resource) + : QtWaylandServer::treeland_window_transition_rect_v1(resource) + , m_manager(manager) + , m_tokenResource(tokenResource) + { + if (m_tokenResource) { + m_tokenDestroyListener.notify = tokenResourceDestroyed; + wl_resource_add_destroy_listener(m_tokenResource, &m_tokenDestroyListener); + } + } + + ~WindowTransitionRectV1(); + + void notifyDiscarded() + { + send_closed(); + } + + bool isExpired() const + { + return m_expiry.hasExpired(); + } + + void associate(SurfaceWrapper *targetWrapper, SurfaceWrapper *originWrapper) + { + m_targetWrapper = targetWrapper; + if (m_targetWrapper) { + m_targetWrapper->setWindowTransitionRect(m_rect, originWrapper); + m_targetWrapper->setWindowTransitionSourceImage(m_sourceImage); + + m_targetDestroyedConnection = + QObject::connect(m_targetWrapper.data(), &QObject::destroyed, [this] { + send_closed(); + }); + } + } + +protected: + void destroy(Resource *resource) override + { + wl_resource_destroy(resource->handle); + } + + void destroy_resource(Resource *) override + { + delete this; + } + + void set_geometry(Resource * /*resource*/, + int32_t x, + int32_t y, + int32_t width, + int32_t height) override; + void set_source_buffer(Resource * /*resource*/, struct ::wl_resource *buffer) override; + +private: + static void tokenResourceDestroyed(wl_listener *listener, void *data); + + bool applySourceBuffer(Resource *resource, struct ::wl_resource *buffer); + + WindowTransitionManagerInterfaceV1Private *m_manager; + wl_resource *m_tokenResource; + QString m_token; + QDeadlineTimer m_expiry; + wl_listener m_tokenDestroyListener; + QRectF m_rect; + QImage m_sourceImage; + uint m_geometrySet : 1 = 0; + uint m_consumed : 1 = 0; + QMetaObject::Connection + m_targetDestroyedConnection; + QPointer m_targetWrapper; +}; + +class WindowTransitionManagerInterfaceV1Private + : public QtWaylandServer::treeland_window_transition_manager_v1 +{ +public: + explicit WindowTransitionManagerInterfaceV1Private() + : QtWaylandServer::treeland_window_transition_manager_v1() + { + } + + wl_global *globalHandle() const + { + return m_global; + } + + QHash m_committedRects; + QHash m_pendingRects; + +protected: + void destroy_global() override + { + qCDebug(lcTlWindowTransition) << "treeland_window_transition_manager_v1 global destroyed"; + } + + void destroy(Resource *resource) override + { + wl_resource_destroy(resource->handle); + } + + void get_window_transition_rect(Resource *resource, + uint32_t rect_id, + struct ::wl_resource *token) override + { + if (!token || wl_resource_get_interface(token) != &xdg_activation_token_v1_interface) { + wl_resource_post_error(resource->handle, + WL_DISPLAY_ERROR_INVALID_OBJECT, + "get_window_transition_rect: token is not an" + " xdg_activation_token_v1"); + return; + } + auto *rectResource = wl_resource_create(resource->client(), + &treeland_window_transition_rect_v1_interface, + resource->version(), + rect_id); + if (!rectResource) { + wl_resource_post_no_memory(resource->handle); + return; + } + new WindowTransitionRectV1(this, token, rectResource); + } +}; + +WindowTransitionRectV1::~WindowTransitionRectV1() +{ + if (m_targetDestroyedConnection) + QObject::disconnect(m_targetDestroyedConnection); + + if (m_targetWrapper) { + m_targetWrapper->clearWindowTransitionRect(); + m_targetWrapper->clearWindowTransitionSourceImage(); + } + + if (m_manager && !m_token.isEmpty()) + m_manager->m_pendingRects.remove(m_token); + + if (m_tokenResource) { + wl_list_remove(&m_tokenDestroyListener.link); + if (m_manager && m_geometrySet && !m_consumed) + m_manager->m_committedRects.remove(m_tokenResource); + } +} + +void WindowTransitionRectV1::tokenResourceDestroyed(wl_listener *listener, void * /*data*/) +{ + WindowTransitionRectV1 *self; + self = wl_container_of(listener, self, m_tokenDestroyListener); + if (self->m_manager && self->m_geometrySet && !self->m_consumed) + self->m_manager->m_committedRects.remove(self->m_tokenResource); + self->m_tokenResource = nullptr; + wl_list_remove(&self->m_tokenDestroyListener.link); + wl_list_init(&self->m_tokenDestroyListener.link); +} + +void WindowTransitionRectV1::set_geometry(Resource *resource, + int32_t x, + int32_t y, + int32_t width, + int32_t height) +{ + if (width < 0 || height < 0) { + wl_resource_post_error(resource->handle, + error_invalid_geometry, + "set_geometry with negative size %dx%d", + width, + height); + return; + } + m_rect = QRectF(x, y, width, height); + m_geometrySet = true; + + if (m_targetWrapper) + m_targetWrapper->updateWindowTransitionRect(m_rect); + + if (!m_consumed && m_tokenResource && m_manager) + m_manager->m_committedRects.insert(m_tokenResource, this); +} + +void WindowTransitionRectV1::set_source_buffer(Resource *resource, struct ::wl_resource *buffer) +{ + if (!buffer) { + m_sourceImage = QImage(); + if (m_targetWrapper) + m_targetWrapper->setWindowTransitionSourceImage(m_sourceImage); + return; + } + applySourceBuffer(resource, buffer); +} + +bool WindowTransitionRectV1::applySourceBuffer(Resource *resource, + struct ::wl_resource *bufferResource) +{ + wlr_buffer *buffer = wlr_buffer_try_from_resource(bufferResource); + if (!buffer) { + wl_resource_post_error(resource->handle, + error_invalid_buffer, + "set_source_buffer: buffer could not be imported"); + return false; + } + + void *data = nullptr; + uint32_t format = 0; + size_t stride = 0; + if (!wlr_buffer_begin_data_ptr_access(buffer, + WLR_BUFFER_DATA_PTR_ACCESS_READ, + &data, + &format, + &stride)) { + wl_resource_post_error(resource->handle, + error_invalid_buffer, + "set_source_buffer: buffer content is not CPU-readable"); + wlr_buffer_unlock(buffer); + return false; + } + + const QImage::Format qFormat = WTools::toImageFormat(format); + if (qFormat == QImage::Format_Invalid) { + wl_resource_post_error(resource->handle, + error_invalid_buffer, + "set_source_buffer: unsupported buffer format 0x%x", + format); + wlr_buffer_end_data_ptr_access(buffer); + wlr_buffer_unlock(buffer); + return false; + } + + QImage image(buffer->width, buffer->height, qFormat); + if (!image.isNull() && image.bytesPerLine() == static_cast(stride)) { + memcpy(image.bits(), data, static_cast(buffer->height) * stride); + } else if (!image.isNull()) { + // Stride mismatch (e.g. row padding): copy row by row. + const int copyBytes = qMin(image.bytesPerLine(), stride); + for (int y = 0; y < buffer->height; ++y) { + memcpy(image.scanLine(y), + static_cast(data) + static_cast(y) * stride, + copyBytes); + } + } + wlr_buffer_end_data_ptr_access(buffer); + wlr_buffer_unlock(buffer); + + if (image.isNull()) { + wl_resource_post_error(resource->handle, + error_invalid_buffer, + "set_source_buffer: failed to allocate image"); + return false; + } + + m_sourceImage = image; + if (m_targetWrapper) + m_targetWrapper->setWindowTransitionSourceImage(m_sourceImage); + return true; +} + +WindowTransitionManagerInterfaceV1::WindowTransitionManagerInterfaceV1(QObject *parent) + : QObject(parent) + , WServerInterface() + , d(new WindowTransitionManagerInterfaceV1Private()) + , m_pendingSweepTimer(this) +{ + m_pendingSweepTimer.setInterval(kPendingRectLifetimeMs / 2); + connect(&m_pendingSweepTimer, + &QTimer::timeout, + this, + &WindowTransitionManagerInterfaceV1::sweepPendingRects); +} + +WindowTransitionManagerInterfaceV1::~WindowTransitionManagerInterfaceV1() = default; + +QByteArrayView WindowTransitionManagerInterfaceV1::interfaceName() const +{ + return d->interfaceName(); +} + +void WindowTransitionManagerInterfaceV1::ensurePendingSweepRunning() +{ + if (!d->m_pendingRects.isEmpty() && !m_pendingSweepTimer.isActive()) + m_pendingSweepTimer.start(); +} + +void WindowTransitionManagerInterfaceV1::sweepPendingRects() +{ + for (auto it = d->m_pendingRects.begin(); it != d->m_pendingRects.end();) { + if (it.value()->isExpired()) { + it.value()->notifyDiscarded(); + it = d->m_pendingRects.erase(it); + } else { + ++it; + } + } + if (d->m_pendingRects.isEmpty()) + m_pendingSweepTimer.stop(); +} + +void WindowTransitionManagerInterfaceV1::takeCommittedRect(const QString &token, + wl_resource *tokenResource) +{ + if (!tokenResource) + return; + auto it = d->m_committedRects.find(tokenResource); + if (it == d->m_committedRects.end()) + return; + auto *rectObj = it.value(); + d->m_committedRects.erase(it); + rectObj->m_consumed = true; + rectObj->m_token = token; + rectObj->m_expiry = QDeadlineTimer(kPendingRectLifetimeMs); + d->m_pendingRects.insert(token, rectObj); + ensurePendingSweepRunning(); +} + +bool WindowTransitionManagerInterfaceV1::hasPendingWindowTransitionRect(const QString &token) const +{ + return d->m_pendingRects.contains(token); +} + +bool WindowTransitionManagerInterfaceV1::associatePendingRect(const QString &token, + SurfaceWrapper *targetWrapper, + SurfaceWrapper *originWrapper) +{ + auto it = d->m_pendingRects.find(token); + if (it == d->m_pendingRects.end()) + return false; + auto *rectObj = it.value(); + d->m_pendingRects.erase(it); + rectObj->associate(targetWrapper, originWrapper); + return true; +} + +void WindowTransitionManagerInterfaceV1::discardPendingRect(const QString &token) +{ + auto it = d->m_pendingRects.find(token); + if (it == d->m_pendingRects.end()) + return; + auto *rectObj = it.value(); + d->m_pendingRects.erase(it); + rectObj->notifyDiscarded(); +} + +void WindowTransitionManagerInterfaceV1::create(WServer *server) +{ + d->init(server->handle(), InterfaceVersion); +} + +void WindowTransitionManagerInterfaceV1::destroy([[maybe_unused]] WServer *server) +{ + d->globalRemove(); +} + +wl_global *WindowTransitionManagerInterfaceV1::global() const +{ + return d->globalHandle(); +} diff --git a/src/modules/window-transition/windowtransitionmanagerinterfacev1.h b/src/modules/window-transition/windowtransitionmanagerinterfacev1.h new file mode 100644 index 0000000000..5aac5d61ee --- /dev/null +++ b/src/modules/window-transition/windowtransitionmanagerinterfacev1.h @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include + +#include +#include +#include +#include + +#include + +class SurfaceWrapper; +class WindowTransitionManagerInterfaceV1Private; + +class WindowTransitionManagerInterfaceV1 + : public QObject + , public WAYLIB_SERVER_NAMESPACE::WServerInterface +{ + Q_OBJECT +public: + explicit WindowTransitionManagerInterfaceV1(QObject *parent = nullptr); + ~WindowTransitionManagerInterfaceV1() override; + + QByteArrayView interfaceName() const override; + + static constexpr int InterfaceVersion = 1; + + void takeCommittedRect(const QString &token, wl_resource *tokenResource); + void discardPendingRect(const QString &token); + bool hasPendingWindowTransitionRect(const QString &token) const; + bool associatePendingRect(const QString &token, + SurfaceWrapper *targetWrapper, + SurfaceWrapper *originWrapper); + +protected: + void create(WAYLIB_SERVER_NAMESPACE::WServer *server) override; + void destroy(WAYLIB_SERVER_NAMESPACE::WServer *server) override; + wl_global *global() const override; + +private: + friend class WindowTransitionRectV1; + std::unique_ptr d; + QTimer m_pendingSweepTimer; + void ensurePendingSweepRunning(); + void sweepPendingRects(); +}; diff --git a/src/seat/helper.cpp b/src/seat/helper.cpp index 0f8b7cf443..9964f86949 100644 --- a/src/seat/helper.cpp +++ b/src/seat/helper.cpp @@ -2215,6 +2215,8 @@ void Helper::init(Treeland::Treeland *treeland) m_idleInhibitManager = wlr_idle_inhibit_v1_create(m_server->handle()); listeners()->add(&m_idleInhibitManager->events.new_inhibitor, this, &Helper::onNewIdleInhibitor); + m_windowTransitionManagerV1 = m_server->attach(); + m_activationManagerV1 = m_server->attach( [this](WSurface *surface, WSeat *seat) -> bool { // Determine whether the surface can transfer activation for the same seat @@ -2242,19 +2244,61 @@ void Helper::init(Treeland::Treeland *treeland) return false; }); + m_activationManagerV1->setWindowTransitionRectConsumer( + [this](const QString &token, wl_resource *tokenResource) { + if (!m_windowTransitionManagerV1) + return; + m_windowTransitionManagerV1->takeCommittedRect(token, tokenResource); + }); connect(m_activationManagerV1, &ActivationManagerInterfaceV1::activateRequested, this, - [this](ActivationManagerInterfaceV1::TokenDisposition disposition, WSurface *wsurface, WSeat *seat) { + [this](const QString &token, + ActivationManagerInterfaceV1::TokenDisposition disposition, + WSurface *wsurface, + WSeat *seat, + WSurface *originatingSurface) { auto wrapper = m_rootSurfaceContainer->getSurface(wsurface); if (!wrapper) { qCWarning(lcTlCore) << "Activation request for unknown surface!"; return; } - // Don't use hasActiveCapability() here — it also checks UnMinimized, - // but minimized windows should be allowed to activate (which unminimizes them). + if (originatingSurface && m_windowTransitionManagerV1 + && m_windowTransitionManagerV1->hasPendingWindowTransitionRect(token)) { + if (wsurface->mapped()) { + qCWarning(lcTlWindowTransition) + << "Window transition rect discarded: target surface" + << "is already mapped (client activation arrived too late)"; + // Discard the pending rect object if any. + m_windowTransitionManagerV1->discardPendingRect(token); + } else { + auto *originWrapper = + m_rootSurfaceContainer->getSurface(originatingSurface); + if (originWrapper) { + bool associated = + m_windowTransitionManagerV1->associatePendingRect(token, + wrapper, + originWrapper); + if (!associated) { + qCWarning(lcTlWindowTransition) + << "Window transition rect was destroyed" + << "before activation, using default animation"; + } + } else { + qCWarning(lcTlWindowTransition) + << "Window transition: originating surface wrapper" + << "not found, skipping"; + // Discard the pending rect object. + m_windowTransitionManagerV1->discardPendingRect(token); + } + } + } + if (!wsurface->mapped()) { - qCWarning(lcTlCore) << "Activation request for unmapped surface!"; + if (disposition == ActivationManagerInterfaceV1::TokenDisposition::Active) + wrapper->setPendingActivation(seat); + else + qCWarning(lcTlCore) << "Activation request for unmapped surface!"; return; } if (!wrapper->hasInitializeContainer()) { diff --git a/src/seat/helper.h b/src/seat/helper.h index 82e9680241..3259e04c1e 100644 --- a/src/seat/helper.h +++ b/src/seat/helper.h @@ -11,6 +11,7 @@ #include "modules/wallpaper/wallpapermanagerinterfacev1.h" #include "modules/wallpaper/wallpapernotifierinterfacev1.h" #include "modules/show-desktop/showdesktopinterfacev1.h" +#include "modules/window-transition/windowtransitionmanagerinterfacev1.h" #include "utils/fpsdisplaymanager.h" #include @@ -459,6 +460,7 @@ private Q_SLOTS: }; std::vector seatDragEntries; ActivationManagerInterfaceV1 *m_activationManagerV1 = nullptr; + WindowTransitionManagerInterfaceV1 *m_windowTransitionManagerV1 = nullptr; ShellHandler *m_shellHandler = nullptr; WXdgDecorationManager *m_xdgDecorationManager = nullptr; WXdgDialogManagerV1 *m_xdgDialogManagerV1 = nullptr; diff --git a/src/surface/surfacewrapper.cpp b/src/surface/surfacewrapper.cpp index 76959b7a38..34079966e4 100644 --- a/src/surface/surfacewrapper.cpp +++ b/src/surface/surfacewrapper.cpp @@ -12,6 +12,7 @@ #include "wtoplevelsurface.h" #include "wxdgtoplevelsurface.h" +#include #include #include #include @@ -25,6 +26,7 @@ #include #include +#include #include #include @@ -202,6 +204,15 @@ void SurfaceWrapper::invalidate() m_wrapperAboutToRemove = true; Q_EMIT aboutToBeInvalidated(); + // Drop any pending window transition rect so it cannot be consumed later by + // a stale mapping path on this wrapper. + m_windowTransitionPending = false; + m_windowTransitionLocalRect = QRectF(); + m_windowTransitionOriginWrapper = nullptr; + m_hasWindowTransitionRect = false; + m_pendingActivation = false; + m_pendingActivationSeat.clear(); + if (!m_skipDockPreView) setSkipDockPreView(true); @@ -230,6 +241,14 @@ void SurfaceWrapper::invalidate() m_surfaceItem->disconnect(this); } +void SurfaceWrapper::dropWindowTransitionSourceBuffer() +{ + if (m_windowTransitionSourceBuffer) { + wlr_buffer_drop(m_windowTransitionSourceBuffer); + m_windowTransitionSourceBuffer = nullptr; + } +} + SurfaceWrapper::~SurfaceWrapper() { if (!m_wrapperAboutToRemove) { @@ -252,6 +271,7 @@ SurfaceWrapper::~SurfaceWrapper() delete m_geometryAnimation; m_geometryAnimation = nullptr; } + dropWindowTransitionSourceBuffer(); if (m_windowAnimation) { delete m_windowAnimation; m_windowAnimation = nullptr; @@ -1294,6 +1314,22 @@ bool SurfaceWrapper::setAttention(bool attention) return true; } +void SurfaceWrapper::setPendingActivation(WSeat *seat) +{ + m_pendingActivation = true; + m_pendingActivationSeat = seat; +} + +bool SurfaceWrapper::takePendingActivation(WSeat *&seat) +{ + if (!m_pendingActivation) + return false; + m_pendingActivation = false; + seat = m_pendingActivationSeat.data(); + m_pendingActivationSeat.clear(); + return true; +} + bool SurfaceWrapper::isInputPopupLike() const { return m_type == Type::InputPopup || m_isIMCandidatePanel; @@ -1486,17 +1522,164 @@ void SurfaceWrapper::geometryChange(const QRectF &newGeo, const QRectF &oldGeome updateClipRect(); } +void SurfaceWrapper::tryFlushPendingActivation() +{ + if (!m_pendingActivation) + return; + + if (!hasInitializeContainer()) { + connect(this, + &SurfaceWrapper::hasInitializeContainerChanged, + this, + &SurfaceWrapper::tryFlushPendingActivation, + Qt::SingleShotConnection); + return; + } + + WSeat *seat = nullptr; + if (takePendingActivation(seat)) + Helper::instance()->forceActivateSurface(this, Qt::OtherFocusReason, seat); +} + +void SurfaceWrapper::startWindowTransition() +{ + if (!m_hasWindowTransitionRect) + return; + m_windowTransitionPending = true; + + if (!m_windowAnimation) { + auto globalRectOpt = computeGlobalWindowTransitionRect(); + if (!globalRectOpt) { + // Origin wrapper or its surface item is gone — fall back to default. + m_windowTransitionPending = false; + m_hasWindowTransitionRect = false; + createNewOrClose(OPEN_ANIMATION); + return; + } + const QRectF fromGeometry = *globalRectOpt; + + if (!m_windowTransitionSourceImage.isNull()) + m_windowTransitionSourceBuffer = + WImageBufferImpl::create(m_windowTransitionSourceImage); + + m_windowAnimation = m_engine->createWindowTransition(this, + fromGeometry, + QRectF(), // will update later + container(), + m_windowTransitionSourceBuffer, + OPEN_ANIMATION); + m_windowAnimation->setProperty("enableBlur", m_blur); + } + + // Wait until the surface item reports ready before computing the target + // geometry. + if (!m_surfaceItem->isReady()) { + connect(m_surfaceItem, + &WSurfaceItem::readyChanged, + this, + &SurfaceWrapper::startWindowTransition, + Qt::SingleShotConnection); + return; + } + + // Defer to the next event-loop iteration so that the window's final + // position and size has set before we sample them. + QTimer::singleShot(0, this, &SurfaceWrapper::finishWindowTransitionOpen); +} + +void SurfaceWrapper::finishWindowTransitionOpen() +{ + m_windowTransitionPending = false; + // The surface may have unmapped while we were waiting. Drop the + // component we created above so it does not block a later close path. + if (m_wrapperAboutToRemove || !surface() || !surface()->mapped()) { + if (m_windowAnimation) { + m_windowAnimation->deleteLater(); + m_windowAnimation = nullptr; + } + dropWindowTransitionSourceBuffer(); + return; + } + + // For maximized/fullscreen the target is the state geometry. + const QRectF stateGeometry = targetGeometryForState(m_surfaceState); + const bool useStateGeometry = + (m_surfaceState == State::Maximized || m_surfaceState == State::Fullscreen) + && stateGeometry.isValid(); + const QRectF toGeometry = useStateGeometry ? stateGeometry : QRectF(position(), size()); + + m_windowAnimation->setProperty("toGeometry", toGeometry); + + if (Helper::instance()->noAnimation()) { + onShowAnimationFinished(); + return; + } + bool ok = connect(m_windowAnimation, SIGNAL(finished()), this, SLOT(onShowAnimationFinished())); + Q_ASSERT(ok); + ok = QMetaObject::invokeMethod(m_windowAnimation, "start"); + Q_ASSERT(ok); + Q_EMIT windowAnimationRunningChanged(); +} + +void SurfaceWrapper::startWindowCloseTransition() +{ + Q_ASSERT(m_hasWindowTransitionRect); + + auto globalRectOpt = computeGlobalWindowTransitionRect(); + if (!globalRectOpt) { + // Origin wrapper or its surface item is gone — fall back to default. + m_hasWindowTransitionRect = false; + createNewOrClose(CLOSE_ANIMATION); + return; + } + + const QRectF fromGeometry = QRectF(position(), size()); + const QRectF toGeometry = *globalRectOpt; + + if (!m_windowTransitionSourceImage.isNull()) + m_windowTransitionSourceBuffer = WImageBufferImpl::create(m_windowTransitionSourceImage); + + m_windowAnimation = m_engine->createWindowTransition(this, + fromGeometry, + toGeometry, + container(), + m_windowTransitionSourceBuffer, + CLOSE_ANIMATION); + m_windowAnimation->setProperty("enableBlur", m_blur); + + if (Helper::instance()->noAnimation()) { + onHideAnimationFinished(); + return; + } + + bool ok = connect(m_windowAnimation, SIGNAL(finished()), this, SLOT(onHideAnimationFinished())); + Q_ASSERT(ok); + ok = QMetaObject::invokeMethod(m_windowAnimation, "start"); + Q_ASSERT(ok); + Q_EMIT windowAnimationRunningChanged(); +} + void SurfaceWrapper::createNewOrClose(uint direction) { if (!m_windowAnimationEnabled) return; - if (m_windowAnimation) + if (m_windowAnimation || m_windowTransitionPending) return; if (m_container.isNull()) return; + if (direction == OPEN_ANIMATION && windowTransitionRectActive()) { + startWindowTransition(); + return; + } + + if (direction == CLOSE_ANIMATION && windowTransitionRectActive()) { + startWindowCloseTransition(); + return; + } + switch (m_type) { case Type::SplashScreen: [[fallthrough]]; @@ -1719,6 +1902,11 @@ void SurfaceWrapper::onWindowAnimationFinished() m_windowAnimation->deleteLater(); m_windowAnimation = nullptr; + // The QML BufferItem holds a lock on the buffer (set during component + // creation), so it stays alive until the animation item is deleted by the + // deleteLater above. + dropWindowTransitionSourceBuffer(); + Q_EMIT windowAnimationRunningChanged(); if (m_wrapperAboutToRemove) { @@ -1762,6 +1950,7 @@ void SurfaceWrapper::onMappedChanged() if (!m_prelaunchSplash) { createNewOrClose(OPEN_ANIMATION); } else { + m_hasWindowTransitionRect = false; syncPrelaunchMappedState(); startPrelaunchSplashHideSequence(); } @@ -1773,6 +1962,9 @@ void SurfaceWrapper::onMappedChanged() } } + if (mapped) + tryFlushPendingActivation(); + if (m_coverContent) { m_coverContent->setProperty("mapped", mapped); } @@ -2521,6 +2713,61 @@ void SurfaceWrapper::disableWindowAnimation(bool disable) m_windowAnimationEnabled = !disable; } +void SurfaceWrapper::setWindowTransitionRect(const QRectF &localRect, SurfaceWrapper *originWrapper) +{ + // The rect is honored only when the surface is not yet mapped. + if (surface() && surface()->mapped()) + return; + + m_windowTransitionLocalRect = localRect; + m_windowTransitionOriginWrapper = originWrapper; + m_hasWindowTransitionRect = true; +} + +void SurfaceWrapper::updateWindowTransitionRect(const QRectF &localRect) +{ + m_windowTransitionLocalRect = localRect; +} + +void SurfaceWrapper::clearWindowTransitionRect() +{ + m_hasWindowTransitionRect = false; + m_windowTransitionLocalRect = QRectF(); + m_windowTransitionOriginWrapper = nullptr; +} + +void SurfaceWrapper::setWindowTransitionSourceImage(const QImage &image) +{ + m_windowTransitionSourceImage = image; +} + +void SurfaceWrapper::clearWindowTransitionSourceImage() +{ + m_windowTransitionSourceImage = QImage(); +} + +bool SurfaceWrapper::hasWindowTransitionRect() const +{ + return m_hasWindowTransitionRect; +} + +bool SurfaceWrapper::windowTransitionRectActive() const +{ + return m_hasWindowTransitionRect && !m_windowTransitionLocalRect.isEmpty(); +} + +std::optional SurfaceWrapper::computeGlobalWindowTransitionRect() const +{ + if (!m_hasWindowTransitionRect || !m_windowTransitionOriginWrapper) + return std::nullopt; + auto *surfItem = m_windowTransitionOriginWrapper->surfaceItem(); + if (!surfItem || !surfItem->surface() || !surfItem->shellSurface()) + return std::nullopt; + const QPointF mappedTopLeft = surfItem->mapFromSurface(m_windowTransitionLocalRect.topLeft()); + return QRectF(m_windowTransitionOriginWrapper->position() + mappedTopLeft, + m_windowTransitionLocalRect.size()); +} + void SurfaceWrapper::updateStackingLayer() { int newExplicitAlwaysOnTop = m_alwaysOnTop; diff --git a/src/surface/surfacewrapper.h b/src/surface/surfacewrapper.h index f65e97e20f..71c63ea1f2 100644 --- a/src/surface/surfacewrapper.h +++ b/src/surface/surfacewrapper.h @@ -6,17 +6,24 @@ #include #include +#include +#include #include #include #include #include -#include + +#include Q_MOC_INCLUDE() Q_MOC_INCLUDE() WAYLIB_SERVER_USE_NAMESPACE +WAYLIB_SERVER_BEGIN_NAMESPACE +class WSeat; +WAYLIB_SERVER_END_NAMESPACE + class QmlEngine; class Output; class SurfaceContainer; @@ -306,6 +313,14 @@ class SurfaceWrapper : public QQuickItem bool hasInitializeContainer() const; void setHasInitializeContainer(bool value); void disableWindowAnimation(bool disable = true); + void setWindowTransitionRect(const QRectF &localRect, SurfaceWrapper *originWrapper); + void updateWindowTransitionRect(const QRectF &localRect); + void clearWindowTransitionRect(); + bool hasWindowTransitionRect() const; + bool windowTransitionRectActive() const; + std::optional computeGlobalWindowTransitionRect() const; + void setWindowTransitionSourceImage(const QImage &image); + void clearWindowTransitionSourceImage(); void setHideByShowDesk(bool show); void setHideByLockScreen(bool hide); @@ -323,6 +338,9 @@ class SurfaceWrapper : public QQuickItem bool attention() const; bool setAttention(bool attention); + void setPendingActivation(WSeat *seat); + bool takePendingActivation(WSeat *&seat); + public Q_SLOTS: void minimize(bool onAnimation = true); void restoreFromMinimized(bool onAnimation = true); @@ -423,6 +441,10 @@ public Q_SLOTS: void updateClipRect(); void geometryChange(const QRectF &newGeo, const QRectF &oldGeometry) override; void createNewOrClose(uint direction); + void startWindowTransition(); + void finishWindowTransitionOpen(); + void startWindowCloseTransition(); + void tryFlushPendingActivation(); void itemChange(ItemChange change, const ItemChangeData &data) override; QRectF targetGeometryForState(State state) const; @@ -438,6 +460,7 @@ public Q_SLOTS: Q_SLOT void onPrelaunchGeometryAnimationFinished(); bool startStateChangeAnimation(SurfaceWrapper::State targetState, const QRectF &targetGeometry); void onWindowAnimationFinished(); + void dropWindowTransitionSourceBuffer(); Q_SLOT void onShowAnimationFinished(); Q_SLOT void onHideAnimationFinished(); void updateStackingLayer(); @@ -539,6 +562,17 @@ public Q_SLOTS: bool m_socketEnabled{ false }; bool m_windowAnimationEnabled{ true }; + + uint m_pendingActivation : 1 = 0; // activation requested before the surface mapped + uint m_windowTransitionPending : 1 = 0; + uint m_hasWindowTransitionRect : 1 = 0; + + QPointer m_pendingActivationSeat; + QRectF m_windowTransitionLocalRect; + QPointer + m_windowTransitionOriginWrapper; + QImage m_windowTransitionSourceImage; + wlr_buffer *m_windowTransitionSourceBuffer = nullptr; const QString m_appId; };