diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index a134fe72f3..efa6a9fcf0 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -29,3 +29,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..51da4cc9fe --- /dev/null +++ b/examples/test-window-transition/main.cpp @@ -0,0 +1,676 @@ +// 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 +#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) + { + if (!display || !display->compositor()) + return; + + if (!m_sourceSurface) + m_sourceSurface = display->compositor()->create_surface(); + + 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(); + + // The compositor renders this surface live during the transition, so + // it must be kept alive with valid content until closed. + wl_surface_attach(m_sourceSurface, m_shmBuffer->buffer(), 0, 0); + wl_surface_damage(m_sourceSurface, 0, 0, pixmap.width(), pixmap.height()); + wl_surface_commit(m_sourceSurface); + + set_source_surface(m_sourceSurface); + } + + void clearSourceImage() + { + set_source_surface(nullptr); + } + + ~WindowTransitionRect() override + { + if (m_sourceSurface) + wl_surface_destroy(m_sourceSurface); + delete m_shmBuffer; + } + +protected: + void treeland_window_transition_rect_v1_closed() override + { + Q_EMIT closed(); + } + +Q_SIGNALS: + void closed(); + +private: + ::wl_surface *m_sourceSurface = nullptr; + QtWaylandClient::QWaylandShmBuffer *m_shmBuffer = nullptr; +}; + +// --------------------------------------------------------------------------- +// Test image / icon palette +// --------------------------------------------------------------------------- + +struct IconPalette +{ + QColor from; + QColor to; +}; + +static constexpr int kPaletteCount = 2; +static const IconPalette kIconPalettes[kPaletteCount] = { + { QColor(63, 81, 181), QColor(33, 150, 243) }, // indigo -> blue + { QColor(76, 175, 80), QColor(0, 150, 136) }, // green -> teal +}; + +static QPixmap +createTestImage(const QString &text, const QColor &from, const QColor &to, int w, int h) +{ + QPixmap pm(w, h); + QPainter p(&pm); + QLinearGradient grad(0, 0, w, h); + grad.setColorAt(0, from); + grad.setColorAt(1, to); + 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, text); + return pm; +} + +// --------------------------------------------------------------------------- +// Image / icon widget +// --------------------------------------------------------------------------- + +class IconWidget : public QWidget +{ +public: + explicit IconWidget(const QPixmap &pixmap, QWidget *parent = nullptr) + : QWidget(parent) + , m_pixmap(pixmap) + { + } + + void setPixmap(const QPixmap &pixmap) + { + m_pixmap = pixmap; + update(); + } + + 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) + { + setWindowTitle("Window Transition - Sender (press Space / .)"); + + auto *central = new QWidget(this); + auto *layout = new QVBoxLayout(central); + + auto *iconsLayout = new QHBoxLayout; + iconsLayout->setSpacing(24); + for (int i = 0; i < kIconCount; ++i) { + auto *column = new QVBoxLayout; + auto *icon = new IconWidget(iconPixmap(i), central); + icon->setFixedSize(200, 200); + column->addWidget(icon, 0, Qt::AlignCenter); + + auto *caption = new QLabel(m_iconCaptions.at(i), central); + caption->setAlignment(Qt::AlignCenter); + column->addWidget(caption); + + iconsLayout->addLayout(column); + m_iconWidgets.append(icon); + } + layout->addLayout(iconsLayout); + + auto *label = new QLabel("SPACE: launch receiver from icon 1 (new process)\n" + ".: launch receiver from icon 2 (new process)\n" + "ENTER: launch modal from icon 1 (same process)\n" + "0: toggle disable animation (hide icons)\n" + "B: toggle commit source surface\n" + "P: toggle icon color", + central); + label->setAlignment(Qt::AlignCenter); + layout->addWidget(label); + + m_sourceBufferIndicator = new QLabel("Commit source surface: ON", central); + m_sourceBufferIndicator->setAlignment(Qt::AlignCenter); + layout->addWidget(m_sourceBufferIndicator); + + setCentralWidget(central); + resize(560, 400); + } + + ~SenderWindow() override + { + // Destroy all rect objects so the compositor falls back + // to the default close animation once sender is gone. + for (auto &live : std::as_const(m_rects)) { + live.rect->destroy(); + } + m_rects.clear(); + } + +protected: + void keyPressEvent(QKeyEvent *event) override + { + if (event->key() == Qt::Key_Space) + launchReceiver(0); + else if (event->key() == Qt::Key_Period) + launchReceiver(1); + else if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) + launchModal(0); + else if (event->key() == Qt::Key_0) + toggleAnimation(); + else if (event->key() == Qt::Key_B) + toggleSourceBuffer(); + else if (event->key() == Qt::Key_P) + toggleIconPalette(); + QMainWindow::keyPressEvent(event); + } + + void resizeEvent(QResizeEvent *event) override + { + // The icon widgets are centered in the layout, so resizing the main + // window moves them. Re-set the persistent rect(s) with the updated + // geometry so the open/close animation follows the new position. + updateRectGeometries(); + QMainWindow::resizeEvent(event); + } + +private: + static constexpr int kIconCount = 2; + + QPixmap iconPixmap(int iconIndex) const + { + const auto &palette = kIconPalettes[m_paletteIndex]; + return createTestImage(m_iconCaptions.at(iconIndex), palette.from, palette.to, 200, 200); + } + + // Geometry (relative to this sender surface) of the icon widget that is + // the source of the window transition. + QRect animationRect(int iconIndex) const + { + auto *icon = m_iconWidgets.at(iconIndex); + const QPoint iconPos = icon->mapTo(this, QPoint(0, 0)); + return QRect(iconPos, icon->size()); + } + + void toggleAnimation() + { + m_animationDisabled = !m_animationDisabled; + for (auto *icon : std::as_const(m_iconWidgets)) + icon->setVisible(!m_animationDisabled); + // Hiding/showing the icons reflows the layout; force it before reading + // back the icon geometry. + centralWidget()->layout()->activate(); + updateRectGeometries(); + } + + void toggleSourceBuffer() + { + m_commitSourceBuffer = !m_commitSourceBuffer; + if (m_sourceBufferIndicator) + m_sourceBufferIndicator->setText(m_commitSourceBuffer ? "Commit source surface: ON" + : "Commit source surface: OFF"); + + auto *display = getWlDisplay(this); + for (auto &live : std::as_const(m_rects)) { + if (m_commitSourceBuffer && display) + live.rect->setSourceImage(display, iconPixmap(live.iconIndex)); + else + live.rect->clearSourceImage(); + } + } + + void toggleIconPalette() + { + m_paletteIndex = (m_paletteIndex + 1) % kPaletteCount; + + for (int i = 0; i < kIconCount; ++i) + m_iconWidgets.at(i)->setPixmap(iconPixmap(i)); + + // Re-commit the live source surfaces so a running transition shows the + // new color immediately, exercising the live-source path. + if (!m_commitSourceBuffer) + return; + auto *display = getWlDisplay(this); + if (!display) + return; + for (auto &live : std::as_const(m_rects)) + live.rect->setSourceImage(display, iconPixmap(live.iconIndex)); + } + + 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. + for (auto &live : std::as_const(m_rects)) { + const QRect rect = m_animationDisabled ? QRect() : animationRect(live.iconIndex); + live.rect->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; + } + + // Create the activation token, identify the originating surface and + // commit it. The token string is delivered by the done event, after + // which the window transition rect can be created for it. + auto *rawToken = m_actMgr->get_activation_token(); + if (!rawToken) { + qWarning() << "Failed to create activation token"; + return nullptr; + } + auto *token = new ActivationToken(rawToken, this); + token->set_surface(surface); + token->commit(); + return token; + } + + void createTransitionRect(const QString &tokenStr, int iconIndex) + { + if (!m_transitionMgr || !m_transitionMgr->isInitialized()) + return; + + const QRect rectGeo = m_animationDisabled ? QRect() : animationRect(iconIndex); + auto *rawRect = m_transitionMgr->get_window_transition_rect(tokenStr); + if (!rawRect) + return; + + // 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] { + for (int i = 0; i < m_rects.size(); ++i) { + if (m_rects.at(i).rect == rect) { + m_rects.removeAt(i); + break; + } + } + rect->destroy(); + }); + rect->set_geometry(rectGeo.x(), rectGeo.y(), rectGeo.width(), rectGeo.height()); + // Attach the icon image as a live source surface. The + // compositor renders it as the starting visual of the open + // animation (and the ending visual of the close animation). + auto *display = getWlDisplay(this); + if (display) { + if (m_commitSourceBuffer) + rect->setSourceImage(display, iconPixmap(iconIndex)); + else + rect->clearSourceImage(); + } + m_rects.append(LiveRect{ rect, iconIndex }); + + // Make sure the rect and its geometry reach the compositor before the + // token is handed to the target window, which may activate as soon as + // it starts. + if (display) + display->flushRequests(); + } + + void launchReceiver(int iconIndex) + { + auto *token = createLaunchToken(); + if (!token) + return; + connect(token, &ActivationToken::done, this, [this, iconIndex](const QString &tokenStr) { + createTransitionRect(tokenStr, iconIndex); + onReceiverToken(tokenStr); + }); + } + + void launchModal(int iconIndex) + { + auto *token = createLaunchToken(); + if (!token) + return; + connect(token, &ActivationToken::done, this, [this, iconIndex](const QString &tokenStr) { + createTransitionRect(tokenStr, iconIndex); + onModalToken(tokenStr); + }); + } + + 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; + + struct LiveRect + { + WindowTransitionRect *rect; + int iconIndex; + }; + + QList m_rects; + QList m_iconWidgets; + QList m_iconCaptions{ QStringLiteral("SPACE"), QStringLiteral(".") }; + int m_paletteIndex = 0; + 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("Window\nTransition", + QColor(63, 81, 181), + QColor(33, 150, 243), + 800, + 600)) + { + setWindowTitle("Window Transition - Receiver"); + resize(800, 600); + + auto *central = new IconWidget(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 33699fd8e8..2f88b40d33 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -173,6 +173,7 @@ qt_add_qml_module(libtreeland interfaces/multitaskviewinterface.h interfaces/plugininterface.h interfaces/proxyinterface.h + interfaces/windowtransition.h modules/resource/treelandremotesource.cpp modules/resource/treelandremotesource.h modules/resource/treelanddebugsocket.cpp @@ -201,6 +202,8 @@ qt_add_qml_module(libtreeland surface/surfaceproxy.h surface/surfacewrapper.cpp surface/surfacewrapper.h + surface/surfacewindowtransition.cpp + surface/surfacewindowtransition.h utils/cmdline.cpp utils/cmdline.h utils/propertymonitor.cpp @@ -253,6 +256,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 e60133ac2b..608e4c5566 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 54458f0270..ae5b2e8362 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..7262c58d37 --- /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 sourceSurface: null + property int direction: 0 + + 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 + shadowBlur: surface.shadowBlurRadius + shadowOffsetX: surface.shadowOffsetX + shadowOffsetY: surface.shadowOffsetY + shadowColor: surface.shadowColor + } + + 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 + } + + SurfaceItemContent { + id: sourceImage + + anchors.fill: parent + visible: !!root.sourceSurface + surface: root.sourceSurface + smooth: true + live: 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(); + }) + } + } +} diff --git a/src/core/qmlengine.cpp b/src/core/qmlengine.cpp index 0a5d001481..5da6f9d02d 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") @@ -149,6 +150,24 @@ QQuickItem *QmlEngine::createGeometryAnimation(SurfaceWrapper *surface, }); } +QQuickItem *QmlEngine::createWindowTransition(SurfaceWrapper *surface, + const QRectF &startGeo, + const QRectF &endGeo, + QQuickItem *parent, + WSurface *sourceSurface, + int direction) +{ + return createComponent(windowTransitionComponent, + parent, + { + { "surface", QVariant::fromValue(surface) }, + { "fromGeometry", QVariant::fromValue(startGeo) }, + { "toGeometry", QVariant::fromValue(endGeo) }, + { "sourceSurface", QVariant::fromValue(sourceSurface) }, + { "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 3c031eaf38..1c406bda4f 100644 --- a/src/core/qmlengine.h +++ b/src/core/qmlengine.h @@ -16,6 +16,7 @@ QT_END_NAMESPACE WAYLIB_SERVER_BEGIN_NAMESPACE class WOutputItem; +class WSurface; WAYLIB_SERVER_END_NAMESPACE WAYLIB_SERVER_USE_NAMESPACE @@ -45,6 +46,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, + WSurface *sourceSurface = nullptr, + int direction = 0); QQuickItem *createMenuBar(WOutputItem *output, QQuickItem *parent); QQuickItem *createWorkspaceSwitcher(Workspace *parent); QQuickItem *createNewAnimation(SurfaceWrapper *surface, QQuickItem *parent, uint direction); @@ -88,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/interfaces/windowtransition.h b/src/interfaces/windowtransition.h new file mode 100644 index 0000000000..d945d9cc04 --- /dev/null +++ b/src/interfaces/windowtransition.h @@ -0,0 +1,56 @@ +// 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 + +WAYLIB_SERVER_BEGIN_NAMESPACE +class WOutput; +class WSurface; +WAYLIB_SERVER_END_NAMESPACE + +WAYLIB_SERVER_USE_NAMESPACE + +// Refcounted handle to a client-provided window-transition source surface. +class WindowTransitionSource : public QObject +{ + Q_OBJECT +public: + using QObject::QObject; + + virtual WSurface *surface() const = 0; + + virtual void retain(const QList &outputs) = 0; + virtual void release(const QList &outputs) = 0; +}; + +class WindowTransitionTarget : public QObject +{ + Q_OBJECT +public: + using QObject::QObject; + + virtual void setWindowTransitionRect(const QRectF &localRect, + WindowTransitionTarget *origin) = 0; + virtual void updateWindowTransitionRect(const QRectF &localRect) = 0; + + virtual void setWindowTransitionSource(WindowTransitionSource *source) = 0; + // Drop the live source only + virtual void clearWindowTransitionSource() = 0; + // Drop the whole transition and notify the client that the rect is done. + virtual void resetWindowTransition() = 0; + + virtual std::optional mapTransitionPointToGlobal( + const QPointF &surfacePoint) const = 0; + +Q_SIGNALS: + void windowTransitionFinished(); +}; diff --git a/src/modules/CMakeLists.txt b/src/modules/CMakeLists.txt index 329b3b4e81..3be317a402 100644 --- a/src/modules/CMakeLists.txt +++ b/src/modules/CMakeLists.txt @@ -49,6 +49,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..63a3e9fdc2 100644 --- a/src/modules/activation/activationmanagerinterfacev1.cpp +++ b/src/modules/activation/activationmanagerinterfacev1.cpp @@ -118,6 +118,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 +146,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)"); @@ -177,9 +185,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 +214,34 @@ class ActivationManagerInterfaceV1Private return; } - auto disposition = dispositionForToken(token); - // Retrieve the seat associated with the token before consuming it. + auto 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 +286,14 @@ 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); + send_done(token); } diff --git a/src/modules/activation/activationmanagerinterfacev1.h b/src/modules/activation/activationmanagerinterfacev1.h index d37519bdf2..e7acbca375 100644 --- a/src/modules/activation/activationmanagerinterfacev1.h +++ b/src/modules/activation/activationmanagerinterfacev1.h @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -40,9 +41,15 @@ class ActivationManagerInterfaceV1 : public QObject, public WAYLIB_SERVER_NAMESP 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..65ae1c1676 --- /dev/null +++ b/src/modules/window-transition/CMakeLists.txt @@ -0,0 +1,18 @@ +find_package(TreelandProtocols 0.7.0 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..e944f1ad8a --- /dev/null +++ b/src/modules/window-transition/windowtransitionmanagerinterfacev1.cpp @@ -0,0 +1,458 @@ +// 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 "interfaces/windowtransition.h" +#include "qwayland-server-treeland-window-transition-unstable-v1.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +WAYLIB_SERVER_USE_NAMESPACE +using namespace Qt::StringLiterals; + +namespace { +constexpr int kPendingRectLifetimeMs = 60'000; +} // namespace + +class WindowTransitionManagerInterfaceV1Private; + +static const wlr_surface_role transitionSourceRole = { + .name = "treeland_window_transition_source_v1", + .no_object = true, + .client_commit = nullptr, + .commit = nullptr, + .map = nullptr, + .unmap = nullptr, + .destroy = nullptr, +}; + +class WindowTransitionSourceImpl : public WindowTransitionSource +{ +public: + explicit WindowTransitionSourceImpl(wlr_surface *handle) + : WindowTransitionSource() + , m_wrapper(new WSurface(handle)) + { + } + + ~WindowTransitionSourceImpl() override + { + const auto outputs = m_refs.keys(); + for (auto *output : outputs) + m_wrapper->leaveOutput(output); + delete m_wrapper; + } + + WSurface *surface() const override + { + return m_wrapper; + } + + void retain(const QList &outputs) override + { + for (auto *output : outputs) { + if (++m_refs[output] == 1) + m_wrapper->enterOutput(output); + } + m_wrapper->notifyFrameDone(); + } + + void release(const QList &outputs) override + { + for (auto *output : outputs) { + auto it = m_refs.find(output); + if (it == m_refs.end()) + continue; + if (--it.value() <= 0) { + m_refs.erase(it); + m_wrapper->leaveOutput(output); + } + } + } + +private: + WSurface *m_wrapper; + QHash m_refs; +}; + +struct SourceSurfaceEntry +{ + wlr_surface *handle = nullptr; + WindowTransitionSourceImpl *source = nullptr; + // Owns the native destroy listener registered on the source surface + // wrapper; destroying the entry detaches it automatically. + std::unique_ptr listenerOwner; +}; + +class WindowTransitionRectV1 : public QtWaylandServer::treeland_window_transition_rect_v1 +{ + friend class WindowTransitionManagerInterfaceV1; + +public: + WindowTransitionRectV1(WindowTransitionManagerInterfaceV1Private *manager, + const QString &token, + wl_resource *resource) + : QtWaylandServer::treeland_window_transition_rect_v1(resource) + , m_manager(manager) + , m_token(token) + , m_expiry(QDeadlineTimer(kPendingRectLifetimeMs)) + { + } + + ~WindowTransitionRectV1(); + + void notifyDiscarded() + { + if (m_closed) + return; + m_closed = true; + send_closed(); + } + + void detachManager() + { + m_manager = nullptr; + } + + bool isExpired() const + { + return m_expiry.hasExpired(); + } + + void associate(WindowTransitionTarget *target, WindowTransitionTarget *origin) + { + m_target = target; + if (m_target) { + m_target->setWindowTransitionRect(m_rect, origin); + m_target->setWindowTransitionSource(m_source.data()); + m_targetTransitionFinishedConnection = + QObject::connect(m_target.data(), + &WindowTransitionTarget::windowTransitionFinished, + [this] { + notifyDiscarded(); + }); + } + } + +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_surface(Resource * /*resource*/, struct ::wl_resource *surface) override; + +private: + void clearSourceSurface(); + void releaseSourceSurface(); + + WindowTransitionManagerInterfaceV1Private *m_manager; + QString m_token; + QDeadlineTimer m_expiry; + QRectF m_rect; + QMetaObject::Connection m_targetTransitionFinishedConnection; + QPointer m_target; + bool m_closed = false; + + // Owned by the manager, not by this rect; only a weak reference. + QPointer m_source; +}; + +class WindowTransitionManagerInterfaceV1Private + : public QtWaylandServer::treeland_window_transition_manager_v1 +{ +public: + explicit WindowTransitionManagerInterfaceV1Private(WindowTransitionManagerInterfaceV1 *q) + : QtWaylandServer::treeland_window_transition_manager_v1() + , q(q) + { + } + + ~WindowTransitionManagerInterfaceV1Private(); + + wl_global *globalHandle() const + { + return m_global; + } + + // Returns the shared handle for a transition source surface, creating and + // taking ownership of it on first use. + WindowTransitionSource *acquireSourceSurface(wlr_surface *surface); + + WindowTransitionManagerInterfaceV1 *q; + QHash m_pendingRects; + QHash m_sourceSurfaces; + +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, + const QString &token) override + { + 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; + } + auto *rect = new WindowTransitionRectV1(this, token, rectResource); + // One rectangle per token: close an earlier one so the client can + // release it. + if (auto *previous = m_pendingRects.take(token)) + previous->notifyDiscarded(); + m_pendingRects.insert(token, rect); + q->ensurePendingSweepRunning(); + } +}; + +WindowTransitionManagerInterfaceV1Private::~WindowTransitionManagerInterfaceV1Private() +{ + for (auto *rect : std::as_const(m_pendingRects)) + rect->detachManager(); + m_pendingRects.clear(); + + // Deleting an entry destroys its listenerOwner, which detaches the + // surface destroy listener registered on the wrapper. + const auto entries = m_sourceSurfaces; + m_sourceSurfaces.clear(); + for (auto *entry : entries) { + delete entry->source; + delete entry; + } +} + +WindowTransitionSource *WindowTransitionManagerInterfaceV1Private::acquireSourceSurface( + wlr_surface *surface) +{ + if (auto *entry = m_sourceSurfaces.value(surface)) + return entry->source; + + auto *entry = new SourceSurfaceEntry; + entry->handle = surface; + entry->source = new WindowTransitionSourceImpl(surface); + entry->listenerOwner = std::make_unique(); + + // Track the native surface's destruction through the wrapper's listener + // graph instead of a hand-rolled wl_listener. Registering on the wrapper + // with the entry's owner token means destroying either side detaches the + // listener; the callback itself is safe to delete the entry from. + entry->source->surface() + ->listeners(entry->listenerOwner.get()) + ->add(&surface->events.destroy, this, [this, entry] { + m_sourceSurfaces.remove(entry->handle); + delete entry->source; + delete entry; + }); + + m_sourceSurfaces.insert(surface, entry); + return entry->source; +} + +WindowTransitionRectV1::~WindowTransitionRectV1() +{ + if (m_targetTransitionFinishedConnection) + QObject::disconnect(m_targetTransitionFinishedConnection); + + if (m_target) { + m_target->resetWindowTransition(); + } + + releaseSourceSurface(); + + if (m_manager && m_manager->m_pendingRects.value(m_token) == this) + m_manager->m_pendingRects.remove(m_token); +} + +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); + + if (m_target) + m_target->updateWindowTransitionRect(m_rect); + + if (m_source) + m_source->surface()->notifyFrameDone(); +} + +void WindowTransitionRectV1::set_source_surface(Resource *resource, + struct ::wl_resource *surfaceResource) +{ + if (!surfaceResource) { + clearSourceSurface(); + return; + } + + wlr_surface *surface = wlr_surface_from_resource(surfaceResource); + if (!surface + || wl_resource_get_client(surfaceResource) != wl_resource_get_client(resource->handle)) { + wl_resource_post_error(resource->handle, + error_invalid_surface, + "set_source_surface: surface is not a wl_surface of this client"); + return; + } + + if (m_source && m_source->surface()->handle() == surface) + return; + + // The surface must not already carry a role. + if (!wlr_surface_set_role(surface, + &transitionSourceRole, + resource->handle, + error_invalid_surface)) { + return; + } + + releaseSourceSurface(); + + auto *source = m_manager->acquireSourceSurface(surface); + m_source = source; + + if (m_target) + m_target->setWindowTransitionSource(source); + + source->surface()->notifyFrameDone(); +} + +void WindowTransitionRectV1::clearSourceSurface() +{ + releaseSourceSurface(); + if (m_target) + m_target->clearWindowTransitionSource(); +} + +void WindowTransitionRectV1::releaseSourceSurface() +{ + // The handle is owned by the manager; only drop our weak reference. + m_source = nullptr; +} + +WindowTransitionManagerInterfaceV1::WindowTransitionManagerInterfaceV1(QObject *parent) + : QObject(parent) + , WServerInterface() + , d(new WindowTransitionManagerInterfaceV1Private(this)) + , 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(); +} + +bool WindowTransitionManagerInterfaceV1::hasPendingWindowTransitionRect(const QString &token) const +{ + return d->m_pendingRects.contains(token); +} + +bool WindowTransitionManagerInterfaceV1::associatePendingRect(const QString &token, + WindowTransitionTarget *target, + WindowTransitionTarget *origin) +{ + 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(target, origin); + 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..a13133f026 --- /dev/null +++ b/src/modules/window-transition/windowtransitionmanagerinterfacev1.h @@ -0,0 +1,48 @@ +// 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 + +class WindowTransitionTarget; +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 discardPendingRect(const QString &token); + bool hasPendingWindowTransitionRect(const QString &token) const; + bool associatePendingRect(const QString &token, + WindowTransitionTarget *target, + WindowTransitionTarget *origin); + +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; + friend class WindowTransitionManagerInterfaceV1Private; + std::unique_ptr d; + QTimer m_pendingSweepTimer; + void ensurePendingSweepRunning(); + void sweepPendingRects(); +}; diff --git a/src/seat/helper.cpp b/src/seat/helper.cpp index 333f3e432d..5fb863f0c7 100644 --- a/src/seat/helper.cpp +++ b/src/seat/helper.cpp @@ -1197,6 +1197,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 @@ -1227,16 +1229,66 @@ void Helper::init(Treeland::Treeland *treeland) 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; } + if (m_windowTransitionManagerV1 + && m_windowTransitionManagerV1->hasPendingWindowTransitionRect(token)) { + if (!originatingSurface) { + // Without an originating surface the rect cannot be + // mapped to global coordinates, so it will never be + // used. Close it now instead of waiting for the + // pending sweep. + qCDebug(lcTlWindowTransition) + << "Window transition rect discarded: token has no" + << "originating surface" + << "appId:" << wrapper->appId(); + m_windowTransitionManagerV1->discardPendingRect(token); + } else if (wsurface->mapped()) { + qCDebug(lcTlWindowTransition) + << "Window transition rect discarded: target surface" + << "is already mapped (client activation arrived too late)" + << "appId:" << wrapper->appId(); + // 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->windowTransition(), + originWrapper->windowTransition()); + if (!associated) { + qCDebug(lcTlWindowTransition) + << "Window transition rect was destroyed" + << "before activation, using default animation" + << "appId:" << wrapper->appId(); + } + } else { + qCDebug(lcTlWindowTransition) + << "Window transition: originating surface wrapper" + << "not found, skipping" + << "appId:" << wrapper->appId(); + // Discard the pending rect object. + m_windowTransitionManagerV1->discardPendingRect(token); + } + } + } // Don't use hasActiveCapability() here — it also checks UnMinimized, // but minimized windows should be allowed to activate (which unminimizes them). 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 413954c232..40acc567a7 100644 --- a/src/seat/helper.h +++ b/src/seat/helper.h @@ -12,6 +12,7 @@ #include "modules/wallpaper/wallpapernotifierinterfacev1.h" #include "modules/show-desktop/showdesktopinterfacev1.h" #include "modules/xwindow-control/xwindowcontrolinterfacev1.h" +#include "modules/window-transition/windowtransitionmanagerinterfacev1.h" #include "utils/fpsdisplaymanager.h" #include @@ -430,6 +431,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/surfacewindowtransition.cpp b/src/surface/surfacewindowtransition.cpp new file mode 100644 index 0000000000..4529d243b9 --- /dev/null +++ b/src/surface/surfacewindowtransition.cpp @@ -0,0 +1,178 @@ +// 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 "surfacewindowtransition.h" + +#include "surface/surfacewrapper.h" + +#include +#include + +#include + +SurfaceWindowTransition::SurfaceWindowTransition(SurfaceWrapper *wrapper) + : WindowTransitionTarget() + , m_wrapper(wrapper) +{ +} + +bool SurfaceWindowTransition::active() const +{ + return m_hasRect && !m_localRect.isEmpty(); +} + +WSurface *SurfaceWindowTransition::sourceSurface() const +{ + auto *source = m_source.data(); + return source ? source->surface() : nullptr; +} + +std::optional SurfaceWindowTransition::globalRect() const +{ + if (!m_hasRect || !m_origin) + return std::nullopt; + + const auto topLeft = m_origin->mapTransitionPointToGlobal(m_localRect.topLeft()); + if (!topLeft) + return std::nullopt; + + return QRectF(*topLeft, m_localRect.size()); +} + +void SurfaceWindowTransition::begin() +{ + if (m_phase == Phase::Playing) + return; + + m_phase = Phase::Playing; + m_retainedOutputs = m_wrapper->outputs(); + if (m_source) + m_source->retain(m_retainedOutputs); +} + +void SurfaceWindowTransition::endAnimation(bool wrapperAboutToRemove) +{ + releaseSourceOutputs(); + + if (wrapperAboutToRemove) { + if (m_sourceDestroyConnection) + QObject::disconnect(m_sourceDestroyConnection); + m_sourceDestroyConnection = { }; + m_phase = Phase::Idle; + m_hasRect = false; + m_localRect = { }; + m_origin = nullptr; + m_source = nullptr; + Q_EMIT windowTransitionFinished(); + } else { + m_phase = m_hasRect ? Phase::Armed : Phase::Idle; + } +} + +void SurfaceWindowTransition::resetWindowTransition() +{ + const bool wasArmed = m_hasRect; + + releaseSourceOutputs(); + if (m_sourceDestroyConnection) + QObject::disconnect(m_sourceDestroyConnection); + m_sourceDestroyConnection = { }; + m_source = nullptr; + m_phase = Phase::Idle; + m_hasRect = false; + m_localRect = { }; + m_origin = nullptr; + + m_wrapper->updateWindowTransitionAnimationSource(); + + if (wasArmed) + Q_EMIT windowTransitionFinished(); +} + +void SurfaceWindowTransition::abort() +{ + releaseSourceOutputs(); + m_wrapper->updateWindowTransitionAnimationSource(); +} + +void SurfaceWindowTransition::setWindowTransitionRect(const QRectF &localRect, + WindowTransitionTarget *origin) +{ + // The rect is honored only when the surface is not yet mapped. + if (m_wrapper->surface() && m_wrapper->surface()->mapped()) + return; + + m_localRect = localRect; + m_origin = origin; + m_hasRect = true; + m_phase = Phase::Armed; +} + +void SurfaceWindowTransition::updateWindowTransitionRect(const QRectF &localRect) +{ + if (!m_hasRect) + return; + + m_localRect = localRect; +} + +void SurfaceWindowTransition::setWindowTransitionSource(WindowTransitionSource *source) +{ + if (m_source.data() == source) + return; + + releaseSourceOutputs(); + + if (m_sourceDestroyConnection) + QObject::disconnect(m_sourceDestroyConnection); + m_sourceDestroyConnection = { }; + m_source = source; + if (source) { + // The handle is owned by the manager and may outlive the animation; + // track its surface so the QML item never keeps a dangling pointer. + m_sourceDestroyConnection = + QObject::connect(source->surface(), + &WSurface::beforeDestroy, + this, + &SurfaceWindowTransition::onSourceSurfaceDestroyed); + } + + m_wrapper->updateWindowTransitionAnimationSource(); +} + +void SurfaceWindowTransition::clearWindowTransitionSource() +{ + releaseSourceOutputs(); + if (m_sourceDestroyConnection) + QObject::disconnect(m_sourceDestroyConnection); + m_sourceDestroyConnection = { }; + m_source = nullptr; + + m_wrapper->updateWindowTransitionAnimationSource(); +} + +std::optional SurfaceWindowTransition::mapTransitionPointToGlobal( + const QPointF &surfacePoint) const +{ + auto *item = m_wrapper->surfaceItem(); + if (!item || !item->surface() || !item->shellSurface()) + return std::nullopt; + + return m_wrapper->position() + item->mapFromSurface(surfacePoint); +} + +void SurfaceWindowTransition::releaseSourceOutputs() +{ + if (m_source && !m_retainedOutputs.isEmpty()) + m_source->release(m_retainedOutputs); + m_retainedOutputs.clear(); +} + +void SurfaceWindowTransition::onSourceSurfaceDestroyed() +{ + m_sourceDestroyConnection = { }; + m_source = nullptr; + m_retainedOutputs.clear(); + + m_wrapper->updateWindowTransitionAnimationSource(); +} diff --git a/src/surface/surfacewindowtransition.h b/src/surface/surfacewindowtransition.h new file mode 100644 index 0000000000..7cf74c78e6 --- /dev/null +++ b/src/surface/surfacewindowtransition.h @@ -0,0 +1,63 @@ +// 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 "interfaces/windowtransition.h" + +#include +#include +#include +#include + +#include + +class SurfaceWrapper; + +// Idle --setWindowTransitionRect--> Armed --begin--> Playing +// Playing --endAnimation(false)--> Armed --begin--> ... +// Playing --endAnimation(true)--> Idle (source left, rect finished) +// Armed/Playing --resetWindowTransition--> Idle (source left, rect finished) +class SurfaceWindowTransition : public WindowTransitionTarget +{ + Q_OBJECT +public: + enum class Phase + { + Idle, + Armed, + Playing, + }; + + explicit SurfaceWindowTransition(SurfaceWrapper *wrapper); + + // True when a non-empty transition rectangle is configured. + bool active() const; + WSurface *sourceSurface() const; + std::optional globalRect() const; + + // Lifecycle driven by SurfaceWrapper. + void begin(); + void endAnimation(bool wrapperAboutToRemove); + void abort(); + + void setWindowTransitionRect(const QRectF &localRect, WindowTransitionTarget *origin) override; + void updateWindowTransitionRect(const QRectF &localRect) override; + void setWindowTransitionSource(WindowTransitionSource *source) override; + void clearWindowTransitionSource() override; + void resetWindowTransition() override; + std::optional mapTransitionPointToGlobal(const QPointF &surfacePoint) const override; + +private: + void releaseSourceOutputs(); + void onSourceSurfaceDestroyed(); + + SurfaceWrapper *m_wrapper; + Phase m_phase = Phase::Idle; + bool m_hasRect = false; + QRectF m_localRect; + QPointer m_origin; + QPointer m_source; + QList m_retainedOutputs; + QMetaObject::Connection m_sourceDestroyConnection; +}; diff --git a/src/surface/surfacewrapper.cpp b/src/surface/surfacewrapper.cpp index e3c4111673..7254a28447 100644 --- a/src/surface/surfacewrapper.cpp +++ b/src/surface/surfacewrapper.cpp @@ -7,6 +7,7 @@ #include "core/qmlengine.h" #include "output/output.h" #include "seat/helper.h" +#include "surface/surfacewindowtransition.h" #include "treelanduserconfig.hpp" #include "workspace/workspace.h" #include "wtoplevelsurface.h" @@ -70,6 +71,7 @@ SurfaceWrapper::SurfaceWrapper(QmlEngine *qmlEngine, , m_modal(false) , m_appId(appId) { + m_windowTransition = std::make_unique(this); QQmlEngine::setContextForObject(this, qmlEngine->rootContext()); setup(); @@ -109,6 +111,7 @@ SurfaceWrapper::SurfaceWrapper(SurfaceWrapper *original, QQuickItem *parent) , m_modal(false) , m_appId(original->m_appId) { + m_windowTransition = std::make_unique(this); QQmlEngine::setContextForObject(this, m_engine->rootContext()); if (original->m_shellSurface) { @@ -181,6 +184,7 @@ SurfaceWrapper::SurfaceWrapper(QmlEngine *qmlEngine, , m_modal(false) , m_appId(appId) { + m_windowTransition = std::make_unique(this); QQmlEngine::setContextForObject(this, qmlEngine->rootContext()); if (initialSize.isValid() && initialSize.width() > 0 && initialSize.height() > 0) { // Also set implicit size to keep QML layout consistent @@ -203,6 +207,19 @@ 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. + if (m_windowTransitionPending && m_windowAnimation) { + m_windowAnimation->disconnect(this); + m_windowAnimation->deleteLater(); + m_windowAnimation = nullptr; + } + m_windowTransitionPending = false; + if (!isWindowAnimationRunning()) + m_windowTransition->resetWindowTransition(); + m_pendingActivation = false; + m_pendingActivationSeat.clear(); + if (!m_skipDockPreView) setSkipDockPreView(true); @@ -241,6 +258,10 @@ SurfaceWrapper::~SurfaceWrapper() } invalidate(); } + // Safety net for the path above: if a window animation was still running we + // are destroyed without onWindowAnimationFinished(), so make sure the + // transition source is not left entered on any output. + m_windowTransition->abort(); if (m_titleBar) { delete m_titleBar; m_titleBar = nullptr; @@ -1333,6 +1354,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; @@ -1555,17 +1592,171 @@ 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_windowTransition->active()) { + m_windowTransitionPending = false; + if (m_windowAnimation) { + m_windowAnimation->disconnect(this); + m_windowAnimation->deleteLater(); + m_windowAnimation = nullptr; + Q_EMIT windowAnimationRunningChanged(); + } + return; + } + m_windowTransitionPending = true; + + if (!m_windowAnimation) { + auto globalRectOpt = m_windowTransition->globalRect(); + if (!globalRectOpt) { + // Origin wrapper or its surface item is gone — fall back to default. + m_windowTransitionPending = false; + m_windowTransition->resetWindowTransition(); + createNewOrClose(OPEN_ANIMATION); + return; + } + + beginWindowTransition(OPEN_ANIMATION, *globalRectOpt, + QRectF()); // will update later + } + + restackWindowAnimationAbove(); + + // 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. + QMetaObject::invokeMethod(this, + &SurfaceWrapper::finishWindowTransitionOpen, + Qt::QueuedConnection); +} + +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; + } + m_windowTransition->endAnimation(false); + 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_windowTransition->active()); + + auto globalRectOpt = m_windowTransition->globalRect(); + if (!globalRectOpt) { + // Origin wrapper or its surface item is gone — fall back to default. + m_windowTransition->resetWindowTransition(); + createNewOrClose(CLOSE_ANIMATION); + return; + } + + beginWindowTransition(CLOSE_ANIMATION, QRectF(position(), size()), *globalRectOpt); + + restackWindowAnimationAbove(); + + 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::beginWindowTransition(int direction, + const QRectF &fromGeometry, + const QRectF &toGeometry) +{ + m_windowTransition->begin(); + + m_windowAnimation = m_engine->createWindowTransition(this, + fromGeometry, + toGeometry, + container(), + m_windowTransition->sourceSurface(), + direction); + m_windowAnimation->setProperty("enableBlur", m_blur); +} + 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 && m_windowTransition->active()) { + startWindowTransition(); + return; + } + + if (direction == CLOSE_ANIMATION && m_windowTransition->active()) { + startWindowCloseTransition(); + return; + } + switch (m_type) { case Type::SplashScreen: [[fallthrough]]; @@ -1751,6 +1942,10 @@ void SurfaceWrapper::onWindowAnimationFinished() m_windowAnimation->deleteLater(); m_windowAnimation = nullptr; + // Stop pacing the transition source now that the animation is over. The + // source itself stays referenced until the rect is done with it. + m_windowTransition->endAnimation(m_wrapperAboutToRemove); + Q_EMIT windowAnimationRunningChanged(); if (m_wrapperAboutToRemove) { @@ -1794,6 +1989,7 @@ void SurfaceWrapper::onMappedChanged() if (!m_prelaunchSplash) { createNewOrClose(OPEN_ANIMATION); } else { + m_windowTransition->resetWindowTransition(); syncPrelaunchMappedState(); startPrelaunchSplashHideSequence(); } @@ -1805,6 +2001,9 @@ void SurfaceWrapper::onMappedChanged() } } + if (mapped) + tryFlushPendingActivation(); + if (m_coverContent) { m_coverContent->setProperty("mapped", mapped); } @@ -2710,6 +2909,18 @@ void SurfaceWrapper::disableWindowAnimation(bool disable) m_windowAnimationEnabled = !disable; } +WindowTransitionTarget *SurfaceWrapper::windowTransition() const +{ + return m_windowTransition.get(); +} + +void SurfaceWrapper::updateWindowTransitionAnimationSource() +{ + if (m_windowAnimation) + m_windowAnimation->setProperty("sourceSurface", + QVariant::fromValue(m_windowTransition->sourceSurface())); +} + void SurfaceWrapper::updateStackingLayer() { int newExplicitAlwaysOnTop = m_alwaysOnTop; diff --git a/src/surface/surfacewrapper.h b/src/surface/surfacewrapper.h index d1c58a6241..8c0f4f7f4b 100644 --- a/src/surface/surfacewrapper.h +++ b/src/surface/surfacewrapper.h @@ -12,6 +12,7 @@ #include #include +#include #include Q_MOC_INCLUDE() @@ -19,10 +20,15 @@ Q_MOC_INCLUDE() WAYLIB_SERVER_USE_NAMESPACE +WAYLIB_SERVER_BEGIN_NAMESPACE +class WSeat; +WAYLIB_SERVER_END_NAMESPACE + class QmlEngine; class Output; class SurfaceContainer; - +class SurfaceWindowTransition; +class WindowTransitionTarget; class SurfaceWrapper : public QQuickItem { friend class Helper; @@ -30,6 +36,7 @@ class SurfaceWrapper : public QQuickItem friend class SurfaceProxy; friend class ShellHandler; friend class LockScreen; + friend class SurfaceWindowTransition; Q_OBJECT QML_ELEMENT QML_UNCREATABLE("SurfaceWrapper objects are created by c++") @@ -333,6 +340,7 @@ class SurfaceWrapper : public QQuickItem bool hasInitializeContainer() const; void setHasInitializeContainer(bool value); void disableWindowAnimation(bool disable = true); + WindowTransitionTarget *windowTransition() const; void setHideByShowDesk(bool show); void setHideByLockScreen(bool hide); @@ -354,6 +362,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); @@ -456,6 +467,12 @@ public Q_SLOTS: void updateClipRect(); void geometryChange(const QRectF &newGeo, const QRectF &oldGeometry) override; void createNewOrClose(uint direction); + void beginWindowTransition(int direction, const QRectF &fromGeometry, const QRectF &toGeometry); + void updateWindowTransitionAnimationSource(); + void startWindowTransition(); + void finishWindowTransitionOpen(); + void startWindowCloseTransition(); + void tryFlushPendingActivation(); void itemChange(ItemChange change, const ItemChangeData &data) override; QRectF targetGeometryForState(State state) const; @@ -589,12 +606,18 @@ public Q_SLOTS: uint m_resizable : 1; uint m_maximizable : 1; uint m_modal : 1; + uint m_pendingActivation : 1 = 0; // activation requested before the surface mapped + uint m_windowTransitionPending : 1 = 0; + SurfaceRole m_surfaceRole = SurfaceRole::Normal; std::optional m_autoPlaceCursorOffset; std::optional m_clientRequstPos; bool m_socketEnabled{ false }; bool m_windowAnimationEnabled{ true }; + + QPointer m_pendingActivationSeat; + std::unique_ptr m_windowTransition; const QString m_appId; }; diff --git a/tests/protocols/CMakeLists.txt b/tests/protocols/CMakeLists.txt index 6693143c3e..b313acd35d 100644 --- a/tests/protocols/CMakeLists.txt +++ b/tests/protocols/CMakeLists.txt @@ -67,6 +67,7 @@ add_subdirectory(treeland-show-desktop-v1) add_subdirectory(treeland-xwindow-control-unstable-v1) add_subdirectory(treeland-region-watch-unstable-v1) add_subdirectory(treeland-show-desktop-desktop-v1) +add_subdirectory(treeland-window-transition-unstable-v1) add_subdirectory(treeland-wine-window-state-unstable-v1) add_subdirectory(treeland-wine-window-management-unstable-v1) diff --git a/tests/protocols/INDEX.md b/tests/protocols/INDEX.md index 3d672d88a9..517692540a 100644 --- a/tests/protocols/INDEX.md +++ b/tests/protocols/INDEX.md @@ -52,6 +52,7 @@ Wayland 线上请求与事件;本文档规定发出请求后,测试必须观 | [treeland-xwindow-control-unstable-v1](treeland-xwindow-control-unstable-v1/README.md) | P | `set_xwindow_position_relative` 失败回调;成功路径需 Xwayland,当前环境未覆盖 | | [wine-window-management-unstable-v1](treeland-wine-window-management-unstable-v1/README.md) | P / E | 真实 wrapper 的位置与置顶层同步 | | [wine-window-state-unstable-v1](treeland-wine-window-state-unstable-v1/README.md) | P / E | 真实 wrapper 的最小化、attention 与可见性同步 | +| [window-transition-unstable-v1](treeland-window-transition-unstable-v1/README.md) | P / I / E | transition rect 进入生产 pending 集合、同 token 覆盖发一次 `closed`、旧 rect 析构不误删新 rect、销毁回收;map 前激活把**未 map** 的目标 arm 成 40×40 带 source surface(同 token 消费、他 token 不受影响),并跟随几何更新、0×0 禁用/重启用、source 清空/重设;激活迟到与无 originating surface 各发一次 `closed`;`invalid_geometry` / `invalid_surface` 协议错误 | | [drm (`wl_drm`)](drm/README.md) | P / V(GPU 条件) | wlroots native global、GBM DMA-BUF 与真实纹理 readback | | [input-method-unstable-v2](input-method-unstable-v2/README.md) | P / E | focused text-input 与 input method 的生产状态和编辑回传 | | [virtual-keyboard-unstable-v1](virtual-keyboard-unstable-v1/README.md) | P / E | 有效 keymap 的真实 seat 与 shortcut 链路 | @@ -104,13 +105,13 @@ xdg-shell 由共享 xdg-toplevel 夹具覆盖(多个 toplevel 测试复用) request stub 算作 request 覆盖;生成的 client-protocol 文件本身不计入。`destroy` 也单列, 因为它证明资源生命周期,却通常不承载主要业务语义。 -- **已注册并有测试目录的 24 个当前协议**:XML 共 221 条 request,其中测试客户端直接 - 调用了 **167 条(75.6%)**。 -- 去掉 59 条 `destroy` 生命周期 request 后,剩余 162 条工厂、配置和业务 request 中有 - **112 条(69.1%)** 被直接调用。 -- 24 个协议中 **19 个(79.2%)** 至少有一条 E 级生产业务链路;仅 DDM、output-manager +- **已注册并有测试目录的 25 个当前协议**:XML 共 226 条 request,其中测试客户端直接 + 调用了 **172 条(76.1%)**。 +- 去掉 61 条 `destroy` 生命周期 request 后,剩余 165 条工厂、配置和业务 request 中有 + **115 条(69.7%)** 被直接调用。 +- 25 个协议中 **20 个(80.0%)** 至少有一条 E 级生产业务链路;仅 DDM、output-manager color-control、wallpaper-color 仍停留在 I/P 层。 -- 当前 XML 共有 114 条 server event。这里**不发布“事件百分比”**:listener 中出现一个 +- 当前 XML 共有 115 条 server event。这里**不发布“事件百分比”**:listener 中出现一个 callback、或 fixture 手工发出一次 event,都不能证明事件负载或其业务来源被断言。下表只 列出已实际断言的关键 event,并明确列出尚未逐项验证的 event。 @@ -141,6 +142,7 @@ request stub 算作 request 覆盖;生成的 client-protocol 文件本身不 | layer-shell-extension-unstable-v1 | 4 / 4 | `begin_resize` 的 `resize_rejected(bad_serial)` | 成功 resize、clamp、`resizing` 生命周期 | | wine-window-management-unstable-v1 | 5 / 5 | `window_id/configure_position/configure_stacking`;真实 QQuickItem 位置/Z 值 | bottom/insert-after、多窗口 sibling、无效 sibling、重复 bind、越界坐标 | | wine-window-state-unstable-v1 | 6 / 7 | `state_changed`;真实最小化、attention 与可见性 | `activate/activate_denied`、重复 bind、toplevel 销毁后的 inert 状态 | +| window-transition-unstable-v1 | 5 / 5 | `get_window_transition_rect` 进入生产 pending 集合;同 token 覆盖发一次 `closed`,旧 rect 析构不误删当前 rect;销毁后集合移除;map 前激活把未 map 的目标 arm 成 40×40 带 source surface(同 token 消费、他 token 不受影响),并跟随几何更新、0×0 禁用/重启用、source 清空/重设;激活迟到与无 originating surface 各发一次 `closed`;`invalid_geometry` / `invalid_surface` 协议错误 | 目标销毁触发的 `closed`、60 s sweep、跨 client 的 `invalid_surface`、rect 全局位置、source surface 身份 | \* XML 共有 37 条 request;其中 manager `destroy` 的 `since=2` 高于生产 global 的 v1。 测试保留该调用用于兼容性/错误边界,正常可发布的 v1 request 集为其余 36 条。 diff --git a/tests/protocols/treeland-window-transition-unstable-v1/CMakeLists.txt b/tests/protocols/treeland-window-transition-unstable-v1/CMakeLists.txt new file mode 100644 index 0000000000..3324c22d6a --- /dev/null +++ b/tests/protocols/treeland-window-transition-unstable-v1/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +treeland_add_protocol_test( + NAME treeland_window_transition_unstable_v1 + XML "${TREELAND_PROTOCOLS_DATA_DIR}/treeland-window-transition-unstable-v1.xml" + SETUP "${CMAKE_CURRENT_SOURCE_DIR}/setup.cpp" + CLIENT "${CMAKE_CURRENT_SOURCE_DIR}/treeland-window-transition-unstable-v1.c" + EXTRA_XMLS "${WAYLAND_PROTOCOLS_DATADIR}/staging/xdg-activation/xdg-activation-v1.xml" +) diff --git a/tests/protocols/treeland-window-transition-unstable-v1/README.md b/tests/protocols/treeland-window-transition-unstable-v1/README.md new file mode 100644 index 0000000000..c2d8716164 --- /dev/null +++ b/tests/protocols/treeland-window-transition-unstable-v1/README.md @@ -0,0 +1,69 @@ +# `treeland-window-transition-unstable-v1` 测试规范 + +## 范围 + +- XML / interface:`treeland_window_transition_manager_v1` / `treeland_window_transition_rect_v1` +- 测试源码:`tests/protocols/treeland-window-transition-unstable-v1/` +- Fixture:desktop integration fixture(`protocol_test_setup` 取得生产 + `WindowTransitionManagerInterfaceV1`、创建 headless output,并记录 + `ShellHandler::surfaceWrapperAdded` 的 wrapper 以便回读真实 transition 状态) +- 覆盖等级:**P / I / E** + +`treeland_window_transition_manager_v1.get_window_transition_rect` 为激活 token 创建 +transition rect。测试通过 server bridge 在 compositor 线程回读生产 +`WindowTransitionManagerInterfaceV1::hasPendingWindowTransitionRect()`,证明 rect 真的进入 +`m_pendingRects`,而不只是请求可被派发。同一 token 再次创建会让前一个 rect 收到一次 +`closed`;客户端销毁 rect 后 pending 集合移除,而销毁被覆盖的旧 rect 不会误删当前 rect。 + +E 级场景走真实 `xdg-activation-v1` 链路:源 toplevel 用激活 token 字符串创建 rect +(geometry + source surface),目标 toplevel 在 map 前 `activate` 该 token,生产侧 +`associatePendingRect()` 消费 pending rect 并武装目标 `SurfaceWindowTransition`;回读 +`globalRect()` 的尺寸、`active()` 与 `sourceSurface()`,以及被武装 wrapper 的 mapped 状态, +因此断言落在"被武装的是那个尚未 map 的目标窗口"上,而不是"某个窗口被 arm 了"。 + +两条协议错误路径各自使用独立连接(`wl_resource_post_error` 会终止连接),并断言 +`wl_display_get_protocol_error()` 的 interface 与 code。 + +## 必须观察到的结果 + +| 场景 | 客户端动作 | 必须观察到的结果 | 证据层级 | +| --- | --- | --- | --- | +| pending 存储 | `get_window_transition_rect(token)` | 生产 `hasPendingWindowTransitionRect(token)` == 1,且没有任何 transition 被武装 | I | +| 过期前不关闭 | 已存储的 pending rect | 未收到 `closed` | P | +| 同 token 覆盖 | 对同一 token 再 `get_window_transition_rect` | 前一个 rect 收到恰好 1 次 `closed`,新的不关闭;再 roundtrip 不重复 | P | +| 旧 rect 析构 | 销毁被覆盖的旧 rect | 该 token 仍在 pending 集合(新 rect 未被误删) | I | +| 客户端销毁 | `treeland_window_transition_rect_v1.destroy` | pending 集合移除该 token | I | +| 激活关联 | 目标 map 前 `xdg_activation_v1.activate(token, target)` | 恰好 1 个 transition 被武装,且属于**未 map** 的窗口,带 40×40 的 rect 与 source surface;该 token 的 pending 被消费,另一个 token 的 rect 不被消费 | E | +| 几何更新 | 关联后 `set_geometry(3, 4, 20, 30)` | 被武装的 rect 变为 20×30,仍 armed | E | +| 0×0 禁用 | `set_geometry(0, 0, 0, 0)` | 不再 `active()`(armed_count == 0),但关联仍在(rect_present == 1,尺寸 0×0) | E | +| 重新启用 | `set_geometry(0, 0, 40, 40)` | 重新 `active()`,rect 40×40 | E | +| source 清空 / 重设 | `set_source_surface(NULL)`,随后连续两次设同一个 surface | 清空后无 source surface 且仍 armed;重设后 source surface 回来且不报错 | E | +| 激活迟到 | 目标已 map 后 `xdg_activation_v1.activate(token, target)` | rect 收到恰好 1 次 `closed`,pending 清空,且没有任何 transition 被武装 | E | +| 无 originating surface | token 未 `set_surface`,目标未 map 时 `activate` | rect 收到恰好 1 次 `closed`,pending 清空,且没有任何 transition 被武装 | E | +| 负几何 | `set_geometry(0, 0, -1, 4)` | `invalid_geometry` 协议错误 | P | +| 角色冲突 | 对已带 role 的 mapped toplevel surface `set_source_surface` | `invalid_surface` 协议错误 | P | + +## 生产结果 + +- `WindowTransitionManagerInterfaceV1::m_pendingRects` 的真实内容,经 + `hasPendingWindowTransitionRect()` 回读;`consume` 与 `discard` 都以 token 为键,因此 + 另一个 token 的 rect 会留在集合里。 +- `closed` 事件来自 `WindowTransitionRectV1::notifyDiscarded()`(同 token 覆盖、激活迟到、 + 无 originating surface 三条路径),断言按次数而非布尔,覆盖协议"至多一次"的约定。 +- 被武装目标的 `SurfaceWindowTransition` 状态经 `globalRect()`(尺寸)、`active()`、 + `sourceSurface()` 与所属 wrapper 的 `mapped()` 回读,对应 `Helper` 的 + `associatePendingRect()`;几何更新、0×0 禁用与 source 清空分别对应 + `updateWindowTransitionRect()` / `setWindowTransitionSource()` / + `clearWindowTransitionSource()`。 + +## 已知边界 / 下一项结果 + +- 未覆盖目标销毁触发的 rect `closed`:依赖关闭动画时序,客户端侧没有 roundtrip 之外 + 的确定性同步点。 +- 未覆盖 `set_geometry` 的 60 s sweep 过期:需要真实等待或给常量加测试注入点。 +- 未覆盖跨 client 的 `invalid_surface` 分支(生产里的 `wl_resource_get_client` 不匹配): + libwayland-server 对不属于本连接的对象 id 解析为 NULL,生产代码会走"清空 source" + 而不是报错,该分支是否可达需实测确认。 +- 只断言 rect 的尺寸,未断言其全局位置(位置依赖 output 布局与源窗口位置)。 +- 只断言 source surface 存在,未断言它就是客户端设置的那个 surface(wrapper 未暴露 + surface 标识;加强这一断言需要生产侧新增访问器)。 diff --git a/tests/protocols/treeland-window-transition-unstable-v1/setup.cpp b/tests/protocols/treeland-window-transition-unstable-v1/setup.cpp new file mode 100644 index 0000000000..f89206c6cb --- /dev/null +++ b/tests/protocols/treeland-window-transition-unstable-v1/setup.cpp @@ -0,0 +1,84 @@ +// 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 "server-bridge.h" +#include "treeland-window-transition-unstable-v1.h" + +#include "core/shellhandler.h" +#include "modules/window-transition/windowtransitionmanagerinterfacev1.h" +#include "seat/helper.h" +#include "surface/surfacewindowtransition.h" +#include "surface/surfacewrapper.h" + +#include +#include +#include +#include +#include + +#include + +namespace { +WindowTransitionManagerInterfaceV1 *g_manager = nullptr; +QList> g_wrappers; +} + +void protocol_test_setup(Helper *helper) +{ + g_manager = find_server_interface(helper); + Q_ASSERT(g_manager); + add_headless_output(helper->backend(), false); + QObject::connect(helper->shellHandler(), + &ShellHandler::surfaceWrapperAdded, + helper, + [](SurfaceWrapper *wrapper) { + g_wrappers.append(QPointer(wrapper)); + }); +} + +extern "C" void window_transition_read_server_state(void *data) +{ + auto *state = static_cast(data); + state->manager_present = g_manager ? 1 : 0; + state->pending = 0; + state->armed_count = 0; + state->rect_present = 0; + state->rect_mapped = 0; + state->rect_w = 0; + state->rect_h = 0; + state->source_set = 0; + if (g_manager && state->token[0] != '\0') { + state->pending = g_manager->hasPendingWindowTransitionRect( + QString::fromUtf8(state->token)) + ? 1 + : 0; + } + + // Report the rectangle the production manager associated with a real + // SurfaceWindowTransition. globalRect() resolves only for a transition that + // holds the manager's rect, so its value names the armed window instead of + // trusting the first wrapper that happens to be active. + for (const auto &wrapper : std::as_const(g_wrappers)) { + if (!wrapper) + continue; + auto *transition = + qobject_cast(wrapper->windowTransition()); + if (!transition) + continue; + if (transition->active()) + ++state->armed_count; + + // The rect survives a 0x0 geometry (that only disables it), so presence + // and size are reported separately from active(). + if (state->rect_present) + continue; + const auto rect = transition->globalRect(); + if (!rect) + continue; + state->rect_present = 1; + state->rect_mapped = + (wrapper->surface() && wrapper->surface()->mapped()) ? 1 : 0; + state->rect_w = qRound(rect->width()); + state->rect_h = qRound(rect->height()); + state->source_set = transition->sourceSurface() ? 1 : 0; + } +} diff --git a/tests/protocols/treeland-window-transition-unstable-v1/treeland-window-transition-unstable-v1.c b/tests/protocols/treeland-window-transition-unstable-v1/treeland-window-transition-unstable-v1.c new file mode 100644 index 0000000000..8a16d668c5 --- /dev/null +++ b/tests/protocols/treeland-window-transition-unstable-v1/treeland-window-transition-unstable-v1.c @@ -0,0 +1,878 @@ +// 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 +/* + * Coverage level P / I / E. + * + * The client drives treeland_window_transition_manager_v1 and reads back the + * production WindowTransitionManagerInterfaceV1 pending set and the armed + * SurfaceWindowTransition through the server bridge, so stored/associated + * rectangles are proven rather than a dispatchable request. + * + * The activation scenarios need a real xdg-activation-v1 token: an originating + * mapped toplevel attaches the rectangle to the token, then a target toplevel + * activates the token before it maps (association, which also proves the armed + * window is the unmapped target) or after it maps (the "activation arrived too + * late" fallback that closes the rectangle). + * + * The protocol-error paths each use their own connection because + * wl_resource_post_error tears the connection down. + */ + +#include "client-connection.h" +#include "server-bridge-api.h" +#include "treeland-window-transition-unstable-v1-client-protocol.h" +#include "treeland-window-transition-unstable-v1.h" +#include "xdg-activation-v1-client-protocol.h" +#include "xdg-toplevel-client.h" + +#include +#include + +#define WINDOW_TRANSITION_MANAGER "treeland_window_transition_manager_v1" + +struct rect_events { + int closed_count; +}; + +static void rect_handle_closed(void *data, struct treeland_window_transition_rect_v1 *rect) +{ + (void)rect; + ((struct rect_events *)data)->closed_count++; +} + +static const struct treeland_window_transition_rect_v1_listener rect_listener = { + .closed = rect_handle_closed, +}; + +static struct treeland_window_transition_rect_v1 *create_rect( + struct treeland_window_transition_manager_v1 *manager, + const char *token, + struct rect_events *events) +{ + struct treeland_window_transition_rect_v1 *rect = + treeland_window_transition_manager_v1_get_window_transition_rect(manager, token); + if (rect) + treeland_window_transition_rect_v1_add_listener(rect, &rect_listener, events); + return rect; +} + +static int read_state(const char *token, struct window_transition_server_state *state) +{ + memset(state, 0, sizeof(*state)); + snprintf(state->token, sizeof(state->token), "%s", token); + if (!invoke_on_server_thread(window_transition_read_server_state, state)) + return 0; + return state->manager_present; +} + +/* 1 when the token is in the production pending set, 0 when it is not, -1 when + * the production state could not be read at all. */ +static int read_pending(const char *token) +{ + struct window_transition_server_state state; + if (!read_state(token, &state)) + return -1; + return state.pending; +} + +/* + * The associated rectangle is described by the production wrapper the manager + * armed, so these assertions are about the real window transition rather than + * about the request being dispatched. + */ +static int expect_association(const struct window_transition_server_state *state, + int expected_mapped) +{ + if (state->armed_count != 1) { + TEST_ERROR("window-transition: expected exactly one armed transition, got %d\n", + state->armed_count); + return 0; + } + if (!state->rect_present) { + TEST_ERROR("window-transition: armed transition exposes no rectangle\n"); + return 0; + } + if (state->rect_mapped != expected_mapped) { + TEST_ERROR("window-transition: armed transition mapped=%d, expected %d\n", + state->rect_mapped, expected_mapped); + return 0; + } + if (!state->source_set) { + TEST_ERROR("window-transition: armed transition has no source surface\n"); + return 0; + } + return 1; +} + +static int expect_rect_size(const struct window_transition_server_state *state, + int width, + int height) +{ + if (!state->rect_present) { + TEST_ERROR("window-transition: no associated rectangle to size\n"); + return 0; + } + if (state->rect_w != width || state->rect_h != height) { + TEST_ERROR("window-transition: associated rect is %dx%d, expected %dx%d\n", + state->rect_w, state->rect_h, width, height); + return 0; + } + return 1; +} + +struct activation_token_state { + int done; + char token[128]; +}; + +static void token_handle_done(void *data, + struct xdg_activation_token_v1 *token, + const char *token_string) +{ + (void)token; + struct activation_token_state *state = data; + state->done = 1; + if (token_string) + snprintf(state->token, sizeof(state->token), "%s", token_string); +} + +static const struct xdg_activation_token_v1_listener token_listener = { + .done = token_handle_done, +}; + +/* + * Obtains a committed activation token string. `originating` may be NULL, which + * leaves the token without the set_surface hint: the compositor then has no + * surface to map the rectangle to and must discard the pending rect. + */ +static int obtain_token(struct client_connection *connection, + struct xdg_activation_v1 *activation, + struct wl_surface *originating, + char *token, + size_t token_size) +{ + struct xdg_activation_token_v1 *request = xdg_activation_v1_get_activation_token(activation); + if (!request) + return 0; + + struct activation_token_state state = { 0 }; + if (originating) + xdg_activation_token_v1_set_surface(request, originating); + xdg_activation_token_v1_add_listener(request, &token_listener, &state); + xdg_activation_token_v1_commit(request); + + const int ok = wl_display_roundtrip(connection->display) >= 0 + && state.done && state.token[0] != '\0'; + if (ok) + snprintf(token, token_size, "%s", state.token); + xdg_activation_token_v1_destroy(request); + return ok; +} + +static int run_pending_and_events(const char *socket_name) +{ + struct client_connection connection = { 0 }; + if (!client_connect(&connection, socket_name)) + return 0; + + struct treeland_window_transition_manager_v1 *manager = + client_bind(&connection, + WINDOW_TRANSITION_MANAGER, + &treeland_window_transition_manager_v1_interface, + treeland_window_transition_manager_v1_interface.version); + if (!manager) { + TEST_ERROR("window-transition: manager missing\n"); + client_disconnect(&connection); + return 0; + } + + int ok = 1; + struct window_transition_server_state state; + + /* A rectangle created for a token is kept in the production pending set. */ + struct rect_events events_a = { 0 }; + struct treeland_window_transition_rect_v1 *rect_a = + create_rect(manager, "treeland-transition-token-a", &events_a); + if (!rect_a || wl_display_roundtrip(connection.display) < 0) { + TEST_ERROR("window-transition: failed to create the first rectangle\n"); + ok = 0; + } else if (!read_state("treeland-transition-token-a", &state)) { + TEST_ERROR("window-transition: production manager not reachable\n"); + ok = 0; + } else if (!state.pending) { + TEST_ERROR("window-transition: pending rect for token-a not stored\n"); + ok = 0; + } else if (state.armed_count != 0 || state.rect_present != 0) { + TEST_ERROR("window-transition: a stored rect armed a transition by itself\n"); + ok = 0; + } else if (events_a.closed_count) { + TEST_ERROR("window-transition: pending rect closed prematurely\n"); + ok = 0; + } + + /* Creating another rectangle for the same token closes the previous one. */ + struct rect_events events_first = { 0 }; + struct rect_events events_second = { 0 }; + struct treeland_window_transition_rect_v1 *rect_first = NULL; + struct treeland_window_transition_rect_v1 *rect_second = NULL; + if (ok) { + rect_first = create_rect(manager, "treeland-transition-token-dup", &events_first); + rect_second = create_rect(manager, "treeland-transition-token-dup", &events_second); + if (!rect_first || !rect_second || wl_display_roundtrip(connection.display) < 0) { + TEST_ERROR("window-transition: failed to create the superseded pair\n"); + ok = 0; + } else if (events_first.closed_count != 1) { + TEST_ERROR("window-transition: superseded rect got %d closed events, expected 1\n", + events_first.closed_count); + ok = 0; + } else if (events_second.closed_count) { + TEST_ERROR("window-transition: replacement rect closed unexpectedly\n"); + ok = 0; + } + } + + /* closed is a one-shot notification; the superseded rect must not repeat it. */ + if (ok) { + if (wl_display_roundtrip(connection.display) < 0) { + ok = 0; + } else if (events_first.closed_count != 1) { + TEST_ERROR("window-transition: superseded rect repeated closed\n"); + ok = 0; + } + } + + /* Destroying the superseded rect must not drop the replacement from the + * pending set: only the rect the manager currently holds for the token is + * removed. */ + if (ok) { + treeland_window_transition_rect_v1_destroy(rect_first); + rect_first = NULL; + if (wl_display_roundtrip(connection.display) < 0) { + ok = 0; + } else if (read_pending("treeland-transition-token-dup") != 1) { + TEST_ERROR("window-transition: destroying the superseded rect dropped the" + " replacement\n"); + ok = 0; + } + } + + /* Destroying the client object removes it from the pending set. */ + struct rect_events events_idle = { 0 }; + struct treeland_window_transition_rect_v1 *rect_idle = NULL; + if (ok) { + rect_idle = create_rect(manager, "treeland-transition-token-idle", &events_idle); + if (!rect_idle || wl_display_roundtrip(connection.display) < 0) { + TEST_ERROR("window-transition: failed to create the recyclable rect\n"); + ok = 0; + } else { + treeland_window_transition_rect_v1_destroy(rect_idle); + rect_idle = NULL; + if (wl_display_roundtrip(connection.display) < 0) { + ok = 0; + } else if (read_pending("treeland-transition-token-idle") != 0) { + TEST_ERROR("window-transition: destroyed rect still pending\n"); + ok = 0; + } + } + } + + if (rect_idle) + treeland_window_transition_rect_v1_destroy(rect_idle); + if (rect_second) + treeland_window_transition_rect_v1_destroy(rect_second); + if (rect_first) + treeland_window_transition_rect_v1_destroy(rect_first); + if (rect_a) + treeland_window_transition_rect_v1_destroy(rect_a); + treeland_window_transition_manager_v1_destroy(manager); + client_disconnect(&connection); + return ok; +} + +/* + * E: once the rectangle is associated the object stays live. The armed + * transition must follow later set_geometry calls, a 0x0 geometry must disable + * it without dropping the association, and the source surface may be cleared + * and attached again. + */ +struct associated_rect { + struct wl_display *display; + struct treeland_window_transition_rect_v1 *rect; + struct wl_surface *source; + struct rect_events *events; + const char *token; +}; + +static int run_associated_rect_updates(const struct associated_rect *associated) +{ + struct window_transition_server_state state; + int ok = 1; + + /* The client may update the geometry; the armed transition must follow the + * latest value. */ + treeland_window_transition_rect_v1_set_geometry(associated->rect, 3, 4, 20, 30); + if (wl_display_roundtrip(associated->display) < 0) { + ok = 0; + } else if (!read_state(associated->token, &state)) { + ok = 0; + } else if (!expect_association(&state, 0) || !expect_rect_size(&state, 20, 30)) { + ok = 0; + } + + /* A 0x0 geometry is the documented way to disable the rectangle: the + * association stays alive, only the animation source goes away. */ + if (ok) { + treeland_window_transition_rect_v1_set_geometry(associated->rect, 0, 0, 0, 0); + if (wl_display_roundtrip(associated->display) < 0) { + ok = 0; + } else if (!read_state(associated->token, &state)) { + ok = 0; + } else if (state.armed_count != 0) { + TEST_ERROR("window-transition: 0x0 geometry did not disable the rect\n"); + ok = 0; + } else if (!state.rect_present) { + TEST_ERROR("window-transition: 0x0 geometry dropped the association\n"); + ok = 0; + } else if (!expect_rect_size(&state, 0, 0)) { + ok = 0; + } + } + + /* ...and a positive size again re-enables it. */ + if (ok) { + treeland_window_transition_rect_v1_set_geometry(associated->rect, 0, 0, 40, 40); + if (wl_display_roundtrip(associated->display) < 0) { + ok = 0; + } else if (!read_state(associated->token, &state)) { + ok = 0; + } else if (!expect_association(&state, 0) || !expect_rect_size(&state, 40, 40)) { + ok = 0; + } + } + + /* Passing null clears the source surface without disarming the rectangle. */ + if (ok) { + treeland_window_transition_rect_v1_set_source_surface(associated->rect, NULL); + if (wl_display_roundtrip(associated->display) < 0) { + ok = 0; + } else if (!read_state(associated->token, &state)) { + ok = 0; + } else if (state.source_set) { + TEST_ERROR("window-transition: null source surface did not clear the source\n"); + ok = 0; + } else if (state.armed_count != 1 || !state.rect_present) { + TEST_ERROR("window-transition: clearing the source disarmed the rect\n"); + ok = 0; + } + } + + /* ...and the same surface may be attached again, repeatedly. */ + if (ok) { + treeland_window_transition_rect_v1_set_source_surface(associated->rect, associated->source); + treeland_window_transition_rect_v1_set_source_surface(associated->rect, associated->source); + if (wl_display_roundtrip(associated->display) < 0) { + ok = 0; + } else if (!read_state(associated->token, &state)) { + ok = 0; + } else if (!expect_association(&state, 0) || !expect_rect_size(&state, 40, 40)) { + ok = 0; + } else if (associated->events->closed_count) { + TEST_ERROR("window-transition: associated rect was closed during the updates\n"); + ok = 0; + } + } + + return ok; +} + +/* + * E: an originating toplevel attaches a rectangle (geometry + live source + * surface) to an activation token, and a target toplevel activates that token + * before it maps. The production manager must consume the pending rect and arm + * the still unmapped target with the rectangle and the source surface. + */ +static int run_associate_on_activation(const char *socket_name) +{ + struct client_connection connection = { 0 }; + struct xdg_toplevel_client origin = { 0 }; + struct xdg_toplevel_client target = { 0 }; + struct wl_surface *source = NULL; + struct treeland_window_transition_rect_v1 *rect = NULL; + struct treeland_window_transition_rect_v1 *decoy = NULL; + if (!client_connect(&connection, socket_name)) + return 0; + + struct treeland_window_transition_manager_v1 *manager = + client_bind(&connection, + WINDOW_TRANSITION_MANAGER, + &treeland_window_transition_manager_v1_interface, + treeland_window_transition_manager_v1_interface.version); + struct xdg_activation_v1 *activation = + client_bind(&connection, + xdg_activation_v1_interface.name, + &xdg_activation_v1_interface, + xdg_activation_v1_interface.version); + struct wl_compositor *compositor = + client_bind(&connection, "wl_compositor", &wl_compositor_interface, 1); + if (!manager || !activation || !compositor) { + TEST_ERROR("window-transition: missing manager, activation or compositor\n"); + client_disconnect(&connection); + return 0; + } + + int ok = 1; + char token[128]; + struct window_transition_server_state state; + + /* A: the mapped originating toplevel that owns the activation token. */ + if (!xdg_toplevel_client_create(&connection, &origin)) { + TEST_ERROR("window-transition: failed to create the originating toplevel\n"); + ok = 0; + } + + if (ok) { + source = wl_compositor_create_surface(compositor); + if (!source) { + TEST_ERROR("window-transition: failed to create the source surface\n"); + ok = 0; + } + } + + if (ok && !obtain_token(&connection, activation, origin.surface, token, sizeof(token))) { + TEST_ERROR("window-transition: failed to obtain activation token\n"); + ok = 0; + } + + struct rect_events events = { 0 }; + if (ok) { + rect = treeland_window_transition_manager_v1_get_window_transition_rect(manager, token); + if (!rect) { + TEST_ERROR("window-transition: failed to create the transition rect\n"); + ok = 0; + } else { + treeland_window_transition_rect_v1_add_listener(rect, &rect_listener, &events); + treeland_window_transition_rect_v1_set_geometry(rect, 0, 0, 40, 40); + treeland_window_transition_rect_v1_set_source_surface(rect, source); + if (wl_display_roundtrip(connection.display) < 0) { + TEST_ERROR("window-transition: rect configuration was rejected\n"); + ok = 0; + } + } + } + + /* A second rectangle under an unrelated token must survive the activation: + * the manager consumes the pending rect of this token only. */ + struct rect_events decoy_events = { 0 }; + if (ok) { + decoy = create_rect(manager, "treeland-transition-token-decoy", &decoy_events); + if (!decoy || wl_display_roundtrip(connection.display) < 0) { + TEST_ERROR("window-transition: failed to create the decoy rectangle\n"); + ok = 0; + } + } + + if (ok) { + if (!read_state(token, &state)) { + TEST_ERROR("window-transition: production manager not reachable\n"); + ok = 0; + } else if (!state.pending) { + TEST_ERROR("window-transition: rect not pending before activation\n"); + ok = 0; + } else if (state.armed_count != 0 || state.rect_present != 0) { + TEST_ERROR("window-transition: a transition was armed before activation\n"); + ok = 0; + } + } + + /* B: the target toplevel, created but not mapped yet. */ + if (ok + && (!xdg_toplevel_client_create_pending(&connection, &target) + || wl_display_roundtrip(connection.display) < 0)) { + TEST_ERROR("window-transition: failed to create the pending target\n"); + ok = 0; + } + + if (ok) { + xdg_activation_v1_activate(activation, token, target.surface); + if (wl_display_roundtrip(connection.display) < 0) { + TEST_ERROR("window-transition: activation was rejected\n"); + ok = 0; + } + } + + /* The pending rect is consumed and the still unmapped target is armed with + * the rectangle and the live source surface. */ + if (ok) { + if (!read_state(token, &state)) { + TEST_ERROR("window-transition: production manager not reachable\n"); + ok = 0; + } else if (state.pending) { + TEST_ERROR("window-transition: pending rect survived association\n"); + ok = 0; + } else if (!expect_association(&state, 0)) { + ok = 0; + } else if (!expect_rect_size(&state, 40, 40)) { + ok = 0; + } else if (events.closed_count) { + TEST_ERROR("window-transition: associated rect was closed while the target lives\n"); + ok = 0; + } else if (read_pending("treeland-transition-token-decoy") != 1) { + TEST_ERROR("window-transition: association consumed an unrelated token's rect\n"); + ok = 0; + } else if (decoy_events.closed_count) { + TEST_ERROR("window-transition: decoy rect was closed by the activation\n"); + ok = 0; + } + } + + /* The rectangle stays live after the association. */ + if (ok) { + const struct associated_rect associated = { + .display = connection.display, + .rect = rect, + .source = source, + .events = &events, + .token = token, + }; + ok = run_associated_rect_updates(&associated); + } + + if (decoy) + treeland_window_transition_rect_v1_destroy(decoy); + if (rect) + treeland_window_transition_rect_v1_destroy(rect); + if (source) + wl_surface_destroy(source); + if (origin.surface) + xdg_toplevel_client_destroy(&origin); + if (target.surface) + xdg_toplevel_client_destroy(&target); + client_disconnect(&connection); + return ok; +} + +/* + * E: the target is already mapped when the activation arrives. The compositor + * falls back to its default animation, drops the pending rectangle and closes + * it without arming any transition. + */ +static int run_closed_when_activation_late(const char *socket_name) +{ + struct client_connection connection = { 0 }; + struct xdg_toplevel_client origin = { 0 }; + struct xdg_toplevel_client target = { 0 }; + struct treeland_window_transition_rect_v1 *rect = NULL; + if (!client_connect(&connection, socket_name)) + return 0; + + struct treeland_window_transition_manager_v1 *manager = + client_bind(&connection, + WINDOW_TRANSITION_MANAGER, + &treeland_window_transition_manager_v1_interface, + treeland_window_transition_manager_v1_interface.version); + struct xdg_activation_v1 *activation = + client_bind(&connection, + xdg_activation_v1_interface.name, + &xdg_activation_v1_interface, + xdg_activation_v1_interface.version); + if (!manager || !activation) { + TEST_ERROR("window-transition: missing manager or activation\n"); + client_disconnect(&connection); + return 0; + } + + int ok = 1; + char token[128]; + + /* Both the origin and the target are mapped before the activation. */ + if (!xdg_toplevel_client_create(&connection, &origin)) { + TEST_ERROR("window-transition: failed to create the originating toplevel\n"); + ok = 0; + } + if (ok && !xdg_toplevel_client_create(&connection, &target)) { + TEST_ERROR("window-transition: failed to create the mapped target\n"); + ok = 0; + } + if (ok && !obtain_token(&connection, activation, origin.surface, token, sizeof(token))) { + TEST_ERROR("window-transition: failed to obtain activation token\n"); + ok = 0; + } + + struct rect_events events = { 0 }; + if (ok) { + rect = treeland_window_transition_manager_v1_get_window_transition_rect(manager, token); + if (!rect) { + TEST_ERROR("window-transition: failed to create the transition rect\n"); + ok = 0; + } else { + treeland_window_transition_rect_v1_add_listener(rect, &rect_listener, &events); + treeland_window_transition_rect_v1_set_geometry(rect, 0, 0, 40, 40); + if (wl_display_roundtrip(connection.display) < 0) { + TEST_ERROR("window-transition: rect configuration was rejected\n"); + ok = 0; + } + } + } + + struct window_transition_server_state state; + if (ok) { + if (!read_state(token, &state)) { + TEST_ERROR("window-transition: production manager not reachable\n"); + ok = 0; + } else if (!state.pending) { + TEST_ERROR("window-transition: rect not pending before late activation\n"); + ok = 0; + } + } + + if (ok) { + /* target.surface is already mapped, so the compositor discards the + * pending rect and must emit closed. */ + xdg_activation_v1_activate(activation, token, target.surface); + if (wl_display_roundtrip(connection.display) < 0) { + TEST_ERROR("window-transition: late activation was rejected\n"); + ok = 0; + } + } + + if (ok && events.closed_count != 1) { + TEST_ERROR("window-transition: late activation got %d closed events, expected 1\n", + events.closed_count); + ok = 0; + } + + if (ok) { + if (!read_state(token, &state)) { + ok = 0; + } else if (state.pending) { + TEST_ERROR("window-transition: late activation left the rect pending\n"); + ok = 0; + } else if (state.armed_count != 0 || state.rect_present != 0) { + TEST_ERROR("window-transition: late activation armed a transition\n"); + ok = 0; + } + } + + if (rect) + treeland_window_transition_rect_v1_destroy(rect); + if (origin.surface) + xdg_toplevel_client_destroy(&origin); + if (target.surface) + xdg_toplevel_client_destroy(&target); + client_disconnect(&connection); + return ok; +} + +/* + * E: the activation token carries no originating surface (the client never + * called set_surface). The rectangle cannot be mapped to global coordinates, + * so the compositor must discard the pending rect right away and close it + * instead of keeping it until the pending sweep expires. + */ +static int run_discarded_without_origin_surface(const char *socket_name) +{ + struct client_connection connection = { 0 }; + struct xdg_toplevel_client target = { 0 }; + struct treeland_window_transition_rect_v1 *rect = NULL; + if (!client_connect(&connection, socket_name)) + return 0; + + struct treeland_window_transition_manager_v1 *manager = + client_bind(&connection, + WINDOW_TRANSITION_MANAGER, + &treeland_window_transition_manager_v1_interface, + treeland_window_transition_manager_v1_interface.version); + struct xdg_activation_v1 *activation = + client_bind(&connection, + xdg_activation_v1_interface.name, + &xdg_activation_v1_interface, + xdg_activation_v1_interface.version); + if (!manager || !activation) { + TEST_ERROR("window-transition: missing manager or activation\n"); + client_disconnect(&connection); + return 0; + } + + int ok = 1; + char token[128]; + struct window_transition_server_state state; + + /* The target is a real toplevel but stays unmapped, so only the missing + * originating surface can make the compositor drop the rect. */ + if (!xdg_toplevel_client_create_pending(&connection, &target) + || wl_display_roundtrip(connection.display) < 0) { + TEST_ERROR("window-transition: failed to create the pending target\n"); + ok = 0; + } + + if (ok && !obtain_token(&connection, activation, NULL, token, sizeof(token))) { + TEST_ERROR("window-transition: failed to obtain activation token\n"); + ok = 0; + } + + struct rect_events events = { 0 }; + if (ok) { + rect = treeland_window_transition_manager_v1_get_window_transition_rect(manager, token); + if (!rect) { + TEST_ERROR("window-transition: failed to create the transition rect\n"); + ok = 0; + } else { + treeland_window_transition_rect_v1_add_listener(rect, &rect_listener, &events); + treeland_window_transition_rect_v1_set_geometry(rect, 0, 0, 40, 40); + if (wl_display_roundtrip(connection.display) < 0) { + TEST_ERROR("window-transition: rect configuration was rejected\n"); + ok = 0; + } + } + } + + if (ok && read_pending(token) != 1) { + TEST_ERROR("window-transition: rect without an originating surface is not pending\n"); + ok = 0; + } + + if (ok) { + xdg_activation_v1_activate(activation, token, target.surface); + if (wl_display_roundtrip(connection.display) < 0) { + TEST_ERROR("window-transition: activation was rejected\n"); + ok = 0; + } + } + + if (ok) { + if (events.closed_count != 1) { + TEST_ERROR("window-transition: rect without an originating surface got %d closed" + " events, expected 1\n", + events.closed_count); + ok = 0; + } else if (!read_state(token, &state)) { + TEST_ERROR("window-transition: production manager not reachable\n"); + ok = 0; + } else if (state.pending) { + TEST_ERROR("window-transition: rect without an originating surface stayed pending\n"); + ok = 0; + } else if (state.armed_count != 0 || state.rect_present != 0) { + TEST_ERROR("window-transition: rect without an originating surface armed a" + " transition\n"); + ok = 0; + } + } + + if (rect) + treeland_window_transition_rect_v1_destroy(rect); + if (target.surface) + xdg_toplevel_client_destroy(&target); + client_disconnect(&connection); + return ok; +} + +static int run_invalid_geometry(const char *socket_name) +{ + struct client_connection connection = { 0 }; + if (!client_connect(&connection, socket_name)) + return 0; + + struct treeland_window_transition_manager_v1 *manager = + client_bind(&connection, + WINDOW_TRANSITION_MANAGER, + &treeland_window_transition_manager_v1_interface, + treeland_window_transition_manager_v1_interface.version); + if (!manager) { + TEST_ERROR("window-transition: manager missing\n"); + client_disconnect(&connection); + return 0; + } + + struct treeland_window_transition_rect_v1 *rect = + treeland_window_transition_manager_v1_get_window_transition_rect( + manager, "treeland-transition-token-geometry"); + if (!rect) { + TEST_ERROR("window-transition: failed to create the transition rect\n"); + client_disconnect(&connection); + return 0; + } + + /* A negative width or height is the invalid_geometry path. */ + treeland_window_transition_rect_v1_set_geometry(rect, 0, 0, -1, 4); + + const struct wl_interface *error_interface = NULL; + uint32_t error_id = 0; + const int rejected = + wl_display_roundtrip(connection.display) < 0 + && wl_display_get_protocol_error(connection.display, &error_interface, &error_id) + == TREELAND_WINDOW_TRANSITION_RECT_V1_ERROR_INVALID_GEOMETRY + && error_interface == &treeland_window_transition_rect_v1_interface; + if (!rejected) + TEST_ERROR("window-transition: negative geometry did not raise invalid_geometry\n"); + client_disconnect(&connection); + return rejected; +} + +static int run_invalid_surface(const char *socket_name) +{ + struct client_connection connection = { 0 }; + struct xdg_toplevel_client toplevel = { 0 }; + if (!client_connect(&connection, socket_name)) + return 0; + + struct treeland_window_transition_manager_v1 *manager = + client_bind(&connection, + WINDOW_TRANSITION_MANAGER, + &treeland_window_transition_manager_v1_interface, + treeland_window_transition_manager_v1_interface.version); + if (!manager) { + TEST_ERROR("window-transition: manager missing\n"); + client_disconnect(&connection); + return 0; + } + + /* A mapped toplevel already carries an xdg role, so it must be rejected as + * a transition source surface. */ + if (!xdg_toplevel_client_create(&connection, &toplevel)) { + TEST_ERROR("window-transition: failed to create the role-holding toplevel\n"); + treeland_window_transition_manager_v1_destroy(manager); + client_disconnect(&connection); + return 0; + } + + struct treeland_window_transition_rect_v1 *rect = + treeland_window_transition_manager_v1_get_window_transition_rect( + manager, "treeland-transition-token-surface"); + if (!rect) { + TEST_ERROR("window-transition: failed to create the transition rect\n"); + client_disconnect(&connection); + return 0; + } + + treeland_window_transition_rect_v1_set_source_surface(rect, toplevel.surface); + + const struct wl_interface *error_interface = NULL; + uint32_t error_id = 0; + const int rejected = + wl_display_roundtrip(connection.display) < 0 + && wl_display_get_protocol_error(connection.display, &error_interface, &error_id) + == TREELAND_WINDOW_TRANSITION_RECT_V1_ERROR_INVALID_SURFACE + && error_interface == &treeland_window_transition_rect_v1_interface; + if (!rejected) + TEST_ERROR("window-transition: role-holding surface did not raise invalid_surface\n"); + client_disconnect(&connection); + return rejected; +} + +int protocol_test_run(const char *socket_name) +{ + int failed = 0; + if (!run_pending_and_events(socket_name)) + failed = 1; + if (!run_associate_on_activation(socket_name)) + failed = 1; + if (!run_closed_when_activation_late(socket_name)) + failed = 1; + if (!run_discarded_without_origin_surface(socket_name)) + failed = 1; + if (!run_invalid_geometry(socket_name)) + failed = 1; + if (!run_invalid_surface(socket_name)) + failed = 1; + return failed; +} diff --git a/tests/protocols/treeland-window-transition-unstable-v1/treeland-window-transition-unstable-v1.h b/tests/protocols/treeland-window-transition-unstable-v1/treeland-window-transition-unstable-v1.h new file mode 100644 index 0000000000..045fe0f07a --- /dev/null +++ b/tests/protocols/treeland-window-transition-unstable-v1/treeland-window-transition-unstable-v1.h @@ -0,0 +1,44 @@ +// 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 + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Shared POD snapshot between the C client and the C++ fixture. + * + * `token` is an *input*: the client fills it with the activation token string + * passed to get_window_transition_rect before invoking the read on the + * compositor thread, so the callback must not clear it. The remaining fields + * are outputs written on the compositor thread. + * + * The `rect_*` fields describe the rectangle the production manager actually + * handed to a SurfaceWindowTransition, read from that wrapper's real state + * instead of from any wrapper that happens to be armed: + * + * armed_count wrappers whose transition is active() (non-empty rect) + * rect_present an associated rect is reachable (globalRect() has a value) + * rect_mapped that rect's wrapper surface is already mapped + * rect_w/rect_h that rect's size in pixels, 0x0 while it is disabled + * source_set that transition carries a source surface + */ +struct window_transition_server_state { + char token[128]; /* input: token string to query */ + int manager_present; /* output: production manager interface found */ + int pending; /* output: hasPendingWindowTransitionRect(token) */ + int armed_count; /* output: wrappers with an active transition rect */ + int rect_present; /* output: an associated rect is reachable */ + int rect_mapped; /* output: the associated rect's wrapper is mapped */ + int rect_w; /* output: associated rect width in px */ + int rect_h; /* output: associated rect height in px */ + int source_set; /* output: the associated transition has a source surface */ +}; + +void window_transition_read_server_state(void *data); + +#ifdef __cplusplus +} +#endif diff --git a/tests/protocols/wayland-xdg-activation-v1/setup.cpp b/tests/protocols/wayland-xdg-activation-v1/setup.cpp index f8faeef1eb..18327bdc8d 100644 --- a/tests/protocols/wayland-xdg-activation-v1/setup.cpp +++ b/tests/protocols/wayland-xdg-activation-v1/setup.cpp @@ -23,11 +23,17 @@ void protocol_test_setup(Helper *helper) g_mgr = find_server_interface(helper); if (g_mgr) { - QObject::connect(g_mgr, &ActivationManagerInterfaceV1::activateRequested, - helper, [](ActivationManagerInterfaceV1::TokenDisposition disposition, - WAYLIB_SERVER_NAMESPACE::WSurface *surface, - WAYLIB_SERVER_NAMESPACE::WSeat *seat) { - (void)seat; + QObject::connect(g_mgr, + &ActivationManagerInterfaceV1::activateRequested, + helper, + [](const QString &token, + ActivationManagerInterfaceV1::TokenDisposition disposition, + WAYLIB_SERVER_NAMESPACE::WSurface *surface, + WAYLIB_SERVER_NAMESPACE::WSeat *seat, + WAYLIB_SERVER_NAMESPACE::WSurface *originatingSurface) { + Q_UNUSED(token); + Q_UNUSED(seat); + Q_UNUSED(originatingSurface); g_surface_ptr = surface; g_activate_requested = true; g_captured_disposition = static_cast(disposition);