diff --git a/src/surface/surfacewrapper.cpp b/src/surface/surfacewrapper.cpp index 7226b3daa..4d6d3d23e 100644 --- a/src/surface/surfacewrapper.cpp +++ b/src/surface/surfacewrapper.cpp @@ -203,6 +203,7 @@ void SurfaceWrapper::invalidate() { Q_ASSERT_X(!m_wrapperAboutToRemove, Q_FUNC_INFO, "Can't call `invalidate` twice!"); m_wrapperAboutToRemove = true; + m_pendingPrelaunchXWaylandStackSync = false; Q_EMIT aboutToBeInvalidated(); if (!m_skipDockPreView) @@ -540,6 +541,10 @@ void SurfaceWrapper::setup() &WXWaylandSurface::belowChanged, this, &SurfaceWrapper::updateXWaylandStackingState); + connect(xwaylandSurface, + &WXWaylandSurface::x11MapCompleted, + this, + &SurfaceWrapper::syncPrelaunchXWaylandStacking); updateX11SkipFlags(); updateXWaylandStackingState(); } @@ -609,6 +614,12 @@ void SurfaceWrapper::setActivate(bool activate) Q_ASSERT(!activate || hasActiveCapability()); m_isActivated = activate; + // Retry a deferred prelaunch X11 stack sync on activation (not only from the + // splash handoff): a map that happened while deactivated is repaired here, + // mirroring the raising-activation policy of Helper::setActivatedSurface. + if (activate) + syncPrelaunchXWaylandStacking(); + if (m_attention && m_isActivated) setAttention(false); @@ -808,6 +819,8 @@ void SurfaceWrapper::completeSplashTransition(const QSizeF &targetImplicitSize, m_decoration->stackBefore(m_surfaceItem); } + requestPrelaunchXWaylandStackSync(); + m_surfaceItem->setVisible(true); if (m_type == Type::XWayland && (m_surfaceState == State::Maximized || m_surfaceState == State::Fullscreen)) { @@ -836,6 +849,60 @@ void SurfaceWrapper::completeSplashTransition(const QSizeF &targetImplicitSize, updateHasActiveCapability(ActiveControlState::MappedOrSplash, surface() && surface()->mapped()); } +void SurfaceWrapper::requestPrelaunchXWaylandStackSync() +{ + if (m_isProxy || m_type != Type::XWayland) + return; + + auto *xwaylandSurface = qobject_cast(m_shellSurface); + if (!xwaylandSurface || xwaylandSurface->isBypassManager()) + return; + + m_pendingPrelaunchXWaylandStackSync = true; + if (!xwaylandSurface->isX11Mapped()) { + qCDebug(lcTlSurface) + << "Deferring prelaunch XWayland stacking until X11 map completes for" << appId(); + return; + } + + syncPrelaunchXWaylandStacking(); +} + +void SurfaceWrapper::syncPrelaunchXWaylandStacking() +{ + if (!m_pendingPrelaunchXWaylandStackSync) + return; + + auto *xwaylandSurface = qobject_cast(m_shellSurface); + if (m_wrapperAboutToRemove || m_isProxy || m_type != Type::XWayland || !xwaylandSurface + || xwaylandSurface->isBypassManager()) { + m_pendingPrelaunchXWaylandStackSync = false; + return; + } + + if (!m_isActivated || !xwaylandSurface->isX11Mapped()) + return; + + // Clear before restacking so a re-entrant activation during the child + // reorder cannot repeat the restack. + m_pendingPrelaunchXWaylandStackSync = false; + + // 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 after wlroots has finished handling XCB_MAP_NOTIFY, mirroring the stacking + // direction updateXWaylandStackingState() gives to _NET_WM_STATE_ABOVE/BELOW windows. + if (!xwaylandSurface->isAbove() && xwaylandSurface->isBelow()) { + stackToFirst(); + xwaylandSurface->restack(nullptr, WXWaylandSurface::XCB_STACK_MODE_BELOW); + } else { + stackToLast(); + xwaylandSurface->restack(nullptr, WXWaylandSurface::XCB_STACK_MODE_ABOVE); + } + qCDebug(lcTlSurface) + << "Synchronized active prelaunch XWayland stacking after X11 map for" << appId(); +} + WSurface *SurfaceWrapper::surface() const { if (!m_shellSurface) @@ -2721,10 +2788,19 @@ void SurfaceWrapper::updateXWaylandStackingState() const bool below = !above && xwaylandSurface->isBelow(); setAlwaysOnBottom(below); setAlwaysOnTop(above); + + // Guards are mandatory: wlroots only puts a surface into its stack list + // after the map handler, and restack() asserts the window is not + // override-redirect. + const bool canRestack = xwaylandSurface->isX11Mapped() && !xwaylandSurface->isBypassManager(); if (above) { stackToLast(); + if (canRestack) + xwaylandSurface->restack(nullptr, WXWaylandSurface::XCB_STACK_MODE_ABOVE); } else if (below) { stackToFirst(); + if (canRestack) + xwaylandSurface->restack(nullptr, WXWaylandSurface::XCB_STACK_MODE_BELOW); } } diff --git a/src/surface/surfacewrapper.h b/src/surface/surfacewrapper.h index 9f926aa5e..415cf3bd5 100644 --- a/src/surface/surfacewrapper.h +++ b/src/surface/surfacewrapper.h @@ -494,6 +494,8 @@ public Q_SLOTS: void updateActivateCapability(); void updateFocusCapability(); void completeSplashTransition(const QSizeF &targetImplicitSize, bool hideDecoration = false); + void requestPrelaunchXWaylandStackSync(); + void syncPrelaunchXWaylandStacking(); // wayland set by treeland-dde-shell, x11 set by bypassManager/windowTypes void setSkipDockPreView(bool skip); @@ -597,6 +599,7 @@ public Q_SLOTS: bool m_socketEnabled{ false }; bool m_windowAnimationEnabled{ true }; + bool m_pendingPrelaunchXWaylandStackSync{ false }; const QString m_appId; }; diff --git a/waylib/src/server/protocols/private/wxwaylandsurface_p.h b/waylib/src/server/protocols/private/wxwaylandsurface_p.h index b49b8508a..b48d88dd7 100644 --- a/waylib/src/server/protocols/private/wxwaylandsurface_p.h +++ b/waylib/src/server/protocols/private/wxwaylandsurface_p.h @@ -68,6 +68,8 @@ class Q_DECL_HIDDEN WXWaylandSurfacePrivate : public WToplevelSurfacePrivate uint minimized:1; uint fullscreen:1; uint activated:1; + bool x11Mapped = false; + quint64 x11MapGeneration = 0; private: // XWayland owns this handle and destroys it after notifying the diff --git a/waylib/src/server/protocols/wxwayland.cpp b/waylib/src/server/protocols/wxwayland.cpp index 9bf800f5b..4794b6b17 100644 --- a/waylib/src/server/protocols/wxwayland.cpp +++ b/waylib/src/server/protocols/wxwayland.cpp @@ -74,6 +74,7 @@ class Q_DECL_HIDDEN WXWaylandPrivate : public WWaylandResourcePrivate void xcbPollReplies(); void xcbAsyncTimeoutForWindow(xcb_window_t windowId); + void setX11Mapped(xcb_window_t windowId, bool mapped); W_DECLARE_PUBLIC(WXWayland) @@ -96,10 +97,6 @@ bool xwayland_user_event_handler(wlr_xwayland *xwayland, xcb_generic_event_t *ev return false; const uint8_t response_type = event->response_type & ~0x80; - if (response_type != XCB_PROPERTY_NOTIFY) - return false; - - auto *pe = reinterpret_cast(event); auto *self = WXWayland::fromHandle(xwayland); if (!self) @@ -107,6 +104,31 @@ bool xwayland_user_event_handler(wlr_xwayland *xwayland, xcb_generic_event_t *ev auto *d = self->d_func(); + // XSendEvent can forge MAP_NOTIFY/UNMAP_NOTIFY to the root window + // (SubstructureNotifyMask). Only genuine server notifications may drive + // the X11 map state machine; wlroots still processes the event below. + const bool synthetic = (event->response_type & 0x80) != 0; + + // This handler runs before wlroots handles the event; the queued completion + // in WXWaylandSurface::setX11Mapped therefore lands after wlroots restacked + // the newly mapped window below. Do not make that signal synchronous. + if (!synthetic && response_type == XCB_MAP_NOTIFY) { + auto *me = reinterpret_cast(event); + d->setX11Mapped(me->window, true); + return false; + } + + if (!synthetic && response_type == XCB_UNMAP_NOTIFY) { + auto *ue = reinterpret_cast(event); + d->setX11Mapped(ue->window, false); + return false; + } + + if (response_type != XCB_PROPERTY_NOTIFY) + return false; + + auto *pe = reinterpret_cast(event); + // Trigger async property reading infrastructure if this window is being tracked. if (!d->asyncProps.isEmpty()) { d->xcbPollReplies(); @@ -146,6 +168,20 @@ bool xwayland_user_event_handler(wlr_xwayland *xwayland, xcb_generic_event_t *ev return false; } +void WXWaylandPrivate::setX11Mapped(xcb_window_t windowId, bool mapped) +{ + for (auto *surface : std::as_const(surfaceList)) { + QPointer guard(surface); + if (!guard) + continue; + if (guard->handle()->window_id != windowId) + continue; + + guard->setX11Mapped(mapped); + break; + } +} + void WXWaylandPrivate::init() { W_Q(WXWayland); diff --git a/waylib/src/server/protocols/wxwaylandsurface.cpp b/waylib/src/server/protocols/wxwaylandsurface.cpp index 877b99dbb..bf20da944 100644 --- a/waylib/src/server/protocols/wxwaylandsurface.cpp +++ b/waylib/src/server/protocols/wxwaylandsurface.cpp @@ -58,6 +58,7 @@ void WXWaylandSurfacePrivate::init() }); q->listeners()->add(&m_handle->events.dissociate, this, [this, q] (void *) { Q_ASSERT(surface); + q->setX11Mapped(false); Q_EMIT q->aboutToDissociate(); delete surface; surface = nullptr; @@ -519,6 +520,12 @@ bool WXWaylandSurface::isBelow() const return d->handle()->below; } +bool WXWaylandSurface::isX11Mapped() const +{ + W_DC(WXWaylandSurface); + return d->x11Mapped; +} + bool WXWaylandSurface::isModal() const { W_DC(WXWaylandSurface); @@ -662,4 +669,34 @@ void WXWaylandSurface::restack(WXWaylandSurface *sibling, StackMode mode) wlr_xwayland_surface_restack(handle(), nullptr, static_cast(mode)); } +void WXWaylandSurface::setX11Mapped(bool mapped) +{ + W_D(WXWaylandSurface); + + if (d->x11Mapped == mapped) + return; + + d->x11Mapped = mapped; + const quint64 generation = ++d->x11MapGeneration; + if (!mapped) + return; + + // wlroots invokes the user event handler before its own XCB_MAP_NOTIFY handler. + // Defer notification so consumers run after wlroots performs the initial X11 restack. + QPointer guard(this); + QMetaObject::invokeMethod( + this, + [guard, generation] { + if (!guard) + return; + + const auto *d = guard->d_func(); + if (!d->x11Mapped || d->x11MapGeneration != generation) + return; + + Q_EMIT guard->x11MapCompleted(); + }, + Qt::QueuedConnection); +} + WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/protocols/wxwaylandsurface.h b/waylib/src/server/protocols/wxwaylandsurface.h index e0cd61215..95a99ffe6 100644 --- a/waylib/src/server/protocols/wxwaylandsurface.h +++ b/waylib/src/server/protocols/wxwaylandsurface.h @@ -12,6 +12,7 @@ WAYLIB_SERVER_BEGIN_NAMESPACE class WXWayland; +class WXWaylandPrivate; class WSeat; class WXWaylandSurfacePrivate; class WAYLIB_SERVER_EXPORT WXWaylandSurface : public WToplevelSurface @@ -115,6 +116,7 @@ class WAYLIB_SERVER_EXPORT WXWaylandSurface : public WToplevelSurface bool isBypassManager() const; bool isAbove() const; bool isBelow() const; + bool isX11Mapped() const; bool isModal() const; WindowTypes windowTypes() const; DecorationsFlags decorationsFlags() const; @@ -136,6 +138,10 @@ public Q_SLOTS: void associated(); // Emitted before WXWaylandSurfacePrivate handles notify_dissociate cleanup. void aboutToDissociate(); + // Emitted asynchronously (queued) after wlroots has handled XCB_MAP_NOTIFY, + // and only while the X11 window is still mapped. Assumes the Wayland event + // loop runs on the single Qt main thread (see WServerPrivate). + void x11MapCompleted(); void parentXWaylandSurfaceChanged(); void childrenChanged(); @@ -150,6 +156,10 @@ public Q_SLOTS: void requestConfigure(QRect geometry, ConfigureFlags flags); void requestActivate(); + +private: + friend class WXWaylandPrivate; + void setX11Mapped(bool mapped); }; WAYLIB_SERVER_END_NAMESPACE