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..9f4405005b 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) @@ -714,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); @@ -724,6 +884,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 +956,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 +972,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 +1063,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 +1105,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 +1143,7 @@ void SurfaceWrapper::setTilingGeometry(const QRectF &newTilingGeometry) } Q_EMIT tilingGeometryChanged(); + tryApplyDeferredSurfaceState(); } bool SurfaceWrapper::positionAutomatic() const @@ -1017,15 +1268,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 +1336,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 { + 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 { - if (m_geometryAnimation) { - m_geometryAnimation->disconnect(this); - m_geometryAnimation->deleteLater(); - m_geometryAnimation = nullptr; + 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 +1703,7 @@ void SurfaceWrapper::setNoDecoration(bool newNoDecoration) setNoCornerRadius(newNoDecoration); updateDecoration(); + refreshConfiguredInitialXdgMaximize(); } void SurfaceWrapper::updateDecoration() @@ -1307,8 +1851,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 +1870,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 +1960,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 +1996,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 +2063,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 +2166,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 +2200,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 +2234,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 +2259,10 @@ void SurfaceWrapper::onMappedChanged() updateHasActiveCapability(ActiveControlState::MappedOrSplash, mapped); updateFocusControlState(FocusControlState::Mapped, mapped); updateVisible(); + if (mapped) { + tryApplyInitialMaximize(); + tryApplyDeferredSurfaceState(); + } } void SurfaceWrapper::onSocketEnabledChanged() @@ -1757,13 +2399,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 +2752,7 @@ void SurfaceWrapper::setNoTitleBar(bool newNoTitleBar) m_noTitleBar = newNoTitleBar; updateTitleBar(); + refreshConfiguredInitialXdgMaximize(); } bool SurfaceWrapper::noCornerRadius() const @@ -2262,6 +2964,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();