From 8c6304a6ec1d913323e18f9796b47db3453a2350 Mon Sep 17 00:00:00 2001 From: LFRon Date: Thu, 23 Jul 2026 22:02:33 +0800 Subject: [PATCH 1/2] fix(surface): negotiate launch-time maximize before presentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit XWayland and XDG clients can request maximization before Treeland has created or exposed a SurfaceWrapper. Handling that request only after map leaves the client and compositor state out of sync, or produces a visible second maximize transition after the window is presented. For XDG toplevels, notify Treeland synchronously from Waylib's initial empty commit after scheduling the default 0x0 configure. ShellHandler selects the output work area and folds the size and maximized state into wlroots' pending initial configure. Carry the accepted geometry across asynchronous AppId and prelaunch wrapper creation, adopt it without a second configure or animation, and reveal the client after a matching buffer is committed. Keep XWayland's post-map hidden configure path. Preserve only reliable normal geometry for restore and persistence, replay state changes deferred during launch transitions, and retain a 1500 ms presentation timeout so an unresponsive client cannot remain hidden indefinitely. Expose the required request-state helpers through qwlroots and Waylib, and report hidden XWayland configure failure instead of treating it as success. Log: 修复XWayland及原生Wayland应用启动时请求最大化但窗口未正确最大化的问题 Influence: XWayland/XDG窗口初始最大化协商、预启动Splash交接及窗口化尺寸恢复 --- qwlroots/src/types/qwxdgshell.h | 24 + qwlroots/src/types/qwxwaylandsurface.h | 6 + src/core/shellhandler.cpp | 264 +++++- src/core/shellhandler.h | 25 + src/surface/surfacewrapper.cpp | 851 ++++++++++++++++-- src/surface/surfacewrapper.h | 49 +- .../server/protocols/wxdgtoplevelsurface.cpp | 28 +- .../server/protocols/wxdgtoplevelsurface.h | 8 + .../src/server/protocols/wxwaylandsurface.cpp | 10 +- .../src/server/protocols/wxwaylandsurface.h | 3 + .../qtquick/wxdgtoplevelsurfaceitem.cpp | 8 - .../server/qtquick/wxwaylandsurfaceitem.cpp | 33 +- .../src/server/qtquick/wxwaylandsurfaceitem.h | 3 + 13 files changed, 1181 insertions(+), 131 deletions(-) diff --git a/qwlroots/src/types/qwxdgshell.h b/qwlroots/src/types/qwxdgshell.h index 967ae14217..ad51bb77f8 100644 --- a/qwlroots/src/types/qwxdgshell.h +++ b/qwlroots/src/types/qwxdgshell.h @@ -87,6 +87,30 @@ class QW_CLASS_OBJECT(xdg_toplevel) QW_SIGNAL(set_app_id, char*) public: + QW_ALWAYS_INLINE bool is_initial_commit() const + { + const auto *toplevel = handle(); + return toplevel && toplevel->base && toplevel->base->initial_commit; + } + + QW_ALWAYS_INLINE bool is_maximize_requested() const + { + const auto *toplevel = handle(); + return toplevel && toplevel->requested.maximized; + } + + QW_ALWAYS_INLINE bool is_minimize_requested() const + { + const auto *toplevel = handle(); + return toplevel && toplevel->requested.minimized; + } + + QW_ALWAYS_INLINE bool is_fullscreen_requested() const + { + const auto *toplevel = handle(); + return toplevel && toplevel->requested.fullscreen; + } + QW_FUNC_STATIC(xdg_toplevel, from_resource, qw_xdg_toplevel *, wl_resource *resource) QW_FUNC_STATIC(xdg_toplevel, try_from_wlr_surface, qw_xdg_toplevel *, wlr_surface *surface) diff --git a/qwlroots/src/types/qwxwaylandsurface.h b/qwlroots/src/types/qwxwaylandsurface.h index 6755b104f1..dc20bd6ec9 100644 --- a/qwlroots/src/types/qwxwaylandsurface.h +++ b/qwlroots/src/types/qwxwaylandsurface.h @@ -43,6 +43,12 @@ class QW_CLASS_OBJECT(xwayland_surface) QW_SIGNAL(dissociate) public: + QW_ALWAYS_INLINE bool is_maximized() const + { + const auto *surface = handle(); + return surface && surface->maximized_horz && surface->maximized_vert; + } + QW_FUNC_STATIC(xwayland_surface, try_from_wlr_surface, qw_xwayland_surface *, wlr_surface *surface) QW_FUNC_MEMBER(xwayland_surface, activate, void, bool activated) diff --git a/src/core/shellhandler.cpp b/src/core/shellhandler.cpp index 8f12b6a325..ae252d61fd 100644 --- a/src/core/shellhandler.cpp +++ b/src/core/shellhandler.cpp @@ -14,6 +14,7 @@ #include "modules/prelaunch-splash/prelaunchsplash.h" #include "modules/wine-window-management/winewindowmanagement.h" #include "modules/wine-window-state/winewindowstate.h" +#include "output/output.h" #include "rootsurfacecontainer.h" #include "seat/helper.h" #include "seat/seatsmanager.h" @@ -57,6 +58,21 @@ WAYLIB_SERVER_USE_NAMESPACE #define TREELAND_XDG_SHELL_VERSION 5 +namespace { +bool isValidRestoreSize(const QSize &size) +{ + return size.isValid() && size.width() > 0 && size.height() > 0; +} + +QSize restoreSizeForPrelaunchWrapper(const SurfaceWrapper *wrapper) +{ + QSize size = wrapper->normalGeometry().size().toSize(); + if (!isValidRestoreSize(size)) + size = QSize(qRound(wrapper->implicitWidth()), qRound(wrapper->implicitHeight())); + return isValidRestoreSize(size) ? size : QSize(); +} +} // namespace + ShellHandler::ShellHandler(RootSurfaceContainer *rootContainer, WServer *server) : m_rootSurfaceContainer(rootContainer) , m_backgroundContainer(new LayerSurfaceContainer(rootContainer)) @@ -127,6 +143,8 @@ void ShellHandler::handlePrelaunchSplashRequested(const QString &appId, const QString &instanceId, QW_NAMESPACE::qw_buffer *iconBuffer) { + m_unmatchedPrelaunchAppIds.remove(appId); + auto skipSplash = [this, appId, iconBuffer] { if (iconBuffer) { iconBuffer->unlock(); @@ -178,6 +196,10 @@ void ShellHandler::createPrelaunchSplash(const QString &appId, Q_UNUSED(instanceId); // TODO: will be provided by AM DBus in future if (!m_pendingPrelaunchAppIds.contains(appId)) { + // A real window may have consumed the pending identity while DConfig was still loading. + // Preserve the now-available restore size for that wrapper instead of creating a late + // splash. + updateUnmatchedPrelaunchLastSize(appId, lastSize); if (iconBuffer) { iconBuffer->unlock(); } @@ -244,6 +266,9 @@ void ShellHandler::createPrelaunchSplash(const QString &appId, qCDebug(lcTlShell) << "Prelaunch splash timeout, destroy wrapper appId=" << wrapper->appId(); + rememberUnmatchedPrelaunchAppId( + wrapper->appId(), + restoreSizeForPrelaunchWrapper(wrapper)); m_prelaunchWrappers.removeAt(idx); m_rootSurfaceContainer->destroyForSurface(wrapper); }); @@ -254,8 +279,8 @@ void ShellHandler::handlePrelaunchSplashClosed(const QString &appId, const QStri { Q_UNUSED(instanceId); // TODO: will be provided by AM DBus in future - // Remove pending prelaunch request if it hasn't created a wrapper yet - m_pendingPrelaunchAppIds.remove(appId); + // Remove pending prelaunch request if it hasn't created a wrapper yet. + const bool removedPendingRequest = m_pendingPrelaunchAppIds.remove(appId); // Find and destroy any existing prelaunch wrapper with the matching appId for (int i = 0; i < m_prelaunchWrappers.size(); ++i) { @@ -263,11 +288,91 @@ void ShellHandler::handlePrelaunchSplashClosed(const QString &appId, const QStri if (wrapper->appId() == appId) { qCDebug(lcTlShell) << "Client requested close_splash, destroy wrapper appId=" << appId; + rememberUnmatchedPrelaunchAppId(appId, restoreSizeForPrelaunchWrapper(wrapper)); m_prelaunchWrappers.removeAt(i); m_rootSurfaceContainer->destroyForSurface(wrapper); return; } } + + if (removedPendingRequest) + rememberUnmatchedPrelaunchAppId(appId); +} + +bool ShellHandler::hasPrelaunchAppIdCandidates() const +{ + return !m_prelaunchWrappers.isEmpty() || !m_pendingPrelaunchAppIds.isEmpty() + || !m_closedSplashAppIds.isEmpty() || !m_unmatchedPrelaunchAppIds.isEmpty(); +} + +void ShellHandler::rememberUnmatchedPrelaunchAppId(const QString &appId, + const QSize &lastNormalSize) +{ + if (appId.isEmpty() || m_closedSplashAppIds.contains(appId)) + return; + + const quint64 generation = ++m_prelaunchAppIdGeneration; + UnmatchedPrelaunchInfo info; + info.generation = generation; + if (isValidRestoreSize(lastNormalSize)) + info.lastNormalSize = lastNormalSize; + m_unmatchedPrelaunchAppIds.insert(appId, info); + + const qlonglong configuredTimeout = + Helper::instance()->globalConfig()->prelaunchSplashTimeoutMs(); + const qlonglong requestedRetention = + configuredTimeout > 0 ? configuredTimeout : qlonglong(5000); + const int retentionMs = static_cast( + qBound(qlonglong(5000), requestedRetention, qlonglong(60000))); + qCDebug(lcTlShell) << "Retaining unmatched prelaunch appId" << appId << "normal size" + << info.lastNormalSize << "for" << retentionMs << "ms"; + + QTimer::singleShot(retentionMs, this, [this, appId, generation] { + const auto it = m_unmatchedPrelaunchAppIds.constFind(appId); + if (it != m_unmatchedPrelaunchAppIds.cend() && it->generation == generation) + m_unmatchedPrelaunchAppIds.remove(appId); + }); +} + +void ShellHandler::updateUnmatchedPrelaunchLastSize(const QString &appId, + const QSize &lastNormalSize) +{ + if (!isValidRestoreSize(lastNormalSize)) + return; + + auto it = m_unmatchedPrelaunchAppIds.find(appId); + if (it == m_unmatchedPrelaunchAppIds.end()) + return; + + it->lastNormalSize = lastNormalSize; + for (const QPointer &wrapper : std::as_const(it->waitingWrappers)) { + if (wrapper) + wrapper->setRestoredNormalSize(lastNormalSize); + } + it->waitingWrappers.clear(); + qCDebug(lcTlShell) << "Updated unmatched prelaunch normal size for" << appId << "to" + << lastNormalSize; +} + +void ShellHandler::seedUnmatchedPrelaunchLastSize(const QString &appId, + SurfaceWrapper *wrapper) +{ + if (appId.isEmpty() || !wrapper) + return; + + auto it = m_unmatchedPrelaunchAppIds.find(appId); + if (it == m_unmatchedPrelaunchAppIds.end()) + return; + + if (isValidRestoreSize(it->lastNormalSize)) { + wrapper->setRestoredNormalSize(it->lastNormalSize); + qCDebug(lcTlShell) << "Seeded unmatched prelaunch normal size for" << appId << "as" + << it->lastNormalSize; + return; + } + + if (!it->waitingWrappers.contains(wrapper)) + it->waitingWrappers.append(wrapper); } Workspace *ShellHandler::workspace() const @@ -398,10 +503,28 @@ void ShellHandler::removeXWayland(WXWayland *xwayland) void ShellHandler::onXdgToplevelSurfaceAdded(WXdgToplevelSurface *surface) { - // If there are prelaunch wrappers or closed splash appIds and the resolver is available + surface->safeConnect( + &WXdgToplevelSurface::initialConfigureRequested, + this, + [this, surface] { + if (surface->isMaximizeRequested()) + configureInitialXdgMaximize(surface); + }, + Qt::DirectConnection); + surface->safeConnect(&WToplevelSurface::requestMaximize, this, [this, surface] { + if (surface->isInitialized() && surface->surface() + && (!surface->surface()->mapped() + || !m_rootSurfaceContainer->getSurface(surface))) { + configureInitialXdgMaximize(surface); + } + }); + surface->safeConnect(&WToplevelSurface::requestCancelMaximize, this, [this, surface] { + cancelPendingInitialXdgMaximize(surface); + }); + + // If there are prelaunch identities to match and the resolver is available // -> attempt async resolve; remaining logic continues in the callback on success - if ((!m_prelaunchWrappers.isEmpty() || !m_closedSplashAppIds.isEmpty()) - && m_appIdResolverManager) { + if (hasPrelaunchAppIdCandidates() && m_appIdResolverManager) { int pidfd = surface->pidFD(); if (pidfd >= 0) { // Register pending before starting async resolve (unified list) @@ -435,8 +558,85 @@ void ShellHandler::onXdgToplevelSurfaceAdded(WXdgToplevelSurface *surface) ensureXdgWrapper(surface, QString()); } +bool ShellHandler::configureInitialXdgMaximize(WXdgToplevelSurface *surface) +{ + if (!surface || !surface->isInitialized() || !surface->isMaximizeRequested() + || !surface->hasCapability(WToplevelSurface::Capability::Maximized)) { + return false; + } + + SurfaceWrapper *wrapper = m_rootSurfaceContainer->getSurface(surface); + Output *output = wrapper ? wrapper->ownsOutput() : nullptr; + if (!output) { + if (auto *parentSurface = surface->parentSurface()) { + if (auto *parentWrapper = m_rootSurfaceContainer->getSurface(parentSurface)) + output = parentWrapper->ownsOutput(); + } + } + if (!output) + output = m_rootSurfaceContainer->primaryOutput(); + if (!output) + return false; + + QRectF targetGeometry = output->validGeometry(); + if (!targetGeometry.isValid() || targetGeometry.isEmpty()) + return false; + + QSize configureSize = targetGeometry.size().toSize(); + QSize clippedSize; + if (!surface->checkNewSize(configureSize, &clippedSize)) + configureSize = clippedSize; + if (!configureSize.isValid() || configureSize.isEmpty()) + return false; + targetGeometry.setSize(configureSize); + + // Waylib has already scheduled the 0x0 fallback on this event-loop turn. wlroots keeps one + // idle configure, so these size and state updates replace that fallback atomically. + bool configured = true; + if (wrapper) + configured = wrapper->resize(targetGeometry.size()); + else + surface->resize(configureSize); + if (!configured) + return false; + + surface->setMaximize(true); + if (wrapper) { + wrapper->adoptInitialXdgMaximize(targetGeometry); + } else { + m_pendingInitialXdgMaximizeGeometries.insert(surface, targetGeometry); + } + + qCDebug(lcTlShell) << "Configured initial XDG maximize for" << surface->appId() << "target" + << targetGeometry << "wrapperReady" << bool(wrapper); + return true; +} + +void ShellHandler::cancelPendingInitialXdgMaximize(WXdgToplevelSurface *surface) +{ + const auto it = m_pendingInitialXdgMaximizeGeometries.find(surface); + if (it == m_pendingInitialXdgMaximizeGeometries.end()) + return; + + m_pendingInitialXdgMaximizeGeometries.erase(it); + if (surface->isInitialized()) { + surface->resize(QSize()); + surface->setMaximize(false); + } + qCDebug(lcTlShell) << "Cancelled pending initial XDG maximize for" << surface->appId(); +} + void ShellHandler::ensureXdgWrapper(WXdgToplevelSurface *surface, const QString &targetAppId) { + const QRectF initialMaximizedGeometry = + m_pendingInitialXdgMaximizeGeometries.take(surface); + + if (!targetAppId.isEmpty()) { + const bool wasPending = m_pendingPrelaunchAppIds.remove(targetAppId); + if (wasPending && !m_unmatchedPrelaunchAppIds.contains(targetAppId)) + rememberUnmatchedPrelaunchAppId(targetAppId); + } + // Check if this matches a closed splash screen if (!targetAppId.isEmpty() && m_closedSplashAppIds.contains(targetAppId)) { qCInfo(lcTlShell) << "XDG surface matches closed splash, closing immediately: appId=" @@ -450,13 +650,14 @@ void ShellHandler::ensureXdgWrapper(WXdgToplevelSurface *surface, const QString bool isNewWrapper = true; if (!targetAppId.isEmpty()) { - m_pendingPrelaunchAppIds.remove(targetAppId); for (int i = 0; i < m_prelaunchWrappers.size(); ++i) { auto *candidate = m_prelaunchWrappers[i]; if (candidate->appId() == targetAppId) { qCDebug(lcTlShell) << "match prelaunch xdg" << targetAppId; m_prelaunchWrappers.removeAt(i); - candidate->convertToNormalSurface(surface, SurfaceWrapper::Type::XdgToplevel); + candidate->convertToNormalSurface(surface, + SurfaceWrapper::Type::XdgToplevel, + initialMaximizedGeometry); wrapper = candidate; isNewWrapper = false; // matched from prelaunch, not newly created break; @@ -468,7 +669,11 @@ void ShellHandler::ensureXdgWrapper(WXdgToplevelSurface *surface, const QString wrapper = new SurfaceWrapper(Helper::instance()->qmlEngine(), surface, SurfaceWrapper::Type::XdgToplevel, - targetAppId); + targetAppId, + nullptr, + initialMaximizedGeometry); + if (!surface->parentSurface()) + seedUnmatchedPrelaunchLastSize(targetAppId, wrapper); m_workspace->addSurface(wrapper); isNewWrapper = true; // newly created } @@ -507,6 +712,7 @@ void ShellHandler::ensureXdgWrapper(WXdgToplevelSurface *surface, const QString void ShellHandler::onXdgToplevelSurfaceRemoved(WXdgToplevelSurface *surface) { + m_pendingInitialXdgMaximizeGeometries.remove(surface); auto wrapper = m_rootSurfaceContainer->getSurface(surface); // If async resolve still pending, cancel it. If wrapper never created, just return: compositor // never exposed this surface (from treeland's perspective). @@ -521,13 +727,11 @@ void ShellHandler::onXdgToplevelSurfaceRemoved(WXdgToplevelSurface *surface) if (interface) { delete interface; } - // Persist the last size of a normal window (prefer normalGeometry) when an appId is present - if (m_windowConfigStore && !wrapper->appId().isEmpty()) { - QSizeF sz = wrapper->normalGeometry().size(); - if (!sz.isValid() || sz.isEmpty()) { - sz = wrapper->geometry().size(); - } - const QSize s = sz.toSize(); + // Persist only geometry observed from the real client. A prelaunch splash or a maximized + // presentation geometry is not a valid restore size. + if (m_windowConfigStore && !wrapper->appId().isEmpty() + && wrapper->hasReliableNormalGeometry()) { + const QSize s = wrapper->normalGeometry().size().toSize(); if (s.isValid() && s.width() > 0 && s.height() > 0) { m_windowConfigStore->saveLastSize(wrapper->appId(), s); } @@ -587,11 +791,10 @@ void ShellHandler::onXWaylandSurfaceAdded(WXWaylandSurface *surface) auto raw = surface.data(); if (!raw) return; // surface destroyed before callback - // If prelaunch wrappers or closed splash appIds exist and resolver is - // available, attempt async resolve; if started, remaining logic - // handled in callback, then return - if ((!m_prelaunchWrappers.isEmpty() || !m_closedSplashAppIds.isEmpty()) - && m_appIdResolverManager) { + // If prelaunch identities exist and the resolver is available, + // attempt async resolve; if started, remaining logic is handled in + // the callback. + if (hasPrelaunchAppIdCandidates() && m_appIdResolverManager) { int pidfd = raw->pidFD(); if (pidfd >= 0) { m_pendingAppIdResolveToplevels.append(raw); @@ -644,13 +847,11 @@ void ShellHandler::onXWaylandSurfaceAdded(WXWaylandSurface *surface) } return; // never created } - // Persist XWayland window size - if (m_windowConfigStore && !wrapper->appId().isEmpty()) { - QSizeF sz = wrapper->normalGeometry().size(); - if (!sz.isValid() || sz.isEmpty()) { - sz = wrapper->geometry().size(); - } - const QSize s = sz.toSize(); + // Only task-level XWayland windows may update the per-app restore size. Utility and child + // windows frequently share the same appId and must not overwrite the main window. + if (m_windowConfigStore && !wrapper->appId().isEmpty() && surface->isToplevel() + && !wrapper->skipDockPreView() && wrapper->hasReliableNormalGeometry()) { + const QSize s = wrapper->normalGeometry().size().toSize(); if (s.isValid() && s.width() > 0 && s.height() > 0) { m_windowConfigStore->saveLastSize(wrapper->appId(), s); } @@ -708,6 +909,12 @@ void ShellHandler::onInitialPropertiesReady(WXWaylandSurface *surface, void ShellHandler::ensureXwaylandWrapper(WXWaylandSurface *surface, const QString &targetAppId) { + if (!targetAppId.isEmpty()) { + const bool wasPending = m_pendingPrelaunchAppIds.remove(targetAppId); + if (wasPending && !m_unmatchedPrelaunchAppIds.contains(targetAppId)) + rememberUnmatchedPrelaunchAppId(targetAppId); + } + // Check if this matches a closed splash screen if (!targetAppId.isEmpty() && m_closedSplashAppIds.contains(targetAppId)) { qCDebug(lcTlShell) @@ -721,7 +928,6 @@ void ShellHandler::ensureXwaylandWrapper(WXWaylandSurface *surface, const QStrin bool isNewWrapper = true; if (!targetAppId.isEmpty()) { - m_pendingPrelaunchAppIds.remove(targetAppId); for (int i = 0; i < m_prelaunchWrappers.size(); ++i) { auto *candidate = m_prelaunchWrappers[i]; if (candidate->appId() == targetAppId) { @@ -740,6 +946,8 @@ void ShellHandler::ensureXwaylandWrapper(WXWaylandSurface *surface, const QStrin surface, SurfaceWrapper::Type::XWayland, targetAppId); + if (surface->isToplevel() && !wrapper->skipDockPreView()) + seedUnmatchedPrelaunchLastSize(targetAppId, wrapper); m_workspace->addSurface(wrapper); isNewWrapper = true; // newly created } diff --git a/src/core/shellhandler.h b/src/core/shellhandler.h index 2eb1ad43e2..cab386b020 100644 --- a/src/core/shellhandler.h +++ b/src/core/shellhandler.h @@ -16,7 +16,9 @@ #include #include #include +#include #include +#include Q_MOC_INCLUDE("workspace/workspace.h") Q_MOC_INCLUDE() @@ -157,11 +159,20 @@ private Q_SLOTS: const QString &darkPalette, const QString &lightPalette, qlonglong splashThemeType); + bool hasPrelaunchAppIdCandidates() const; + void rememberUnmatchedPrelaunchAppId(const QString &appId, + const QSize &lastNormalSize = QSize()); + void updateUnmatchedPrelaunchLastSize(const QString &appId, const QSize &lastNormalSize); + void seedUnmatchedPrelaunchLastSize(const QString &appId, SurfaceWrapper *wrapper); // --- helpers (internal) --- // Creates or matches a wrapper from prelaunch splash, then initializes it void ensureXdgWrapper(WAYLIB_SERVER_NAMESPACE::WXdgToplevelSurface *surface, const QString &appId); + bool configureInitialXdgMaximize( + WAYLIB_SERVER_NAMESPACE::WXdgToplevelSurface *surface); + void cancelPendingInitialXdgMaximize( + WAYLIB_SERVER_NAMESPACE::WXdgToplevelSurface *surface); // Creates or matches a wrapper from prelaunch splash, then initializes it void ensureXwaylandWrapper(WAYLIB_SERVER_NAMESPACE::WXWaylandSurface *surface, const QString &appId); @@ -200,11 +211,25 @@ private Q_SLOTS: QSet m_pendingPrelaunchAppIds; // AppIds of closed splash screens, used to close matching real windows QSet m_closedSplashAppIds; + struct UnmatchedPrelaunchInfo + { + quint64 generation = 0; + QSize lastNormalSize; + QList> waitingWrappers; + }; + // Launch context whose splash disappeared before a real surface could be matched. Keep its + // restore size for a short grace period so an already-maximized first buffer cannot replace + // it. + QHash m_unmatchedPrelaunchAppIds; + quint64 m_prelaunchAppIdGeneration = 0; // Dock preview QML object QObject *m_dockPreview = nullptr; // Pending toplevel surfaces (XDG or XWayland) awaiting async AppId resolve; callbacks continue // only if the pointer remains in this list QList m_pendingAppIdResolveToplevels; + // Accepted XDG initial maximize configurations waiting for asynchronous wrapper creation. + QHash + m_pendingInitialXdgMaximizeGeometries; // New protocol based app id resolver (optional, may be null if module not loaded) AppIdResolverManager *m_appIdResolverManager = nullptr; WindowConfigStore *m_windowConfigStore = nullptr; diff --git a/src/surface/surfacewrapper.cpp b/src/surface/surfacewrapper.cpp index 8f4ec1ac5f..4e3692d866 100644 --- a/src/surface/surfacewrapper.cpp +++ b/src/surface/surfacewrapper.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -27,17 +28,41 @@ #include #include +#include #include #define OPEN_ANIMATION 1 #define CLOSE_ANIMATION 2 #define ALWAYSONTOPLAYER 1 +namespace { +constexpr int initialMaximizeCommitTimeoutMs = 1500; +constexpr qreal initialMaximizeSizeTolerance = 2.0; +constexpr int defaultNormalWidth = 800; +constexpr int defaultNormalHeight = 600; + +bool sizesMatch(const QSizeF &first, const QSizeF &second) +{ + return qAbs(first.width() - second.width()) <= initialMaximizeSizeTolerance + && qAbs(first.height() - second.height()) <= initialMaximizeSizeTolerance; +} +} // namespace + SurfaceWrapper::SurfaceWrapper(QmlEngine *qmlEngine, WToplevelSurface *shellSurface, Type type, const QString &appId, QQuickItem *parent) + : SurfaceWrapper(qmlEngine, shellSurface, type, appId, parent, {}) +{ +} + +SurfaceWrapper::SurfaceWrapper(QmlEngine *qmlEngine, + WToplevelSurface *shellSurface, + Type type, + const QString &appId, + QQuickItem *parent, + const QRectF &initialMaximizedGeometry) : QQuickItem(parent) , m_engine(qmlEngine) , m_shellSurface(shellSurface) @@ -71,6 +96,14 @@ SurfaceWrapper::SurfaceWrapper(QmlEngine *qmlEngine, { QQmlEngine::setContextForObject(this, qmlEngine->rootContext()); + if (m_type == Type::XdgToplevel && initialMaximizedGeometry.isValid() + && !initialMaximizedGeometry.isEmpty()) { + m_initialMaximizePending = true; + m_initialMaximizeConfigured = true; + m_initialMaximizeGeometry = initialMaximizedGeometry; + m_maximizedGeometry = initialMaximizedGeometry; + } + setup(); } @@ -177,13 +210,16 @@ SurfaceWrapper::SurfaceWrapper(QmlEngine *qmlEngine, , m_appId(appId) { QQmlEngine::setContextForObject(this, qmlEngine->rootContext()); - if (initialSize.isValid() && initialSize.width() > 0 && initialSize.height() > 0) { + const QSize restoredSize = + initialSize.isValid() && initialSize.width() > 0 && initialSize.height() > 0 + ? initialSize + : QSize(defaultNormalWidth, defaultNormalHeight); + if (restoredSize == initialSize) { // Also set implicit size to keep QML layout consistent - setImplicitSize(initialSize.width(), initialSize.height()); - qCDebug(lcTlSurface) << "Prelaunch Splash: set initial size to" << initialSize; - } else { - setImplicitSize(800, 600); + qCDebug(lcTlSurface) << "Prelaunch Splash: set initial size to" << restoredSize; } + setImplicitSize(restoredSize.width(), restoredSize.height()); + setRestoredNormalSize(restoredSize); m_prelaunchSplash = m_engine->createPrelaunchSplash(this, radius(), iconBuffer, backgroundColor); @@ -196,6 +232,9 @@ void SurfaceWrapper::invalidate() { Q_ASSERT_X(!m_wrapperAboutToRemove, Q_FUNC_INFO, "Can't call `invalidate` twice!"); m_wrapperAboutToRemove = true; + m_initialMaximizePending = false; + m_initialMaximizeConfigured = false; + ++m_initialMaximizeGeneration; Q_EMIT aboutToBeInvalidated(); if (!m_skipDockPreView) @@ -268,6 +307,18 @@ void SurfaceWrapper::setup() Q_ASSERT(m_shellSurface); Q_ASSERT(m_type != Type::SplashScreen); + const auto initialMaximizeRequested = [this] { + if (m_isProxy) + return false; + + const auto *xwaylandSurface = qobject_cast(m_shellSurface); + return xwaylandSurface && xwaylandSurface->isMaximizeRequested(); + }; + // XWayland negotiates its initial state after map. Native XDG initial state is prepared by + // ShellHandler during the initial empty commit and may already be armed before setup(). + if (!hasConfiguredInitialXdgMaximize()) + m_initialMaximizePending = initialMaximizeRequested(); + updateActivateCapability(); updateFocusCapability(); @@ -302,10 +353,29 @@ void SurfaceWrapper::setup() Q_UNREACHABLE(); } + if (m_initialMaximizePending) + m_surfaceItem->setVisible(false); + QQmlEngine::setContextForObject(m_surfaceItem, m_engine->rootContext()); m_surfaceItem->setDelegate(m_engine->surfaceContentComponent()); m_surfaceItem->setResizeMode(WSurfaceItem::ManualResize); m_surfaceItem->setShellSurface(m_shellSurface); + connect(m_surfaceItem, &WSurfaceItem::readyChanged, this, [this] { + if (m_surfaceItem->isReady()) { + tryApplyInitialMaximize(); + tryApplyDeferredSurfaceState(); + } + }); + if (m_type == Type::XWayland) { + connect(this, &QQuickItem::visibleChanged, this, [this] { + if (isVisible()) + tryApplyDeferredSurfaceState(); + }); + connect(m_surfaceItem, &QQuickItem::visibleChanged, this, [this] { + if (m_surfaceItem->isVisible()) + tryApplyDeferredSurfaceState(); + }); + } // Initialize focus policy even if focus capability state never toggles later. m_surfaceItem->setFocusPolicy(hasFocusCapability() ? Qt::StrongFocus : Qt::NoFocus); @@ -349,6 +419,21 @@ void SurfaceWrapper::setup() m_shellSurface->surface()->safeConnect(&WSurface::mappedChanged, this, &SurfaceWrapper::onMappedChanged); + m_shellSurface->surface()->safeConnect(&WSurface::commit, + this, + [this](quint32) { + if (!m_initialMaximizePending + || !m_initialMaximizeConfigured) { + return; + } + // WSurfaceItem observes the native commit + // separately. Check after all direct commit + // handlers have refreshed its implicit size. + QTimer::singleShot( + 0, + this, + &SurfaceWrapper::handleInitialMaximizeCommit); + }); Q_EMIT surfaceItemCreated(); @@ -368,6 +453,18 @@ void SurfaceWrapper::setup() &SurfaceWrapper::updateSizeCapabilities); } updateSizeCapabilities(); + if (m_initialMaximizePending && !isMaximizable()) { + const bool cancelInitialXdgConfigure = hasConfiguredInitialXdgMaximize(); + m_initialMaximizePending = false; + m_initialMaximizeConfigured = false; + ++m_initialMaximizeGeneration; + if (cancelInitialXdgConfigure && m_shellSurface->isInitialized()) { + m_shellSurface->resize(QSize()); + m_shellSurface->setMaximize(false); + } + if (!m_prelaunchSplash) + m_surfaceItem->setVisible(true); + } if (!m_prelaunchSplash) { setImplicitSize(m_surfaceItem->implicitWidth(), m_surfaceItem->implicitHeight()); @@ -507,16 +604,33 @@ void SurfaceWrapper::setup() updateFocusControlState(FocusControlState::Mapped, true); // ActiveControlState::MappedOrSplash is already true syncPrelaunchMappedState(); - startPrelaunchSplashHideSequence(); } } else { updateFocusControlState(FocusControlState::Mapped, surface() && surface()->mapped()); updateHasActiveCapability(ActiveControlState::MappedOrSplash, surface() && surface()->mapped()); } + + // Also cover a request which arrived while setup() was connecting the surface. + if (!m_initialMaximizePending && isMaximizable() && initialMaximizeRequested()) + m_initialMaximizePending = true; + if (m_initialMaximizePending) { + m_surfaceItem->setVisible(false); + qCDebug(lcTlSurface) << "Detected initial maximize request for" << appId() << "type" + << static_cast(m_type); + } + + if (hasConfiguredInitialXdgMaximize()) + activateConfiguredInitialXdgMaximize(); + + tryApplyInitialMaximize(); + if (m_prelaunchSplash && surface() && surface()->mapped()) + startPrelaunchSplashHideSequence(); } -void SurfaceWrapper::convertToNormalSurface(WToplevelSurface *shellSurface, Type type) +void SurfaceWrapper::convertToNormalSurface(WToplevelSurface *shellSurface, + Type type, + const QRectF &initialMaximizedGeometry) { // Conversion only allowed from prelaunch (SplashScreen) state if (m_type != Type::SplashScreen || m_shellSurface != nullptr) { @@ -528,12 +642,36 @@ void SurfaceWrapper::convertToNormalSurface(WToplevelSurface *shellSurface, Type // Assign new shell surface (QPointer auto-detects destruction) m_shellSurface = shellSurface; m_type = type; + if (m_type == Type::XdgToplevel && initialMaximizedGeometry.isValid() + && !initialMaximizedGeometry.isEmpty()) { + m_initialMaximizePending = true; + m_initialMaximizeConfigured = true; + m_initialMaximizeGeometry = initialMaximizedGeometry; + m_maximizedGeometry = initialMaximizedGeometry; + } Q_EMIT typeChanged(); // Call setup() to initialize surfaceItem related features setup(); } +void SurfaceWrapper::adoptInitialXdgMaximize(const QRectF &targetGeometry) +{ + if (m_type != Type::XdgToplevel || !targetGeometry.isValid() + || targetGeometry.isEmpty()) { + return; + } + + m_initialMaximizePending = true; + m_initialMaximizeConfigured = true; + m_initialMaximizeGeometry = targetGeometry; + if (!m_maximizedGeometry.isValid() || m_maximizedGeometry.isEmpty()) + m_maximizedGeometry = targetGeometry; + + activateConfiguredInitialXdgMaximize(); + tryApplyInitialMaximize(); +} + void SurfaceWrapper::setParent(QQuickItem *item) { QObject::setParent(item); @@ -601,6 +739,10 @@ void SurfaceWrapper::syncPrelaunchMappedState() void SurfaceWrapper::startPrelaunchSplashHideSequence() { Q_ASSERT(m_surfaceItem != nullptr); + if (m_initialMaximizePending) { + tryApplyInitialMaximize(); + return; + } if (m_windowAnimation) { qCDebug(lcTlSurface) << "prelaunch splash transition is starting while window " "animation is still running," @@ -609,7 +751,8 @@ void SurfaceWrapper::startPrelaunchSplashHideSequence() return; } if (m_geometryAnimation) { - qCDebug(lcTlSurface) << "prelaunch splash transition already prepared or running, skip"; + qCDebug(lcTlSurface) + << "Prelaunch splash transition deferred until geometry animation finishes"; return; } @@ -636,18 +779,18 @@ void SurfaceWrapper::startPrelaunchSplashHideSequence() << "targetImplicit=" << targetImplicitSize; } + if (hasValidTargetImplicitSize && !captureNormalGeometryFromSurfaceItem(false)) { + qCWarning(lcTlSurface) << "Failed to capture client normal geometry for prelaunch surface" + << appId() << "targetImplicit=" << targetImplicitSize; + } + const bool needImplicitSizeTransition = hasValidTargetImplicitSize && (container() != nullptr) && (!qFuzzyCompare(implicitWidth() + 1.0, targetImplicitSize.width() + 1.0) || !qFuzzyCompare(implicitHeight() + 1.0, targetImplicitSize.height() + 1.0)); if (needImplicitSizeTransition) { const QRectF fromGeometry(position(), size()); - // XWayland clients manage their own position; respect it and don't shift. - // For all other types, keep the center fixed so the window expands from center. - const QPointF toTopLeft = (m_type == Type::XWayland) ? fromGeometry.topLeft() - : fromGeometry.center() - - QPointF(targetImplicitSize.width() / 2.0, targetImplicitSize.height() / 2.0); - const QRectF toGeometry(toTopLeft, targetImplicitSize); + const QRectF toGeometry = m_normalGeometry; m_geometryAnimation = m_engine->createGeometryAnimation(this, fromGeometry, toGeometry, container()); @@ -677,7 +820,7 @@ void SurfaceWrapper::onPrelaunchGeometryAnimationReady() // so the window appears exactly where the animation ended (no position jump). setPosition(toGeo.topLeft()); // Keep normalGeometry in sync so subsequent state transitions use the correct position. - setNormalGeometry(toGeo); + setNormalGeometryFromSurface(toGeo); completeSplashTransition(toGeo.size(), true); } @@ -685,12 +828,15 @@ void SurfaceWrapper::onPrelaunchGeometryAnimationReady() void SurfaceWrapper::onPrelaunchGeometryAnimationFinished() { Q_ASSERT(m_geometryAnimation); + const QPointer finishedAnimation = m_geometryAnimation; m_geometryAnimation->disconnect(this); m_geometryAnimation->deleteLater(); m_geometryAnimation = nullptr; if (m_decoration) m_decoration->setVisible(true); + + continuePendingTransitionsAfterAnimation(finishedAnimation); } void SurfaceWrapper::completeSplashTransition(const QSizeF &targetImplicitSize, bool hideDecoration) @@ -724,6 +870,7 @@ void SurfaceWrapper::completeSplashTransition(const QSizeF &targetImplicitSize, // Now that the splash is hidden and deleted, the surface can be considered active if it's // mapped updateHasActiveCapability(ActiveControlState::MappedOrSplash, surface() && surface()->mapped()); + tryApplyDeferredSurfaceState(); } WSurface *SurfaceWrapper::surface() const @@ -795,6 +942,11 @@ QRectF SurfaceWrapper::normalGeometry() const return m_normalGeometry; } +bool SurfaceWrapper::hasReliableNormalGeometry() const +{ + return m_normalGeometrySource == NormalGeometrySource::Client; +} + void SurfaceWrapper::moveNormalGeometryInOutput(const QPointF &position) { QPointF alignedPosition = alignToPixelGrid(position); @@ -806,12 +958,78 @@ void SurfaceWrapper::moveNormalGeometryInOutput(const QPointF &position) } } -void SurfaceWrapper::setNormalGeometry(const QRectF &newNormalGeometry) +void SurfaceWrapper::setNormalGeometry(const QRectF &newNormalGeometry, bool applyDeferredState) { - if (m_normalGeometry == newNormalGeometry) + if (m_normalGeometry == newNormalGeometry) { + if (applyDeferredState) + tryApplyDeferredSurfaceState(); return; + } m_normalGeometry = newNormalGeometry; Q_EMIT normalGeometryChanged(); + if (applyDeferredState) + tryApplyDeferredSurfaceState(); +} + +void SurfaceWrapper::setNormalGeometryFromSurface(const QRectF &newNormalGeometry, + bool applyDeferredState) +{ + m_normalGeometrySource = NormalGeometrySource::Client; + setNormalGeometry(newNormalGeometry, false); + if (applyDeferredState) + tryApplyDeferredSurfaceState(); +} + +void SurfaceWrapper::setRestoredNormalSize(const QSizeF &size, bool applyInitialMaximize) +{ + if (!size.isValid() || size.isEmpty() + || m_normalGeometrySource == NormalGeometrySource::Client) { + return; + } + + const QPointF topLeft = m_normalGeometrySource == NormalGeometrySource::None + ? position() + : m_normalGeometry.topLeft(); + m_normalGeometrySource = NormalGeometrySource::Restored; + setNormalGeometry(QRectF(topLeft, size), false); + if (applyInitialMaximize) + tryApplyInitialMaximize(); + tryApplyDeferredSurfaceState(); +} + +bool SurfaceWrapper::hasUsableNormalGeometry() const +{ + return m_normalGeometrySource != NormalGeometrySource::None && m_normalGeometry.isValid() + && !m_normalGeometry.isEmpty(); +} + +bool SurfaceWrapper::captureNormalGeometryFromSurfaceItem(bool applyDeferredState) +{ + if (!m_surfaceItem || !m_surfaceItem->isReady() + || (m_type != Type::XdgToplevel && m_type != Type::XWayland)) { + return false; + } + + const QSizeF surfaceSize(m_surfaceItem->implicitWidth(), m_surfaceItem->implicitHeight()); + if (surfaceSize.isEmpty() || !surfaceSize.isValid()) + return false; + + QRectF clientNormalGeometry = geometry(); + if (m_prelaunchSplash) { + QPointF topLeft = m_normalGeometry.isValid() ? m_normalGeometry.topLeft() : position(); + if (m_type != Type::XWayland) { + topLeft = geometry().center() + - QPointF(surfaceSize.width() / 2.0, surfaceSize.height() / 2.0); + } + clientNormalGeometry = QRectF(topLeft, surfaceSize); + } else if (!clientNormalGeometry.isValid() || clientNormalGeometry.isEmpty()) { + clientNormalGeometry = QRectF(position(), surfaceSize); + } + + setNormalGeometryFromSurface(clientNormalGeometry, applyDeferredState); + qCDebug(lcTlSurface) << "Captured client normal geometry for" << appId() + << clientNormalGeometry << "prelaunch" << bool(m_prelaunchSplash); + return true; } QRectF SurfaceWrapper::maximizedGeometry() const @@ -831,14 +1049,29 @@ void SurfaceWrapper::setMaximizedGeometry(const QRectF &newMaximizedGeometry) // to avoid incorrect sizing of Xwayland windows. updateSurfaceSizeRatio(); - if (m_surfaceState == State::Maximized) { + if (m_pendingState == State::Maximized && m_geometryAnimation) { + m_pendingGeometry = newMaximizedGeometry; + m_geometryAnimation->setProperty("toGeometry", newMaximizedGeometry); + } + if (hasConfiguredInitialXdgMaximize()) { + m_initialMaximizeGeometry = + QRectF(alignToPixelGrid(newMaximizedGeometry.topLeft()), + newMaximizedGeometry.size()); + refreshConfiguredInitialXdgMaximize(); + tryApplyInitialMaximize(); + } else if (m_initialMaximizePending) { + if (m_initialMaximizeConfigured) { + m_initialMaximizeConfigured = false; + ++m_initialMaximizeGeneration; + } + tryApplyInitialMaximize(); + } else if (m_surfaceState == State::Maximized) { setPosition(newMaximizedGeometry.topLeft()); resize(newMaximizedGeometry.size()); - } else if (m_pendingState == State::Maximized && m_geometryAnimation) { - m_geometryAnimation->setProperty("targetGeometry", newMaximizedGeometry); } Q_EMIT maximizedGeometryChanged(); + tryApplyDeferredSurfaceState(); } QRectF SurfaceWrapper::fullscreenGeometry() const @@ -858,16 +1091,19 @@ void SurfaceWrapper::setFullscreenGeometry(const QRectF &newFullscreenGeometry) // to avoid incorrect sizing of Xwayland windows. updateSurfaceSizeRatio(); + if (m_pendingState == State::Fullscreen && m_geometryAnimation) { + m_pendingGeometry = newFullscreenGeometry; + m_geometryAnimation->setProperty("toGeometry", newFullscreenGeometry); + } if (m_surfaceState == State::Fullscreen) { setPosition(newFullscreenGeometry.topLeft()); resize(newFullscreenGeometry.size()); - } else if (m_pendingState == State::Fullscreen && m_geometryAnimation) { - m_geometryAnimation->setProperty("targetGeometry", newFullscreenGeometry); } Q_EMIT fullscreenGeometryChanged(); updateClipRect(); + tryApplyDeferredSurfaceState(); } QRectF SurfaceWrapper::tilingGeometry() const @@ -893,6 +1129,7 @@ void SurfaceWrapper::setTilingGeometry(const QRectF &newTilingGeometry) } Q_EMIT tilingGeometryChanged(); + tryApplyDeferredSurfaceState(); } bool SurfaceWrapper::positionAutomatic() const @@ -1017,15 +1254,62 @@ void SurfaceWrapper::setSurfaceState(State newSurfaceState) if (m_wrapperAboutToRemove) return; - if (m_geometryAnimation) + if (m_initialMaximizePending) { + if (newSurfaceState == State::Maximized) { + tryApplyInitialMaximize(); + } else { + deferSurfaceState(newSurfaceState); + } + return; + } + + if (m_geometryAnimation) { + deferSurfaceState(newSurfaceState); + return; + } + + if (m_windowAnimation) { + deferSurfaceState(newSurfaceState); + return; + } + + if (m_surfaceState == newSurfaceState) { + m_hasDeferredSurfaceState = false; return; + } - if (m_surfaceState == newSurfaceState) + if (!hasInitializeContainer() || !m_surfaceItem) { + deferSurfaceState(newSurfaceState); return; + } if (container()->filterSurfaceStateChange(this, newSurfaceState, m_surfaceState)) return; + if (newSurfaceState == State::Minimized) { + doSetSurfaceState(newSurfaceState); + return; + } + + const bool isManagedToplevel = + m_type == Type::XdgToplevel || m_type == Type::XWayland; + const bool waitingForSurfaceGeometry = + isManagedToplevel && (!m_surfaceItem->isReady() || !geometry().isValid()); + const bool waitingForMappedXwayland = + m_type == Type::XWayland + && (!surface() || !surface()->mapped() || !isVisible()); + if (waitingForSurfaceGeometry || waitingForMappedXwayland) { + deferSurfaceState(newSurfaceState); + return; + } + + if (isManagedToplevel && m_surfaceState == State::Normal + && newSurfaceState != State::Normal + && !captureNormalGeometryFromSurfaceItem()) { + deferSurfaceState(newSurfaceState); + return; + } + QRectF targetGeometry; if (newSurfaceState == State::Maximized) { @@ -1038,17 +1322,262 @@ void SurfaceWrapper::setSurfaceState(State newSurfaceState) targetGeometry = m_tilingGeometry; } - if (targetGeometry.isValid()) { - startStateChangeAnimation(newSurfaceState, targetGeometry); + if (!targetGeometry.isValid() + || (isManagedToplevel && newSurfaceState == State::Normal + && !hasUsableNormalGeometry())) { + deferSurfaceState(newSurfaceState); + return; + } + + startStateChangeAnimation(newSurfaceState, targetGeometry); +} + +void SurfaceWrapper::deferSurfaceState(State newSurfaceState) +{ + m_deferredSurfaceState = newSurfaceState; + m_hasDeferredSurfaceState = true; +} + +void SurfaceWrapper::tryApplyDeferredSurfaceState() +{ + if (!m_hasDeferredSurfaceState || m_geometryAnimation || m_initialMaximizePending + || m_wrapperAboutToRemove) { + return; + } + + const State deferredState = m_deferredSurfaceState; + m_hasDeferredSurfaceState = false; + setSurfaceState(deferredState); +} + +void SurfaceWrapper::tryApplyInitialMaximize() +{ + if (m_type == Type::XdgToplevel) { + if (!hasConfiguredInitialXdgMaximize() || m_wrapperAboutToRemove || m_windowAnimation + || m_geometryAnimation || !hasInitializeContainer() || !m_surfaceItem + || !m_surfaceItem->isReady() || !surface() || !surface()->mapped()) { + return; + } + + armInitialMaximizeCommitTimeout(); + handleInitialMaximizeCommit(); + return; + } + + if (!m_initialMaximizePending || m_initialMaximizeConfigured || m_wrapperAboutToRemove + || m_windowAnimation || m_geometryAnimation || !hasInitializeContainer() || !m_surfaceItem + || !m_surfaceItem->isReady() || !surface() || !surface()->mapped() + || !m_maximizedGeometry.isValid() || m_maximizedGeometry.isEmpty()) { + return; + } + + const QRectF targetGeometry(alignToPixelGrid(m_maximizedGeometry.topLeft()), + m_maximizedGeometry.size()); + const QSizeF surfaceSize(m_surfaceItem->implicitWidth(), m_surfaceItem->implicitHeight()); + const bool surfaceAlreadyMaximized = sizesMatch(surfaceSize, targetGeometry.size()); + + // A target-sized first buffer is presentation state, not evidence of the client's normal + // bounds. Preserve a restored prelaunch/DConfig size in that case. + if (!hasReliableNormalGeometry() && !surfaceAlreadyMaximized) + captureNormalGeometryFromSurfaceItem(false); + + if (!hasUsableNormalGeometry()) { + const QSizeF fallbackSize(qMin(defaultNormalWidth, targetGeometry.width()), + qMin(defaultNormalHeight, targetGeometry.height())); + setRestoredNormalSize(fallbackSize, false); + qCDebug(lcTlSurface) << "Using non-persisted normal geometry fallback for" << appId() + << fallbackSize << "before initial maximize"; + } + if (!hasUsableNormalGeometry()) + return; + + // Keep the last normal buffer available for capture, then hide the real item before the + // state configure is sent. Presentation resumes only after the matching buffer commit. + m_surfaceItem->setVisible(false); + bool configured = false; + setXwaylandPositionFromSurface(false); + if (m_type == Type::XWayland) { + auto *xwaylandItem = qobject_cast(m_surfaceItem); + configured = + xwaylandItem && xwaylandItem->configureSurfaceWhileHidden(targetGeometry); } else { - if (m_geometryAnimation) { - m_geometryAnimation->disconnect(this); - m_geometryAnimation->deleteLater(); - m_geometryAnimation = nullptr; + configured = resize(targetGeometry.size()); + } + + if (!configured) { + setXwaylandPositionFromSurface(true); + qCDebug(lcTlSurface) << "Initial maximize configure is not ready for" << appId() + << "target" << targetGeometry; + return; + } + + m_initialMaximizeGeometry = targetGeometry; + m_initialMaximizeConfigured = true; + if (m_surfaceState != State::Maximized) + doSetSurfaceState(State::Maximized); + + qCDebug(lcTlSurface) << "Configured hidden initial maximize for" << appId() << "type" + << static_cast(m_type) << "target" << targetGeometry + << "normal" << m_normalGeometry << "normalFromClient" + << hasReliableNormalGeometry() << "prelaunch" + << bool(m_prelaunchSplash); + + armInitialMaximizeCommitTimeout(); +} + +bool SurfaceWrapper::hasConfiguredInitialXdgMaximize() const +{ + return m_type == Type::XdgToplevel && m_initialMaximizePending + && m_initialMaximizeConfigured && m_initialMaximizeGeometry.isValid() + && !m_initialMaximizeGeometry.isEmpty(); +} + +void SurfaceWrapper::refreshConfiguredInitialXdgMaximize() +{ + if (!hasConfiguredInitialXdgMaximize() || !m_shellSurface + || !m_shellSurface->isInitialized()) { + return; + } + + if (resize(m_initialMaximizeGeometry.size())) + m_shellSurface->setMaximize(true); +} + +void SurfaceWrapper::activateConfiguredInitialXdgMaximize() +{ + if (!hasConfiguredInitialXdgMaximize() || !m_surfaceItem || !isMaximizable()) + return; + + const QRectF targetGeometry(alignToPixelGrid(m_initialMaximizeGeometry.topLeft()), + m_initialMaximizeGeometry.size()); + m_initialMaximizeGeometry = targetGeometry; + + if (!hasUsableNormalGeometry()) { + const QSizeF fallbackSize(qMin(defaultNormalWidth, targetGeometry.width()), + qMin(defaultNormalHeight, targetGeometry.height())); + setRestoredNormalSize(fallbackSize, false); + qCDebug(lcTlSurface) << "Using non-persisted normal geometry fallback for" << appId() + << fallbackSize << "before initial XDG maximize"; + } + + m_surfaceItem->setVisible(false); + setPosition(targetGeometry.topLeft()); + if (m_surfaceState != State::Maximized) + doSetSurfaceState(State::Maximized, false); + + qCDebug(lcTlSurface) << "Adopted configured initial XDG maximize for" << appId() << "target" + << targetGeometry << "normal" << m_normalGeometry << "prelaunch" + << bool(m_prelaunchSplash); +} + +void SurfaceWrapper::cancelConfiguredInitialXdgMaximize() +{ + if (!hasConfiguredInitialXdgMaximize()) + return; + + m_initialMaximizePending = false; + m_initialMaximizeConfigured = false; + ++m_initialMaximizeGeneration; + + if (hasUsableNormalGeometry()) + resize(m_normalGeometry.size()); + else + m_shellSurface->resize(QSize()); + + if (m_surfaceState == State::Maximized) + doSetSurfaceState(State::Normal); + else + m_shellSurface->setMaximize(false); + + if (m_prelaunchSplash) { + if (surface() && surface()->mapped()) + startPrelaunchSplashHideSequence(); + } else { + m_surfaceItem->setVisible(true); + if (surface() && surface()->mapped() && !m_windowAnimation) + createNewOrClose(OPEN_ANIMATION); + } + + qCDebug(lcTlSurface) << "Cancelled configured initial XDG maximize for" << appId(); + tryApplyDeferredSurfaceState(); +} + +void SurfaceWrapper::armInitialMaximizeCommitTimeout() +{ + const quint64 generation = ++m_initialMaximizeGeneration; + QTimer::singleShot(initialMaximizeCommitTimeoutMs, this, [this, generation] { + if (!m_initialMaximizePending || !m_initialMaximizeConfigured + || generation != m_initialMaximizeGeneration) { + return; } - doSetSurfaceState(newSurfaceState); + if (initialMaximizeTargetCommitted()) { + finishInitialMaximize(false); + return; + } + + qCWarning(lcTlSurface) << "Timed out waiting for initial maximized buffer from" << appId() + << "target" << m_initialMaximizeGeometry << "actual" + << QSizeF(m_surfaceItem->implicitWidth(), + m_surfaceItem->implicitHeight()); + finishInitialMaximize(true); + }); +} + +void SurfaceWrapper::handleInitialMaximizeCommit() +{ + if (!m_initialMaximizePending || !m_initialMaximizeConfigured || m_wrapperAboutToRemove + || !surface() || !surface()->mapped()) { + return; + } + + if (!initialMaximizeTargetCommitted()) { + qCDebug(lcTlSurface) << "Waiting for initial maximized buffer from" << appId() + << "target" << m_initialMaximizeGeometry.size() << "actual" + << QSizeF(m_surfaceItem->implicitWidth(), + m_surfaceItem->implicitHeight()); + return; + } + + finishInitialMaximize(false); +} + +bool SurfaceWrapper::initialMaximizeTargetCommitted() const +{ + if (!m_surfaceItem || !m_initialMaximizeGeometry.isValid()) + return false; + + const QSizeF actualSize(m_surfaceItem->implicitWidth(), m_surfaceItem->implicitHeight()); + const QSizeF targetSize = m_initialMaximizeGeometry.size(); + return sizesMatch(actualSize, targetSize); +} + +void SurfaceWrapper::finishInitialMaximize(bool timedOut) +{ + if (!m_initialMaximizePending) + return; + + const QRectF targetGeometry = m_initialMaximizeGeometry; + m_initialMaximizePending = false; + m_initialMaximizeConfigured = false; + ++m_initialMaximizeGeneration; + + setPosition(targetGeometry.topLeft()); + setImplicitSize(targetGeometry.width(), targetGeometry.height()); + setXwaylandPositionFromSurface(true); + + if (m_prelaunchSplash) { + completeSplashTransition(targetGeometry.size()); + } else { + m_surfaceItem->setVisible(true); + updateBoundingRect(); + if (surface() && surface()->mapped() && !m_windowAnimation) + createNewOrClose(OPEN_ANIMATION); } + + qCDebug(lcTlSurface) << "Presented initial maximized surface for" << appId() << "target" + << targetGeometry << "timedOut" << timedOut; + tryApplyDeferredSurfaceState(); } QBindable SurfaceWrapper::bindableSurfaceState() @@ -1160,6 +1689,7 @@ void SurfaceWrapper::setNoDecoration(bool newNoDecoration) setNoCornerRadius(newNoDecoration); updateDecoration(); + refreshConfiguredInitialXdgMaximize(); } void SurfaceWrapper::updateDecoration() @@ -1307,8 +1837,14 @@ void SurfaceWrapper::geometryChange(const QRectF &newGeo, const QRectF &oldGeome if (m_container && m_container->filterSurfaceGeometryChanged(this, newGeometry, oldGeometry)) return; - if (isNormal() && !m_geometryAnimation) { - setNormalGeometry(newGeometry); + if (isNormal() && !m_geometryAnimation && !m_initialMaximizePending) { + const bool comesFromSurface = + m_shellSurface && !m_prelaunchSplash && m_surfaceItem && m_surfaceItem->isReady() + && (m_type == Type::XdgToplevel || m_type == Type::XWayland); + if (comesFromSurface) + setNormalGeometryFromSurface(newGeometry, false); + else + setNormalGeometry(newGeometry, false); } if (widthValid() && heightValid()) { @@ -1320,6 +1856,7 @@ void SurfaceWrapper::geometryChange(const QRectF &newGeo, const QRectF &oldGeome if (newGeometry.size() != oldGeometry.size()) updateBoundingRect(); updateClipRect(); + tryApplyDeferredSurfaceState(); } void SurfaceWrapper::createNewOrClose(uint direction) @@ -1409,7 +1946,7 @@ void SurfaceWrapper::itemChange(ItemChange change, const ItemChangeData &data) return QQuickItem::itemChange(change, data); } -void SurfaceWrapper::doSetSurfaceState(State newSurfaceState) +void SurfaceWrapper::doSetSurfaceState(State newSurfaceState, bool configureShellSurface) { if (m_wrapperAboutToRemove) return; @@ -1445,45 +1982,49 @@ void SurfaceWrapper::doSetSurfaceState(State newSurfaceState) } } - switch (m_previousSurfaceState.value()) { - case State::Maximized: - m_shellSurface->setMaximize(false); - break; - case State::Minimized: - m_shellSurface->setMinimize(false); - updateFocusControlState(FocusControlState::UnMinimized, true); - updateHasActiveCapability(ActiveControlState::UnMinimized, true); - break; - case State::Fullscreen: - m_shellSurface->setFullScreen(false); - break; - case State::Normal: - [[fallthrough]]; - case State::Tiling: - [[fallthrough]]; - default: - break; + if (configureShellSurface) { + switch (m_previousSurfaceState.value()) { + case State::Maximized: + m_shellSurface->setMaximize(false); + break; + case State::Minimized: + m_shellSurface->setMinimize(false); + updateFocusControlState(FocusControlState::UnMinimized, true); + updateHasActiveCapability(ActiveControlState::UnMinimized, true); + break; + case State::Fullscreen: + m_shellSurface->setFullScreen(false); + break; + case State::Normal: + [[fallthrough]]; + case State::Tiling: + [[fallthrough]]; + default: + break; + } } m_previousSurfaceState.notify(); - switch (m_surfaceState.value()) { - case State::Maximized: - m_shellSurface->setMaximize(true); - break; - case State::Minimized: - updateFocusControlState(FocusControlState::UnMinimized, false); - updateHasActiveCapability(ActiveControlState::UnMinimized, false); - m_shellSurface->setMinimize(true); - break; - case State::Fullscreen: - m_shellSurface->setFullScreen(true); - break; - case State::Normal: - [[fallthrough]]; - case State::Tiling: - [[fallthrough]]; - default: - break; + if (configureShellSurface) { + switch (m_surfaceState.value()) { + case State::Maximized: + m_shellSurface->setMaximize(true); + break; + case State::Minimized: + updateFocusControlState(FocusControlState::UnMinimized, false); + updateHasActiveCapability(ActiveControlState::UnMinimized, false); + m_shellSurface->setMinimize(true); + break; + case State::Fullscreen: + m_shellSurface->setFullScreen(true); + break; + case State::Normal: + [[fallthrough]]; + case State::Tiling: + [[fallthrough]]; + default: + break; + } } m_surfaceState.notify(); updateTitleBar(); @@ -1508,27 +2049,86 @@ void SurfaceWrapper::onAnimationReady() Q_ASSERT(m_pendingState != m_surfaceState); Q_ASSERT(m_pendingGeometry.isValid()); - if (!resize(m_pendingGeometry.size(), true)) { - // abort change state if cannot resize + auto deferPendingState = [this] { + const State deferredState = m_pendingState; + setXwaylandPositionFromSurface(true); + const QPointer failedAnimation = m_geometryAnimation; m_geometryAnimation->disconnect(this); m_geometryAnimation->deleteLater(); m_geometryAnimation = nullptr; + deferSurfaceState(deferredState); + continuePendingTransitionsAfterAnimation(failedAnimation); + }; + + if (!resize(m_pendingGeometry.size(), true)) { + // abort change state if cannot resize + deferPendingState(); + return; + } + + const bool completingXwaylandPrelaunch = + m_type == Type::XWayland && m_prelaunchSplash; + if (completingXwaylandPrelaunch) { + // WXWaylandSurfaceItem intentionally ignores configure requests while it is hidden. + // GeometryAnimation.hideSource already owns presentation at this point, so making the + // real item visible enables the native configure without exposing it directly. + if (!isVisible()) { + deferPendingState(); + return; + } + m_surfaceItem->setVisible(true); + Q_ASSERT(m_surfaceItem->isVisible()); + } + + if (m_type == Type::XWayland + && (!surface() || !surface()->mapped() || !isVisible() + || !m_surfaceItem->isVisible())) { + deferPendingState(); return; } QPointF alignedPos = alignToPixelGrid(m_pendingGeometry.topLeft()); setPosition(alignedPos); - doSetSurfaceState(m_pendingState); - resize(m_pendingGeometry.size()); + bool resizeCallSucceeded = true; + if (m_type == Type::XWayland) { + // XWayland's configure path can reject a hidden item. Configure first so state is not + // acknowledged unless the native geometry request was actually sent. + resizeCallSucceeded = resize(m_pendingGeometry.size()); + if (!resizeCallSucceeded) { + qCDebug(lcTlSurface) << "Deferred XWayland state because native configure was skipped" + << appId() << m_pendingGeometry; + deferPendingState(); + return; + } + doSetSurfaceState(m_pendingState); + } else { + doSetSurfaceState(m_pendingState); + resizeCallSucceeded = resize(m_pendingGeometry.size()); + } + + if (m_type == Type::XWayland) { + qCDebug(lcTlSurface) << "Requested XWayland state geometry for" << appId() + << "state" << static_cast(m_pendingState) + << "target" << m_pendingGeometry + << "prelaunch" << completingXwaylandPrelaunch + << "surfaceItemVisible" << m_surfaceItem->isVisible() + << "resizeCallSucceeded" << resizeCallSucceeded; + } + + if (m_prelaunchSplash && surface() && surface()->mapped()) + completeSplashTransition(m_pendingGeometry.size()); } void SurfaceWrapper::onAnimationFinished() { setXwaylandPositionFromSurface(true); Q_ASSERT(m_geometryAnimation); + const QPointer finishedAnimation = m_geometryAnimation; m_geometryAnimation->disconnect(this); m_geometryAnimation->deleteLater(); m_geometryAnimation = nullptr; + + continuePendingTransitionsAfterAnimation(finishedAnimation); } bool SurfaceWrapper::startStateChangeAnimation(State targetState, const QRectF &targetGeometry) @@ -1552,6 +2152,24 @@ bool SurfaceWrapper::startStateChangeAnimation(State targetState, const QRectF & return ok; } +void SurfaceWrapper::continuePendingTransitionsAfterAnimation(QQuickItem *animation) +{ + Q_ASSERT(animation); + connect(animation, &QObject::destroyed, this, [this] { + // QObject::destroyed is emitted while the object is being torn down. Continue on the + // next event-loop turn so every ShaderEffectSource child has released hideSource. + QTimer::singleShot(0, this, [this] { + if (m_wrapperAboutToRemove) + return; + + tryApplyInitialMaximize(); + if (m_prelaunchSplash && surface() && surface()->mapped()) + startPrelaunchSplashHideSequence(); + tryApplyDeferredSurfaceState(); + }); + }); +} + void SurfaceWrapper::onWindowAnimationFinished() { Q_ASSERT(m_windowAnimation); @@ -1568,11 +2186,13 @@ void SurfaceWrapper::onWindowAnimationFinished() void SurfaceWrapper::onShowAnimationFinished() { + Q_ASSERT(m_windowAnimation); + const QPointer finishedAnimation = m_windowAnimation; onWindowAnimationFinished(); - if (m_prelaunchSplash && surface() && surface()->mapped()) { - startPrelaunchSplashHideSequence(); - } + // onWindowAnimationFinished() disconnects the animation before scheduling its deletion, so + // install the destruction continuation afterwards. + continuePendingTransitionsAfterAnimation(finishedAnimation); } void SurfaceWrapper::onHideAnimationFinished() @@ -1600,7 +2220,11 @@ void SurfaceWrapper::onMappedChanged() if (!m_isProxy) { if (mapped) { if (!m_prelaunchSplash) { - createNewOrClose(OPEN_ANIMATION); + if (m_initialMaximizePending) { + tryApplyInitialMaximize(); + } else if (!m_geometryAnimation) { + createNewOrClose(OPEN_ANIMATION); + } } else { syncPrelaunchMappedState(); startPrelaunchSplashHideSequence(); @@ -1621,6 +2245,10 @@ void SurfaceWrapper::onMappedChanged() updateHasActiveCapability(ActiveControlState::MappedOrSplash, mapped); updateFocusControlState(FocusControlState::Mapped, mapped); updateVisible(); + if (mapped) { + tryApplyInitialMaximize(); + tryApplyDeferredSurfaceState(); + } } void SurfaceWrapper::onSocketEnabledChanged() @@ -1757,13 +2385,72 @@ void SurfaceWrapper::maximize() || !isMaximizable()) return; + // ShellHandler folds a native XDG launch-time request into the initial configure. Calling + // set_maximized before the initial empty commit would violate wlroots' initialized + // precondition, while waiting for map would turn it into a second visible state change. + if (m_type == Type::XdgToplevel && (!surface() || !surface()->mapped())) + return; + + const bool prelaunchGeometryTransition = + m_geometryAnimation && m_pendingState == m_surfaceState; + const bool isLaunchTimeRequest = + m_type == Type::XWayland + && (!hasInitializeContainer() || m_prelaunchSplash || m_windowAnimation + || prelaunchGeometryTransition || !m_surfaceItem || !m_surfaceItem->isReady() + || !surface() || !surface()->mapped()); + if (!m_initialMaximizePending && isLaunchTimeRequest) { + m_initialMaximizePending = true; + // A running NewAnimation uses a live ShaderEffectSource. Keep its source intact until + // the animation object is gone; tryApplyInitialMaximize() hides it immediately after. + if (m_surfaceItem && (!m_windowAnimation || m_prelaunchSplash)) + m_surfaceItem->setVisible(false); + qCDebug(lcTlSurface) << "Promoted launch-time maximize request for" << appId() << "type" + << static_cast(m_type); + tryApplyInitialMaximize(); + return; + } + setSurfaceState(State::Maximized); } void SurfaceWrapper::unmaximize() { - if (m_surfaceState != State::Maximized) + if (hasConfiguredInitialXdgMaximize()) { + cancelConfiguredInitialXdgMaximize(); return; + } + + if (m_initialMaximizePending) { + if (!m_initialMaximizeConfigured && m_surfaceState == State::Normal) { + m_initialMaximizePending = false; + ++m_initialMaximizeGeneration; + setXwaylandPositionFromSurface(true); + + if (m_prelaunchSplash && surface() && surface()->mapped()) { + startPrelaunchSplashHideSequence(); + } else if (!m_prelaunchSplash && !m_windowAnimation && !m_geometryAnimation) { + m_surfaceItem->setVisible(true); + if (surface() && surface()->mapped()) + createNewOrClose(OPEN_ANIMATION); + } + + qCDebug(lcTlSurface) << "Cancelled unconfigured initial maximize for" << appId(); + return; + } + + setSurfaceState(State::Normal); + return; + } + + if (m_surfaceState != State::Maximized) { + const bool maximizeAnimationPending = + m_geometryAnimation && m_pendingState == State::Maximized; + const bool maximizeStateDeferred = + m_hasDeferredSurfaceState && m_deferredSurfaceState == State::Maximized; + if (maximizeAnimationPending || maximizeStateDeferred) + setSurfaceState(State::Normal); + return; + } setSurfaceState(State::Normal); } @@ -2051,6 +2738,7 @@ void SurfaceWrapper::setNoTitleBar(bool newNoTitleBar) m_noTitleBar = newNoTitleBar; updateTitleBar(); + refreshConfiguredInitialXdgMaximize(); } bool SurfaceWrapper::noCornerRadius() const @@ -2262,6 +2950,11 @@ void SurfaceWrapper::setHasInitializeContainer(bool value) // m_prelaunchSplash can't get mapped signal createNewOrClose(OPEN_ANIMATION); } + + if (value) { + tryApplyInitialMaximize(); + tryApplyDeferredSurfaceState(); + } } void SurfaceWrapper::disableWindowAnimation(bool disable) diff --git a/src/surface/surfacewrapper.h b/src/surface/surfacewrapper.h index 76ba6123af..c4e0e502cd 100644 --- a/src/surface/surfacewrapper.h +++ b/src/surface/surfacewrapper.h @@ -180,6 +180,7 @@ class SurfaceWrapper : public QQuickItem QRectF geometry() const; QRectF normalGeometry() const; + bool hasReliableNormalGeometry() const; void moveNormalGeometryInOutput(const QPointF &position); QPointF alignToPixelGrid(const QPointF &pos) const; QRectF alignGeometryToPixelGrid(const QRectF &geometry) const; @@ -381,6 +382,19 @@ public Q_SLOTS: void typeChanged(); private: + SurfaceWrapper(QmlEngine *qmlEngine, + WToplevelSurface *shellSurface, + Type type, + const QString &appId, + QQuickItem *parent, + const QRectF &initialMaximizedGeometry); + + enum class NormalGeometrySource : quint8 { + None, + Restored, + Client, + }; + ~SurfaceWrapper() override; using QObject::deleteLater; using QQuickItem::setParentItem; @@ -390,7 +404,12 @@ public Q_SLOTS: void setParent(QQuickItem *item); void setActivate(bool activate); void updateActiveState(); - void setNormalGeometry(const QRectF &newNormalGeometry); + void setNormalGeometry(const QRectF &newNormalGeometry, bool applyDeferredState = true); + void setNormalGeometryFromSurface(const QRectF &newNormalGeometry, + bool applyDeferredState = true); + void setRestoredNormalSize(const QSizeF &size, bool applyInitialMaximize = true); + bool hasUsableNormalGeometry() const; + bool captureNormalGeometryFromSurfaceItem(bool applyDeferredState = true); void updateTitleBar(); void updateDecoration(); void setBoundedRect(const QRectF &newBoundedRect); @@ -399,8 +418,11 @@ public Q_SLOTS: void invalidate(); void setup(); // Initialize m_surfaceItem related features + // Transition from pre-launch mode to normal mode. void convertToNormalSurface(WToplevelSurface *shellSurface, - Type type); // Transition from pre-launch mode to normal mode + Type type, + const QRectF &initialMaximizedGeometry = {}); + void adoptInitialXdgMaximize(const QRectF &targetGeometry); void updateBoundingRect(); void updateVisible(); void updateSubSurfaceStacking(); @@ -410,7 +432,18 @@ public Q_SLOTS: void createNewOrClose(uint direction); void itemChange(ItemChange change, const ItemChangeData &data) override; - void doSetSurfaceState(State newSurfaceState); + void doSetSurfaceState(State newSurfaceState, bool configureShellSurface = true); + void deferSurfaceState(State newSurfaceState); + void tryApplyDeferredSurfaceState(); + void tryApplyInitialMaximize(); + bool hasConfiguredInitialXdgMaximize() const; + void refreshConfiguredInitialXdgMaximize(); + void activateConfiguredInitialXdgMaximize(); + void cancelConfiguredInitialXdgMaximize(); + void armInitialMaximizeCommitTimeout(); + void handleInitialMaximizeCommit(); + bool initialMaximizeTargetCommitted() const; + void finishInitialMaximize(bool timedOut); Q_SLOT void onAnimationReady(); Q_SLOT void onAnimationFinished(); void syncPrelaunchMappedState(); @@ -418,6 +451,7 @@ public Q_SLOTS: Q_SLOT void onPrelaunchGeometryAnimationReady(); Q_SLOT void onPrelaunchGeometryAnimationFinished(); bool startStateChangeAnimation(SurfaceWrapper::State targetState, const QRectF &targetGeometry); + void continuePendingTransitionsAfterAnimation(QQuickItem *animation); void onWindowAnimationFinished(); Q_SLOT void onShowAnimationFinished(); Q_SLOT void onHideAnimationFinished(); @@ -460,8 +494,15 @@ public Q_SLOTS: Type m_type; QPointer m_ownsOutput; QPointF m_positionInOwnsOutput; - SurfaceWrapper::State m_pendingState; + SurfaceWrapper::State m_pendingState = State::Normal; QRectF m_pendingGeometry; + SurfaceWrapper::State m_deferredSurfaceState = State::Normal; + bool m_hasDeferredSurfaceState = false; + NormalGeometrySource m_normalGeometrySource = NormalGeometrySource::None; + bool m_initialMaximizePending = false; + bool m_initialMaximizeConfigured = false; + QRectF m_initialMaximizeGeometry; + quint64 m_initialMaximizeGeneration = 0; QPointer m_windowAnimation; QPointer m_minimizeAnimation; QPointer m_showDesktopAnimation; diff --git a/waylib/src/server/protocols/wxdgtoplevelsurface.cpp b/waylib/src/server/protocols/wxdgtoplevelsurface.cpp index e14647dbeb..b445ad5392 100644 --- a/waylib/src/server/protocols/wxdgtoplevelsurface.cpp +++ b/waylib/src/server/protocols/wxdgtoplevelsurface.cpp @@ -4,6 +4,7 @@ #include "wxdgtoplevelsurface.h" #include "private/wtoplevelsurface_p.h" +#include "wayliblogging.h" #include "wseat.h" #include "wtools.h" @@ -39,6 +40,10 @@ class Q_DECL_HIDDEN WXdgToplevelSurfacePrivate : public WToplevelSurfacePrivate void instantRelease() override; void updateSizeFromCommit(); + inline bool isMaximizeRequested() const { + return handle()->is_maximize_requested(); + } + W_DECLARE_PUBLIC(WXdgToplevelSurface) WSurface *surface = nullptr; @@ -148,8 +153,17 @@ void WXdgToplevelSurfacePrivate::connect() W_Q(WXdgToplevelSurface); auto surface = qw_xdg_surface::from(nativeHandle()->base); - q->surface()->safeConnect(&WSurface::commit, q, [this] { + q->surface()->safeConnect(&WSurface::commit, q, [this, q] { updateSizeFromCommit(); + + // The initial xdg_surface configure is a protocol handshake and must not depend on + // whether a QtQuick item has already been created. Treeland may wait for asynchronous + // app-id resolution before creating that item. + if (handle()->is_initial_commit()) { + qCDebug(lcWlSurface) << "Scheduling initial XDG toplevel configure for" << q; + handle()->set_size(0, 0); + Q_EMIT q->initialConfigureRequested(); + } }); QObject::connect(surface, &qw_xdg_surface::notify_configure, q, [this] (wlr_xdg_surface_configure *event) { on_configure(event); @@ -165,7 +179,7 @@ void WXdgToplevelSurfacePrivate::connect() Q_EMIT q->requestResize(seat, WTools::toQtEdge(event->edges), event->serial); }); QObject::connect(handle(), &qw_xdg_toplevel::notify_request_maximize, q, [q, this] () { - if ((*handle())->requested.maximized) { + if (isMaximizeRequested()) { Q_EMIT q->requestMaximize(); } else { Q_EMIT q->requestCancelMaximize(); @@ -173,12 +187,12 @@ void WXdgToplevelSurfacePrivate::connect() }); QObject::connect(handle(), &qw_xdg_toplevel::notify_request_minimize, q, [q, this] () { // Wayland clients can't request unset minimization on this surface - if ((*handle())->requested.minimized) { + if (handle()->is_minimize_requested()) { Q_EMIT q->requestMinimize(); } }); QObject::connect(handle(), &qw_xdg_toplevel::notify_request_fullscreen, q, [q, this] () { - if ((*handle())->requested.fullscreen) { + if (handle()->is_fullscreen_requested()) { Q_EMIT q->requestFullscreen(); } else { Q_EMIT q->requestCancelFullscreen(); @@ -277,6 +291,12 @@ bool WXdgToplevelSurface::isResizeing() const return d->resizeing; } +bool WXdgToplevelSurface::isMaximizeRequested() const +{ + W_DC(WXdgToplevelSurface); + return d->isMaximizeRequested(); +} + bool WXdgToplevelSurface::isActivated() const { W_DC(WXdgToplevelSurface); diff --git a/waylib/src/server/protocols/wxdgtoplevelsurface.h b/waylib/src/server/protocols/wxdgtoplevelsurface.h index 4a8480d094..7ea8447684 100644 --- a/waylib/src/server/protocols/wxdgtoplevelsurface.h +++ b/waylib/src/server/protocols/wxdgtoplevelsurface.h @@ -42,6 +42,9 @@ class WAYLIB_SERVER_EXPORT WXdgToplevelSurface : public WXdgSurface WSurface *parentSurface() const override; bool isResizeing() const; + // The latest state requested by the client, including requests received before a + // compositor-side SurfaceItem has been created. + bool isMaximizeRequested() const; bool isActivated() const override; bool isMaximized() const override; bool isMinimized() const override; @@ -74,6 +77,11 @@ public Q_SLOTS: void close() override; Q_SIGNALS: + // Emitted synchronously during the initial empty commit after a default 0x0 configure has + // been scheduled. Compositor policy may override the scheduled size and states before the + // configure is dispatched. + void initialConfigureRequested(); + void parentXdgSurfaceChanged(); void resizeingChanged(); diff --git a/waylib/src/server/protocols/wxwaylandsurface.cpp b/waylib/src/server/protocols/wxwaylandsurface.cpp index 1de80085b5..de9c190694 100644 --- a/waylib/src/server/protocols/wxwaylandsurface.cpp +++ b/waylib/src/server/protocols/wxwaylandsurface.cpp @@ -48,7 +48,7 @@ class Q_DECL_HIDDEN WXWaylandSurfacePrivate : public WToplevelSurfacePrivate WWRAP_HANDLE_FUNCTIONS(qw_xwayland_surface, wlr_xwayland_surface) inline bool isMaximized() const { - return nativeHandle()->maximized_horz && nativeHandle()->maximized_vert; + return handle()->is_maximized(); } wl_client *waylandClient() const override { @@ -136,7 +136,7 @@ void WXWaylandSurfacePrivate::init() } }); QObject::connect(handle(), &qw_xwayland_surface::notify_request_maximize, q, [this, q] { - if (nativeHandle()->maximized_horz && nativeHandle()->maximized_vert) { + if (isMaximized()) { Q_EMIT q->requestMaximize(); } else { Q_EMIT q->requestCancelMaximize(); @@ -383,6 +383,12 @@ bool WXWaylandSurface::hasChild() const return wl_list_empty(&d->nativeHandle()->children) == 0; } +bool WXWaylandSurface::isMaximizeRequested() const +{ + W_DC(WXWaylandSurface); + return d->isMaximized(); +} + bool WXWaylandSurface::isMaximized() const { W_DC(WXWaylandSurface); diff --git a/waylib/src/server/protocols/wxwaylandsurface.h b/waylib/src/server/protocols/wxwaylandsurface.h index 3a06201618..a3f790cd7e 100644 --- a/waylib/src/server/protocols/wxwaylandsurface.h +++ b/waylib/src/server/protocols/wxwaylandsurface.h @@ -92,6 +92,9 @@ class WAYLIB_SERVER_EXPORT WXWaylandSurface : public WToplevelSurface const QList &children() const; bool isToplevel() const; bool hasChild() const; + // The latest EWMH state requested by the X11 client. Unlike isMaximized(), this also + // reflects state read during association before the compositor has acknowledged it. + bool isMaximizeRequested() const; bool isMaximized() const override; bool isMinimized() const override; bool isFullScreen() const override; diff --git a/waylib/src/server/qtquick/wxdgtoplevelsurfaceitem.cpp b/waylib/src/server/qtquick/wxdgtoplevelsurfaceitem.cpp index 65b6a448b4..beac198cf3 100644 --- a/waylib/src/server/qtquick/wxdgtoplevelsurfaceitem.cpp +++ b/waylib/src/server/qtquick/wxdgtoplevelsurfaceitem.cpp @@ -76,14 +76,6 @@ void WXdgToplevelSurfaceItem::onSurfaceCommit() Q_EMIT maximumSizeChanged(); } - auto xdg_surface = toplevelSurface()->handle()->handle()->base; - if (xdg_surface->initial_commit) { - /* When an xdg_surface performs an initial commit, the compositor must - * reply with a configure so the client can map the surface. - * configures the xdg_toplevel with 0,0 size to let the client pick the - * dimensions itself. */ - toplevelSurface()->handle()->set_size(0, 0); - } } void WXdgToplevelSurfaceItem::initSurface() diff --git a/waylib/src/server/qtquick/wxwaylandsurfaceitem.cpp b/waylib/src/server/qtquick/wxwaylandsurfaceitem.cpp index b13e942853..fbb58c7bf6 100644 --- a/waylib/src/server/qtquick/wxwaylandsurfaceitem.cpp +++ b/waylib/src/server/qtquick/wxwaylandsurfaceitem.cpp @@ -13,7 +13,7 @@ class Q_DECL_HIDDEN WXWaylandSurfaceItemPrivate : public WSurfaceItemPrivate { Q_DECLARE_PUBLIC(WXWaylandSurfaceItem) public: - void configureSurface(const QRect &newGeometry); + bool configureSurface(const QRect &newGeometry); QSize expectSurfaceSize() const; QPoint explicitSurfacePosition() const; static inline WXWaylandSurfaceItemPrivate *get(WXWaylandSurfaceItem *qq) { @@ -23,19 +23,21 @@ class Q_DECL_HIDDEN WXWaylandSurfaceItemPrivate : public WSurfaceItemPrivate public: QPointF surfacePosition; bool positionConfigured = false; + bool allowConfigureWhileHidden = false; QPointer parentSurfaceItem; QSize minimumSize; QSize maximumSize; }; -void WXWaylandSurfaceItemPrivate::configureSurface(const QRect &newGeometry) +bool WXWaylandSurfaceItemPrivate::configureSurface(const QRect &newGeometry) { Q_Q(WXWaylandSurfaceItem); - if (!q->isVisible()) - return; + if (!q->isVisible() && !allowConfigureWhileHidden) + return false; q->xwaylandSurface()->configure(newGeometry); q->updateSurfaceState(); + return true; } QSize WXWaylandSurfaceItemPrivate::expectSurfaceSize() const @@ -185,6 +187,26 @@ QPointF WXWaylandSurfaceItem::implicitPosition() const return QPointF(epos) / ssr - QPointF(leftPadding(), topPadding()); } +bool WXWaylandSurfaceItem::configureSurfaceWhileHidden(const QRectF &geometry) +{ + Q_D(WXWaylandSurfaceItem); + if (!geometry.isValid() || geometry.isEmpty()) + return false; + + const QPointF previousPosition = d->surfacePosition; + d->surfacePosition = geometry.topLeft(); + d->allowConfigureWhileHidden = true; + const bool configured = WSurfaceItem::resizeSurface(geometry.size()); + d->allowConfigureWhileHidden = false; + + if (!configured) { + d->surfacePosition = previousPosition; + return false; + } + + d->positionConfigured = true; + return true; +} void WXWaylandSurfaceItem::onSurfaceCommit() { @@ -221,8 +243,7 @@ void WXWaylandSurfaceItem::initSurface() bool WXWaylandSurfaceItem::doResizeSurface(const QSize &newSize) { Q_D(WXWaylandSurfaceItem); - d->configureSurface(QRect(d->explicitSurfacePosition(), newSize)); - return true; + return d->configureSurface(QRect(d->explicitSurfacePosition(), newSize)); } QRectF WXWaylandSurfaceItem::getContentGeometry() const diff --git a/waylib/src/server/qtquick/wxwaylandsurfaceitem.h b/waylib/src/server/qtquick/wxwaylandsurfaceitem.h index 95abdf7bfa..8989eaf53c 100644 --- a/waylib/src/server/qtquick/wxwaylandsurfaceitem.h +++ b/waylib/src/server/qtquick/wxwaylandsurfaceitem.h @@ -37,6 +37,9 @@ class WAYLIB_SERVER_EXPORT WXWaylandSurfaceItem : public WSurfaceItem void moveTo(const QPointF &pos, bool configSurface); QPointF implicitPosition() const; + // Initial state negotiation may need to configure the X11 window before its first + // compositor-visible frame. The visibility bypass is limited to this single call. + bool configureSurfaceWhileHidden(const QRectF &geometry); Q_SIGNALS: void implicitPositionChanged(); From b3809204fdbad21e1d877f7dcedcf4068d0d7093 Mon Sep 17 00:00:00 2001 From: LFRon Date: Fri, 24 Jul 2026 11:26:05 +0800 Subject: [PATCH 2/2] fix(xwayland): sync active prelaunch stacking on handoff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prelaunch wrappers can already be active when their managed XWayland surface is attached. Because the activated wrapper does not change, the normal activation path does not raise the new native X11 window. Treeland then reports the app as active while X11 input can remain stacked behind another window. Before exposing the real surface, raise both the Treeland item and native X11 window. Limit this to active managed surfaces so inactive and override-redirect windows are unaffected. Log: 修复XWayland预启动窗口切换后点击穿透的问题 Influence: XWayland预启动Splash切换到真实窗口时的堆叠顺序 --- src/surface/surfacewrapper.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/surface/surfacewrapper.cpp b/src/surface/surfacewrapper.cpp index 4e3692d866..9f4405005b 100644 --- a/src/surface/surfacewrapper.cpp +++ b/src/surface/surfacewrapper.cpp @@ -860,6 +860,20 @@ void SurfaceWrapper::completeSplashTransition(const QSizeF &targetImplicitSize, m_decoration->stackBefore(m_surfaceItem); } + if (m_isActivated && m_type == Type::XWayland) { + auto *xwaylandSurface = qobject_cast(m_shellSurface); + if (xwaylandSurface && !xwaylandSurface->isBypassManager()) { + // wlroots initially places a managed XWayland window at the bottom of the native + // X11 stack. A prelaunch wrapper is already activated, so the normal activation + // path cannot observe a wrapper change and raise the newly attached X11 window. + // Synchronize both stacks exactly when the real surface takes over presentation. + stackToLast(); + xwaylandSurface->restack(nullptr, WXWaylandSurface::XCB_STACK_MODE_ABOVE); + qCDebug(lcTlSurface) + << "Synchronized active prelaunch XWayland stacking for" << appId(); + } + } + m_surfaceItem->setVisible(true); Q_ASSERT(m_prelaunchSplash); m_prelaunchSplash->setVisible(false);