diff --git a/quickshell/Services/CompositorService.qml b/quickshell/Services/CompositorService.qml index 7940b2d83..69f211dcd 100644 --- a/quickshell/Services/CompositorService.qml +++ b/quickshell/Services/CompositorService.qml @@ -135,11 +135,34 @@ Singleton { repeat: false onTriggered: { _sortScheduled = false; - sortedToplevels = computeSortedToplevels(); + const next = computeSortedToplevels(); + // Avoid reassigning (and invalidating bindings) when contents are equivalent. + if (!_toplevelListEquivalent(next, sortedToplevels)) + sortedToplevels = next; + _recomputeFrameBlocked(); toplevelsChanged(); } } + function _toplevelListEquivalent(a, b) { + if (!a || !b || a.length !== b.length) + return false; + for (let i = 0; i < a.length; i++) { + const x = a[i]; + const y = b[i]; + if (x === y) + continue; + if (!x || !y) + return false; + // Only niri's enriched snapshots support value comparison. + if (x.niriWindowId === undefined || y.niriWindowId === undefined) + return false; + if (x.niriWindowId !== y.niriWindowId || x.niriWorkspaceId !== y.niriWorkspaceId || x.appId !== y.appId || x.title !== y.title || !!x.activated !== !!y.activated || !!x.fullscreen !== !!y.fullscreen || !!x.maximized !== !!y.maximized || !!x.minimized !== !!y.minimized) + return false; + } + return true; + } + function scheduleSort() { if (_sortScheduled) return; @@ -164,8 +187,10 @@ Singleton { if (event.name === "workspace" || event.name === "workspacev2" || event.name === "focusedmon" || event.name === "focusedmonv2" || event.name === "activespecial") Hyprland.refreshMonitors(); } catch (e) {} - if (event.name === "activespecial") + if (event.name === "activespecial") { root.updateHyprlandVisibleSpecialWorkspaces(event); + root._recomputeFrameBlocked(); + } root.scheduleSort(); } } @@ -175,6 +200,10 @@ Singleton { function onWindowsChanged() { root.scheduleSort(); } + // Workspace switches affect the fullscreen check's active workspace. + function onAllWorkspacesChanged() { + root._recomputeFrameBlocked(); + } } Component.onCompleted: { @@ -182,6 +211,7 @@ Singleton { detectCompositor(); updateHyprlandVisibleSpecialWorkspaces(null); scheduleSort(); + _recomputeFrameBlocked(); Qt.callLater(() => { NiriService.generateNiriLayoutConfig(); HyprlandService.generateLayoutConfig(); @@ -592,10 +622,74 @@ Singleton { return false; } - function connectedFrameBlockedOnScreen(screenOrName) { - if (hasFullscreenToplevelOnScreen(screenOrName)) + // Per-screen cache for connectedFrameBlockedOnScreen to avoid recomputing on every consumer binding. + property var frameBlockedByScreen: ({}) + + function _computeConnectedFrameBlocked(screenName) { + if (hasFullscreenToplevelOnScreen(screenName)) return true; - return hyprlandSpecialWorkspaceBlocksConnectedFrame(screenOrName); + return hyprlandSpecialWorkspaceBlocksConnectedFrame(screenName); + } + + function _recomputeFrameBlocked() { + const screens = Quickshell.screens || []; + const next = {}; + let changed = false; + for (let i = 0; i < screens.length; i++) { + const name = screens[i]?.name; + if (!name) + continue; + const blocked = _computeConnectedFrameBlocked(name); + next[name] = blocked; + if (frameBlockedByScreen[name] !== blocked) + changed = true; + } + if (!changed) { + for (const name in frameBlockedByScreen) { + if (!(name in next)) { + changed = true; + break; + } + } + } + if (changed) + frameBlockedByScreen = next; + } + + function connectedFrameBlockedOnScreen(screenOrName) { + const screenName = _screenName(screenOrName); + if (!screenName) + return false; + const cached = frameBlockedByScreen[screenName]; + if (cached !== undefined) + return cached; + return _computeConnectedFrameBlocked(screenName); + } + + Connections { + target: ToplevelManager + function onActiveToplevelChanged() { + root._recomputeFrameBlocked(); + } + } + + // Track active toplevel's fullscreen/activated state directly (no per-property signals from ToplevelManager). + Connections { + target: ToplevelManager.activeToplevel + ignoreUnknownSignals: true + function onFullscreenChanged() { + root._recomputeFrameBlocked(); + } + function onActivatedChanged() { + root._recomputeFrameBlocked(); + } + } + + Connections { + target: Quickshell + function onScreensChanged() { + root._recomputeFrameBlocked(); + } } function _screenForName(screenOrName) { diff --git a/quickshell/Services/NiriService.qml b/quickshell/Services/NiriService.qml index 3b97bee8c..7f86e42a7 100644 --- a/quickshell/Services/NiriService.qml +++ b/quickshell/Services/NiriService.qml @@ -435,26 +435,31 @@ Singleton { function handleWindowFocusChanged(data) { const focusedWindowId = data.id; + // Only clone windows whose focus flag changes; skip reassignment if nothing changed. let focusedWindow = null; - const updatedWindows = []; + let changed = false; + const updatedWindows = new Array(windows.length); for (var i = 0; i < windows.length; i++) { const w = windows[i]; - const updatedWindow = {}; - - for (let prop in w) { - updatedWindow[prop] = w[prop]; + const isFocused = w.id === focusedWindowId; + if (!!w.is_focused === isFocused) { + updatedWindows[i] = w; + if (isFocused) + focusedWindow = w; + continue; } - - updatedWindow.is_focused = (w.id === focusedWindowId); - if (updatedWindow.is_focused) { + const updatedWindow = Object.assign({}, w, { + "is_focused": isFocused + }); + if (isFocused) focusedWindow = updatedWindow; - } - - updatedWindows.push(updatedWindow); + updatedWindows[i] = updatedWindow; + changed = true; } - windows = updatedWindows; + if (changed) + windows = updatedWindows; if (focusedWindow) { const ws = root.workspaces[focusedWindow.workspace_id]; @@ -490,26 +495,29 @@ Singleton { setWorkspaces(updatedWorkspaces); } - const updatedWindows = []; + let changed = false; + const updatedWindows = new Array(windows.length); for (var i = 0; i < windows.length; i++) { const w = windows[i]; - const updatedWindow = {}; - - for (let prop in w) { - updatedWindow[prop] = w[prop]; - } - + let isFocused; if (data.active_window_id !== null && data.active_window_id !== undefined) { - updatedWindow.is_focused = (w.id == data.active_window_id); + isFocused = (w.id == data.active_window_id); } else { - updatedWindow.is_focused = w.workspace_id == data.workspace_id ? false : w.is_focused; + isFocused = w.workspace_id == data.workspace_id ? false : !!w.is_focused; } - - updatedWindows.push(updatedWindow); + if (!!w.is_focused === isFocused) { + updatedWindows[i] = w; + continue; + } + updatedWindows[i] = Object.assign({}, w, { + "is_focused": isFocused + }); + changed = true; } - windows = updatedWindows; + if (changed) + windows = updatedWindows; } function handleWindowsChanged(data) { diff --git a/quickshell/Services/PopoutService.qml b/quickshell/Services/PopoutService.qml index cd0d98542..168a19539 100644 --- a/quickshell/Services/PopoutService.qml +++ b/quickshell/Services/PopoutService.qml @@ -58,6 +58,65 @@ Singleton { property string pendingThemeInstall: "" property string pendingPluginInstall: "" + // Deferred unload: keep popouts warm while the session is active and reclaim them on lock/monitors-off. + property var _pendingUnloads: ({}) + + Connections { + target: SessionService + function onSessionLocked() { + root._flushPendingUnloads(); + } + } + + Connections { + target: IdleService + function onMonitorsOffChanged() { + if (IdleService.monitorsOff) + root._flushPendingUnloads(); + } + } + + function _scheduleUnload(key) { + _pendingUnloads[key] = true; + } + + function _flushPendingUnloads() { + const keys = Object.keys(_pendingUnloads); + _pendingUnloads = ({}); + for (let i = 0; i < keys.length; i++) { + const unload = _deferredUnloaders[keys[i]]; + if (unload) + unload(); + } + } + + function _popoutStillPresented(popout) { + return !!popout && (popout.shouldBeVisible === true || popout.isClosing === true); + } + + function _unloadPopoutNow(popoutName, loaderName) { + const loader = root[loaderName]; + if (!loader) + return; + if (_popoutStillPresented(root[popoutName])) + return; + root[popoutName] = null; + loader.active = false; + } + + readonly property var _deferredUnloaders: ({ + "controlCenter": () => _unloadPopoutNow("controlCenterPopout", "controlCenterLoader"), + "notificationCenter": () => _unloadPopoutNow("notificationCenterPopout", "notificationCenterLoader"), + "appDrawer": () => _unloadPopoutNow("appDrawerPopout", "appDrawerLoader"), + "processList": () => _unloadPopoutNow("processListPopout", "processListPopoutLoader"), + "battery": () => _unloadPopoutNow("batteryPopout", "batteryPopoutLoader"), + "vpn": () => _unloadPopoutNow("vpnPopout", "vpnPopoutLoader"), + "systemUpdate": () => _unloadPopoutNow("systemUpdatePopout", "systemUpdateLoader"), + "layout": () => _unloadPopoutNow("layoutPopout", "layoutPopoutLoader"), + "clipboardHistory": () => _unloadPopoutNow("clipboardHistoryPopout", "clipboardHistoryPopoutLoader"), + "settings": () => _unloadSettingsNow() + }) + function setPosition(popout, x, y, width, section, screen) { if (popout && popout.setTriggerPosition && arguments.length >= 6) { popout.setTriggerPosition(x, y, width, section, screen); @@ -76,10 +135,7 @@ Singleton { } function unloadControlCenter() { - if (!controlCenterLoader) - return; - controlCenterPopout = null; - controlCenterLoader.active = false; + _scheduleUnload("controlCenter"); } function toggleControlCenter(x, y, width, section, screen) { @@ -101,10 +157,7 @@ Singleton { } function unloadNotificationCenter() { - if (!notificationCenterLoader) - return; - notificationCenterPopout = null; - notificationCenterLoader.active = false; + _scheduleUnload("notificationCenter"); } function toggleNotificationCenter(x, y, width, section, screen) { @@ -126,10 +179,7 @@ Singleton { } function unloadAppDrawer() { - if (!appDrawerLoader) - return; - appDrawerPopout = null; - appDrawerLoader.active = false; + _scheduleUnload("appDrawer"); } function toggleAppDrawer(x, y, width, section, screen) { @@ -151,10 +201,7 @@ Singleton { } function unloadProcessListPopout() { - if (!processListPopoutLoader) - return; - processListPopout = null; - processListPopoutLoader.active = false; + _scheduleUnload("processList"); } function toggleProcessList(x, y, width, section, screen) { @@ -268,10 +315,7 @@ Singleton { } function unloadBattery() { - if (!batteryPopoutLoader) - return; - batteryPopout = null; - batteryPopoutLoader.active = false; + _scheduleUnload("battery"); } function toggleBattery(x, y, width, section, screen) { @@ -293,10 +337,7 @@ Singleton { } function unloadVpn() { - if (!vpnPopoutLoader) - return; - vpnPopout = null; - vpnPopoutLoader.active = false; + _scheduleUnload("vpn"); } function toggleVpn(x, y, width, section, screen) { @@ -319,10 +360,7 @@ Singleton { } function unloadSystemUpdate() { - if (!systemUpdateLoader) - return; - systemUpdatePopout = null; - systemUpdateLoader.active = false; + _scheduleUnload("systemUpdate"); } function toggleSystemUpdate(x, y, width, section, screen) { @@ -441,10 +479,16 @@ Singleton { } function unloadSettings() { - if (settingsModalLoader) { - settingsModal = null; - settingsModalLoader.active = false; - } + _scheduleUnload("settings"); + } + + function _unloadSettingsNow() { + if (!settingsModalLoader) + return; + if (settingsModal && settingsModal.visible) + return; + settingsModal = null; + settingsModalLoader.active = false; } function _onSettingsModalLoaded() { @@ -484,17 +528,11 @@ Singleton { } function unloadClipboardHistoryPopout() { - if (!clipboardHistoryPopoutLoader) - return; - clipboardHistoryPopout = null; - clipboardHistoryPopoutLoader.active = false; + _scheduleUnload("clipboardHistory"); } function unloadLayoutPopout() { - if (!layoutPopoutLoader) - return; - layoutPopout = null; - layoutPopoutLoader.active = false; + _scheduleUnload("layout"); } property bool _dankLauncherV2WantsOpen: false diff --git a/quickshell/Widgets/DankPopout.qml b/quickshell/Widgets/DankPopout.qml index 92793414e..9d74c8e75 100644 --- a/quickshell/Widgets/DankPopout.qml +++ b/quickshell/Widgets/DankPopout.qml @@ -134,12 +134,7 @@ Item { } } - Connections { - target: CompositorService - function onToplevelsChanged() { - root._maybeResolveBackend(); - } - } + // Backend re-resolution on toplevel activity is covered by CompositorService.frameBlockedByScreen. function _usesConnectedBackendForScreen(targetScreen) { return CompositorService.usesConnectedFrameChromeForScreen(targetScreen); diff --git a/quickshell/Widgets/DankPopoutConnected.qml b/quickshell/Widgets/DankPopoutConnected.qml index 50626e188..0552f2cb8 100644 --- a/quickshell/Widgets/DankPopoutConnected.qml +++ b/quickshell/Widgets/DankPopoutConnected.qml @@ -38,6 +38,7 @@ Item { property bool contentHandlesKeys: false property bool fullHeightSurface: false property bool _primeContent: false + property bool _contentWarm: false property bool _resizeActive: false property real _chromeAnimTravelX: 1 property real _chromeAnimTravelY: 1 @@ -472,6 +473,7 @@ Item { isClosing = false; animationsEnabled = false; _primeContent = true; + _contentWarm = true; _supersededClose = false; const screenChanged = _lastOpenedScreen !== null && _lastOpenedScreen !== screen; @@ -1334,7 +1336,8 @@ Item { Loader { id: contentLoader anchors.fill: parent - active: root._primeContent || shouldBeVisible || contentWindow.visible + // _contentWarm keeps the tree loaded across close for fast re-open; reclaimed by PopoutService on lock/idle. + active: root._primeContent || shouldBeVisible || contentWindow.visible || root._contentWarm asynchronous: false } } diff --git a/quickshell/Widgets/DankPopoutStandalone.qml b/quickshell/Widgets/DankPopoutStandalone.qml index 1f6aec4e5..b3cd4dc9e 100644 --- a/quickshell/Widgets/DankPopoutStandalone.qml +++ b/quickshell/Widgets/DankPopoutStandalone.qml @@ -57,6 +57,7 @@ Item { property bool contentHandlesKeys: false property bool fullHeightSurface: false property bool _primeContent: false + property bool _contentWarm: false property bool _resizeActive: false property real _surfaceMarginLeft: 0 property real _surfaceMarginTop: 0 @@ -305,6 +306,7 @@ Item { isClosing = false; animationsEnabled = false; _primeContent = true; + _contentWarm = true; _frozenMaskX = maskX; _frozenMaskY = maskY; @@ -891,7 +893,8 @@ Item { Loader { id: contentLoader anchors.fill: parent - active: root._primeContent || shouldBeVisible || contentWindow.visible + // _contentWarm keeps the tree loaded across close for fast re-open; reclaimed by PopoutService on lock/idle. + active: root._primeContent || shouldBeVisible || contentWindow.visible || root._contentWarm asynchronous: false } }