From dac91b22cea68b513774259e945261d4dbd01153 Mon Sep 17 00:00:00 2001 From: glyvut Date: Thu, 20 Aug 2026 10:07:45 +0800 Subject: [PATCH] feat(window-transition): implement window transition protocol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the server-side implementation of treeland-window-transition-unstable-v1, which plays a window open/close transition relative to a source rectangle attached to an xdg-activation token. A client attaches a persistent transition rectangle (geometry plus an optional source image) to an xdg_activation_token_v1 before committing the token. At activation the compositor associates the rectangle with the target window, animating from the rectangle's global position on open and back to it on close. The rectangle stays alive, so set_geometry and set_source_buffer update it immediately. 新增 treeland-window-transition-unstable-v1 的服务端实现,基于关联到 xdg-activation token 的源矩形播放窗口打开/关闭转场。 客户端在提交 token 前挂载一个持久的转场矩形(几何信息及可选源图像)。 激活时合成器将矩形关联到目标窗口,打开时从矩形的全局位置播放动画, 关闭时过渡回该矩形。矩形持续存活,set_geometry / set_source_buffer 可立即更新。 Log: 实现窗口转场协议,基于激活 token 的源矩形播放开/关转场 Influence: 新增窗口转场模块、QML 动画组件及 test-window-transition 样例; 激活流程支持矩形关联并播放开/关动画。 --- examples/CMakeLists.txt | 1 + .../test-window-transition/CMakeLists.txt | 30 + examples/test-window-transition/main.cpp | 568 ++++++++++++++++++ src/CMakeLists.txt | 1 + src/common/treelandlogging.cpp | 3 + src/common/treelandlogging.h | 3 + src/core/qml/Animations/WindowTransition.qml | 136 +++++ src/core/qmlengine.cpp | 19 + src/core/qmlengine.h | 7 + src/modules/CMakeLists.txt | 1 + .../activationmanagerinterfacev1.cpp | 89 ++- .../activation/activationmanagerinterfacev1.h | 16 +- src/modules/window-transition/CMakeLists.txt | 18 + .../windowtransitionmanagerinterfacev1.cpp | 397 ++++++++++++ .../windowtransitionmanagerinterfacev1.h | 49 ++ src/seat/helper.cpp | 52 +- src/seat/helper.h | 2 + src/surface/surfacewrapper.cpp | 249 +++++++- src/surface/surfacewrapper.h | 36 +- 19 files changed, 1638 insertions(+), 39 deletions(-) create mode 100644 examples/test-window-transition/CMakeLists.txt create mode 100644 examples/test-window-transition/main.cpp create mode 100644 src/core/qml/Animations/WindowTransition.qml create mode 100644 src/modules/window-transition/CMakeLists.txt create mode 100644 src/modules/window-transition/windowtransitionmanagerinterfacev1.cpp create mode 100644 src/modules/window-transition/windowtransitionmanagerinterfacev1.h 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; };