From 8af71036cca800064f3fb22dd201109ade6c5ee9 Mon Sep 17 00:00:00 2001 From: bbedward Date: Thu, 23 Jul 2026 14:10:17 -0400 Subject: [PATCH] frame: merge bars with frame window and get things on Top layer by default --- quickshell/Common/SettingsData.qml | 29 + quickshell/DMSShell.qml | 11 + quickshell/Modules/DankBar/DankBar.qml | 21 +- quickshell/Modules/DankBar/DankBarBody.qml | 1211 ++++++++++++++++ quickshell/Modules/DankBar/DankBarContent.qml | 23 +- .../DankBar/DankBarHoverController.qml | 3 +- quickshell/Modules/DankBar/DankBarWindow.qml | 1242 +---------------- quickshell/Modules/DankBar/LeftSection.qml | 7 +- quickshell/Modules/DankBar/RightSection.qml | 7 +- quickshell/Modules/Dock/Dock.qml | 877 +----------- quickshell/Modules/Dock/DockBody.qml | 829 +++++++++++ .../Modules/Dock/DockContextMenuBase.qml | 13 +- quickshell/Modules/Frame/FrameBarHost.qml | 114 ++ quickshell/Modules/Frame/FrameDockHost.qml | 30 + quickshell/Modules/Frame/FrameExclusions.qml | 39 +- quickshell/Modules/Frame/FrameWindow.qml | 96 +- quickshell/Modules/Settings/DankBarTab.qml | 2 + quickshell/Services/BarWidgetService.qml | 70 +- quickshell/Services/CompositorService.qml | 64 +- quickshell/Widgets/DankPopoutStandalone.qml | 13 +- .../translations/settings_search_index.json | 28 + 21 files changed, 2608 insertions(+), 2121 deletions(-) create mode 100644 quickshell/Modules/DankBar/DankBarBody.qml create mode 100644 quickshell/Modules/Dock/DockBody.qml create mode 100644 quickshell/Modules/Frame/FrameBarHost.qml create mode 100644 quickshell/Modules/Frame/FrameDockHost.qml diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 7f2c6181a..9508cbc3e 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -2762,6 +2762,35 @@ Singleton { return edges; } + function getOverlayBarEdgesForScreen(screen) { + if (!screen) + return []; + var edges = []; + for (var i = 0; i < barConfigs.length; i++) { + var bc = barConfigs[i]; + if (!bc.enabled || !(bc.useOverlayLayer ?? false)) + continue; + var prefs = bc.screenPreferences || ["all"]; + if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) + continue; + switch (bc.position ?? 0) { + case SettingsData.Position.Top: + edges.push("top"); + break; + case SettingsData.Position.Bottom: + edges.push("bottom"); + break; + case SettingsData.Position.Left: + edges.push("left"); + break; + case SettingsData.Position.Right: + edges.push("right"); + break; + } + } + return edges; + } + function frameEdgeInsetForSide(screen, side) { if (!frameEnabled || !screen) return 0; diff --git a/quickshell/DMSShell.qml b/quickshell/DMSShell.qml index dbe65ff1a..136c93404 100644 --- a/quickshell/DMSShell.qml +++ b/quickshell/DMSShell.qml @@ -314,6 +314,17 @@ Item { } } + Binding { + target: BarWidgetService + property: "dockContextMenu" + value: dockContextMenuLoader.item + } + Binding { + target: BarWidgetService + property: "dockTrashContextMenu" + value: dockTrashContextMenuLoader.item + } + Connections { target: SettingsData function onDockShowTrashChanged() { diff --git a/quickshell/Modules/DankBar/DankBar.qml b/quickshell/Modules/DankBar/DankBar.qml index cb8692f30..a504df08b 100644 --- a/quickshell/Modules/DankBar/DankBar.qml +++ b/quickshell/Modules/DankBar/DankBar.qml @@ -13,6 +13,9 @@ Item { signal colorPickerRequested signal barReady(var barConfig) + Component.onCompleted: BarWidgetService.registerDankBarItem(root.barConfig?.id, root) + Component.onDestruction: BarWidgetService.unregisterDankBarItem(root.barConfig?.id, root) + property alias barVariants: barVariants property var hyprlandOverviewLoader: null property bool systemTrayMenuOpen: false @@ -161,14 +164,18 @@ Item { id: barVariants model: { const prefs = root.barConfig?.screenPreferences || ["all"]; - if (prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all")) { - return Quickshell.screens; + const wantsAll = prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all"); + let base; + if (wantsAll) { + base = Quickshell.screens; + } else { + base = Quickshell.screens.filter(screen => SettingsData.isScreenInPreferences(screen, prefs)); + if (base.length === 0 && root.barConfig?.showOnLastDisplay && Quickshell.screens.length === 1) + base = Quickshell.screens; } - const filtered = Quickshell.screens.filter(screen => SettingsData.isScreenInPreferences(screen, prefs)); - if (filtered.length === 0 && root.barConfig?.showOnLastDisplay && Quickshell.screens.length === 1) { - return Quickshell.screens; - } - return filtered; + // Connected frame mode renders the bar inside the frame surface; skip the standalone window there + // unless this bar wants the overlay layer, which the frame surface cannot provide. + return base.filter(screen => !CompositorService.frameHostsBarForConfig(screen, root.barConfig)); } delegate: DankBarWindow { diff --git a/quickshell/Modules/DankBar/DankBarBody.qml b/quickshell/Modules/DankBar/DankBarBody.qml new file mode 100644 index 000000000..aa581a3a4 --- /dev/null +++ b/quickshell/Modules/DankBar/DankBarBody.qml @@ -0,0 +1,1211 @@ +import QtQuick +import Quickshell +import qs.Common +import qs.Services + +Item { + id: barWindow + readonly property var log: Log.scoped("DankBarBody") + + required property var hostWindow + required property var rootWindow + required property var barConfig + required property var modelData + readonly property var screen: modelData + property var hyprlandOverviewLoader: rootWindow ? rootWindow.hyprlandOverviewLoader : null + + property var leftWidgetsModel + property var centerWidgetsModel + property var rightWidgetsModel + + readonly property bool barRevealed: inputMask.showing + + property var controlCenterButtonRef: null + property var clockButtonRef: null + property var systemUpdateButtonRef: null + + function triggerSystemUpdate() { + const loader = PopoutService.systemUpdateLoader; + if (!loader) + return; + loader.active = true; + if (!loader.item) + return; + const popout = loader.item; + const barPosition = axis?.edge === "left" ? 2 : (axis?.edge === "right" ? 3 : (axis?.edge === "top" ? 0 : 1)); + if (systemUpdateButtonRef && popout.setTriggerPosition) { + const screenPos = systemUpdateButtonRef.mapToItem(null, 0, 0); + const pos = SettingsData.getPopupTriggerPosition(screenPos, barWindow.screen, barWindow.effectiveBarThickness, systemUpdateButtonRef.width, barConfig?.spacing ?? 4, barPosition, barConfig); + const section = systemUpdateButtonRef.section || "right"; + popout.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen, barPosition, barWindow.effectiveBarThickness, barConfig?.spacing ?? 4, barConfig); + } else { + popout.screen = barWindow.screen; + } + PopoutManager.requestPopout(popout, undefined, "systemUpdate"); + } + + function triggerControlCenter() { + const loader = PopoutService.controlCenterLoader; + if (!loader) + return; + loader.active = true; + if (!loader.item) { + return; + } + + if (controlCenterButtonRef && loader.item.setTriggerPosition) { + const screenPos = controlCenterButtonRef.mapToItem(null, 0, 0); + const barPosition = axis?.edge === "left" ? 2 : (axis?.edge === "right" ? 3 : (axis?.edge === "top" ? 0 : 1)); + const pos = SettingsData.getPopupTriggerPosition(screenPos, barWindow.screen, barWindow.effectiveBarThickness, controlCenterButtonRef.width, barConfig?.spacing ?? 4, barPosition, barConfig); + const section = controlCenterButtonRef.section || "right"; + loader.item.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen, barPosition, barWindow.effectiveBarThickness, barConfig?.spacing ?? 4, barConfig); + } else { + loader.item.triggerScreen = barWindow.screen; + } + + loader.item.toggle(); + if (loader.item.shouldBeVisible && NetworkService.wifiEnabled) { + NetworkService.scanWifi(); + } + } + + function triggerDashTab(tabId) { + const loader = PopoutService.dankDashPopoutLoader; + if (!loader) + return false; + loader.active = true; + if (!loader.item) { + return false; + } + + let section = "center"; + if (clockButtonRef && clockButtonRef.visualContent && loader.item.setTriggerPosition) { + const barPosition = axis?.edge === "left" ? 2 : (axis?.edge === "right" ? 3 : (axis?.edge === "top" ? 0 : 1)); + section = clockButtonRef.section || "center"; + + let triggerPos, triggerWidth; + if (section === "center") { + const centerSection = barWindow.isVertical ? (barWindow.axis?.edge === "left" ? topBarContent.vCenterSection : topBarContent.vCenterSection) : topBarContent.hCenterSection; + if (centerSection) { + if (barWindow.isVertical) { + const centerY = centerSection.height / 2; + triggerPos = centerSection.mapToItem(null, 0, centerY); + triggerWidth = centerSection.height; + } else { + triggerPos = centerSection.mapToItem(null, 0, 0); + triggerWidth = centerSection.width; + } + } else { + triggerPos = clockButtonRef.visualContent.mapToItem(null, 0, 0); + triggerWidth = clockButtonRef.visualWidth; + } + } else { + triggerPos = clockButtonRef.visualContent.mapToItem(null, 0, 0); + triggerWidth = clockButtonRef.visualWidth; + } + + const pos = SettingsData.getPopupTriggerPosition(triggerPos, barWindow.screen, barWindow.effectiveBarThickness, triggerWidth, barConfig?.spacing ?? 4, barPosition, barConfig); + loader.item.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen, barPosition, barWindow.effectiveBarThickness, barConfig?.spacing ?? 4, barConfig); + } else { + loader.item.triggerScreen = barWindow.screen; + } + + if (loader.item.requestTab) + loader.item.requestTab(tabId); + PopoutManager.requestPopout(loader.item, undefined, (barConfig?.id ?? "default") + "-" + section + "-" + tabId); + return true; + } + + function triggerWallpaperBrowser() { + triggerDashTab("wallpaper"); + } + + property var blurRegion: null + property var _blurWidgetItems: [] + + function registerBlurWidget(item) { + if (_blurWidgetItems.indexOf(item) >= 0) + return; + _blurWidgetItems = _blurWidgetItems.concat([item]); + _blurRebuildTimer.restart(); + } + + function unregisterBlurWidget(item) { + const idx = _blurWidgetItems.indexOf(item); + if (idx < 0) + return; + const arr = _blurWidgetItems.slice(); + arr.splice(idx, 1); + _blurWidgetItems = arr; + _blurRebuildTimer.restart(); + } + + Timer { + id: _blurRebuildTimer + interval: 1 + onTriggered: barBlur.rebuild() + } + + Connections { + target: barWindow + function onUsesConnectedFrameChromeChanged() { + _blurRebuildTimer.restart(); + } + function onUsesFrameBarChromeChanged() { + // Rebuild immediately so the bar region never overlaps FrameWindow's during chrome handoff + barBlur.rebuild(); + } + function onBarRevealedChanged() { + barBlur.rebuild(); + } + } + + Component { + id: blurRegionComp + Region {} + } + + Component { + id: blurSubRegionComp + Region { + property Item w + item: w + radius: Theme.cornerRadius + } + } + + Component { + id: blurWingRegionComp + + // r×r square at a bar end minus a quarter-disc — the swoop BarCanvas paints + Region { + id: wingRegion + + property bool atEnd: false + + readonly property real r: barBackground.wing + readonly property real bx: topBarMouseArea.x + barUnitInset.x + topBarSlide.x + readonly property real by: topBarMouseArea.y + barUnitInset.y + topBarSlide.y + readonly property real bw: barUnitInset.width + readonly property real bh: barUnitInset.height + + x: { + switch (barPos) { + case SettingsData.Position.Left: + return bx + bw; + case SettingsData.Position.Right: + return bx - r; + default: + return atEnd ? bx + bw - r : bx; + } + } + y: { + switch (barPos) { + case SettingsData.Position.Top: + return by + bh; + case SettingsData.Position.Bottom: + return by - r; + default: + return atEnd ? by + bh - r : by; + } + } + width: r + height: r + + Region { + intersection: Intersection.Subtract + radius: wingRegion.r + width: wingRegion.r * 2 + height: wingRegion.r * 2 + x: { + switch (barPos) { + case SettingsData.Position.Left: + return wingRegion.bx + wingRegion.bw; + case SettingsData.Position.Right: + return wingRegion.bx - wingRegion.r * 2; + default: + return wingRegion.atEnd ? wingRegion.bx + wingRegion.bw - wingRegion.r * 2 : wingRegion.bx; + } + } + y: { + switch (barPos) { + case SettingsData.Position.Top: + return wingRegion.by + wingRegion.bh; + case SettingsData.Position.Bottom: + return wingRegion.by - wingRegion.r * 2; + default: + return wingRegion.atEnd ? wingRegion.by + wingRegion.bh - wingRegion.r * 2 : wingRegion.by; + } + } + } + } + } + + Item { + id: barBlur + visible: false + + readonly property bool barHasTransparency: barWindow._backgroundAlpha > 0 && barWindow._backgroundAlpha < 1 + + function rebuild() { + teardown(); + if (!BlurService.enabled || !BlurService.available) + return; + if (!barWindow.barRevealed && CompositorService.isHyprland) + return; + // FrameWindow owns the blur region for the whole edge while the bar wears frame chrome + if (FrameTransitionState.effectiveFrameEnabled && barWindow.usesFrameBarChrome) + return; + + const widgets = barWindow._blurWidgetItems.filter(w => w && w.visible && w.width > 0 && w.height > 0); + const hasBar = barHasTransparency; + if (!hasBar && widgets.length === 0) + return; + + const region = blurRegionComp.createObject(barWindow); + if (!region) { + log.warn("BarBlur: Failed to create blur region"); + return; + } + + if (hasBar) { + region.x = Qt.binding(() => topBarMouseArea.x + barUnitInset.x + topBarSlide.x); + region.y = Qt.binding(() => topBarMouseArea.y + barUnitInset.y + topBarSlide.y); + region.width = Qt.binding(() => barUnitInset.width); + region.height = Qt.binding(() => barUnitInset.height); + region.radius = Qt.binding(() => barBackground.rt); + } + + const subRegions = []; + for (let i = 0; i < widgets.length; i++) { + const sub = blurSubRegionComp.createObject(region, { + w: widgets[i] + }); + if (sub) + subRegions.push(sub); + } + + if (hasBar && barBackground.gothEnabled && barWindow._wingR > 0) { + for (const atEnd of [false, true]) { + const wing = blurWingRegionComp.createObject(region, { + atEnd: atEnd + }); + if (wing) + subRegions.push(wing); + } + } + + region.regions = subRegions; + + barWindow.blurRegion = region; + } + + function teardown() { + const old = barWindow.blurRegion; + if (!old) + return; + barWindow.blurRegion = null; + old.destroy(); + } + + onBarHasTransparencyChanged: _blurRebuildTimer.restart() + + Connections { + target: BlurService + function onEnabledChanged() { + barBlur.rebuild(); + } + } + + Connections { + target: FrameTransitionState + function onEffectiveFrameEnabledChanged() { + barBlur.rebuild(); + } + } + + Connections { + target: topBarSlide + function onXChanged() { + if (barWindow.blurRegion) + barWindow.blurRegion.changed(); + } + function onYChanged() { + if (barWindow.blurRegion) + barWindow.blurRegion.changed(); + } + } + + Connections { + target: barBackground + function onGothEnabledChanged() { + _blurRebuildTimer.restart(); + } + function onWingChanged() { + if (barWindow.blurRegion) + barWindow.blurRegion.changed(); + } + } + + Component.onCompleted: rebuild() + Component.onDestruction: teardown() + } + + signal colorPickerRequested + + onColorPickerRequested: rootWindow.colorPickerRequested() + + property alias axis: axis + + AxisContext { + id: axis + edge: { + switch (barConfig?.position ?? 0) { + case SettingsData.Position.Top: + return "top"; + case SettingsData.Position.Bottom: + return "bottom"; + case SettingsData.Position.Left: + return "left"; + case SettingsData.Position.Right: + return "right"; + default: + return "top"; + } + } + } + + readonly property bool isVertical: axis.isVertical + + readonly property color _surfaceContainer: Theme.surfaceContainer + readonly property string _barId: barConfig?.id ?? "default" + property real _backgroundAlpha: barConfig?.transparency ?? 1.0 + readonly property color _bgColor: (FrameTransitionState.effectiveFrameEnabled && usesFrameBarChrome) ? Theme.withAlpha(SettingsData.effectiveFrameColor, SettingsData.frameOpacity) : Theme.withAlpha(_surfaceContainer, _backgroundAlpha) + + function _updateBackgroundAlpha() { + const live = SettingsData.barConfigs.find(c => c.id === _barId); + _backgroundAlpha = (live ?? barConfig)?.transparency ?? 1.0; + } + readonly property real _dpr: CompositorService.getScreenScale(barWindow.screen) + + property string screenName: modelData.name + + readonly property bool usesConnectedFrameChrome: CompositorService.usesConnectedFrameChromeForScreen(screenName) + readonly property bool usesFrameBarChrome: CompositorService.frameWindowVisibleForScreen(screenName) + readonly property var renderBarConfig: SettingsData.effectiveBarConfigForRender(barConfig, usesFrameBarChrome) + + property bool gothCornersEnabled: renderBarConfig?.gothCornersEnabled ?? false + property real wingtipsRadius: renderBarConfig?.gothCornerRadiusOverride ? (renderBarConfig?.gothCornerRadiusValue ?? 12) : Theme.cornerRadius + readonly property real _wingR: Math.max(0, wingtipsRadius) + + // Shadow buffer: extra window space for shadow to render beyond bar bounds + readonly property bool _shadowActive: (Theme.elevationEnabled && (typeof SettingsData !== "undefined" ? (SettingsData.barElevationEnabled ?? true) : false)) || (renderBarConfig?.shadowIntensity ?? 0) > 0 + readonly property real _shadowBuffer: { + if (!_shadowActive) + return 0; + const hasOverride = (renderBarConfig?.shadowIntensity ?? 0) > 0; + if (hasOverride) { + const blur = (renderBarConfig.shadowIntensity ?? 0) * 0.2; + const offset = blur * 0.5; + return Theme.snap(Math.max(16, blur + offset + 8), _dpr); + } + return Theme.snap(Theme.elevationRenderPadding(Theme.elevationLevel2, "top", 4, 8, 16), _dpr); + } + + // Flatten/spacing collapse for maximized windows only applies to frame-integrated layout + readonly property bool flattenForMaximizedWindow: !FrameTransitionState.effectiveFrameEnabled || usesFrameBarChrome + + property bool hasMaximizedToplevel: false + property bool shouldHideForWindows: false + + function _updateHasMaximizedToplevel() { + if (!(barConfig?.maximizeDetection ?? true)) { + hasMaximizedToplevel = false; + return; + } + if (CompositorService.isMango) { + const out = MangoService.outputs[screenName]; + const active = new Set((out?.activeTags) || []); + const wins = MangoService.windows || []; + for (let i = 0; i < wins.length; i++) { + const w = wins[i]; + if (!w || w.monitor !== screenName || w.is_minimized) + continue; + if (active.size > 0 && !(w.tags || []).some(t => active.has(t))) + continue; + if (w.is_maximized || w.is_fullscreen) { + hasMaximizedToplevel = true; + return; + } + } + hasMaximizedToplevel = false; + return; + } + if (!CompositorService.isHyprland && !CompositorService.isNiri) { + hasMaximizedToplevel = false; + return; + } + + const filtered = CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName); + for (let i = 0; i < filtered.length; i++) { + if (filtered[i]?.maximized) { + hasMaximizedToplevel = true; + return; + } + } + hasMaximizedToplevel = false; + } + + function _updateShouldHideForWindows() { + if (!(barConfig?.showOnWindowsOpen ?? false)) { + shouldHideForWindows = false; + return; + } + if (!(barConfig?.autoHide ?? false)) { + shouldHideForWindows = false; + return; + } + if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango) { + shouldHideForWindows = false; + return; + } + + if (CompositorService.isNiri) { + let currentWorkspaceId = null; + for (let i = 0; i < NiriService.allWorkspaces.length; i++) { + const ws = NiriService.allWorkspaces[i]; + if (ws.output === screenName && ws.is_active) { + currentWorkspaceId = ws.id; + break; + } + } + + if (currentWorkspaceId === null) { + shouldHideForWindows = false; + return; + } + + let hasTiled = false; + let hasFloatingTouchingBar = false; + const pos = barConfig?.position ?? 0; + const barThickness = barWindow.effectiveBarThickness + (barConfig?.spacing ?? 4); + + for (let i = 0; i < NiriService.windows.length; i++) { + const win = NiriService.windows[i]; + if (win.workspace_id !== currentWorkspaceId) + continue; + + if (!win.is_floating) { + hasTiled = true; + continue; + } + + const tilePos = win.layout?.tile_pos_in_workspace_view; + const winSize = win.layout?.window_size || win.layout?.tile_size; + if (!tilePos || !winSize) + continue; + + switch (pos) { + case SettingsData.Position.Top: + if (tilePos[1] < barThickness) + hasFloatingTouchingBar = true; + break; + case SettingsData.Position.Bottom: + const screenHeight = barWindow.screen?.height ?? 0; + if (tilePos[1] + winSize[1] > screenHeight - barThickness) + hasFloatingTouchingBar = true; + break; + case SettingsData.Position.Left: + if (tilePos[0] < barThickness) + hasFloatingTouchingBar = true; + break; + case SettingsData.Position.Right: + const screenWidth = barWindow.screen?.width ?? 0; + if (tilePos[0] + winSize[0] > screenWidth - barThickness) + hasFloatingTouchingBar = true; + break; + } + } + + shouldHideForWindows = hasTiled || hasFloatingTouchingBar; + return; + } + + const filtered = CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName); + shouldHideForWindows = filtered.length > 0; + } + + property real effectiveSpacing: (FrameTransitionState.effectiveFrameEnabled && usesFrameBarChrome) ? 0 : ((flattenForMaximizedWindow && hasMaximizedToplevel) ? 0 : (barConfig?.spacing ?? 4)) + + Behavior on effectiveSpacing { + enabled: barWindow.hostWindow?.visible ?? false + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } + + readonly property int notificationCount: NotificationService.notifications.length + readonly property real effectiveBarThickness: (FrameTransitionState.effectiveFrameEnabled && usesFrameBarChrome) ? SettingsData.frameBarSize : Theme.snap(Math.max(barWindow.widgetThickness + (barConfig?.innerPadding ?? 4) + 4, Theme.barHeight - 4 - (8 - (barConfig?.innerPadding ?? 4))), _dpr) + readonly property bool effectiveOpenOnOverview: FrameTransitionState.effectiveFrameEnabled ? SettingsData.frameShowOnOverview : (barConfig?.openOnOverview ?? false) + readonly property real widgetThickness: Theme.snap(Math.max(20, 26 + (barConfig?.innerPadding ?? 4) * 0.6), _dpr) + + readonly property bool hasAdjacentTopBar: { + if (barConfig?.autoHide ?? false) + return false; + if (!isVertical) + return false; + return SettingsData.barConfigs.some(bc => { + if (!bc.enabled || bc.id === barConfig?.id) + return false; + if (bc.autoHide) + return false; + if (!(bc.visible ?? true)) + return false; + if (bc.position !== SettingsData.Position.Top && bc.position !== 0) + return false; + const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); + if (!onThisScreen) + return false; + if (bc.showOnLastDisplay && screenName !== barWindow.screenName) + return false; + return true; + }); + } + + readonly property bool hasAdjacentBottomBar: { + if (barConfig?.autoHide ?? false) + return false; + if (!isVertical) + return false; + const result = SettingsData.barConfigs.some(bc => { + if (!bc.enabled || bc.id === barConfig?.id) + return false; + if (bc.autoHide) + return false; + if (!(bc.visible ?? true)) + return false; + if (bc.position !== SettingsData.Position.Bottom && bc.position !== 1) + return false; + const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); + if (!onThisScreen) + return false; + if (bc.showOnLastDisplay && screenName !== barWindow.screenName) + return false; + return true; + }); + return result; + } + + readonly property bool hasAdjacentLeftBar: { + if (barConfig?.autoHide ?? false) + return false; + if (isVertical) + return false; + const result = SettingsData.barConfigs.some(bc => { + if (!bc.enabled || bc.id === barConfig?.id) + return false; + if (bc.autoHide) + return false; + if (!(bc.visible ?? true)) + return false; + if (bc.position !== SettingsData.Position.Left && bc.position !== 2) + return false; + const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); + if (!onThisScreen) + return false; + if (bc.showOnLastDisplay && screenName !== barWindow.screenName) + return false; + return true; + }); + return result; + } + + readonly property bool hasAdjacentRightBar: { + if (barConfig?.autoHide ?? false) + return false; + if (isVertical) + return false; + const result = SettingsData.barConfigs.some(bc => { + if (!bc.enabled || bc.id === barConfig?.id) + return false; + if (bc.autoHide) + return false; + if (!(bc.visible ?? true)) + return false; + if (bc.position !== SettingsData.Position.Right && bc.position !== 3) + return false; + const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); + if (!onThisScreen) + return false; + if (bc.showOnLastDisplay && screenName !== barWindow.screenName) + return false; + return true; + }); + return result; + } + + readonly property real surfaceImplicitHeight: !isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((renderBarConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0 + readonly property real surfaceImplicitWidth: isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((renderBarConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0 + + Component.onCompleted: { + updateGpuTempConfig(); + _updateBackgroundAlpha(); + _updateHasMaximizedToplevel(); + _updateShouldHideForWindows(); + } + + Connections { + target: PluginService + function onPluginLoaded(pluginId) { + log.info("DankBar: Plugin loaded:", pluginId); + SettingsData.widgetDataChanged(); + } + function onPluginUnloaded(pluginId) { + log.info("DankBar: Plugin unloaded:", pluginId); + SettingsData.widgetDataChanged(); + } + } + + function updateGpuTempConfig() { + const leftWidgets = barConfig?.leftWidgets || []; + const centerWidgets = barConfig?.centerWidgets || []; + const rightWidgets = barConfig?.rightWidgets || []; + const allWidgets = [...leftWidgets, ...centerWidgets, ...rightWidgets]; + + const hasGpuTempWidget = allWidgets.some(widget => { + const widgetId = typeof widget === "string" ? widget : widget.id; + const widgetEnabled = typeof widget === "string" ? true : (widget.enabled !== false); + return widgetId === "gpuTemp" && widgetEnabled; + }); + + DgopService.gpuTempEnabled = hasGpuTempWidget || SessionData.nvidiaGpuTempEnabled || SessionData.nonNvidiaGpuTempEnabled; + DgopService.nvidiaGpuTempEnabled = hasGpuTempWidget || SessionData.nvidiaGpuTempEnabled; + DgopService.nonNvidiaGpuTempEnabled = hasGpuTempWidget || SessionData.nonNvidiaGpuTempEnabled; + } + + Connections { + function onBarConfigChanged() { + barWindow.updateGpuTempConfig(); + barWindow._updateBackgroundAlpha(); + barWindow._updateHasMaximizedToplevel(); + barWindow._updateShouldHideForWindows(); + } + + target: rootWindow + } + + Connections { + target: SettingsData + function onBarConfigsChanged() { + barWindow._updateBackgroundAlpha(); + } + } + + Connections { + target: CompositorService + function onToplevelsChanged() { + barWindow._updateHasMaximizedToplevel(); + barWindow._updateShouldHideForWindows(); + } + } + + Connections { + target: NiriService + function onAllWorkspacesChanged() { + barWindow._updateHasMaximizedToplevel(); + barWindow._updateShouldHideForWindows(); + } + } + + Connections { + function onNvidiaGpuTempEnabledChanged() { + barWindow.updateGpuTempConfig(); + } + + function onNonNvidiaGpuTempEnabledChanged() { + barWindow.updateGpuTempConfig(); + } + + target: SessionData + } + + readonly property int barPos: barConfig?.position ?? 0 + + readonly property bool reserveExclusiveWhenAutoHidden: FrameTransitionState.effectiveFrameEnabled && usesFrameBarChrome && !!barWindow.screen && SettingsData.isScreenInPreferences(barWindow.screen, SettingsData.frameScreenPreferences) + + readonly property real surfaceExclusiveZone: (!(barConfig?.visible ?? true) || (topBarCore.autoHide && !barWindow.reserveExclusiveWhenAutoHidden)) ? -1 : (barWindow.effectiveBarThickness + effectiveSpacing + (usesFrameBarChrome ? 0 : (barConfig?.bottomGap ?? 0))) + + readonly property alias inputMaskItem: inputMask + + Item { + id: inputMask + + readonly property int barThickness: Theme.px(barWindow.effectiveBarThickness + barWindow.effectiveSpacing, barWindow._dpr) + + readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview + readonly property bool effectiveVisible: (barConfig?.visible ?? true) || inOverviewWithShow + readonly property bool showing: effectiveVisible && (topBarCore.reveal || inOverviewWithShow || !topBarCore.autoHide) + + readonly property int maskThickness: showing ? barThickness : 1 + + x: { + if (!axis.isVertical) { + return 0; + } else { + switch (barPos) { + case SettingsData.Position.Left: + return 0; + case SettingsData.Position.Right: + return parent.width - maskThickness; + default: + return 0; + } + } + } + y: { + if (axis.isVertical) { + return 0; + } else { + switch (barPos) { + case SettingsData.Position.Top: + return 0; + case SettingsData.Position.Bottom: + return parent.height - maskThickness; + default: + return 0; + } + } + } + width: axis.isVertical ? maskThickness : parent.width + height: axis.isVertical ? parent.height : maskThickness + } + + readonly property bool clickThroughEnabled: barConfig?.clickThrough ?? false + + readonly property var _leftSection: topBarContent ? (barWindow.isVertical ? topBarContent.vLeftSection : topBarContent.hLeftSection) : null + readonly property var _centerSection: topBarContent ? (barWindow.isVertical ? topBarContent.vCenterSection : topBarContent.hCenterSection) : null + readonly property var _rightSection: topBarContent ? (barWindow.isVertical ? topBarContent.vRightSection : topBarContent.hRightSection) : null + readonly property real _revealProgress: topBarSlide.x + topBarSlide.y + + function containsGlobalPoint(gx, gy, padding) { + const pad = padding !== undefined ? padding : 16; + if (!inputMask.showing) + return false; + const topLeft = inputMask.mapToItem(null, 0, 0); + return gx >= topLeft.x - pad && gx < topLeft.x + inputMask.width + pad && gy >= topLeft.y - pad && gy < topLeft.y + inputMask.height + pad; + } + + function sectionRect(section, isCenter, _dep) { + if (!section) + return { + "x": 0, + "y": 0, + "w": 0, + "h": 0 + }; + + const pos = section.mapToItem(barWindow.hostWindow.contentItem, 0, 0); + const implW = section.implicitWidth || 0; + const implH = section.implicitHeight || 0; + + const offsetX = isCenter && !barWindow.isVertical ? (section.width - implW) / 2 : 0; + const offsetY = !barWindow.isVertical ? (section.height - implH) / 2 : (isCenter ? (section.height - implH) / 2 : 0); + + const edgePad = 2; + return { + "x": pos.x + offsetX - edgePad, + "y": pos.y + offsetY - edgePad, + "w": implW + edgePad * 2, + "h": implH + edgePad * 2 + }; + } + + Item { + id: topBarCore + anchors.fill: parent + layer.enabled: false + + property bool autoHide: barConfig?.autoHide ?? false + property bool revealSticky: false + // In click-through mode the hidden bar's input mask covers the full + // band while the revealed bar's mask covers only the widget sections, + // so the pointer position is unknowable while it is over a gap. An + // enter on the hidden bar therefore only reveals once the pointer + // reaches a thin strip at the screen edge; anything else (including + // the spurious enter generated by the mask expanding underneath a + // resting pointer) keeps the bar hidden. + property bool gapEnterSuppressed: false + readonly property bool hoverReveal: topBarMouseArea.containsMouse && !gapEnterSuppressed + readonly property bool ipcReveal: !!SettingsData.barIpcRevealStates[barConfig?.id ?? ""] + + onRevealChanged: { + if (reveal && barWindow.clickThroughEnabled) + revealSettle.restart(); + } + + // The input mask updates lag reveal transitions, generating spurious + // enter/leave pairs. Hides are deferred until the transition settles; + // the timer re-evaluates against the post-transition mask. + Timer { + id: revealSettle + interval: 600 + repeat: false + onTriggered: topBarCore.evaluateReveal() + } + + function inEdgeStrip(x, y) { + const band = barWindow.isVertical ? topBarMouseArea.width : topBarMouseArea.height; + const strip = Math.max(8, band * 0.15); + switch (barPos) { + case SettingsData.Position.Bottom: + return y >= band - strip; + case SettingsData.Position.Left: + return x <= strip; + case SettingsData.Position.Right: + return x >= band - strip; + default: + return y <= strip; + } + } + + Timer { + id: revealHold + interval: barConfig?.autoHideDelay ?? 250 + repeat: false + onTriggered: { + if (!topBarCore.hoverReveal && !topBarCore.popoutPinsReveal) + topBarCore.revealSticky = false; + } + } + + property bool hasActivePopout: false + + readonly property bool popoutPinsReveal: !!(hasActivePopout && !(barConfig?.autoHideStrict ?? false)) + + onHasActivePopoutChanged: evaluateReveal() + + onPopoutPinsRevealChanged: evaluateReveal() + + function updateActivePopoutState() { + if (!barWindow.screen) + return; + const screenName = barWindow.screen.name; + const activePopout = PopoutManager.currentPopoutsByScreen[screenName]; + const activeTrayMenu = TrayMenuManager.activeTrayMenus[screenName]; + const trayOpen = rootWindow.systemTrayMenuOpen; + + const hasVisiblePopout = activePopout && activePopout.shouldBeVisible; + topBarCore.hasActivePopout = !!(hasVisiblePopout || activeTrayMenu || trayOpen); + } + + Connections { + target: PopoutManager + + function onPopoutChanged() { + topBarCore.updateActivePopoutState(); + } + + function onPopoutOpening() { + topBarCore.evaluateReveal(); + } + } + + Connections { + target: TrayMenuManager + + function onActiveTrayMenusChanged() { + topBarCore.updateActivePopoutState(); + } + } + + property bool reveal: { + const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview; + if (inOverviewWithShow) + return true; + + const showOnWindowsSetting = barConfig?.showOnWindowsOpen ?? false; + if (showOnWindowsSetting && autoHide && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango)) { + if (barWindow.shouldHideForWindows) + return hoverReveal || popoutPinsReveal || revealSticky || ipcReveal; + return true; + } + + if (CompositorService.isNiri && NiriService.inOverview) + return hoverReveal || popoutPinsReveal || revealSticky || ipcReveal; + + return (barConfig?.visible ?? true) && (!autoHide || hoverReveal || popoutPinsReveal || revealSticky || ipcReveal); + } + + Connections { + function onBarConfigChanged() { + topBarCore.autoHide = barConfig?.autoHide ?? false; + topBarCore.evaluateReveal(); + } + + target: rootWindow + } + + Component.onCompleted: topBarCore.updateActivePopoutState() + + function evaluateReveal() { + if (!autoHide) + return; + + if (hoverReveal) { + SettingsData.setBarIpcReveal(barConfig?.id ?? "", false); + revealSticky = true; + revealHold.stop(); + return; + } + + if (popoutPinsReveal) { + revealSticky = true; + revealHold.stop(); + return; + } + + if (revealSettle.running) + return; + + revealHold.interval = barConfig?.autoHideDelay ?? 250; + revealHold.restart(); + } + + Connections { + target: topBarMouseArea + function onContainsMouseChanged() { + if (!topBarMouseArea.containsMouse) { + topBarCore.gapEnterSuppressed = false; + } else if (barWindow.clickThroughEnabled && !topBarCore.reveal) { + topBarCore.gapEnterSuppressed = true; + } + topBarCore.evaluateReveal(); + } + } + + MouseArea { + id: topBarMouseArea + y: !barWindow.isVertical ? (barPos === SettingsData.Position.Bottom ? parent.height - height : 0) : 0 + x: barWindow.isVertical ? (barPos === SettingsData.Position.Right ? parent.width - width : 0) : 0 + height: !barWindow.isVertical ? Theme.px(barWindow.effectiveBarThickness + barWindow.effectiveSpacing, barWindow._dpr) : undefined + width: barWindow.isVertical ? Theme.px(barWindow.effectiveBarThickness + barWindow.effectiveSpacing, barWindow._dpr) : undefined + anchors { + left: !barWindow.isVertical ? parent.left : (barPos === SettingsData.Position.Left ? parent.left : undefined) + right: !barWindow.isVertical ? parent.right : (barPos === SettingsData.Position.Right ? parent.right : undefined) + top: barWindow.isVertical ? parent.top : undefined + bottom: barWindow.isVertical ? parent.bottom : undefined + } + readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview + hoverEnabled: (barConfig?.autoHide ?? false) && !inOverview && !topBarCore.popoutPinsReveal + acceptedButtons: Qt.NoButton + enabled: (barConfig?.autoHide ?? false) && !inOverview + onPositionChanged: mouse => { + if (!topBarCore.gapEnterSuppressed) + return; + if (!topBarCore.inEdgeStrip(mouse.x, mouse.y)) + return; + topBarCore.gapEnterSuppressed = false; + topBarCore.evaluateReveal(); + } + + Item { + id: topBarContainer + anchors.fill: parent + + transform: Translate { + id: topBarSlide + x: barWindow.isVertical ? Theme.snap(topBarCore.reveal ? 0 : (barPos === SettingsData.Position.Right ? barWindow.surfaceImplicitWidth : -barWindow.surfaceImplicitWidth), barWindow._dpr) : 0 + y: !barWindow.isVertical ? Theme.snap(topBarCore.reveal ? 0 : (barPos === SettingsData.Position.Bottom ? barWindow.surfaceImplicitHeight : -barWindow.surfaceImplicitHeight), barWindow._dpr) : 0 + + Behavior on x { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } + + Behavior on y { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } + } + + Item { + id: barUnitInset + property int spacingPx: Theme.px(barWindow.effectiveSpacing, barWindow._dpr) + anchors.fill: parent + anchors.leftMargin: !barWindow.isVertical ? spacingPx : (axis.edge === "left" ? spacingPx : 0) + anchors.rightMargin: !barWindow.isVertical ? spacingPx : (axis.edge === "right" ? spacingPx : 0) + anchors.topMargin: barWindow.isVertical ? (barWindow.hasAdjacentTopBar ? 0 : spacingPx) : (axis.outerVisualEdge() === "bottom" ? 0 : spacingPx) + anchors.bottomMargin: barWindow.isVertical ? (barWindow.hasAdjacentBottomBar ? 0 : spacingPx) : (axis.outerVisualEdge() === "bottom" ? spacingPx : 0) + + BarCanvas { + id: barBackground + barWindow: barWindow + axis: axis + barConfig: barWindow.renderBarConfig + } + + MouseArea { + anchors.fill: parent + z: -2 + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onClicked: { + const screenName = barWindow.screen?.name; + if (!screenName) + return; + if (PopoutManager.currentPopoutsByScreen[screenName]) + PopoutManager.closeAllPopouts(); + if (ModalManager.currentModalsByScreen[screenName]) + ModalManager.closeAllModalsExcept(null); + TrayMenuManager.closeAllMenus(); + } + } + + MouseArea { + id: scrollArea + anchors.fill: parent + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + z: -1 + + property real touchpadAccumulatorY: 0 + property real touchpadAccumulatorX: 0 + property real mouseAccumulatorY: 0 + property real mouseAccumulatorX: 0 + property bool actionInProgress: false + + Timer { + id: cooldownTimer + interval: 100 + onTriggered: parent.actionInProgress = false + } + + function handleScrollAction(behavior, direction) { + switch (behavior) { + case "workspace": + topBarContent.switchWorkspace(direction); + return true; + case "column": + if (!CompositorService.isNiri) + return false; + if (direction > 0) + NiriService.moveColumnRight(barWindow.screenName); + else + NiriService.moveColumnLeft(barWindow.screenName); + return true; + default: + return false; + } + } + + function processWheel(wheel) { + if (!(barConfig?.scrollEnabled ?? true) || actionInProgress) { + wheel.accepted = false; + return; + } + + const deltaY = wheel.angleDelta.y; + const deltaX = wheel.angleDelta.x; + const isTouchpadY = wheel.pixelDelta && wheel.pixelDelta.y !== 0; + const isTouchpadX = wheel.pixelDelta && wheel.pixelDelta.x !== 0; + const xBehavior = barConfig?.scrollXBehavior ?? "column"; + const yBehavior = barConfig?.scrollYBehavior ?? "workspace"; + const reverse = SettingsData.reverseScrolling ? -1 : 1; + + if (CompositorService.isNiri && xBehavior !== "none" && Math.abs(deltaX) > Math.abs(deltaY)) { + if (isTouchpadX) { + touchpadAccumulatorX += deltaX; + if (Math.abs(touchpadAccumulatorX) >= 500) { + const direction = touchpadAccumulatorX * reverse < 0 ? 1 : -1; + if (handleScrollAction(xBehavior, direction)) { + actionInProgress = true; + cooldownTimer.restart(); + } + touchpadAccumulatorX = 0; + } + } else { + mouseAccumulatorX += deltaX; + if (Math.abs(mouseAccumulatorX) >= 120) { + const direction = mouseAccumulatorX * reverse < 0 ? 1 : -1; + if (handleScrollAction(xBehavior, direction)) { + actionInProgress = true; + cooldownTimer.restart(); + } + mouseAccumulatorX = 0; + } + } + wheel.accepted = false; + return; + } + + if (yBehavior === "none") { + wheel.accepted = false; + return; + } + + if (isTouchpadY) { + touchpadAccumulatorY += deltaY; + if (Math.abs(touchpadAccumulatorY) >= 500) { + const direction = touchpadAccumulatorY * reverse < 0 ? 1 : -1; + if (handleScrollAction(yBehavior, direction)) { + actionInProgress = true; + cooldownTimer.restart(); + } + touchpadAccumulatorY = 0; + } + } else { + mouseAccumulatorY += deltaY; + if (Math.abs(mouseAccumulatorY) >= 120) { + const direction = mouseAccumulatorY * reverse < 0 ? 1 : -1; + if (handleScrollAction(yBehavior, direction)) { + actionInProgress = true; + cooldownTimer.restart(); + } + mouseAccumulatorY = 0; + } + } + + wheel.accepted = false; + } + + onWheel: wheel => processWheel(wheel) + } + + DankBarContent { + id: topBarContent + barWindow: barWindow + rootWindow: barWindow.rootWindow + barConfig: barWindow.barConfig + leftWidgetsModel: barWindow.leftWidgetsModel + centerWidgetsModel: barWindow.centerWidgetsModel + rightWidgetsModel: barWindow.rightWidgetsModel + } + + // Passive: tracks cursor without intercepting clicks or scroll + HoverHandler { + id: hoverPopoutHandler + enabled: (barConfig?.hoverPopouts ?? false) && !barWindow.clickThroughEnabled + + property real lastGlobalX: 0 + property real lastGlobalY: 0 + + onPointChanged: { + const gp = barUnitInset.mapToItem(null, point.position.x, point.position.y); + lastGlobalX = gp.x; + lastGlobalY = gp.y; + topBarContent.queueHoverPopout(gp.x, gp.y); + } + + onHoveredChanged: { + topBarContent.updateHoverBarHovered(hovered); + } + } + } + } + } + } +} diff --git a/quickshell/Modules/DankBar/DankBarContent.qml b/quickshell/Modules/DankBar/DankBarContent.qml index 7d26b61d7..c96423590 100644 --- a/quickshell/Modules/DankBar/DankBarContent.qml +++ b/quickshell/Modules/DankBar/DankBarContent.qml @@ -38,37 +38,44 @@ Item { readonly property real _barInsetPaddingRaw: SettingsData.barInsetPaddingSyncAll ? SettingsData.barInsetPaddingShared : (barConfig?.barInsetPadding ?? -1) readonly property real _barInsetPaddingAuto: _barIsVertical ? Theme.spacingXS : _edgeBaseMargin readonly property real _barInsetPadding: _barInsetPaddingRaw < 0 ? _barInsetPaddingAuto : _barInsetPaddingRaw - // Connected-frame Bar Inset Padding: absolute free-end inset (auto < 0 = frameThickness, 0 = edge-to-edge). - // FrameExclusions already moves a free bar end inward by frameThickness so use frame inset to mangage the gap + // Connected-frame Bar Inset Padding: absolute free-end gap the hosted bar spans full-width into + // (auto < 0 = frameThickness so widgets align with the interior cutout, 0 = edge-to-edge). The extra + // beyond frameThickness is what an adjacent bar end adds on top of its corner alignment. readonly property real _frameInsetResolved: SettingsData.frameBarInsetPadding < 0 ? SettingsData.frameThickness : SettingsData.frameBarInsetPadding readonly property real _frameInsetExtra: Math.max(0, _frameInsetResolved - SettingsData.frameThickness) + // Horizontal bars span the full width and own the corners; the perpendicular vertical bar + // tucks in below/above. Where they meet, inset the corner widget so it centres in the + // frameBarSize corner cell, aligning it with the vertical bar's widget column. + readonly property real _widgetThicknessValue: _hasBarWindow ? barWindow.widgetThickness : 30 + readonly property real _cornerAlignInset: Math.max(_frameInsetExtra, (SettingsData.frameBarSize - _widgetThicknessValue) / 2) + readonly property real _leftMargin: { if (_barIsVertical) return _edgeBaseMargin; if (_usesFrameBarChrome) - return hasAdjacentLeftBarLive ? (_edgeBaseMargin + SettingsData.frameBarSize + _frameInsetExtra) : _frameInsetExtra; + return hasAdjacentLeftBarLive ? _cornerAlignInset : _frameInsetResolved; return Math.max(0, _barInsetPadding); } readonly property real _rightMargin: { if (_barIsVertical) return _edgeBaseMargin; if (_usesFrameBarChrome) - return hasAdjacentRightBarLive ? (_edgeBaseMargin + SettingsData.frameBarSize + _frameInsetExtra) : _frameInsetExtra; + return hasAdjacentRightBarLive ? _cornerAlignInset : _frameInsetResolved; return Math.max(0, _barInsetPadding); } readonly property real _topMargin: { if (!_barIsVertical) return 0; if (_usesFrameBarChrome) - return hasAdjacentTopBarLive ? (outlineThickness + SettingsData.frameThickness + _frameInsetExtra) : _frameInsetExtra; + return hasAdjacentTopBarLive ? (_edgeBaseMargin + SettingsData.frameBarSize + _frameInsetExtra) : _frameInsetResolved; return Math.max(0, _barInsetPadding); } readonly property real _bottomMargin: { if (!_barIsVertical) return 0; if (_usesFrameBarChrome) - return hasAdjacentBottomBarLive ? (outlineThickness + SettingsData.frameThickness + _frameInsetExtra) : _frameInsetExtra; + return hasAdjacentBottomBarLive ? (_edgeBaseMargin + SettingsData.frameBarSize + _frameInsetExtra) : _frameInsetResolved; return Math.max(0, _barInsetPadding); } @@ -677,6 +684,7 @@ Item { LeftSection { id: hLeftSection objectName: "leftSection" + edgeIsScreenEdge: !topBarContent.hasAdjacentLeftBarLive overrideAxisLayout: true forceVerticalLayout: false anchors { @@ -710,6 +718,7 @@ Item { RightSection { id: hRightSection objectName: "rightSection" + edgeIsScreenEdge: !topBarContent.hasAdjacentRightBarLive overrideAxisLayout: true forceVerticalLayout: false anchors { @@ -782,6 +791,7 @@ Item { LeftSection { id: vLeftSection objectName: "leftSection" + edgeIsScreenEdge: !topBarContent.hasAdjacentTopBarLive overrideAxisLayout: true forceVerticalLayout: true width: parent.width @@ -850,6 +860,7 @@ Item { RightSection { id: vRightSection objectName: "rightSection" + edgeIsScreenEdge: !topBarContent.hasAdjacentBottomBarLive overrideAxisLayout: true forceVerticalLayout: true width: parent.width diff --git a/quickshell/Modules/DankBar/DankBarHoverController.qml b/quickshell/Modules/DankBar/DankBarHoverController.qml index c213ca0ae..1ca49326e 100644 --- a/quickshell/Modules/DankBar/DankBarHoverController.qml +++ b/quickshell/Modules/DankBar/DankBarHoverController.qml @@ -930,7 +930,8 @@ Item { if (_barHovered) return; const excludedBar = _barExitPending ? barWindow : null; - if (cursorOverHoverChain(gx, gy, excludedBar)) + const staleOverExitedBar = excludedBar?.containsGlobalPoint?.(gx, gy, 0) ?? false; + if (!staleOverExitedBar && cursorOverHoverChain(gx, gy, excludedBar)) return; _barExitPending = false; closeHoverSurfaces(); diff --git a/quickshell/Modules/DankBar/DankBarWindow.qml b/quickshell/Modules/DankBar/DankBarWindow.qml index 2a6eb16e2..160dbeeb1 100644 --- a/quickshell/Modules/DankBar/DankBarWindow.qml +++ b/quickshell/Modules/DankBar/DankBarWindow.qml @@ -1,5 +1,4 @@ import QtQuick -import QtQuick.Controls import Quickshell import Quickshell.Wayland import qs.Common @@ -9,848 +8,73 @@ PanelWindow { id: barWindow readonly property var log: Log.scoped("DankBarWindow") - Component.onDestruction: KeyboardFocus.unregisterBarWindow(barWindow) - required property var rootWindow required property var barConfig property var modelData: item - property var hyprlandOverviewLoader: rootWindow ? rootWindow.hyprlandOverviewLoader : null property var leftWidgetsModel property var centerWidgetsModel property var rightWidgetsModel - readonly property bool barRevealed: inputMask.showing - - property var controlCenterButtonRef: null - property var clockButtonRef: null - property var systemUpdateButtonRef: null + readonly property bool isVertical: body.isVertical + readonly property int barPos: body.barPos + readonly property bool barRevealed: body.barRevealed function triggerSystemUpdate() { - const loader = PopoutService.systemUpdateLoader; - if (!loader) - return; - loader.active = true; - if (!loader.item) - return; - const popout = loader.item; - const barPosition = axis?.edge === "left" ? 2 : (axis?.edge === "right" ? 3 : (axis?.edge === "top" ? 0 : 1)); - if (systemUpdateButtonRef && popout.setTriggerPosition) { - const screenPos = systemUpdateButtonRef.mapToItem(null, 0, 0); - const pos = SettingsData.getPopupTriggerPosition(screenPos, barWindow.screen, barWindow.effectiveBarThickness, systemUpdateButtonRef.width, barConfig?.spacing ?? 4, barPosition, barConfig); - const section = systemUpdateButtonRef.section || "right"; - popout.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen, barPosition, barWindow.effectiveBarThickness, barConfig?.spacing ?? 4, barConfig); - } else { - popout.screen = barWindow.screen; - } - PopoutManager.requestPopout(popout, undefined, "systemUpdate"); + body.triggerSystemUpdate(); } - function triggerControlCenter() { - const loader = PopoutService.controlCenterLoader; - if (!loader) - return; - loader.active = true; - if (!loader.item) { - return; - } - - if (controlCenterButtonRef && loader.item.setTriggerPosition) { - const screenPos = controlCenterButtonRef.mapToItem(null, 0, 0); - const barPosition = axis?.edge === "left" ? 2 : (axis?.edge === "right" ? 3 : (axis?.edge === "top" ? 0 : 1)); - const pos = SettingsData.getPopupTriggerPosition(screenPos, barWindow.screen, barWindow.effectiveBarThickness, controlCenterButtonRef.width, barConfig?.spacing ?? 4, barPosition, barConfig); - const section = controlCenterButtonRef.section || "right"; - loader.item.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen, barPosition, barWindow.effectiveBarThickness, barConfig?.spacing ?? 4, barConfig); - } else { - loader.item.triggerScreen = barWindow.screen; - } - - loader.item.toggle(); - if (loader.item.shouldBeVisible && NetworkService.wifiEnabled) { - NetworkService.scanWifi(); - } + body.triggerControlCenter(); } - function triggerDashTab(tabId) { - const loader = PopoutService.dankDashPopoutLoader; - if (!loader) - return false; - loader.active = true; - if (!loader.item) { - return false; - } - - let section = "center"; - if (clockButtonRef && clockButtonRef.visualContent && loader.item.setTriggerPosition) { - const barPosition = axis?.edge === "left" ? 2 : (axis?.edge === "right" ? 3 : (axis?.edge === "top" ? 0 : 1)); - section = clockButtonRef.section || "center"; - - let triggerPos, triggerWidth; - if (section === "center") { - const centerSection = barWindow.isVertical ? (barWindow.axis?.edge === "left" ? topBarContent.vCenterSection : topBarContent.vCenterSection) : topBarContent.hCenterSection; - if (centerSection) { - if (barWindow.isVertical) { - const centerY = centerSection.height / 2; - triggerPos = centerSection.mapToItem(null, 0, centerY); - triggerWidth = centerSection.height; - } else { - triggerPos = centerSection.mapToItem(null, 0, 0); - triggerWidth = centerSection.width; - } - } else { - triggerPos = clockButtonRef.visualContent.mapToItem(null, 0, 0); - triggerWidth = clockButtonRef.visualWidth; - } - } else { - triggerPos = clockButtonRef.visualContent.mapToItem(null, 0, 0); - triggerWidth = clockButtonRef.visualWidth; - } - - const pos = SettingsData.getPopupTriggerPosition(triggerPos, barWindow.screen, barWindow.effectiveBarThickness, triggerWidth, barConfig?.spacing ?? 4, barPosition, barConfig); - loader.item.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen, barPosition, barWindow.effectiveBarThickness, barConfig?.spacing ?? 4, barConfig); - } else { - loader.item.triggerScreen = barWindow.screen; - } - - if (loader.item.requestTab) - loader.item.requestTab(tabId); - PopoutManager.requestPopout(loader.item, undefined, (barConfig?.id ?? "default") + "-" + section + "-" + tabId); - return true; + body.triggerDashTab(tabId); } - function triggerWallpaperBrowser() { - triggerDashTab("wallpaper"); + body.triggerWallpaperBrowser(); + } + function registerBlurWidget(item) { + body.registerBlurWidget(item); + } + function unregisterBlurWidget(item) { + body.unregisterBlurWidget(item); + } + function containsGlobalPoint(gx, gy, padding) { + return body.containsGlobalPoint(gx, gy, padding); } readonly property bool usesOverlayLayer: CompositorService.framePeerSurfacesUseOverlayForScreen(barWindow.screen) || (barConfig?.useOverlayLayer ?? false) - readonly property var dBarLayer: LayerShell.fromEnv("DMS_DANKBAR_LAYER", barWindow.usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top) - property var blurRegion: null - property var _blurWidgetItems: [] - - function registerBlurWidget(item) { - if (_blurWidgetItems.indexOf(item) >= 0) - return; - _blurWidgetItems = _blurWidgetItems.concat([item]); - _blurRebuildTimer.restart(); - } - - function unregisterBlurWidget(item) { - const idx = _blurWidgetItems.indexOf(item); - if (idx < 0) - return; - const arr = _blurWidgetItems.slice(); - arr.splice(idx, 1); - _blurWidgetItems = arr; - _blurRebuildTimer.restart(); - } - - Timer { - id: _blurRebuildTimer - interval: 1 - onTriggered: barBlur.rebuild() - } - - Connections { - target: barWindow - function onUsesConnectedFrameChromeChanged() { - _blurRebuildTimer.restart(); - } - function onUsesFrameBarChromeChanged() { - // Rebuild immediately so the bar region never overlaps FrameWindow's during chrome handoff - barBlur.rebuild(); - } - function onBarRevealedChanged() { - barBlur.rebuild(); - } - } - - Component { - id: blurRegionComp - Region {} - } - - Component { - id: blurSubRegionComp - Region { - property Item w - item: w - radius: Theme.cornerRadius - } - } - - Component { - id: blurWingRegionComp - - // r×r square at a bar end minus a quarter-disc — the swoop BarCanvas paints - Region { - id: wingRegion - - property bool atEnd: false - - readonly property real r: barBackground.wing - readonly property real bx: topBarMouseArea.x + barUnitInset.x + topBarSlide.x - readonly property real by: topBarMouseArea.y + barUnitInset.y + topBarSlide.y - readonly property real bw: barUnitInset.width - readonly property real bh: barUnitInset.height - - x: { - switch (barPos) { - case SettingsData.Position.Left: - return bx + bw; - case SettingsData.Position.Right: - return bx - r; - default: - return atEnd ? bx + bw - r : bx; - } - } - y: { - switch (barPos) { - case SettingsData.Position.Top: - return by + bh; - case SettingsData.Position.Bottom: - return by - r; - default: - return atEnd ? by + bh - r : by; - } - } - width: r - height: r - - Region { - intersection: Intersection.Subtract - radius: wingRegion.r - width: wingRegion.r * 2 - height: wingRegion.r * 2 - x: { - switch (barPos) { - case SettingsData.Position.Left: - return wingRegion.bx + wingRegion.bw; - case SettingsData.Position.Right: - return wingRegion.bx - wingRegion.r * 2; - default: - return wingRegion.atEnd ? wingRegion.bx + wingRegion.bw - wingRegion.r * 2 : wingRegion.bx; - } - } - y: { - switch (barPos) { - case SettingsData.Position.Top: - return wingRegion.by + wingRegion.bh; - case SettingsData.Position.Bottom: - return wingRegion.by - wingRegion.r * 2; - default: - return wingRegion.atEnd ? wingRegion.by + wingRegion.bh - wingRegion.r * 2 : wingRegion.by; - } - } - } - } - } - - Item { - id: barBlur - visible: false - - readonly property bool barHasTransparency: barWindow._backgroundAlpha > 0 && barWindow._backgroundAlpha < 1 - - function rebuild() { - teardown(); - if (!BlurService.enabled || !BlurService.available) - return; - if (!barWindow.barRevealed && CompositorService.isHyprland) - return; - // FrameWindow owns the blur region for the whole edge while the bar wears frame chrome - if (FrameTransitionState.effectiveFrameEnabled && barWindow.usesFrameBarChrome) - return; - - const widgets = barWindow._blurWidgetItems.filter(w => w && w.visible && w.width > 0 && w.height > 0); - const hasBar = barHasTransparency; - if (!hasBar && widgets.length === 0) - return; - - const region = blurRegionComp.createObject(barWindow); - if (!region) { - log.warn("BarBlur: Failed to create blur region"); - return; - } - - if (hasBar) { - region.x = Qt.binding(() => topBarMouseArea.x + barUnitInset.x + topBarSlide.x); - region.y = Qt.binding(() => topBarMouseArea.y + barUnitInset.y + topBarSlide.y); - region.width = Qt.binding(() => barUnitInset.width); - region.height = Qt.binding(() => barUnitInset.height); - region.radius = Qt.binding(() => barBackground.rt); - } - - const subRegions = []; - for (let i = 0; i < widgets.length; i++) { - const sub = blurSubRegionComp.createObject(region, { - w: widgets[i] - }); - if (sub) - subRegions.push(sub); - } - - if (hasBar && barBackground.gothEnabled && barWindow._wingR > 0) { - for (const atEnd of [false, true]) { - const wing = blurWingRegionComp.createObject(region, { - atEnd: atEnd - }); - if (wing) - subRegions.push(wing); - } - } - - region.regions = subRegions; - - barWindow.BackgroundEffect.blurRegion = region; - barWindow.blurRegion = region; - } - - function teardown() { - if (!barWindow.blurRegion) - return; - try { - barWindow.BackgroundEffect.blurRegion = null; - } catch (e) { - log.warn("BarBlur: failed to clear blur region:", e); - } - barWindow.blurRegion.destroy(); - barWindow.blurRegion = null; - } - - onBarHasTransparencyChanged: _blurRebuildTimer.restart() - - Connections { - target: BlurService - function onEnabledChanged() { - barBlur.rebuild(); - } - } - - Connections { - target: FrameTransitionState - function onEffectiveFrameEnabledChanged() { - barBlur.rebuild(); - } - } - - Connections { - target: topBarSlide - function onXChanged() { - if (barWindow.blurRegion) - barWindow.blurRegion.changed(); - } - function onYChanged() { - if (barWindow.blurRegion) - barWindow.blurRegion.changed(); - } - } - - Connections { - target: barBackground - function onGothEnabledChanged() { - _blurRebuildTimer.restart(); - } - function onWingChanged() { - if (barWindow.blurRegion) - barWindow.blurRegion.changed(); - } - } - - Component.onCompleted: rebuild() - Component.onDestruction: teardown() - } + screen: modelData + color: "transparent" WlrLayershell.layer: dBarLayer WlrLayershell.namespace: "dms:bar" - signal colorPickerRequested - - onColorPickerRequested: rootWindow.colorPickerRequested() - - property alias axis: axis - - AxisContext { - id: axis - edge: { - switch (barConfig?.position ?? 0) { - case SettingsData.Position.Top: - return "top"; - case SettingsData.Position.Bottom: - return "bottom"; - case SettingsData.Position.Left: - return "left"; - case SettingsData.Position.Right: - return "right"; - default: - return "top"; - } - } - } - - readonly property bool isVertical: axis.isVertical - - readonly property color _surfaceContainer: Theme.surfaceContainer - readonly property string _barId: barConfig?.id ?? "default" - property real _backgroundAlpha: barConfig?.transparency ?? 1.0 - readonly property color _bgColor: (FrameTransitionState.effectiveFrameEnabled && usesFrameBarChrome) ? Theme.withAlpha(SettingsData.effectiveFrameColor, SettingsData.frameOpacity) : Theme.withAlpha(_surfaceContainer, _backgroundAlpha) - - function _updateBackgroundAlpha() { - const live = SettingsData.barConfigs.find(c => c.id === _barId); - _backgroundAlpha = (live ?? barConfig)?.transparency ?? 1.0; - } - readonly property real _dpr: CompositorService.getScreenScale(barWindow.screen) - - property string screenName: modelData.name - - readonly property bool usesConnectedFrameChrome: CompositorService.usesConnectedFrameChromeForScreen(screenName) - readonly property bool usesFrameBarChrome: CompositorService.frameWindowVisibleForScreen(screenName) - readonly property var renderBarConfig: SettingsData.effectiveBarConfigForRender(barConfig, usesFrameBarChrome) - - property bool gothCornersEnabled: renderBarConfig?.gothCornersEnabled ?? false - property real wingtipsRadius: renderBarConfig?.gothCornerRadiusOverride ? (renderBarConfig?.gothCornerRadiusValue ?? 12) : Theme.cornerRadius - readonly property real _wingR: Math.max(0, wingtipsRadius) - - // Shadow buffer: extra window space for shadow to render beyond bar bounds - readonly property bool _shadowActive: (Theme.elevationEnabled && (typeof SettingsData !== "undefined" ? (SettingsData.barElevationEnabled ?? true) : false)) || (renderBarConfig?.shadowIntensity ?? 0) > 0 - readonly property real _shadowBuffer: { - if (!_shadowActive) - return 0; - const hasOverride = (renderBarConfig?.shadowIntensity ?? 0) > 0; - if (hasOverride) { - const blur = (renderBarConfig.shadowIntensity ?? 0) * 0.2; - const offset = blur * 0.5; - return Theme.snap(Math.max(16, blur + offset + 8), _dpr); - } - return Theme.snap(Theme.elevationRenderPadding(Theme.elevationLevel2, "top", 4, 8, 16), _dpr); - } - - // Flatten/spacing collapse for maximized windows only applies to frame-integrated layout - readonly property bool flattenForMaximizedWindow: !FrameTransitionState.effectiveFrameEnabled || usesFrameBarChrome - - property bool hasMaximizedToplevel: false - property bool shouldHideForWindows: false - - function _updateHasMaximizedToplevel() { - if (!(barConfig?.maximizeDetection ?? true)) { - hasMaximizedToplevel = false; - return; - } - if (CompositorService.isMango) { - const out = MangoService.outputs[screenName]; - const active = new Set((out?.activeTags) || []); - const wins = MangoService.windows || []; - for (let i = 0; i < wins.length; i++) { - const w = wins[i]; - if (!w || w.monitor !== screenName || w.is_minimized) - continue; - if (active.size > 0 && !(w.tags || []).some(t => active.has(t))) - continue; - if (w.is_maximized || w.is_fullscreen) { - hasMaximizedToplevel = true; - return; - } - } - hasMaximizedToplevel = false; - return; - } - if (!CompositorService.isHyprland && !CompositorService.isNiri) { - hasMaximizedToplevel = false; - return; - } - - const filtered = CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName); - for (let i = 0; i < filtered.length; i++) { - if (filtered[i]?.maximized) { - hasMaximizedToplevel = true; - return; - } - } - hasMaximizedToplevel = false; - } - - function _updateShouldHideForWindows() { - if (!(barConfig?.showOnWindowsOpen ?? false)) { - shouldHideForWindows = false; - return; - } - if (!(barConfig?.autoHide ?? false)) { - shouldHideForWindows = false; - return; - } - if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango) { - shouldHideForWindows = false; - return; - } - - if (CompositorService.isNiri) { - let currentWorkspaceId = null; - for (let i = 0; i < NiriService.allWorkspaces.length; i++) { - const ws = NiriService.allWorkspaces[i]; - if (ws.output === screenName && ws.is_active) { - currentWorkspaceId = ws.id; - break; - } - } - - if (currentWorkspaceId === null) { - shouldHideForWindows = false; - return; - } - - let hasTiled = false; - let hasFloatingTouchingBar = false; - const pos = barConfig?.position ?? 0; - const barThickness = barWindow.effectiveBarThickness + (barConfig?.spacing ?? 4); - - for (let i = 0; i < NiriService.windows.length; i++) { - const win = NiriService.windows[i]; - if (win.workspace_id !== currentWorkspaceId) - continue; - - if (!win.is_floating) { - hasTiled = true; - continue; - } - - const tilePos = win.layout?.tile_pos_in_workspace_view; - const winSize = win.layout?.window_size || win.layout?.tile_size; - if (!tilePos || !winSize) - continue; - - switch (pos) { - case SettingsData.Position.Top: - if (tilePos[1] < barThickness) - hasFloatingTouchingBar = true; - break; - case SettingsData.Position.Bottom: - const screenHeight = barWindow.screen?.height ?? 0; - if (tilePos[1] + winSize[1] > screenHeight - barThickness) - hasFloatingTouchingBar = true; - break; - case SettingsData.Position.Left: - if (tilePos[0] < barThickness) - hasFloatingTouchingBar = true; - break; - case SettingsData.Position.Right: - const screenWidth = barWindow.screen?.width ?? 0; - if (tilePos[0] + winSize[0] > screenWidth - barThickness) - hasFloatingTouchingBar = true; - break; - } - } - - shouldHideForWindows = hasTiled || hasFloatingTouchingBar; - return; - } - - const filtered = CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName); - shouldHideForWindows = filtered.length > 0; - } - - property real effectiveSpacing: (FrameTransitionState.effectiveFrameEnabled && usesFrameBarChrome) ? 0 : ((flattenForMaximizedWindow && hasMaximizedToplevel) ? 0 : (barConfig?.spacing ?? 4)) - - Behavior on effectiveSpacing { - enabled: barWindow.visible - NumberAnimation { - duration: Theme.shortDuration - easing.type: Easing.OutCubic - } - } - - readonly property int notificationCount: NotificationService.notifications.length - readonly property real effectiveBarThickness: (FrameTransitionState.effectiveFrameEnabled && usesFrameBarChrome) ? SettingsData.frameBarSize : Theme.snap(Math.max(barWindow.widgetThickness + (barConfig?.innerPadding ?? 4) + 4, Theme.barHeight - 4 - (8 - (barConfig?.innerPadding ?? 4))), _dpr) - readonly property bool effectiveOpenOnOverview: FrameTransitionState.effectiveFrameEnabled ? SettingsData.frameShowOnOverview : (barConfig?.openOnOverview ?? false) - readonly property real widgetThickness: Theme.snap(Math.max(20, 26 + (barConfig?.innerPadding ?? 4) * 0.6), _dpr) - - readonly property bool hasAdjacentTopBar: { - if (barConfig?.autoHide ?? false) - return false; - if (!isVertical) - return false; - return SettingsData.barConfigs.some(bc => { - if (!bc.enabled || bc.id === barConfig?.id) - return false; - if (bc.autoHide) - return false; - if (!(bc.visible ?? true)) - return false; - if (bc.position !== SettingsData.Position.Top && bc.position !== 0) - return false; - const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); - if (!onThisScreen) - return false; - if (bc.showOnLastDisplay && screenName !== barWindow.screenName) - return false; - return true; - }); - } - - readonly property bool hasAdjacentBottomBar: { - if (barConfig?.autoHide ?? false) - return false; - if (!isVertical) - return false; - const result = SettingsData.barConfigs.some(bc => { - if (!bc.enabled || bc.id === barConfig?.id) - return false; - if (bc.autoHide) - return false; - if (!(bc.visible ?? true)) - return false; - if (bc.position !== SettingsData.Position.Bottom && bc.position !== 1) - return false; - const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); - if (!onThisScreen) - return false; - if (bc.showOnLastDisplay && screenName !== barWindow.screenName) - return false; - return true; - }); - return result; - } - - readonly property bool hasAdjacentLeftBar: { - if (barConfig?.autoHide ?? false) - return false; - if (isVertical) - return false; - const result = SettingsData.barConfigs.some(bc => { - if (!bc.enabled || bc.id === barConfig?.id) - return false; - if (bc.autoHide) - return false; - if (!(bc.visible ?? true)) - return false; - if (bc.position !== SettingsData.Position.Left && bc.position !== 2) - return false; - const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); - if (!onThisScreen) - return false; - if (bc.showOnLastDisplay && screenName !== barWindow.screenName) - return false; - return true; - }); - return result; - } - - readonly property bool hasAdjacentRightBar: { - if (barConfig?.autoHide ?? false) - return false; - if (isVertical) - return false; - const result = SettingsData.barConfigs.some(bc => { - if (!bc.enabled || bc.id === barConfig?.id) - return false; - if (bc.autoHide) - return false; - if (!(bc.visible ?? true)) - return false; - if (bc.position !== SettingsData.Position.Right && bc.position !== 3) - return false; - const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); - if (!onThisScreen) - return false; - if (bc.showOnLastDisplay && screenName !== barWindow.screenName) - return false; - return true; - }); - return result; - } - - screen: modelData - implicitHeight: !isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((renderBarConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0 - implicitWidth: isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((renderBarConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0 - color: "transparent" - - Component.onCompleted: { - KeyboardFocus.registerBarWindow(barWindow); - updateGpuTempConfig(); - _updateBackgroundAlpha(); - _updateHasMaximizedToplevel(); - _updateShouldHideForWindows(); - } - - IdleInhibitor { - window: barWindow - enabled: SessionService.idleInhibited || IdleService.externalInhibitActive - } - - Connections { - target: PluginService - function onPluginLoaded(pluginId) { - log.info("DankBar: Plugin loaded:", pluginId); - SettingsData.widgetDataChanged(); - } - function onPluginUnloaded(pluginId) { - log.info("DankBar: Plugin unloaded:", pluginId); - SettingsData.widgetDataChanged(); - } - } - - function updateGpuTempConfig() { - const leftWidgets = barConfig?.leftWidgets || []; - const centerWidgets = barConfig?.centerWidgets || []; - const rightWidgets = barConfig?.rightWidgets || []; - const allWidgets = [...leftWidgets, ...centerWidgets, ...rightWidgets]; - - const hasGpuTempWidget = allWidgets.some(widget => { - const widgetId = typeof widget === "string" ? widget : widget.id; - const widgetEnabled = typeof widget === "string" ? true : (widget.enabled !== false); - return widgetId === "gpuTemp" && widgetEnabled; - }); - - DgopService.gpuTempEnabled = hasGpuTempWidget || SessionData.nvidiaGpuTempEnabled || SessionData.nonNvidiaGpuTempEnabled; - DgopService.nvidiaGpuTempEnabled = hasGpuTempWidget || SessionData.nvidiaGpuTempEnabled; - DgopService.nonNvidiaGpuTempEnabled = hasGpuTempWidget || SessionData.nonNvidiaGpuTempEnabled; - } - - Connections { - function onBarConfigChanged() { - barWindow.updateGpuTempConfig(); - barWindow._updateBackgroundAlpha(); - barWindow._updateHasMaximizedToplevel(); - barWindow._updateShouldHideForWindows(); - } - - target: rootWindow - } - - Connections { - target: SettingsData - function onBarConfigsChanged() { - barWindow._updateBackgroundAlpha(); - } - } - - Connections { - target: CompositorService - function onToplevelsChanged() { - barWindow._updateHasMaximizedToplevel(); - barWindow._updateShouldHideForWindows(); - } - } - - Connections { - target: NiriService - function onAllWorkspacesChanged() { - barWindow._updateHasMaximizedToplevel(); - barWindow._updateShouldHideForWindows(); - } - } - - Connections { - function onNvidiaGpuTempEnabledChanged() { - barWindow.updateGpuTempConfig(); - } - - function onNonNvidiaGpuTempEnabledChanged() { - barWindow.updateGpuTempConfig(); - } - - target: SessionData - } - - readonly property int barPos: barConfig?.position ?? 0 - anchors.top: !isVertical ? (barPos === SettingsData.Position.Top) : true anchors.bottom: !isVertical ? (barPos === SettingsData.Position.Bottom) : true anchors.left: !isVertical ? true : (barPos === SettingsData.Position.Left) anchors.right: !isVertical ? true : (barPos === SettingsData.Position.Right) - readonly property bool reserveExclusiveWhenAutoHidden: FrameTransitionState.effectiveFrameEnabled && usesFrameBarChrome && !!barWindow.screen && SettingsData.isScreenInPreferences(barWindow.screen, SettingsData.frameScreenPreferences) + implicitHeight: body.surfaceImplicitHeight + implicitWidth: body.surfaceImplicitWidth + exclusiveZone: body.surfaceExclusiveZone - exclusiveZone: (!(barConfig?.visible ?? true) || (topBarCore.autoHide && !barWindow.reserveExclusiveWhenAutoHidden)) ? -1 : (barWindow.effectiveBarThickness + effectiveSpacing + (usesFrameBarChrome ? 0 : (barConfig?.bottomGap ?? 0))) + BackgroundEffect.blurRegion: body.blurRegion - Item { - id: inputMask + Component.onCompleted: KeyboardFocus.registerBarWindow(barWindow) + Component.onDestruction: KeyboardFocus.unregisterBarWindow(barWindow) - readonly property int barThickness: Theme.px(barWindow.effectiveBarThickness + barWindow.effectiveSpacing, barWindow._dpr) - - readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview - readonly property bool effectiveVisible: (barConfig?.visible ?? true) || inOverviewWithShow - readonly property bool showing: effectiveVisible && (topBarCore.reveal || inOverviewWithShow || !topBarCore.autoHide) - - readonly property int maskThickness: showing ? barThickness : 1 - - x: { - if (!axis.isVertical) { - return 0; - } else { - switch (barPos) { - case SettingsData.Position.Left: - return 0; - case SettingsData.Position.Right: - return parent.width - maskThickness; - default: - return 0; - } - } - } - y: { - if (axis.isVertical) { - return 0; - } else { - switch (barPos) { - case SettingsData.Position.Top: - return 0; - case SettingsData.Position.Bottom: - return parent.height - maskThickness; - default: - return 0; - } - } - } - width: axis.isVertical ? maskThickness : parent.width - height: axis.isVertical ? parent.height : maskThickness - } - - readonly property bool clickThroughEnabled: barConfig?.clickThrough ?? false - - readonly property var _leftSection: topBarContent ? (barWindow.isVertical ? topBarContent.vLeftSection : topBarContent.hLeftSection) : null - readonly property var _centerSection: topBarContent ? (barWindow.isVertical ? topBarContent.vCenterSection : topBarContent.hCenterSection) : null - readonly property var _rightSection: topBarContent ? (barWindow.isVertical ? topBarContent.vRightSection : topBarContent.hRightSection) : null - readonly property real _revealProgress: topBarSlide.x + topBarSlide.y - - function containsGlobalPoint(gx, gy, padding) { - const pad = padding !== undefined ? padding : 16; - if (!inputMask.showing) - return false; - const topLeft = inputMask.mapToItem(null, 0, 0); - return gx >= topLeft.x - pad && gx < topLeft.x + inputMask.width + pad && gy >= topLeft.y - pad && gy < topLeft.y + inputMask.height + pad; - } - - function sectionRect(section, isCenter, _dep) { - if (!section) - return { - "x": 0, - "y": 0, - "w": 0, - "h": 0 - }; - - const pos = section.mapToItem(barWindow.contentItem, 0, 0); - const implW = section.implicitWidth || 0; - const implH = section.implicitHeight || 0; - - const offsetX = isCenter && !barWindow.isVertical ? (section.width - implW) / 2 : 0; - const offsetY = !barWindow.isVertical ? (section.height - implH) / 2 : (isCenter ? (section.height - implH) / 2 : 0); - - const edgePad = 2; - return { - "x": pos.x + offsetX - edgePad, - "y": pos.y + offsetY - edgePad, - "w": implW + edgePad * 2, - "h": implH + edgePad * 2 - }; + IdleInhibitor { + window: barWindow + enabled: SessionService.idleInhibited || IdleService.externalInhibitActive } mask: Region { - item: clickThroughEnabled ? null : inputMask + item: body.clickThroughEnabled ? null : body.inputMaskItem Region { - readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._leftSection, false, barWindow._revealProgress + barWindow.width * 0) : { + readonly property var r: body.clickThroughEnabled ? body.sectionRect(body._leftSection, false, body._revealProgress) : { "x": 0, "y": 0, "w": 0, @@ -863,7 +87,7 @@ PanelWindow { } Region { - readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._centerSection, true, barWindow._revealProgress + barWindow.width * 0) : { + readonly property var r: body.clickThroughEnabled ? body.sectionRect(body._centerSection, true, body._revealProgress) : { "x": 0, "y": 0, "w": 0, @@ -876,7 +100,7 @@ PanelWindow { } Region { - readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._rightSection, false, barWindow._revealProgress + barWindow.width * 0) : { + readonly property var r: body.clickThroughEnabled ? body.sectionRect(body._rightSection, false, body._revealProgress) : { "x": 0, "y": 0, "w": 0, @@ -889,399 +113,23 @@ PanelWindow { } Region { - readonly property bool active: barWindow.clickThroughEnabled && !inputMask.showing - x: active ? inputMask.x : 0 - y: active ? inputMask.y : 0 - width: active ? inputMask.width : 0 - height: active ? inputMask.height : 0 + readonly property bool active: body.clickThroughEnabled && !body.inputMaskItem.showing + x: active ? body.inputMaskItem.x : 0 + y: active ? body.inputMaskItem.y : 0 + width: active ? body.inputMaskItem.width : 0 + height: active ? body.inputMaskItem.height : 0 } } - Item { - id: topBarCore + DankBarBody { + id: body anchors.fill: parent - layer.enabled: false - - property bool autoHide: barConfig?.autoHide ?? false - property bool revealSticky: false - // In click-through mode the hidden bar's input mask covers the full - // band while the revealed bar's mask covers only the widget sections, - // so the pointer position is unknowable while it is over a gap. An - // enter on the hidden bar therefore only reveals once the pointer - // reaches a thin strip at the screen edge; anything else (including - // the spurious enter generated by the mask expanding underneath a - // resting pointer) keeps the bar hidden. - property bool gapEnterSuppressed: false - readonly property bool hoverReveal: topBarMouseArea.containsMouse && !gapEnterSuppressed - readonly property bool ipcReveal: !!SettingsData.barIpcRevealStates[barConfig?.id ?? ""] - - onRevealChanged: { - if (reveal && barWindow.clickThroughEnabled) - revealSettle.restart(); - } - - // The input mask updates lag reveal transitions, generating spurious - // enter/leave pairs. Hides are deferred until the transition settles; - // the timer re-evaluates against the post-transition mask. - Timer { - id: revealSettle - interval: 600 - repeat: false - onTriggered: topBarCore.evaluateReveal() - } - - function inEdgeStrip(x, y) { - const band = barWindow.isVertical ? topBarMouseArea.width : topBarMouseArea.height; - const strip = Math.max(8, band * 0.15); - switch (barPos) { - case SettingsData.Position.Bottom: - return y >= band - strip; - case SettingsData.Position.Left: - return x <= strip; - case SettingsData.Position.Right: - return x >= band - strip; - default: - return y <= strip; - } - } - - Timer { - id: revealHold - interval: barConfig?.autoHideDelay ?? 250 - repeat: false - onTriggered: { - if (!topBarCore.hoverReveal && !topBarCore.popoutPinsReveal) - topBarCore.revealSticky = false; - } - } - - property bool hasActivePopout: false - - readonly property bool popoutPinsReveal: !!(hasActivePopout && !(barConfig?.autoHideStrict ?? false)) - - onHasActivePopoutChanged: evaluateReveal() - - onPopoutPinsRevealChanged: evaluateReveal() - - function updateActivePopoutState() { - if (!barWindow.screen) - return; - const screenName = barWindow.screen.name; - const activePopout = PopoutManager.currentPopoutsByScreen[screenName]; - const activeTrayMenu = TrayMenuManager.activeTrayMenus[screenName]; - const trayOpen = rootWindow.systemTrayMenuOpen; - - const hasVisiblePopout = activePopout && activePopout.shouldBeVisible; - topBarCore.hasActivePopout = !!(hasVisiblePopout || activeTrayMenu || trayOpen); - } - - Connections { - target: PopoutManager - - function onPopoutChanged() { - topBarCore.updateActivePopoutState(); - } - - function onPopoutOpening() { - topBarCore.evaluateReveal(); - } - } - - Connections { - target: TrayMenuManager - - function onActiveTrayMenusChanged() { - topBarCore.updateActivePopoutState(); - } - } - - property bool reveal: { - const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview; - if (inOverviewWithShow) - return true; - - const showOnWindowsSetting = barConfig?.showOnWindowsOpen ?? false; - if (showOnWindowsSetting && autoHide && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango)) { - if (barWindow.shouldHideForWindows) - return hoverReveal || popoutPinsReveal || revealSticky || ipcReveal; - return true; - } - - if (CompositorService.isNiri && NiriService.inOverview) - return hoverReveal || popoutPinsReveal || revealSticky || ipcReveal; - - return (barConfig?.visible ?? true) && (!autoHide || hoverReveal || popoutPinsReveal || revealSticky || ipcReveal); - } - - Connections { - function onBarConfigChanged() { - topBarCore.autoHide = barConfig?.autoHide ?? false; - topBarCore.evaluateReveal(); - } - - target: rootWindow - } - - Component.onCompleted: topBarCore.updateActivePopoutState() - - function evaluateReveal() { - if (!autoHide) - return; - - if (hoverReveal) { - SettingsData.setBarIpcReveal(barConfig?.id ?? "", false); - revealSticky = true; - revealHold.stop(); - return; - } - - if (popoutPinsReveal) { - revealSticky = true; - revealHold.stop(); - return; - } - - if (revealSettle.running) - return; - - revealHold.interval = barConfig?.autoHideDelay ?? 250; - revealHold.restart(); - } - - Connections { - target: topBarMouseArea - function onContainsMouseChanged() { - if (!topBarMouseArea.containsMouse) { - topBarCore.gapEnterSuppressed = false; - } else if (barWindow.clickThroughEnabled && !topBarCore.reveal) { - topBarCore.gapEnterSuppressed = true; - } - topBarCore.evaluateReveal(); - } - } - - MouseArea { - id: topBarMouseArea - y: !barWindow.isVertical ? (barPos === SettingsData.Position.Bottom ? parent.height - height : 0) : 0 - x: barWindow.isVertical ? (barPos === SettingsData.Position.Right ? parent.width - width : 0) : 0 - height: !barWindow.isVertical ? Theme.px(barWindow.effectiveBarThickness + barWindow.effectiveSpacing, barWindow._dpr) : undefined - width: barWindow.isVertical ? Theme.px(barWindow.effectiveBarThickness + barWindow.effectiveSpacing, barWindow._dpr) : undefined - anchors { - left: !barWindow.isVertical ? parent.left : (barPos === SettingsData.Position.Left ? parent.left : undefined) - right: !barWindow.isVertical ? parent.right : (barPos === SettingsData.Position.Right ? parent.right : undefined) - top: barWindow.isVertical ? parent.top : undefined - bottom: barWindow.isVertical ? parent.bottom : undefined - } - readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview - hoverEnabled: (barConfig?.autoHide ?? false) && !inOverview && !topBarCore.popoutPinsReveal - acceptedButtons: Qt.NoButton - enabled: (barConfig?.autoHide ?? false) && !inOverview - onPositionChanged: mouse => { - if (!topBarCore.gapEnterSuppressed) - return; - if (!topBarCore.inEdgeStrip(mouse.x, mouse.y)) - return; - topBarCore.gapEnterSuppressed = false; - topBarCore.evaluateReveal(); - } - - Item { - id: topBarContainer - anchors.fill: parent - - transform: Translate { - id: topBarSlide - x: barWindow.isVertical ? Theme.snap(topBarCore.reveal ? 0 : (barPos === SettingsData.Position.Right ? barWindow.implicitWidth : -barWindow.implicitWidth), barWindow._dpr) : 0 - y: !barWindow.isVertical ? Theme.snap(topBarCore.reveal ? 0 : (barPos === SettingsData.Position.Bottom ? barWindow.implicitHeight : -barWindow.implicitHeight), barWindow._dpr) : 0 - - Behavior on x { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Easing.OutCubic - } - } - - Behavior on y { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Easing.OutCubic - } - } - } - - Item { - id: barUnitInset - property int spacingPx: Theme.px(barWindow.effectiveSpacing, barWindow._dpr) - anchors.fill: parent - anchors.leftMargin: !barWindow.isVertical ? spacingPx : (axis.edge === "left" ? spacingPx : 0) - anchors.rightMargin: !barWindow.isVertical ? spacingPx : (axis.edge === "right" ? spacingPx : 0) - anchors.topMargin: barWindow.isVertical ? (barWindow.hasAdjacentTopBar ? 0 : spacingPx) : (axis.outerVisualEdge() === "bottom" ? 0 : spacingPx) - anchors.bottomMargin: barWindow.isVertical ? (barWindow.hasAdjacentBottomBar ? 0 : spacingPx) : (axis.outerVisualEdge() === "bottom" ? spacingPx : 0) - - BarCanvas { - id: barBackground - barWindow: barWindow - axis: axis - barConfig: barWindow.renderBarConfig - } - - MouseArea { - anchors.fill: parent - z: -2 - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - onClicked: { - const screenName = barWindow.screen?.name; - if (!screenName) - return; - if (PopoutManager.currentPopoutsByScreen[screenName]) - PopoutManager.closeAllPopouts(); - if (ModalManager.currentModalsByScreen[screenName]) - ModalManager.closeAllModalsExcept(null); - TrayMenuManager.closeAllMenus(); - } - } - - MouseArea { - id: scrollArea - anchors.fill: parent - acceptedButtons: Qt.NoButton - propagateComposedEvents: true - z: -1 - - property real touchpadAccumulatorY: 0 - property real touchpadAccumulatorX: 0 - property real mouseAccumulatorY: 0 - property real mouseAccumulatorX: 0 - property bool actionInProgress: false - - Timer { - id: cooldownTimer - interval: 100 - onTriggered: parent.actionInProgress = false - } - - function handleScrollAction(behavior, direction) { - switch (behavior) { - case "workspace": - topBarContent.switchWorkspace(direction); - return true; - case "column": - if (!CompositorService.isNiri) - return false; - if (direction > 0) - NiriService.moveColumnRight(barWindow.screenName); - else - NiriService.moveColumnLeft(barWindow.screenName); - return true; - default: - return false; - } - } - - function processWheel(wheel) { - if (!(barConfig?.scrollEnabled ?? true) || actionInProgress) { - wheel.accepted = false; - return; - } - - const deltaY = wheel.angleDelta.y; - const deltaX = wheel.angleDelta.x; - const isTouchpadY = wheel.pixelDelta && wheel.pixelDelta.y !== 0; - const isTouchpadX = wheel.pixelDelta && wheel.pixelDelta.x !== 0; - const xBehavior = barConfig?.scrollXBehavior ?? "column"; - const yBehavior = barConfig?.scrollYBehavior ?? "workspace"; - const reverse = SettingsData.reverseScrolling ? -1 : 1; - - if (CompositorService.isNiri && xBehavior !== "none" && Math.abs(deltaX) > Math.abs(deltaY)) { - if (isTouchpadX) { - touchpadAccumulatorX += deltaX; - if (Math.abs(touchpadAccumulatorX) >= 500) { - const direction = touchpadAccumulatorX * reverse < 0 ? 1 : -1; - if (handleScrollAction(xBehavior, direction)) { - actionInProgress = true; - cooldownTimer.restart(); - } - touchpadAccumulatorX = 0; - } - } else { - mouseAccumulatorX += deltaX; - if (Math.abs(mouseAccumulatorX) >= 120) { - const direction = mouseAccumulatorX * reverse < 0 ? 1 : -1; - if (handleScrollAction(xBehavior, direction)) { - actionInProgress = true; - cooldownTimer.restart(); - } - mouseAccumulatorX = 0; - } - } - wheel.accepted = false; - return; - } - - if (yBehavior === "none") { - wheel.accepted = false; - return; - } - - if (isTouchpadY) { - touchpadAccumulatorY += deltaY; - if (Math.abs(touchpadAccumulatorY) >= 500) { - const direction = touchpadAccumulatorY * reverse < 0 ? 1 : -1; - if (handleScrollAction(yBehavior, direction)) { - actionInProgress = true; - cooldownTimer.restart(); - } - touchpadAccumulatorY = 0; - } - } else { - mouseAccumulatorY += deltaY; - if (Math.abs(mouseAccumulatorY) >= 120) { - const direction = mouseAccumulatorY * reverse < 0 ? 1 : -1; - if (handleScrollAction(yBehavior, direction)) { - actionInProgress = true; - cooldownTimer.restart(); - } - mouseAccumulatorY = 0; - } - } - - wheel.accepted = false; - } - - onWheel: wheel => processWheel(wheel) - } - - DankBarContent { - id: topBarContent - barWindow: barWindow - rootWindow: barWindow.rootWindow - barConfig: barWindow.barConfig - leftWidgetsModel: barWindow.leftWidgetsModel - centerWidgetsModel: barWindow.centerWidgetsModel - rightWidgetsModel: barWindow.rightWidgetsModel - } - - // Passive: tracks cursor without intercepting clicks or scroll - HoverHandler { - id: hoverPopoutHandler - enabled: (barConfig?.hoverPopouts ?? false) && !barWindow.clickThroughEnabled - - property real lastGlobalX: 0 - property real lastGlobalY: 0 - - onPointChanged: { - const gp = barUnitInset.mapToItem(null, point.position.x, point.position.y); - lastGlobalX = gp.x; - lastGlobalY = gp.y; - topBarContent.queueHoverPopout(gp.x, gp.y); - } - - onHoveredChanged: { - topBarContent.updateHoverBarHovered(hovered); - } - } - } - } - } + hostWindow: barWindow + modelData: barWindow.modelData + rootWindow: barWindow.rootWindow + barConfig: barWindow.barConfig + leftWidgetsModel: barWindow.leftWidgetsModel + centerWidgetsModel: barWindow.centerWidgetsModel + rightWidgetsModel: barWindow.rightWidgetsModel } } diff --git a/quickshell/Modules/DankBar/LeftSection.qml b/quickshell/Modules/DankBar/LeftSection.qml index ced23971c..92dbaefa3 100644 --- a/quickshell/Modules/DankBar/LeftSection.qml +++ b/quickshell/Modules/DankBar/LeftSection.qml @@ -17,6 +17,9 @@ Item { property real sectionAvailablePrimarySize: 0 property bool overrideAxisLayout: false property bool forceVerticalLayout: false + // False when a perpendicular bar occupies this edge, so the first widget's click area + // doesn't extend past the bar into the neighbour (matters most in the frame surface). + property bool edgeIsScreenEdge: true readonly property bool isVertical: overrideAxisLayout ? forceVerticalLayout : (axis?.isVertical ?? false) property alias widgetLayoutLoader: layoutLoader @@ -68,7 +71,7 @@ Item { isFirst: index === 0 isLast: index === rowRepeater.count - 1 sectionSpacing: parent.rowSpacing - isLeftBarEdge: true + isLeftBarEdge: root.edgeIsScreenEdge isRightBarEdge: false } } @@ -115,7 +118,7 @@ Item { isFirst: index === 0 isLast: index === columnRepeater.count - 1 sectionSpacing: parent.columnSpacing - isTopBarEdge: true + isTopBarEdge: root.edgeIsScreenEdge isBottomBarEdge: false } } diff --git a/quickshell/Modules/DankBar/RightSection.qml b/quickshell/Modules/DankBar/RightSection.qml index 6d6d43342..70fdcdff8 100644 --- a/quickshell/Modules/DankBar/RightSection.qml +++ b/quickshell/Modules/DankBar/RightSection.qml @@ -17,6 +17,9 @@ Item { property real sectionAvailablePrimarySize: 0 property bool overrideAxisLayout: false property bool forceVerticalLayout: false + // False when a perpendicular bar occupies this edge, so the last widget's click area + // doesn't extend past the bar into the neighbour (matters most in the frame surface). + property bool edgeIsScreenEdge: true readonly property bool isVertical: overrideAxisLayout ? forceVerticalLayout : (axis?.isVertical ?? false) property alias widgetLayoutLoader: layoutLoader @@ -71,7 +74,7 @@ Item { isLast: index === rowRepeater.count - 1 sectionSpacing: parent.rowSpacing isLeftBarEdge: false - isRightBarEdge: true + isRightBarEdge: root.edgeIsScreenEdge } } } @@ -118,7 +121,7 @@ Item { isLast: index === columnRepeater.count - 1 sectionSpacing: parent.columnSpacing isTopBarEdge: false - isBottomBarEdge: true + isBottomBarEdge: root.edgeIsScreenEdge } } } diff --git a/quickshell/Modules/Dock/Dock.qml b/quickshell/Modules/Dock/Dock.qml index deb1fd68c..d8eea0167 100644 --- a/quickshell/Modules/Dock/Dock.qml +++ b/quickshell/Modules/Dock/Dock.qml @@ -1,8 +1,6 @@ pragma ComponentBehavior: Bound import QtQuick -import QtQuick.Shapes import Quickshell -import Quickshell.Hyprland import Quickshell.Wayland import qs.Common import qs.Services @@ -10,7 +8,8 @@ import qs.Widgets Variants { id: dockVariants - model: SettingsData.getFilteredScreens("dock") + // Connected frame mode renders the dock inside the frame surface; skip the standalone window there. + model: SettingsData.getFilteredScreens("dock").filter(screen => !CompositorService.frameHostsDockForScreen(screen)) property var contextMenu property var trashContextMenu @@ -18,864 +17,74 @@ Variants { delegate: PanelWindow { id: dock - WindowBlur { - targetWindow: dock - blurEnabled: dock.effectiveBlurEnabled && !dock.usesConnectedFrameChrome - blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x - blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y - blurWidth: dock.hasApps && dock.reveal ? dockBackground.width : 0 - blurHeight: dock.hasApps && dock.reveal ? dockBackground.height : 0 - blurRadius: dock.usesConnectedFrameChrome ? Theme.connectedCornerRadius : dock.surfaceRadius - } - - WlrLayershell.namespace: "dms:dock" - WlrLayershell.layer: dock.usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top - - readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right - - anchors { - top: !isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top) : true - bottom: !isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom) : true - left: !isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Left) - right: !isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Right) - } - property var modelData: item - property bool autoHide: SettingsData.dockAutoHide || SettingsData.dockSmartAutoHide - property real backgroundTransparency: SettingsData.dockTransparency - property bool groupByApp: SettingsData.dockGroupByApp - readonly property int borderThickness: SettingsData.dockBorderEnabled ? SettingsData.dockBorderThickness : 0 - readonly property string connectedBarSide: SettingsData.dockPosition === SettingsData.Position.Top ? "top" : SettingsData.dockPosition === SettingsData.Position.Bottom ? "bottom" : SettingsData.dockPosition === SettingsData.Position.Left ? "left" : "right" - readonly property bool frameDockExclusionActive: dockGeometry.frameExclusionActive - readonly property bool connectedBarActiveOnEdge: dockGeometry.connectedBarActiveOnEdge - readonly property real connectedJoinInset: dockGeometry.connectedJoinInset - readonly property real dockFrameInset: dockGeometry.frameInset - readonly property real surfaceRadius: usesConnectedFrameChrome ? Theme.connectedSurfaceRadius : Theme.cornerRadius - readonly property color surfaceColor: usesConnectedFrameChrome ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency) - readonly property color surfaceBorderColor: usesConnectedFrameChrome ? Theme.withAlpha(BlurService.borderColor, 0) : BlurService.borderColor - readonly property real surfaceBorderWidth: usesConnectedFrameChrome ? 0 : BlurService.borderWidth - readonly property real surfaceTopLeftRadius: usesConnectedFrameChrome && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius - readonly property real surfaceTopRightRadius: usesConnectedFrameChrome && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius - readonly property real surfaceBottomLeftRadius: usesConnectedFrameChrome && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius - readonly property real surfaceBottomRightRadius: usesConnectedFrameChrome && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius - readonly property real horizontalConnectorExtent: usesConnectedFrameChrome && !isVertical ? Theme.connectedCornerRadius : 0 - readonly property real verticalConnectorExtent: usesConnectedFrameChrome && isVertical ? Theme.connectedCornerRadius : 0 - - readonly property int hasApps: dockApps.implicitWidth > 0 || dockApps.implicitHeight > 0 - - readonly property real widgetHeight: SettingsData.dockIconSize - readonly property real effectiveBarHeight: dockGeometry.visualThickness - function getBarHeight(barConfig) { - if (!barConfig) - return 0; - const innerPadding = barConfig.innerPadding ?? 4; - const widgetThickness = Math.max(20, 26 + innerPadding * 0.6); - const barThickness = Math.max(widgetThickness + innerPadding + 4, Theme.barHeight - 4 - (8 - innerPadding)); - const spacing = barConfig.spacing ?? 4; - const bottomGap = barConfig.bottomGap ?? 0; - return barThickness + spacing + bottomGap; - } - - readonly property real barSpacing: { - const defaultBar = SettingsData.barConfigs[0] || SettingsData.getBarConfig("default"); - if (!defaultBar) - return 0; - - const barPos = defaultBar.position ?? SettingsData.Position.Top; - const barIsHorizontal = (barPos === SettingsData.Position.Top || barPos === SettingsData.Position.Bottom); - const barIsVertical = (barPos === SettingsData.Position.Left || barPos === SettingsData.Position.Right); - const samePosition = (SettingsData.dockPosition === barPos); - const dockIsHorizontal = !isVertical; - const dockIsVertical = isVertical; - - if (!(defaultBar.visible ?? true)) - return 0; - const spacing = defaultBar.spacing ?? 4; - const bottomGap = defaultBar.bottomGap ?? 0; - if (dockIsHorizontal && barIsHorizontal && samePosition) { - return spacing + effectiveBarHeight + bottomGap; - } - if (dockIsVertical && barIsVertical && samePosition) { - return spacing + effectiveBarHeight + bottomGap; - } - return 0; - } - - readonly property real adjacentTopBarHeight: { - if (!isVertical || autoHide) - return 0; - const screenName = dock.modelData?.name ?? ""; - const topBar = SettingsData.barConfigs.find(bc => { - if (!bc.enabled || bc.autoHide || !(bc.visible ?? true)) - return false; - if (bc.position !== SettingsData.Position.Top && bc.position !== 0) - return false; - const onThisScreen = bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all") || bc.screenPreferences.includes(screenName); - return onThisScreen; - }); - return getBarHeight(topBar); - } - - readonly property real adjacentLeftBarWidth: { - if (isVertical || autoHide) - return 0; - const screenName = dock.modelData?.name ?? ""; - const leftBar = SettingsData.barConfigs.find(bc => { - if (!bc.enabled || bc.autoHide || !(bc.visible ?? true)) - return false; - if (bc.position !== SettingsData.Position.Left && bc.position !== 2) - return false; - const onThisScreen = bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all") || bc.screenPreferences.includes(screenName); - return onThisScreen; - }); - return getBarHeight(leftBar); - } - - readonly property real dockMargin: SettingsData.dockMargin - readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled - readonly property real effectiveDockBottomGap: dockGeometry.visualOffset - readonly property real effectiveDockMargin: dockGeometry.effectiveMargin - readonly property real positionSpacing: barSpacing + effectiveDockBottomGap + effectiveDockMargin - readonly property real joinedEdgeMargin: dockGeometry.joinedEdgeMargin - readonly property real _dpr: (dock.screen && dock.screen.devicePixelRatio) ? dock.screen.devicePixelRatio : 1 - function px(v) { - return Math.round(v * _dpr) / _dpr; - } - - DockGeometry { - id: dockGeometry - - screen: dock.screen || dock.modelData - edge: dock.connectedBarSide - dockVisible: dock.visible - autoHide: dock.autoHide - iconSize: dock.widgetHeight - spacing: SettingsData.dockSpacing - borderThickness: dock.borderThickness - offset: SettingsData.dockBottomGap - margin: SettingsData.dockMargin - barSpacing: dock.barSpacing - dpr: dock._dpr - } - - // Dock window origin in screen-relative coordinates (FrameWindow space). - function _dockWindowOriginX() { - if (!dock.isVertical) - return 0; - if (SettingsData.dockPosition === SettingsData.Position.Right) - return (dock.screen ? dock.screen.width : 0) - dock.width; - return 0; - } - function _dockWindowOriginY() { - if (dock.isVertical) - return 0; - if (SettingsData.dockPosition === SettingsData.Position.Bottom) - return (dock.screen ? dock.screen.height : 0) - dock.height; - return 0; - } - - readonly property string _dockScreenName: dock.modelData ? dock.modelData.name : (dock.screen ? dock.screen.name : "") - readonly property bool usesConnectedFrameChrome: CompositorService.usesConnectedFrameChromeForScreen(dock._dockScreenName) - readonly property bool usesOverlayLayer: CompositorService.framePeerSurfacesUseOverlayForScreen(dock._dockScreenName) || SettingsData.dockUseOverlayLayer - - function _syncDockChromeState() { - if (!dock._dockScreenName) - return; - if (!dock.usesConnectedFrameChrome) { - ConnectedModeState.clearDockState(dock._dockScreenName); - return; - } - - const presented = dock.visible && (dock.reveal || slideXAnimation.running || slideYAnimation.running) && dock.hasApps; - const phase = !presented ? "hidden" : ((!dock.reveal && (slideXAnimation.running || slideYAnimation.running)) ? "closing" : ((slideXAnimation.running || slideYAnimation.running) ? "opening" : "open")); - const bodyX = dock._dockWindowOriginX() + dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x; - const bodyY = dock._dockWindowOriginY() + dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y; - const bodyW = dock.hasApps ? dockBackground.width : 0; - const bodyH = dock.hasApps ? dockBackground.height : 0; - ConnectedModeState.setDockState(dock._dockScreenName, { - "kind": "dock", - "screenName": dock._dockScreenName, - "phase": phase, - "visible": presented, - "presented": presented, - "reveal": presented, - "barSide": dock.connectedBarSide, - "bodyRect": { - "x": bodyX, - "y": bodyY, - "width": bodyW, - "height": bodyH - }, - "animationOffset": { - "x": dockSlide.x, - "y": dockSlide.y - }, - "scale": 1, - "opacity": Theme.connectedSurfaceColor.a, - "bodyX": bodyX, - "bodyY": bodyY, - "bodyW": bodyW, - "bodyH": bodyH, - "slideX": dockSlide.x, - "slideY": dockSlide.y - }); - } - - function _syncDockSlide() { - if (!dock._dockScreenName || !dock.usesConnectedFrameChrome) - return; - ConnectedModeState.setDockSlide(dock._dockScreenName, dockSlide.x, dockSlide.y); - } - - DeferredAction { - id: dockSlideSync - enabled: dock.usesConnectedFrameChrome - onTriggered: dock._syncDockSlide() - } - - function _queueSlideSync() { - if (!dock.usesConnectedFrameChrome) - return; - dockSlideSync.schedule(); - } - - DeferredAction { - id: dockChromeSync - onTriggered: dock._syncDockChromeState() - } - - property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData) - property bool revealSticky: false - - readonly property bool shouldHideForWindows: { - if (!SettingsData.dockSmartAutoHide) - return false; - if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango) - return false; - - const screenName = dock.modelData?.name ?? ""; - const dockThickness = dockGeometry.motionThickness; - const screenWidth = dock.screen?.width ?? 0; - const screenHeight = dock.screen?.height ?? 0; - - if (CompositorService.isNiri) { - NiriService.windows; - - let currentWorkspaceId = null; - for (let i = 0; i < NiriService.allWorkspaces.length; i++) { - const ws = NiriService.allWorkspaces[i]; - if (ws.output === screenName && ws.is_active) { - currentWorkspaceId = ws.id; - break; - } - } - - if (currentWorkspaceId === null) - return false; - - for (let i = 0; i < NiriService.windows.length; i++) { - const win = NiriService.windows[i]; - if (win.workspace_id !== currentWorkspaceId) - continue; - - // Get window position and size from layout data - const tilePos = win.layout?.tile_pos_in_workspace_view; - const winSize = win.layout?.window_size || win.layout?.tile_size; - - if (tilePos && winSize) { - const winX = tilePos[0]; - const winY = tilePos[1]; - const winW = winSize[0]; - const winH = winSize[1]; - - switch (SettingsData.dockPosition) { - case SettingsData.Position.Top: - if (winY < dockThickness) - return true; - break; - case SettingsData.Position.Bottom: - if (winY + winH > screenHeight - dockThickness) - return true; - break; - case SettingsData.Position.Left: - if (winX < dockThickness) - return true; - break; - case SettingsData.Position.Right: - if (winX + winW > screenWidth - dockThickness) - return true; - break; - } - } else if (!win.is_floating) { - return true; - } - } - - return false; - } - - if (CompositorService.isMango) { - MangoService.windows; - MangoService.outputs; - return CompositorService.mangoDockOverlapForSmartAutoHide(screenName, SettingsData.dockPosition, dockThickness, screenWidth, screenHeight); - } - - // Hyprland implementation (current workspace + visible special workspaces) - Hyprland.focusedWorkspace; - Hyprland.toplevels; - return CompositorService.hyprlandDockOverlapForSmartAutoHide(screenName, SettingsData.dockPosition, dockThickness, screenWidth, screenHeight); - } - - Timer { - id: revealHold - interval: 250 - repeat: false - onTriggered: dock.revealSticky = false - } - - // Flip `reveal` false when a modal claims this edge; reuses the slide animation - readonly property bool _modalRetractActive: { - if (!dock._dockScreenName) - return false; - return ConnectedModeState.dockRetractActiveForSide(dock._dockScreenName, dock.connectedBarSide); - } - - property bool startupRevealDone: false - - Timer { - id: startupRevealTimer - interval: 200 - running: true - onTriggered: dock.startupRevealDone = true - } - - property bool reveal: { - if (!startupRevealDone) - return false; - - if (_modalRetractActive) - return false; - - if (CompositorService.isNiri && NiriService.inOverview && SettingsData.dockOpenOnOverview) { - return true; - } - - // Smart auto-hide: show dock when no windows overlap, hide when they do - if (SettingsData.dockSmartAutoHide) { - if (shouldHideForWindows) - return dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen || revealSticky; - return true; // No overlapping windows - show dock - } - - // Regular auto-hide: always hide unless hovering - return !autoHide || dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen || revealSticky; - } - - onContextMenuOpenChanged: { - if (!contextMenuOpen && autoHide && !dockMouseArea.containsMouse) { - revealSticky = true; - revealHold.restart(); - } - } - - Component.onCompleted: dockChromeSync.schedule() - Component.onDestruction: { - dockChromeSync.cancel(); - dockSlideSync.cancel(); - ConnectedModeState.clearDockState(dock._dockScreenName); - } - - onRevealChanged: dock._syncDockChromeState() - onWidthChanged: dock._syncDockChromeState() - onHeightChanged: dock._syncDockChromeState() - onVisibleChanged: dock._syncDockChromeState() - onHasAppsChanged: dock._syncDockChromeState() - onConnectedBarSideChanged: dock._syncDockChromeState() - onUsesConnectedFrameChromeChanged: dock._syncDockChromeState() - - Connections { - target: SettingsData - function onConnectedFrameModeActiveChanged() { - dockSlideSync.cancel(); - dock._syncDockChromeState(); - } - } - - Connections { - target: SettingsData - function onDockTransparencyChanged() { - dock.backgroundTransparency = SettingsData.dockTransparency; - } - } screen: modelData + color: "transparent" + + WlrLayershell.namespace: "dms:dock" + WlrLayershell.layer: body.usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top + + anchors { + top: !body.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top) : true + bottom: !body.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom) : true + left: !body.isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Left) + right: !body.isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Right) + } + visible: { if (CompositorService.isNiri && NiriService.inOverview) { return SettingsData.dockOpenOnOverview; } return SettingsData.showDock; } - color: "transparent" - readonly property real dockReserveZone: dockGeometry.reserveZone - readonly property bool shouldReserveDockSpace: dockGeometry.shouldReserveSpace + implicitWidth: body.surfaceImplicitWidth + implicitHeight: body.surfaceImplicitHeight + exclusiveZone: body.surfaceExclusiveZone - exclusiveZone: { - if (!dock.shouldReserveDockSpace) - return -1; - if (dock.frameDockExclusionActive) - return -1; - return dock.dockReserveZone; - } - - property real animationHeadroom: Math.ceil(SettingsData.dockIconSize * 0.35) - - implicitWidth: isVertical ? (px(dockGeometry.surfaceThickness + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0 - implicitHeight: !isVertical ? (px(dockGeometry.surfaceThickness + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0 - - Item { - id: maskItem - parent: dock.contentItem - visible: false - readonly property bool expanded: dock.reveal - x: { - const baseX = dockCore.x + dockMouseArea.x; - if (isVertical && SettingsData.dockPosition === SettingsData.Position.Right) - return baseX - (expanded ? animationHeadroom + borderThickness + dock.horizontalConnectorExtent : 0); - return baseX - (expanded ? borderThickness + dock.horizontalConnectorExtent : 0); - } - y: { - const baseY = dockCore.y + dockMouseArea.y; - if (!isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom) - return baseY - (expanded ? animationHeadroom + borderThickness + dock.verticalConnectorExtent : 0); - return baseY - (expanded ? borderThickness + dock.verticalConnectorExtent : 0); - } - width: dockMouseArea.width + (isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.horizontalConnectorExtent * 2 : 0) - height: dockMouseArea.height + (!isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.verticalConnectorExtent * 2 : 0) + WindowBlur { + targetWindow: dock + blurEnabled: body.effectiveBlurEnabled && !body.usesConnectedFrameChrome + blurX: body.blurX + blurY: body.blurY + blurWidth: body.blurWidth + blurHeight: body.blurHeight + blurRadius: body.blurRadius } mask: Region { - item: maskItem + item: body.inputMaskItem + } + + DockBody { + id: body + anchors.fill: parent + hostWindow: dock + modelData: dock.modelData + contextMenu: dockVariants.contextMenu + trashContextMenu: dockVariants.trashContextMenu } PanelWindow { id: dockExclusion screen: dock.screen || dock.modelData - visible: dock.frameDockExclusionActive && dock.shouldReserveDockSpace + visible: body.frameDockExclusionActive && body.shouldReserveDockSpace color: "transparent" mask: Region {} - implicitWidth: dock.isVertical ? dock.dockReserveZone : 1 - implicitHeight: dock.isVertical ? 1 : dock.dockReserveZone - exclusiveZone: visible ? dock.dockReserveZone : -1 + implicitWidth: body.isVertical ? body.dockReserveZone : 1 + implicitHeight: body.isVertical ? 1 : body.dockReserveZone + exclusiveZone: visible ? body.dockReserveZone : -1 WlrLayershell.namespace: "dms:dock-exclusion" WlrLayershell.layer: WlrLayer.Top anchors { - top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top) : true - bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom) : true - left: !dock.isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Left) - right: !dock.isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Right) - } - } - - property var hoveredButton: { - if (!dockApps.children[0]) { - return null; - } - const layoutItem = dockApps.children[0]; - const flowLayout = layoutItem.children[0]; - let repeater = null; - for (var i = 0; i < flowLayout.children.length; i++) { - const child = flowLayout.children[i]; - if (child && typeof child.count !== "undefined" && typeof child.itemAt === "function") { - repeater = child; - break; - } - } - if (!repeater || !repeater.itemAt) { - return null; - } - for (var i = 0; i < repeater.count; i++) { - const item = repeater.itemAt(i); - if (item && item.dockButton && item.dockButton.showTooltip) { - return item.dockButton; - } - } - return null; - } - - DankTooltip { - id: dockTooltip - targetScreen: dock.screen - } - - Timer { - id: tooltipRevealDelay - interval: 250 - repeat: false - onTriggered: dock.showTooltipForHoveredButton() - } - - function showTooltipForHoveredButton() { - dockTooltip.hide(); - if (!dock.hoveredButton || !dock.reveal || slideXAnimation.running || slideYAnimation.running) - return; - - const buttonLocalPos = dock.hoveredButton.mapToItem(null, 0, 0); - const tooltipText = dock.hoveredButton.tooltipText || ""; - if (!tooltipText) - return; - - const screenHeight = dock.screen ? dock.screen.height : 0; - - const gap = Theme.spacingS; - const bgMargin = dockGeometry.bodyEdgeMargin; - const btnW = dock.hoveredButton.width; - const btnH = dock.hoveredButton.height; - - if (!dock.isVertical) { - const isBottom = SettingsData.dockPosition === SettingsData.Position.Bottom; - const tooltipX = buttonLocalPos.x + btnW / 2 + adjacentLeftBarWidth; - const tooltipHeight = 32; - const totalFromEdge = bgMargin + dockBackground.height + dock.borderThickness + gap; - const screenRelativeY = isBottom ? (screenHeight - totalFromEdge - tooltipHeight) : totalFromEdge; - dockTooltip.show(tooltipText, tooltipX, screenRelativeY, dock.screen, false, false); - return; - } - - const isLeft = SettingsData.dockPosition === SettingsData.Position.Left; - const screenWidth = dock.screen ? dock.screen.width : 0; - const totalFromEdge = bgMargin + dockBackground.width + dock.borderThickness + gap; - const tooltipX = isLeft ? totalFromEdge : (screenWidth - totalFromEdge); - const screenRelativeY = buttonLocalPos.y + btnH / 2 + adjacentTopBarHeight; - dockTooltip.show(tooltipText, tooltipX, screenRelativeY, dock.screen, isLeft, !isLeft); - } - - Connections { - target: dock - function onRevealChanged() { - if (!dock.reveal) { - tooltipRevealDelay.stop(); - dockTooltip.hide(); - } else { - tooltipRevealDelay.restart(); - } - } - - function onHoveredButtonChanged() { - dock.showTooltipForHoveredButton(); - } - } - - Item { - id: dockCore - anchors.fill: parent - x: isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? animationHeadroom : 0 - y: !isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? animationHeadroom : 0 - - Connections { - target: dockMouseArea - function onContainsMouseChanged() { - if (dockMouseArea.containsMouse) { - dock.revealSticky = true; - revealHold.stop(); - } else { - if (dock.autoHide && !dock.contextMenuOpen) { - revealHold.restart(); - } - } - } - } - - MouseArea { - id: dockMouseArea - property real currentScreen: modelData ? modelData : dock.screen - property real screenWidth: currentScreen ? currentScreen.geometry.width : 1920 - property real screenHeight: currentScreen ? currentScreen.geometry.height : 1080 - property real maxDockWidth: screenWidth * 0.98 - property real maxDockHeight: screenHeight * 0.98 - - height: { - if (dock.isVertical) { - // Keep the taller hit area regardless of the reveal state to prevent shrinking loop - return Math.min(Math.max(dockBackground.height + 64, 200), maxDockHeight); - } - return dock.reveal ? px(dockGeometry.motionThickness) : 1; - } - width: { - if (dock.isVertical) { - return dock.reveal ? px(dockGeometry.motionThickness) : 1; - } - // Keep the wider hit area regardless of the reveal state to prevent shrinking loop - return Math.min(dockBackground.width + 8 + dock.borderThickness, maxDockWidth); - } - anchors { - top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? undefined : parent.top) : undefined - bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined) : undefined - horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined - left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? undefined : parent.left) : undefined - right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined - verticalCenter: dock.isVertical ? parent.verticalCenter : undefined - } - hoverEnabled: true - acceptedButtons: Qt.NoButton - - Behavior on height { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Easing.OutCubic - } - } - - Behavior on width { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Easing.OutCubic - } - } - - Item { - id: dockContainer - anchors.fill: parent - clip: false - opacity: dock.startupRevealDone ? 1 : 0 - - transform: Translate { - id: dockSlide - x: { - if (!dock.isVertical) - return 0; - if (dock.reveal) - return 0; - if (dock.usesConnectedFrameChrome) { - const retractDist = dockBackground.width + SettingsData.dockSpacing + 10; - return SettingsData.dockPosition === SettingsData.Position.Right ? retractDist : -retractDist; - } - const hideDistance = dockGeometry.motionThickness + 10; - if (SettingsData.dockPosition === SettingsData.Position.Right) { - return hideDistance; - } else { - return -hideDistance; - } - } - y: { - if (dock.isVertical) - return 0; - if (dock.reveal) - return 0; - if (dock.usesConnectedFrameChrome) { - const retractDist = dockBackground.height + SettingsData.dockSpacing + 10; - return SettingsData.dockPosition === SettingsData.Position.Bottom ? retractDist : -retractDist; - } - const hideDistance = dockGeometry.motionThickness + 10; - if (SettingsData.dockPosition === SettingsData.Position.Bottom) { - return hideDistance; - } else { - return -hideDistance; - } - } - - Behavior on x { - NumberAnimation { - id: slideXAnimation - duration: dock.usesConnectedFrameChrome ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration - easing.type: dock.usesConnectedFrameChrome ? Easing.BezierSpline : Easing.OutCubic - easing.bezierCurve: dock.usesConnectedFrameChrome ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : [] - onRunningChanged: if (!running) - dock._syncDockChromeState() - } - } - - Behavior on y { - NumberAnimation { - id: slideYAnimation - duration: dock.usesConnectedFrameChrome ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration - easing.type: dock.usesConnectedFrameChrome ? Easing.BezierSpline : Easing.OutCubic - easing.bezierCurve: dock.usesConnectedFrameChrome ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : [] - onRunningChanged: if (!running) - dock._syncDockChromeState() - } - } - - onXChanged: dock._queueSlideSync() - onYChanged: dock._queueSlideSync() - } - - Item { - id: dockBackground - objectName: "dockBackground" - visible: dock.hasApps - anchors { - top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? parent.top : undefined) : undefined - bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined) : undefined - horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined - left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined) : undefined - right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined - verticalCenter: dock.isVertical ? parent.verticalCenter : undefined - } - anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? dockGeometry.bodyEdgeMargin : 0 - anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? dockGeometry.bodyEdgeMargin : 0 - anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? dockGeometry.bodyEdgeMargin : 0 - anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? dockGeometry.bodyEdgeMargin : 0 - - implicitWidth: dock.isVertical ? (dockApps.implicitHeight + SettingsData.dockSpacing * 2) : (dockApps.implicitWidth + SettingsData.dockSpacing * 2) - implicitHeight: dock.isVertical ? (dockApps.implicitWidth + SettingsData.dockSpacing * 2) : (dockApps.implicitHeight + SettingsData.dockSpacing * 2) - width: implicitWidth - height: implicitHeight - - // Avoid an offscreen texture seam where the connected dock meets the frame. - layer.enabled: !usesConnectedFrameChrome - clip: false - - Rectangle { - anchors.fill: parent - visible: !usesConnectedFrameChrome && (!FrameTransitionState.effectiveConnectedFrameModeActive || dock.reveal) - color: dock.surfaceColor - topLeftRadius: dock.surfaceTopLeftRadius - topRightRadius: dock.surfaceTopRightRadius - bottomLeftRadius: dock.surfaceBottomLeftRadius - bottomRightRadius: dock.surfaceBottomRightRadius - } - - Rectangle { - anchors.fill: parent - visible: !usesConnectedFrameChrome && (!FrameTransitionState.effectiveConnectedFrameModeActive || dock.reveal) - color: "transparent" - topLeftRadius: dock.surfaceTopLeftRadius - topRightRadius: dock.surfaceTopRightRadius - bottomLeftRadius: dock.surfaceBottomLeftRadius - bottomRightRadius: dock.surfaceBottomRightRadius - border.color: dock.surfaceBorderColor - border.width: dock.surfaceBorderWidth - z: 100 - } - - // Sync dockBackground geometry to ConnectedModeState - onXChanged: dock._syncDockChromeState() - onYChanged: dock._syncDockChromeState() - onWidthChanged: dock._syncDockChromeState() - onHeightChanged: dock._syncDockChromeState() - } - - Item { - id: dockConnectedChrome - visible: Theme.isConnectedEffect && dock.reveal && dock.hasApps && !FrameTransitionState.effectiveConnectedFrameModeActive - readonly property real extraLeft: dock.isVertical ? 0 : Theme.connectedCornerRadius - readonly property real extraTop: dock.isVertical ? Theme.connectedCornerRadius : 0 - readonly property real bodyRadius: dock.surfaceRadius - readonly property bool barTop: dock.connectedBarSide === "top" - readonly property bool barBottom: dock.connectedBarSide === "bottom" - readonly property bool barLeft: dock.connectedBarSide === "left" - readonly property bool barRight: dock.connectedBarSide === "right" - - x: dockBackground.x - extraLeft - y: dockBackground.y - extraTop - width: dockBackground.width + extraLeft * 2 - height: dockBackground.height + extraTop * 2 - - ShaderEffect { - anchors.fill: parent - fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/connected_chrome.frag.qsb") - - property real widthPx: width - property real heightPx: height - property vector4d surfaceColor: Qt.vector4d(dock.surfaceColor.r, dock.surfaceColor.g, dock.surfaceColor.b, dock.surfaceColor.a) - property vector4d shadowColor: Qt.vector4d(0, 0, 0, 0) - property vector4d shadowParam: Qt.vector4d(0, 0, 0, 0) - property vector4d ambientParam: Qt.vector4d(0, 0, 0, 0) - property vector4d bodyRect: Qt.vector4d(dockConnectedChrome.extraLeft, dockConnectedChrome.extraTop, dockBackground.width, dockBackground.height) - property vector4d cornerRadius: Qt.vector4d(dockConnectedChrome.barTop || dockConnectedChrome.barLeft ? 0 : dockConnectedChrome.bodyRadius, dockConnectedChrome.barTop || dockConnectedChrome.barRight ? 0 : dockConnectedChrome.bodyRadius, dockConnectedChrome.barBottom || dockConnectedChrome.barRight ? 0 : dockConnectedChrome.bodyRadius, dockConnectedChrome.barBottom || dockConnectedChrome.barLeft ? 0 : dockConnectedChrome.bodyRadius) - property vector4d edgeParam: Qt.vector4d(dockConnectedChrome.barTop ? 0 : (dockConnectedChrome.barBottom ? 1 : (dockConnectedChrome.barLeft ? 2 : 3)), Theme.connectedCornerRadius, 0, 0) - } - } - - Shape { - id: dockBorderShape - x: dockBackground.x - borderThickness - y: dockBackground.y - borderThickness - width: dockBackground.width + borderThickness * 2 - height: dockBackground.height + borderThickness * 2 - visible: SettingsData.dockBorderEnabled && dock.hasApps && !usesConnectedFrameChrome - preferredRendererType: Shape.CurveRenderer - - readonly property real borderThickness: Math.max(1, dock.borderThickness) - readonly property real i: borderThickness / 2 - readonly property real cr: dock.surfaceRadius - readonly property real w: dockBackground.width - readonly property real h: dockBackground.height - - readonly property color borderColor: { - const opacity = SettingsData.dockBorderOpacity; - switch (SettingsData.dockBorderColor) { - case "secondary": - return Theme.withAlpha(Theme.secondary, opacity); - case "primary": - return Theme.withAlpha(Theme.primary, opacity); - default: - return Theme.withAlpha(Theme.surfaceText, opacity); - } - } - - ShapePath { - fillColor: "transparent" - strokeColor: dockBorderShape.borderColor - strokeWidth: dockBorderShape.borderThickness - joinStyle: ShapePath.RoundJoin - capStyle: ShapePath.FlatCap - - PathSvg { - path: { - const bt = dockBorderShape.borderThickness; - const i = dockBorderShape.i; - const cr = dockBorderShape.cr + bt - i; - const w = dockBorderShape.w; - const h = dockBorderShape.h; - - let d = `M ${i + cr} ${i}`; - d += ` L ${i + w + 2 * (bt - i) - cr} ${i}`; - if (cr > 0) - d += ` A ${cr} ${cr} 0 0 1 ${i + w + 2 * (bt - i)} ${i + cr}`; - d += ` L ${i + w + 2 * (bt - i)} ${i + h + 2 * (bt - i) - cr}`; - if (cr > 0) - d += ` A ${cr} ${cr} 0 0 1 ${i + w + 2 * (bt - i) - cr} ${i + h + 2 * (bt - i)}`; - d += ` L ${i + cr} ${i + h + 2 * (bt - i)}`; - if (cr > 0) - d += ` A ${cr} ${cr} 0 0 1 ${i} ${i + h + 2 * (bt - i) - cr}`; - d += ` L ${i} ${i + cr}`; - if (cr > 0) - d += ` A ${cr} ${cr} 0 0 1 ${i + cr} ${i}`; - d += " Z"; - return d; - } - } - } - } - - DockApps { - id: dockApps - - anchors.top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? dockBackground.top : undefined) : undefined - anchors.bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? dockBackground.bottom : undefined) : undefined - anchors.horizontalCenter: !dock.isVertical ? dockBackground.horizontalCenter : undefined - anchors.left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? dockBackground.left : undefined) : undefined - anchors.right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? dockBackground.right : undefined) : undefined - anchors.verticalCenter: dock.isVertical ? dockBackground.verticalCenter : undefined - anchors.topMargin: !dock.isVertical ? SettingsData.dockSpacing : 0 - anchors.bottomMargin: !dock.isVertical ? SettingsData.dockSpacing : 0 - anchors.leftMargin: dock.isVertical ? SettingsData.dockSpacing : 0 - anchors.rightMargin: dock.isVertical ? SettingsData.dockSpacing : 0 - - contextMenu: dockVariants.contextMenu - trashContextMenu: dockVariants.trashContextMenu - groupByApp: dock.groupByApp - isVertical: dock.isVertical - dockScreen: dock.screen - iconSize: dock.widgetHeight - usesOverlayLayer: dock.usesOverlayLayer - } - } + top: !body.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top) : true + bottom: !body.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom) : true + left: !body.isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Left) + right: !body.isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Right) } } } diff --git a/quickshell/Modules/Dock/DockBody.qml b/quickshell/Modules/Dock/DockBody.qml new file mode 100644 index 000000000..09625f804 --- /dev/null +++ b/quickshell/Modules/Dock/DockBody.qml @@ -0,0 +1,829 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Shapes +import Quickshell.Hyprland +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: dock + + required property var hostWindow + required property var modelData + readonly property var screen: modelData + property var contextMenu + property var trashContextMenu + + readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right + + property bool autoHide: SettingsData.dockAutoHide || SettingsData.dockSmartAutoHide + property real backgroundTransparency: SettingsData.dockTransparency + property bool groupByApp: SettingsData.dockGroupByApp + readonly property int borderThickness: SettingsData.dockBorderEnabled ? SettingsData.dockBorderThickness : 0 + readonly property string connectedBarSide: SettingsData.dockPosition === SettingsData.Position.Top ? "top" : SettingsData.dockPosition === SettingsData.Position.Bottom ? "bottom" : SettingsData.dockPosition === SettingsData.Position.Left ? "left" : "right" + readonly property bool frameDockExclusionActive: dockGeometry.frameExclusionActive + readonly property bool connectedBarActiveOnEdge: dockGeometry.connectedBarActiveOnEdge + readonly property real connectedJoinInset: dockGeometry.connectedJoinInset + readonly property real dockFrameInset: dockGeometry.frameInset + readonly property real surfaceRadius: usesConnectedFrameChrome ? Theme.connectedSurfaceRadius : Theme.cornerRadius + readonly property color surfaceColor: usesConnectedFrameChrome ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency) + readonly property color surfaceBorderColor: usesConnectedFrameChrome ? Theme.withAlpha(BlurService.borderColor, 0) : BlurService.borderColor + readonly property real surfaceBorderWidth: usesConnectedFrameChrome ? 0 : BlurService.borderWidth + readonly property real surfaceTopLeftRadius: usesConnectedFrameChrome && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius + readonly property real surfaceTopRightRadius: usesConnectedFrameChrome && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius + readonly property real surfaceBottomLeftRadius: usesConnectedFrameChrome && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius + readonly property real surfaceBottomRightRadius: usesConnectedFrameChrome && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius + readonly property real horizontalConnectorExtent: usesConnectedFrameChrome && !isVertical ? Theme.connectedCornerRadius : 0 + readonly property real verticalConnectorExtent: usesConnectedFrameChrome && isVertical ? Theme.connectedCornerRadius : 0 + + readonly property int hasApps: dockApps.implicitWidth > 0 || dockApps.implicitHeight > 0 + + readonly property real widgetHeight: SettingsData.dockIconSize + readonly property real effectiveBarHeight: dockGeometry.visualThickness + function getBarHeight(barConfig) { + if (!barConfig) + return 0; + const innerPadding = barConfig.innerPadding ?? 4; + const widgetThickness = Math.max(20, 26 + innerPadding * 0.6); + const barThickness = Math.max(widgetThickness + innerPadding + 4, Theme.barHeight - 4 - (8 - innerPadding)); + const spacing = barConfig.spacing ?? 4; + const bottomGap = barConfig.bottomGap ?? 0; + return barThickness + spacing + bottomGap; + } + + readonly property real barSpacing: { + const defaultBar = SettingsData.barConfigs[0] || SettingsData.getBarConfig("default"); + if (!defaultBar) + return 0; + + const barPos = defaultBar.position ?? SettingsData.Position.Top; + const barIsHorizontal = (barPos === SettingsData.Position.Top || barPos === SettingsData.Position.Bottom); + const barIsVertical = (barPos === SettingsData.Position.Left || barPos === SettingsData.Position.Right); + const samePosition = (SettingsData.dockPosition === barPos); + const dockIsHorizontal = !isVertical; + const dockIsVertical = isVertical; + + if (!(defaultBar.visible ?? true)) + return 0; + const spacing = defaultBar.spacing ?? 4; + const bottomGap = defaultBar.bottomGap ?? 0; + if (dockIsHorizontal && barIsHorizontal && samePosition) { + return spacing + effectiveBarHeight + bottomGap; + } + if (dockIsVertical && barIsVertical && samePosition) { + return spacing + effectiveBarHeight + bottomGap; + } + return 0; + } + + readonly property real adjacentTopBarHeight: { + if (!isVertical || autoHide) + return 0; + const screenName = dock.modelData?.name ?? ""; + const topBar = SettingsData.barConfigs.find(bc => { + if (!bc.enabled || bc.autoHide || !(bc.visible ?? true)) + return false; + if (bc.position !== SettingsData.Position.Top && bc.position !== 0) + return false; + const onThisScreen = bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all") || bc.screenPreferences.includes(screenName); + return onThisScreen; + }); + return getBarHeight(topBar); + } + + readonly property real adjacentLeftBarWidth: { + if (isVertical || autoHide) + return 0; + const screenName = dock.modelData?.name ?? ""; + const leftBar = SettingsData.barConfigs.find(bc => { + if (!bc.enabled || bc.autoHide || !(bc.visible ?? true)) + return false; + if (bc.position !== SettingsData.Position.Left && bc.position !== 2) + return false; + const onThisScreen = bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all") || bc.screenPreferences.includes(screenName); + return onThisScreen; + }); + return getBarHeight(leftBar); + } + + readonly property real dockMargin: SettingsData.dockMargin + readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled + readonly property real effectiveDockBottomGap: dockGeometry.visualOffset + readonly property real effectiveDockMargin: dockGeometry.effectiveMargin + readonly property real positionSpacing: barSpacing + effectiveDockBottomGap + effectiveDockMargin + readonly property real joinedEdgeMargin: dockGeometry.joinedEdgeMargin + readonly property real _dpr: (dock.screen && dock.screen.devicePixelRatio) ? dock.screen.devicePixelRatio : 1 + function px(v) { + return Math.round(v * _dpr) / _dpr; + } + + DockGeometry { + id: dockGeometry + + screen: dock.screen || dock.modelData + edge: dock.connectedBarSide + dockVisible: dock.visible + autoHide: dock.autoHide + iconSize: dock.widgetHeight + spacing: SettingsData.dockSpacing + borderThickness: dock.borderThickness + offset: SettingsData.dockBottomGap + margin: SettingsData.dockMargin + barSpacing: dock.barSpacing + dpr: dock._dpr + } + + // Dock window origin in screen-relative coordinates (FrameWindow space). + function _dockWindowOriginX() { + if (!dock.isVertical) + return 0; + if (SettingsData.dockPosition === SettingsData.Position.Right) + return (dock.screen ? dock.screen.width : 0) - dock.width; + return 0; + } + function _dockWindowOriginY() { + if (dock.isVertical) + return 0; + if (SettingsData.dockPosition === SettingsData.Position.Bottom) + return (dock.screen ? dock.screen.height : 0) - dock.height; + return 0; + } + + readonly property string _dockScreenName: dock.modelData ? dock.modelData.name : (dock.screen ? dock.screen.name : "") + readonly property bool usesConnectedFrameChrome: CompositorService.usesConnectedFrameChromeForScreen(dock._dockScreenName) + readonly property bool usesOverlayLayer: CompositorService.framePeerSurfacesUseOverlayForScreen(dock._dockScreenName) || SettingsData.dockUseOverlayLayer + + function _syncDockChromeState() { + if (!dock._dockScreenName) + return; + if (!dock.usesConnectedFrameChrome) { + ConnectedModeState.clearDockState(dock._dockScreenName); + return; + } + + const presented = dock.visible && (dock.reveal || slideXAnimation.running || slideYAnimation.running) && dock.hasApps; + const phase = !presented ? "hidden" : ((!dock.reveal && (slideXAnimation.running || slideYAnimation.running)) ? "closing" : ((slideXAnimation.running || slideYAnimation.running) ? "opening" : "open")); + const bodyX = dock._dockWindowOriginX() + dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x; + const bodyY = dock._dockWindowOriginY() + dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y; + const bodyW = dock.hasApps ? dockBackground.width : 0; + const bodyH = dock.hasApps ? dockBackground.height : 0; + ConnectedModeState.setDockState(dock._dockScreenName, { + "kind": "dock", + "screenName": dock._dockScreenName, + "phase": phase, + "visible": presented, + "presented": presented, + "reveal": presented, + "barSide": dock.connectedBarSide, + "bodyRect": { + "x": bodyX, + "y": bodyY, + "width": bodyW, + "height": bodyH + }, + "animationOffset": { + "x": dockSlide.x, + "y": dockSlide.y + }, + "scale": 1, + "opacity": Theme.connectedSurfaceColor.a, + "bodyX": bodyX, + "bodyY": bodyY, + "bodyW": bodyW, + "bodyH": bodyH, + "slideX": dockSlide.x, + "slideY": dockSlide.y + }); + } + + function _syncDockSlide() { + if (!dock._dockScreenName || !dock.usesConnectedFrameChrome) + return; + ConnectedModeState.setDockSlide(dock._dockScreenName, dockSlide.x, dockSlide.y); + } + + DeferredAction { + id: dockSlideSync + enabled: dock.usesConnectedFrameChrome + onTriggered: dock._syncDockSlide() + } + + function _queueSlideSync() { + if (!dock.usesConnectedFrameChrome) + return; + dockSlideSync.schedule(); + } + + DeferredAction { + id: dockChromeSync + onTriggered: dock._syncDockChromeState() + } + + property bool contextMenuOpen: (dock.contextMenu && dock.contextMenu.visible && dock.contextMenu.screen === modelData) + property bool revealSticky: false + + readonly property bool shouldHideForWindows: { + if (!SettingsData.dockSmartAutoHide) + return false; + if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango) + return false; + + const screenName = dock.modelData?.name ?? ""; + const dockThickness = dockGeometry.motionThickness; + const screenWidth = dock.screen?.width ?? 0; + const screenHeight = dock.screen?.height ?? 0; + + if (CompositorService.isNiri) { + NiriService.windows; + + let currentWorkspaceId = null; + for (let i = 0; i < NiriService.allWorkspaces.length; i++) { + const ws = NiriService.allWorkspaces[i]; + if (ws.output === screenName && ws.is_active) { + currentWorkspaceId = ws.id; + break; + } + } + + if (currentWorkspaceId === null) + return false; + + for (let i = 0; i < NiriService.windows.length; i++) { + const win = NiriService.windows[i]; + if (win.workspace_id !== currentWorkspaceId) + continue; + + // Get window position and size from layout data + const tilePos = win.layout?.tile_pos_in_workspace_view; + const winSize = win.layout?.window_size || win.layout?.tile_size; + + if (tilePos && winSize) { + const winX = tilePos[0]; + const winY = tilePos[1]; + const winW = winSize[0]; + const winH = winSize[1]; + + switch (SettingsData.dockPosition) { + case SettingsData.Position.Top: + if (winY < dockThickness) + return true; + break; + case SettingsData.Position.Bottom: + if (winY + winH > screenHeight - dockThickness) + return true; + break; + case SettingsData.Position.Left: + if (winX < dockThickness) + return true; + break; + case SettingsData.Position.Right: + if (winX + winW > screenWidth - dockThickness) + return true; + break; + } + } else if (!win.is_floating) { + return true; + } + } + + return false; + } + + if (CompositorService.isMango) { + MangoService.windows; + MangoService.outputs; + return CompositorService.mangoDockOverlapForSmartAutoHide(screenName, SettingsData.dockPosition, dockThickness, screenWidth, screenHeight); + } + + // Hyprland implementation (current workspace + visible special workspaces) + Hyprland.focusedWorkspace; + Hyprland.toplevels; + return CompositorService.hyprlandDockOverlapForSmartAutoHide(screenName, SettingsData.dockPosition, dockThickness, screenWidth, screenHeight); + } + + Timer { + id: revealHold + interval: 250 + repeat: false + onTriggered: dock.revealSticky = false + } + + // Flip `reveal` false when a modal claims this edge; reuses the slide animation + readonly property bool _modalRetractActive: { + if (!dock._dockScreenName) + return false; + return ConnectedModeState.dockRetractActiveForSide(dock._dockScreenName, dock.connectedBarSide); + } + + property bool startupRevealDone: false + + Timer { + id: startupRevealTimer + interval: 200 + running: true + onTriggered: dock.startupRevealDone = true + } + + property bool reveal: { + if (!startupRevealDone) + return false; + + if (_modalRetractActive) + return false; + + if (CompositorService.isNiri && NiriService.inOverview && SettingsData.dockOpenOnOverview) { + return true; + } + + // Smart auto-hide: show dock when no windows overlap, hide when they do + if (SettingsData.dockSmartAutoHide) { + if (shouldHideForWindows) + return dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen || revealSticky; + return true; // No overlapping windows - show dock + } + + // Regular auto-hide: always hide unless hovering + return !autoHide || dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen || revealSticky; + } + + onContextMenuOpenChanged: { + if (!contextMenuOpen && autoHide && !dockMouseArea.containsMouse) { + revealSticky = true; + revealHold.restart(); + } + } + + Component.onCompleted: dockChromeSync.schedule() + Component.onDestruction: { + dockChromeSync.cancel(); + dockSlideSync.cancel(); + ConnectedModeState.clearDockState(dock._dockScreenName); + } + + onRevealChanged: dock._syncDockChromeState() + onWidthChanged: dock._syncDockChromeState() + onHeightChanged: dock._syncDockChromeState() + onVisibleChanged: dock._syncDockChromeState() + onHasAppsChanged: dock._syncDockChromeState() + onConnectedBarSideChanged: dock._syncDockChromeState() + onUsesConnectedFrameChromeChanged: dock._syncDockChromeState() + + Connections { + target: SettingsData + function onConnectedFrameModeActiveChanged() { + dockSlideSync.cancel(); + dock._syncDockChromeState(); + } + } + + Connections { + target: SettingsData + function onDockTransparencyChanged() { + dock.backgroundTransparency = SettingsData.dockTransparency; + } + } + + readonly property real dockReserveZone: dockGeometry.reserveZone + readonly property bool shouldReserveDockSpace: dockGeometry.shouldReserveSpace + + readonly property real surfaceExclusiveZone: { + if (!dock.shouldReserveDockSpace) + return -1; + if (dock.frameDockExclusionActive) + return -1; + return dock.dockReserveZone; + } + + property real animationHeadroom: Math.ceil(SettingsData.dockIconSize * 0.35) + + readonly property real surfaceImplicitWidth: isVertical ? (px(dockGeometry.surfaceThickness + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0 + readonly property real surfaceImplicitHeight: !isVertical ? (px(dockGeometry.surfaceThickness + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0 + + readonly property real blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x + readonly property real blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y + readonly property real blurWidth: dock.hasApps && dock.reveal ? dockBackground.width : 0 + readonly property real blurHeight: dock.hasApps && dock.reveal ? dockBackground.height : 0 + readonly property real blurRadius: dock.usesConnectedFrameChrome ? Theme.connectedCornerRadius : dock.surfaceRadius + + Item { + id: maskItem + visible: false + readonly property bool expanded: dock.reveal + x: { + const baseX = dockCore.x + dockMouseArea.x; + if (isVertical && SettingsData.dockPosition === SettingsData.Position.Right) + return baseX - (expanded ? animationHeadroom + borderThickness + dock.horizontalConnectorExtent : 0); + return baseX - (expanded ? borderThickness + dock.horizontalConnectorExtent : 0); + } + y: { + const baseY = dockCore.y + dockMouseArea.y; + if (!isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom) + return baseY - (expanded ? animationHeadroom + borderThickness + dock.verticalConnectorExtent : 0); + return baseY - (expanded ? borderThickness + dock.verticalConnectorExtent : 0); + } + width: dockMouseArea.width + (isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.horizontalConnectorExtent * 2 : 0) + height: dockMouseArea.height + (!isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.verticalConnectorExtent * 2 : 0) + } + + readonly property alias inputMaskItem: maskItem + + property var hoveredButton: { + if (!dockApps.children[0]) { + return null; + } + const layoutItem = dockApps.children[0]; + const flowLayout = layoutItem.children[0]; + let repeater = null; + for (var i = 0; i < flowLayout.children.length; i++) { + const child = flowLayout.children[i]; + if (child && typeof child.count !== "undefined" && typeof child.itemAt === "function") { + repeater = child; + break; + } + } + if (!repeater || !repeater.itemAt) { + return null; + } + for (var i = 0; i < repeater.count; i++) { + const item = repeater.itemAt(i); + if (item && item.dockButton && item.dockButton.showTooltip) { + return item.dockButton; + } + } + return null; + } + + DankTooltip { + id: dockTooltip + targetScreen: dock.screen + } + + Timer { + id: tooltipRevealDelay + interval: 250 + repeat: false + onTriggered: dock.showTooltipForHoveredButton() + } + + function showTooltipForHoveredButton() { + dockTooltip.hide(); + if (!dock.hoveredButton || !dock.reveal || slideXAnimation.running || slideYAnimation.running) + return; + + const buttonLocalPos = dock.hoveredButton.mapToItem(null, 0, 0); + const tooltipText = dock.hoveredButton.tooltipText || ""; + if (!tooltipText) + return; + + const screenHeight = dock.screen ? dock.screen.height : 0; + + const gap = Theme.spacingS; + const bgMargin = dockGeometry.bodyEdgeMargin; + const btnW = dock.hoveredButton.width; + const btnH = dock.hoveredButton.height; + + if (!dock.isVertical) { + const isBottom = SettingsData.dockPosition === SettingsData.Position.Bottom; + const tooltipX = buttonLocalPos.x + btnW / 2 + adjacentLeftBarWidth; + const tooltipHeight = 32; + const totalFromEdge = bgMargin + dockBackground.height + dock.borderThickness + gap; + const screenRelativeY = isBottom ? (screenHeight - totalFromEdge - tooltipHeight) : totalFromEdge; + dockTooltip.show(tooltipText, tooltipX, screenRelativeY, dock.screen, false, false); + return; + } + + const isLeft = SettingsData.dockPosition === SettingsData.Position.Left; + const screenWidth = dock.screen ? dock.screen.width : 0; + const totalFromEdge = bgMargin + dockBackground.width + dock.borderThickness + gap; + const tooltipX = isLeft ? totalFromEdge : (screenWidth - totalFromEdge); + const screenRelativeY = buttonLocalPos.y + btnH / 2 + adjacentTopBarHeight; + dockTooltip.show(tooltipText, tooltipX, screenRelativeY, dock.screen, isLeft, !isLeft); + } + + Connections { + target: dock + function onRevealChanged() { + if (!dock.reveal) { + tooltipRevealDelay.stop(); + dockTooltip.hide(); + } else { + tooltipRevealDelay.restart(); + } + } + + function onHoveredButtonChanged() { + dock.showTooltipForHoveredButton(); + } + } + + Item { + id: dockCore + anchors.fill: parent + x: isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? animationHeadroom : 0 + y: !isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? animationHeadroom : 0 + + Connections { + target: dockMouseArea + function onContainsMouseChanged() { + if (dockMouseArea.containsMouse) { + dock.revealSticky = true; + revealHold.stop(); + } else { + if (dock.autoHide && !dock.contextMenuOpen) { + revealHold.restart(); + } + } + } + } + + MouseArea { + id: dockMouseArea + property real currentScreen: modelData ? modelData : dock.screen + property real screenWidth: currentScreen ? currentScreen.geometry.width : 1920 + property real screenHeight: currentScreen ? currentScreen.geometry.height : 1080 + property real maxDockWidth: screenWidth * 0.98 + property real maxDockHeight: screenHeight * 0.98 + + height: { + if (dock.isVertical) { + // Keep the taller hit area regardless of the reveal state to prevent shrinking loop + return Math.min(Math.max(dockBackground.height + 64, 200), maxDockHeight); + } + return dock.reveal ? px(dockGeometry.motionThickness) : 1; + } + width: { + if (dock.isVertical) { + return dock.reveal ? px(dockGeometry.motionThickness) : 1; + } + // Keep the wider hit area regardless of the reveal state to prevent shrinking loop + return Math.min(dockBackground.width + 8 + dock.borderThickness, maxDockWidth); + } + anchors { + top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? undefined : parent.top) : undefined + bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined) : undefined + horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined + left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? undefined : parent.left) : undefined + right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined + verticalCenter: dock.isVertical ? parent.verticalCenter : undefined + } + hoverEnabled: true + acceptedButtons: Qt.NoButton + + Behavior on height { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } + + Behavior on width { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } + + Item { + id: dockContainer + anchors.fill: parent + clip: false + opacity: dock.startupRevealDone ? 1 : 0 + + transform: Translate { + id: dockSlide + x: { + if (!dock.isVertical) + return 0; + if (dock.reveal) + return 0; + if (dock.usesConnectedFrameChrome) { + const retractDist = dockBackground.width + SettingsData.dockSpacing + 10; + return SettingsData.dockPosition === SettingsData.Position.Right ? retractDist : -retractDist; + } + const hideDistance = dockGeometry.motionThickness + 10; + if (SettingsData.dockPosition === SettingsData.Position.Right) { + return hideDistance; + } else { + return -hideDistance; + } + } + y: { + if (dock.isVertical) + return 0; + if (dock.reveal) + return 0; + if (dock.usesConnectedFrameChrome) { + const retractDist = dockBackground.height + SettingsData.dockSpacing + 10; + return SettingsData.dockPosition === SettingsData.Position.Bottom ? retractDist : -retractDist; + } + const hideDistance = dockGeometry.motionThickness + 10; + if (SettingsData.dockPosition === SettingsData.Position.Bottom) { + return hideDistance; + } else { + return -hideDistance; + } + } + + Behavior on x { + NumberAnimation { + id: slideXAnimation + duration: dock.usesConnectedFrameChrome ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration + easing.type: dock.usesConnectedFrameChrome ? Easing.BezierSpline : Easing.OutCubic + easing.bezierCurve: dock.usesConnectedFrameChrome ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : [] + onRunningChanged: if (!running) + dock._syncDockChromeState() + } + } + + Behavior on y { + NumberAnimation { + id: slideYAnimation + duration: dock.usesConnectedFrameChrome ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration + easing.type: dock.usesConnectedFrameChrome ? Easing.BezierSpline : Easing.OutCubic + easing.bezierCurve: dock.usesConnectedFrameChrome ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : [] + onRunningChanged: if (!running) + dock._syncDockChromeState() + } + } + + onXChanged: dock._queueSlideSync() + onYChanged: dock._queueSlideSync() + } + + Item { + id: dockBackground + objectName: "dockBackground" + visible: dock.hasApps + anchors { + top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? parent.top : undefined) : undefined + bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined) : undefined + horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined + left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined) : undefined + right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined + verticalCenter: dock.isVertical ? parent.verticalCenter : undefined + } + anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? dockGeometry.bodyEdgeMargin : 0 + anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? dockGeometry.bodyEdgeMargin : 0 + anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? dockGeometry.bodyEdgeMargin : 0 + anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? dockGeometry.bodyEdgeMargin : 0 + + implicitWidth: dock.isVertical ? (dockApps.implicitHeight + SettingsData.dockSpacing * 2) : (dockApps.implicitWidth + SettingsData.dockSpacing * 2) + implicitHeight: dock.isVertical ? (dockApps.implicitWidth + SettingsData.dockSpacing * 2) : (dockApps.implicitHeight + SettingsData.dockSpacing * 2) + width: implicitWidth + height: implicitHeight + + // Avoid an offscreen texture seam where the connected dock meets the frame. + layer.enabled: !usesConnectedFrameChrome + clip: false + + Rectangle { + anchors.fill: parent + visible: !usesConnectedFrameChrome && (!FrameTransitionState.effectiveConnectedFrameModeActive || dock.reveal) + color: dock.surfaceColor + topLeftRadius: dock.surfaceTopLeftRadius + topRightRadius: dock.surfaceTopRightRadius + bottomLeftRadius: dock.surfaceBottomLeftRadius + bottomRightRadius: dock.surfaceBottomRightRadius + } + + Rectangle { + anchors.fill: parent + visible: !usesConnectedFrameChrome && (!FrameTransitionState.effectiveConnectedFrameModeActive || dock.reveal) + color: "transparent" + topLeftRadius: dock.surfaceTopLeftRadius + topRightRadius: dock.surfaceTopRightRadius + bottomLeftRadius: dock.surfaceBottomLeftRadius + bottomRightRadius: dock.surfaceBottomRightRadius + border.color: dock.surfaceBorderColor + border.width: dock.surfaceBorderWidth + z: 100 + } + + // Sync dockBackground geometry to ConnectedModeState + onXChanged: dock._syncDockChromeState() + onYChanged: dock._syncDockChromeState() + onWidthChanged: dock._syncDockChromeState() + onHeightChanged: dock._syncDockChromeState() + } + + Item { + id: dockConnectedChrome + visible: Theme.isConnectedEffect && dock.reveal && dock.hasApps && !FrameTransitionState.effectiveConnectedFrameModeActive + readonly property real extraLeft: dock.isVertical ? 0 : Theme.connectedCornerRadius + readonly property real extraTop: dock.isVertical ? Theme.connectedCornerRadius : 0 + readonly property real bodyRadius: dock.surfaceRadius + readonly property bool barTop: dock.connectedBarSide === "top" + readonly property bool barBottom: dock.connectedBarSide === "bottom" + readonly property bool barLeft: dock.connectedBarSide === "left" + readonly property bool barRight: dock.connectedBarSide === "right" + + x: dockBackground.x - extraLeft + y: dockBackground.y - extraTop + width: dockBackground.width + extraLeft * 2 + height: dockBackground.height + extraTop * 2 + + ShaderEffect { + anchors.fill: parent + fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/connected_chrome.frag.qsb") + + property real widthPx: width + property real heightPx: height + property vector4d surfaceColor: Qt.vector4d(dock.surfaceColor.r, dock.surfaceColor.g, dock.surfaceColor.b, dock.surfaceColor.a) + property vector4d shadowColor: Qt.vector4d(0, 0, 0, 0) + property vector4d shadowParam: Qt.vector4d(0, 0, 0, 0) + property vector4d ambientParam: Qt.vector4d(0, 0, 0, 0) + property vector4d bodyRect: Qt.vector4d(dockConnectedChrome.extraLeft, dockConnectedChrome.extraTop, dockBackground.width, dockBackground.height) + property vector4d cornerRadius: Qt.vector4d(dockConnectedChrome.barTop || dockConnectedChrome.barLeft ? 0 : dockConnectedChrome.bodyRadius, dockConnectedChrome.barTop || dockConnectedChrome.barRight ? 0 : dockConnectedChrome.bodyRadius, dockConnectedChrome.barBottom || dockConnectedChrome.barRight ? 0 : dockConnectedChrome.bodyRadius, dockConnectedChrome.barBottom || dockConnectedChrome.barLeft ? 0 : dockConnectedChrome.bodyRadius) + property vector4d edgeParam: Qt.vector4d(dockConnectedChrome.barTop ? 0 : (dockConnectedChrome.barBottom ? 1 : (dockConnectedChrome.barLeft ? 2 : 3)), Theme.connectedCornerRadius, 0, 0) + } + } + + Shape { + id: dockBorderShape + x: dockBackground.x - borderThickness + y: dockBackground.y - borderThickness + width: dockBackground.width + borderThickness * 2 + height: dockBackground.height + borderThickness * 2 + visible: SettingsData.dockBorderEnabled && dock.hasApps && !usesConnectedFrameChrome + preferredRendererType: Shape.CurveRenderer + + readonly property real borderThickness: Math.max(1, dock.borderThickness) + readonly property real i: borderThickness / 2 + readonly property real cr: dock.surfaceRadius + readonly property real w: dockBackground.width + readonly property real h: dockBackground.height + + readonly property color borderColor: { + const opacity = SettingsData.dockBorderOpacity; + switch (SettingsData.dockBorderColor) { + case "secondary": + return Theme.withAlpha(Theme.secondary, opacity); + case "primary": + return Theme.withAlpha(Theme.primary, opacity); + default: + return Theme.withAlpha(Theme.surfaceText, opacity); + } + } + + ShapePath { + fillColor: "transparent" + strokeColor: dockBorderShape.borderColor + strokeWidth: dockBorderShape.borderThickness + joinStyle: ShapePath.RoundJoin + capStyle: ShapePath.FlatCap + + PathSvg { + path: { + const bt = dockBorderShape.borderThickness; + const i = dockBorderShape.i; + const cr = dockBorderShape.cr + bt - i; + const w = dockBorderShape.w; + const h = dockBorderShape.h; + + let d = `M ${i + cr} ${i}`; + d += ` L ${i + w + 2 * (bt - i) - cr} ${i}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${i + w + 2 * (bt - i)} ${i + cr}`; + d += ` L ${i + w + 2 * (bt - i)} ${i + h + 2 * (bt - i) - cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${i + w + 2 * (bt - i) - cr} ${i + h + 2 * (bt - i)}`; + d += ` L ${i + cr} ${i + h + 2 * (bt - i)}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${i} ${i + h + 2 * (bt - i) - cr}`; + d += ` L ${i} ${i + cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${i + cr} ${i}`; + d += " Z"; + return d; + } + } + } + } + + DockApps { + id: dockApps + + anchors.top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? dockBackground.top : undefined) : undefined + anchors.bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? dockBackground.bottom : undefined) : undefined + anchors.horizontalCenter: !dock.isVertical ? dockBackground.horizontalCenter : undefined + anchors.left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? dockBackground.left : undefined) : undefined + anchors.right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? dockBackground.right : undefined) : undefined + anchors.verticalCenter: dock.isVertical ? dockBackground.verticalCenter : undefined + anchors.topMargin: !dock.isVertical ? SettingsData.dockSpacing : 0 + anchors.bottomMargin: !dock.isVertical ? SettingsData.dockSpacing : 0 + anchors.leftMargin: dock.isVertical ? SettingsData.dockSpacing : 0 + anchors.rightMargin: dock.isVertical ? SettingsData.dockSpacing : 0 + + contextMenu: dock.contextMenu + trashContextMenu: dock.trashContextMenu + groupByApp: dock.groupByApp + isVertical: dock.isVertical + dockScreen: dock.screen + iconSize: dock.widgetHeight + usesOverlayLayer: dock.usesOverlayLayer + } + } + } + } +} diff --git a/quickshell/Modules/Dock/DockContextMenuBase.qml b/quickshell/Modules/Dock/DockContextMenuBase.qml index f9109608f..bc6b20f44 100644 --- a/quickshell/Modules/Dock/DockContextMenuBase.qml +++ b/quickshell/Modules/Dock/DockContextMenuBase.qml @@ -61,6 +61,11 @@ PanelWindow { const actualDockHeight = dockBackground ? dockBackground.height : root.dockVisibleHeight; const actualDockWidth = dockBackground ? dockBackground.width : dockWindow.width; const dockMargin = SettingsData.dockMargin + 16; + + // Connected mode hosts the dock inside the fullscreen frame surface, inset from the + // screen edge. Anchor off the dock body's real position instead of the screen edge. + const dockBgPos = dockBackground ? dockBackground.mapToItem(dockWindow.contentItem, 0, 0) : null; + const frameHosted = dockBgPos && dockWindow.width >= screen.width - 1 && dockWindow.height >= screen.height - 1; let x = 0; let y = 0; @@ -68,14 +73,14 @@ PanelWindow { case SettingsData.Position.Left: { const dockTopMargin = Math.round((screen.height - dockWindow.height) / 2); - x = actualDockWidth + dockMargin + 20; + x = frameHosted ? (dockBgPos.x + actualDockWidth + dockMargin + 20) : (actualDockWidth + dockMargin + 20); y = dockTopMargin + buttonPosInDock.y + anchorItem.height / 2; break; } case SettingsData.Position.Right: { const dockTopMargin = Math.round((screen.height - dockWindow.height) / 2); - x = screen.width - actualDockWidth - dockMargin - 20; + x = frameHosted ? (dockBgPos.x - dockMargin - 20) : (screen.width - actualDockWidth - dockMargin - 20); y = dockTopMargin + buttonPosInDock.y + anchorItem.height / 2; break; } @@ -83,7 +88,7 @@ PanelWindow { { const dockLeftMargin = Math.round((screen.width - dockWindow.width) / 2); x = dockLeftMargin + buttonPosInDock.x + anchorItem.width / 2; - y = actualDockHeight + dockMargin + 20; + y = frameHosted ? (dockBgPos.y + actualDockHeight + dockMargin + 20) : (actualDockHeight + dockMargin + 20); break; } case SettingsData.Position.Bottom: @@ -91,7 +96,7 @@ PanelWindow { { const dockLeftMargin = Math.round((screen.width - dockWindow.width) / 2); x = dockLeftMargin + buttonPosInDock.x + anchorItem.width / 2; - y = screen.height - actualDockHeight - dockMargin - 20; + y = frameHosted ? (dockBgPos.y - dockMargin - 20) : (screen.height - actualDockHeight - dockMargin - 20); break; } } diff --git a/quickshell/Modules/Frame/FrameBarHost.qml b/quickshell/Modules/Frame/FrameBarHost.qml new file mode 100644 index 000000000..398940eae --- /dev/null +++ b/quickshell/Modules/Frame/FrameBarHost.qml @@ -0,0 +1,114 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common +import qs.Modules.DankBar +import qs.Services + +// Renders the bar(s) inside the frame surface in connected mode: one DankBarBody per +// active bar edge, positioned in the frame's cutout band. Reuses the existing DankBar +// item (per bar config) as rootWindow so colour-picker, overview loader and widget +// models are shared with the standalone path. +Item { + id: host + + required property var frameWindow + required property var targetScreen + + readonly property string screenName: targetScreen ? targetScreen.name : "" + + readonly property var barSlots: { + SettingsData.barConfigs; + const out = []; + const configs = SettingsData.barConfigs || []; + for (let i = 0; i < configs.length; i++) { + const bc = configs[i]; + if (!bc.enabled || (bc.useOverlayLayer ?? false)) + continue; + const prefs = bc.screenPreferences || ["all"]; + if (!prefs.includes("all") && !SettingsData.isScreenInPreferences(host.targetScreen, prefs)) + continue; + let edge = "top"; + switch (bc.position ?? 0) { + case SettingsData.Position.Bottom: + edge = "bottom"; + break; + case SettingsData.Position.Left: + edge = "left"; + break; + case SettingsData.Position.Right: + edge = "right"; + break; + } + out.push({ + "barId": bc.id, + "edge": edge + }); + } + // Horizontal bars own the corners, so render them last (on top) — their edge widget + // must receive the corner click over the vertical bar's hover area beneath it. + out.sort((a, b) => { + const rank = e => (e === "left" || e === "right") ? 0 : 1; + return rank(a.edge) - rank(b.edge); + }); + return out; + } + + Repeater { + model: host.barSlots + + delegate: Item { + id: slot + + required property var modelData + + readonly property string edge: modelData.edge + readonly property var dankBarItem: BarWidgetService.dankBarItems[modelData.barId] ?? null + readonly property var slotBarConfig: dankBarItem?.barConfig ?? SettingsData.getBarConfig(modelData.barId) + + // Each bar spans its full edge (matching the standalone window extent) so + // DankBarContent's own adjacency margins inset it, rather than double-insetting + // against a pre-carved strip. + x: edge === "right" ? host.frameWindow._windowRegionWidth - host.frameWindow.cutoutRightInset : 0 + y: edge === "bottom" ? host.frameWindow._windowRegionHeight - host.frameWindow.cutoutBottomInset : 0 + width: { + switch (edge) { + case "left": + return host.frameWindow.cutoutLeftInset; + case "right": + return host.frameWindow.cutoutRightInset; + default: + return host.frameWindow._windowRegionWidth; + } + } + height: { + switch (edge) { + case "top": + return host.frameWindow.cutoutTopInset; + case "bottom": + return host.frameWindow.cutoutBottomInset; + default: + return host.frameWindow._windowRegionHeight; + } + } + + Loader { + anchors.fill: parent + active: slot.dankBarItem !== null && slot.slotBarConfig !== null + + sourceComponent: DankBarBody { + hostWindow: host.frameWindow + modelData: host.targetScreen + rootWindow: slot.dankBarItem + barConfig: slot.slotBarConfig + leftWidgetsModel: slot.dankBarItem?.leftWidgetsModel ?? null + centerWidgetsModel: slot.dankBarItem?.centerWidgetsModel ?? null + rightWidgetsModel: slot.dankBarItem?.rightWidgetsModel ?? null + + Component.onCompleted: BarWidgetService.registerFrameBar(host.screenName, this) + Component.onDestruction: BarWidgetService.unregisterFrameBar(host.screenName, this) + } + } + } + } +} diff --git a/quickshell/Modules/Frame/FrameDockHost.qml b/quickshell/Modules/Frame/FrameDockHost.qml new file mode 100644 index 000000000..0063fbe8b --- /dev/null +++ b/quickshell/Modules/Frame/FrameDockHost.qml @@ -0,0 +1,30 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common +import qs.Modules.Dock +import qs.Services + +// Renders the dock inside the frame surface in connected mode: a single DockBody +// positioned by SettingsData.dockPosition. It fills the frame and positions its own +// content via its internal anchors/geometry using hostWindow. +Item { + id: host + + required property var frameWindow + required property var targetScreen + + readonly property string screenName: targetScreen ? targetScreen.name : "" + + // Exposed so FrameWindow can add the dock's hover strip to its input region. + readonly property alias dockMaskItem: dockBody.inputMaskItem + + DockBody { + id: dockBody + anchors.fill: parent + hostWindow: host.frameWindow + modelData: host.targetScreen + contextMenu: BarWidgetService.dockContextMenu + trashContextMenu: BarWidgetService.dockTrashContextMenu + } +} diff --git a/quickshell/Modules/Frame/FrameExclusions.qml b/quickshell/Modules/Frame/FrameExclusions.qml index a69bebfda..1729a02c2 100644 --- a/quickshell/Modules/Frame/FrameExclusions.qml +++ b/quickshell/Modules/Frame/FrameExclusions.qml @@ -16,15 +16,33 @@ Scope { return SettingsData.getActiveBarEdgesForScreen(screen); } - // One thin invisible PanelWindow per edge. - // Skips any edge where a bar already provides its own exclusiveZone. + // Overlay-layer bars stay standalone even in connected mode and reserve their own edge. + readonly property var overlayBarEdges: { + SettingsData.barConfigs; + return SettingsData.getOverlayBarEdgesForScreen(root.screen); + } + + // One thin invisible PanelWindow per edge. A standalone bar reserves its own edge, so + // that edge is skipped — unless the frame hosts the bar (connected mode), where no bar + // window exists and the exclusion must reserve the full bar thickness itself. readonly property bool screenEnabled: CompositorService.frameWindowVisibleForScreen(root.screen) + readonly property bool hostsBar: CompositorService.frameHostsSurfacesForScreen(root.screen) + + function hostsBandForEdge(edge) { + return root.hostsBar && !root.overlayBarEdges.includes(edge); + } + + function exclusionSizeForEdge(edge) { + return root.barEdges.includes(edge) ? SettingsData.frameBarSize : SettingsData.frameThickness; + } Loader { - active: root.screenEnabled && !root.barEdges.includes("top") + readonly property bool isBarEdge: root.barEdges.includes("top") + active: root.screenEnabled && (!isBarEdge || root.hostsBandForEdge("top")) sourceComponent: EdgeExclusion { targetScreen: root.screen + exclusionSize: root.exclusionSizeForEdge("top") anchorTop: true anchorLeft: true anchorRight: true @@ -32,9 +50,11 @@ Scope { } Loader { - active: root.screenEnabled && !root.barEdges.includes("bottom") + readonly property bool isBarEdge: root.barEdges.includes("bottom") + active: root.screenEnabled && (!isBarEdge || root.hostsBandForEdge("bottom")) sourceComponent: EdgeExclusion { targetScreen: root.screen + exclusionSize: root.exclusionSizeForEdge("bottom") anchorBottom: true anchorLeft: true anchorRight: true @@ -42,9 +62,11 @@ Scope { } Loader { - active: root.screenEnabled && !root.barEdges.includes("left") + readonly property bool isBarEdge: root.barEdges.includes("left") + active: root.screenEnabled && (!isBarEdge || root.hostsBandForEdge("left")) sourceComponent: EdgeExclusion { targetScreen: root.screen + exclusionSize: root.exclusionSizeForEdge("left") anchorLeft: true anchorTop: true anchorBottom: true @@ -52,9 +74,11 @@ Scope { } Loader { - active: root.screenEnabled && !root.barEdges.includes("right") + readonly property bool isBarEdge: root.barEdges.includes("right") + active: root.screenEnabled && (!isBarEdge || root.hostsBandForEdge("right")) sourceComponent: EdgeExclusion { targetScreen: root.screen + exclusionSize: root.exclusionSizeForEdge("right") anchorRight: true anchorTop: true anchorBottom: true @@ -63,6 +87,7 @@ Scope { component EdgeExclusion: PanelWindow { required property var targetScreen + property real exclusionSize: SettingsData.frameThickness screen: targetScreen property bool anchorTop: false @@ -72,7 +97,7 @@ Scope { WlrLayershell.namespace: "dms:frame-exclusion" WlrLayershell.layer: WlrLayer.Top - exclusiveZone: SettingsData.frameThickness + exclusiveZone: exclusionSize color: "transparent" mask: Region {} implicitWidth: 1 diff --git a/quickshell/Modules/Frame/FrameWindow.qml b/quickshell/Modules/Frame/FrameWindow.qml index 04fa2a284..1cfd58cf4 100644 --- a/quickshell/Modules/Frame/FrameWindow.qml +++ b/quickshell/Modules/Frame/FrameWindow.qml @@ -32,7 +32,41 @@ PanelWindow { } color: "transparent" - mask: Region {} + // Click-through everywhere except the bar strips it hosts in connected mode. + mask: Region { + Region { + readonly property bool present: win._connectedActive && win.barEdges.includes("top") + x: 0 + y: 0 + width: present ? win._windowRegionWidth : 0 + height: present ? win.cutoutTopInset : 0 + } + Region { + readonly property bool present: win._connectedActive && win.barEdges.includes("bottom") + x: 0 + y: present ? win._windowRegionHeight - win.cutoutBottomInset : 0 + width: present ? win._windowRegionWidth : 0 + height: present ? win.cutoutBottomInset : 0 + } + Region { + readonly property bool present: win._connectedActive && win.barEdges.includes("left") + x: 0 + y: 0 + width: present ? win.cutoutLeftInset : 0 + height: present ? win._windowRegionHeight : 0 + } + Region { + readonly property bool present: win._connectedActive && win.barEdges.includes("right") + x: present ? win._windowRegionWidth - win.cutoutRightInset : 0 + y: 0 + width: present ? win.cutoutRightInset : 0 + height: present ? win._windowRegionHeight : 0 + } + // The frame-hosted dock's hover strip, so hover-reveal works through the frame surface. + Region { + item: frameDockHostLoader.item ? frameDockHostLoader.item.dockMaskItem : null + } + } readonly property var barEdges: { SettingsData.barConfigs; @@ -51,6 +85,16 @@ PanelWindow { readonly property var _modalDescriptor: ConnectedModeState.surfaceDescriptor(win._screenName, "modal") readonly property bool _connectedActive: CompositorService.usesConnectedFrameChromeForScreen(win.targetScreen) + readonly property bool _dockHostedHere: { + if (!win._connectedActive || !SettingsData.showDock || SettingsData.dockUseOverlayLayer) + return false; + const screens = SettingsData.getFilteredScreens("dock"); + for (let i = 0; i < screens.length; i++) { + if (screens[i] && screens[i].name === win._screenName) + return true; + } + return false; + } readonly property string _barSide: { const edges = win.barEdges; if (edges.includes("top")) @@ -191,6 +235,33 @@ PanelWindow { return Math.max(0, Math.round(Theme.px(value, win._dpr))); } + function _pointInBand(lx, ly, bx, by, bw, bh, pad) { + return lx >= bx - pad && lx < bx + bw + pad && ly >= by - pad && ly < by + bh + pad; + } + + function containsGlobalPoint(gx, gy, padding) { + if (!win._connectedActive || !win.contentItem) + return false; + const origin = win.contentItem.mapToItem(null, 0, 0); + if (!origin) + return false; + const pad = padding !== undefined ? padding : 16; + const lx = gx - origin.x; + const ly = gy - origin.y; + const w = win._windowRegionWidth; + const h = win._windowRegionHeight; + const edges = win.barEdges; + if (edges.includes("top") && win._pointInBand(lx, ly, 0, 0, w, win.cutoutTopInset, pad)) + return true; + if (edges.includes("bottom") && win._pointInBand(lx, ly, 0, h - win.cutoutBottomInset, w, win.cutoutBottomInset, pad)) + return true; + if (edges.includes("left") && win._pointInBand(lx, ly, 0, 0, win.cutoutLeftInset, h, pad)) + return true; + if (edges.includes("right") && win._pointInBand(lx, ly, w - win.cutoutRightInset, 0, win.cutoutRightInset, h, pad)) + return true; + return false; + } + readonly property int cutoutTopInset: win._regionInt(barEdges.includes("top") ? SettingsData.frameBarSize : SettingsData.frameThickness) readonly property int cutoutBottomInset: win._regionInt(barEdges.includes("bottom") ? SettingsData.frameBarSize : SettingsData.frameThickness) readonly property int cutoutLeftInset: win._regionInt(barEdges.includes("left") ? SettingsData.frameBarSize : SettingsData.frameThickness) @@ -1049,10 +1120,12 @@ PanelWindow { } Component.onCompleted: { + KeyboardFocus.registerBarWindow(win); win._scheduleBlurRebuild(); win._scheduleSurfaceRefresh(); } Component.onDestruction: { + KeyboardFocus.unregisterBarWindow(win); blurRebuildAction.cancel(); surfaceRefreshAction.cancel(); win._teardownBlur(); @@ -1101,4 +1174,25 @@ PanelWindow { property vector4d chromeK3: win._sdfSlots[3].k property vector4d chromeParam3: win._sdfSlots[3].param } + + Loader { + anchors.fill: parent + z: 1 + active: win._connectedActive + sourceComponent: FrameBarHost { + frameWindow: win + targetScreen: win.targetScreen + } + } + + Loader { + id: frameDockHostLoader + anchors.fill: parent + z: 1 + active: win._dockHostedHere + sourceComponent: FrameDockHost { + frameWindow: win + targetScreen: win.targetScreen + } + } } diff --git a/quickshell/Modules/Settings/DankBarTab.qml b/quickshell/Modules/Settings/DankBarTab.qml index dcc9459da..da2fe1360 100644 --- a/quickshell/Modules/Settings/DankBarTab.qml +++ b/quickshell/Modules/Settings/DankBarTab.qml @@ -1256,6 +1256,8 @@ Item { } SettingsToggleCard { + settingKey: "hoverPopouts" + tags: ["bar", "hover", "popout", "reveal", "widget", "delay"] iconName: "touch_app" title: I18n.tr("Hover Popouts") description: I18n.tr("Open widget popouts by hovering over the bar. Moving to another widget switches the popout.") diff --git a/quickshell/Services/BarWidgetService.qml b/quickshell/Services/BarWidgetService.qml index 80033de05..2af48d52c 100644 --- a/quickshell/Services/BarWidgetService.qml +++ b/quickshell/Services/BarWidgetService.qml @@ -12,6 +12,50 @@ Singleton { property var widgetRegistry: ({}) property var dankBarRepeater: null + // Bars rendered inside the frame surface (connected mode) have no DankBarWindow in the + // repeater, so they self-register here (screenName -> DankBarBody) for trigger routing. + property var frameHostedBars: ({}) + + // Shared dock context-menu windows (created in DMSShell) so a frame-hosted dock can reach them. + property var dockContextMenu: null + property var dockTrashContextMenu: null + + function registerFrameBar(screenName, body) { + if (!screenName || !body) + return; + const next = Object.assign({}, frameHostedBars); + next[screenName] = body; + frameHostedBars = next; + } + + function unregisterFrameBar(screenName, body) { + if (!screenName || frameHostedBars[screenName] !== body) + return; + const next = Object.assign({}, frameHostedBars); + delete next[screenName]; + frameHostedBars = next; + } + + // DankBar items self-register here (barConfig id -> DankBar) so frame-hosted bars can + // resolve their models/rootWindow reactively, independent of repeater load ordering. + property var dankBarItems: ({}) + + function registerDankBarItem(barId, item) { + if (!barId || !item) + return; + const next = Object.assign({}, dankBarItems); + next[barId] = item; + dankBarItems = next; + } + + function unregisterDankBarItem(barId, item) { + if (!barId || dankBarItems[barId] !== item) + return; + const next = Object.assign({}, dankBarItems); + delete next[barId]; + dankBarItems = next; + } + signal widgetRegistered(string widgetId, string screenName) signal widgetUnregistered(string widgetId, string screenName) @@ -141,6 +185,9 @@ Singleton { } function getBarWindowForScreen(screenName) { + if (frameHostedBars[screenName]) + return frameHostedBars[screenName]; + if (!dankBarRepeater) return null; @@ -170,17 +217,18 @@ Singleton { } function getFirstBarWindow() { - if (!dankBarRepeater || dankBarRepeater.count === 0) - return null; + if (dankBarRepeater) { + for (var i = 0; i < dankBarRepeater.count; i++) { + const barItem = dankBarRepeater.itemAt(i)?.item; + if (barItem?.barVariants?.instances?.length > 0) + return barItem.barVariants.instances[0]; + } + } - const loader = dankBarRepeater.itemAt(0); - if (!loader?.item) - return null; - - const barItem = loader.item; - if (!barItem.barVariants?.instances || barItem.barVariants.instances.length === 0) - return null; - - return barItem.barVariants.instances[0]; + for (const screenName in frameHostedBars) { + if (frameHostedBars[screenName]) + return frameHostedBars[screenName]; + } + return null; } } diff --git a/quickshell/Services/CompositorService.qml b/quickshell/Services/CompositorService.qml index b2c2d3ae0..ccfecbf95 100644 --- a/quickshell/Services/CompositorService.qml +++ b/quickshell/Services/CompositorService.qml @@ -533,43 +533,6 @@ Singleton { return false; } - function hasFullscreenToplevelOnScreen(screenOrName) { - const screenName = _screenName(screenOrName); - if (!screenName) - return false; - - if (isNiri) { - const active = ToplevelManager.activeToplevel; - if (active?.fullscreen && active?.activated && _toplevelOnScreen(active, screenName)) - return true; - - const filtered = filterCurrentWorkspace(sortedToplevels, screenName); - for (let i = 0; i < filtered.length; i++) { - if (filtered[i]?.fullscreen) - return true; - } - return false; - } - - if (isHyprland) { - const filtered = filterCurrentWorkspace(sortedToplevels, screenName); - for (let i = 0; i < filtered.length; i++) { - if (filtered[i]?.fullscreen) - return true; - } - return false; - } - - if (!ToplevelManager.toplevels?.values) - return false; - - for (const toplevel of ToplevelManager.toplevels.values) { - if (toplevel?.fullscreen && _toplevelOnScreen(toplevel, screenName)) - return true; - } - return false; - } - function _hyprlandToplevelMapped(hyprToplevel) { if (!hyprToplevel) return false; @@ -630,12 +593,6 @@ Singleton { // 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(screenName); - } - function _recomputeFrameBlocked() { const screens = Quickshell.screens || []; const next = {}; @@ -644,7 +601,7 @@ Singleton { const name = screens[i]?.name; if (!name) continue; - const blocked = _computeConnectedFrameBlocked(name); + const blocked = hyprlandSpecialWorkspaceBlocksConnectedFrame(name); next[name] = blocked; if (frameBlockedByScreen[name] !== blocked) changed = true; @@ -668,7 +625,7 @@ Singleton { const cached = frameBlockedByScreen[screenName]; if (cached !== undefined) return cached; - return _computeConnectedFrameBlocked(screenName); + return hyprlandSpecialWorkspaceBlocksConnectedFrame(screenName); } Connections { @@ -730,6 +687,23 @@ Singleton { return FrameTransitionState.effectiveConnectedFrameModeActive && frameWindowVisibleForScreen(screenOrName); } + // Connected mode renders the bar inside the frame surface. True whenever connected + // chrome is configured for the screen, independent of the fullscreen block, so the + // standalone bar surface stays suppressed while the frame-hosted bar hides with the frame. + function frameHostsSurfacesForScreen(screenOrName) { + return FrameTransitionState.effectiveConnectedFrameModeActive && frameConfiguredForScreen(screenOrName); + } + + // A surface set to the overlay layer cannot live inside the frame's single top-layer window, + // so it stays a standalone window (which resolves itself onto the overlay layer) even in connected mode. + function frameHostsBarForConfig(screenOrName, barConfig) { + return frameHostsSurfacesForScreen(screenOrName) && !(barConfig?.useOverlayLayer ?? false); + } + + function frameHostsDockForScreen(screenOrName) { + return frameHostsSurfacesForScreen(screenOrName) && !SettingsData.dockUseOverlayLayer; + } + function framePeerSurfacesUseOverlayForScreen(screenOrName) { return frameWindowVisibleForScreen(screenOrName); } diff --git a/quickshell/Widgets/DankPopoutStandalone.qml b/quickshell/Widgets/DankPopoutStandalone.qml index a4e84cc9c..bff5fe7bd 100644 --- a/quickshell/Widgets/DankPopoutStandalone.qml +++ b/quickshell/Widgets/DankPopoutStandalone.qml @@ -215,6 +215,9 @@ Item { // keep the bg window in active-update mode. property bool _bgCommitWindow: false + // True only while the overlay is actually on screen, not merely declared. + readonly property bool _overlayActive: overlayLoader.item?.visible ?? false + Timer { id: bgCommitSettleTimer interval: 250 @@ -236,6 +239,9 @@ Item { return; _blurCommitSuppress = true; blurCommitPulseTimer.restart(); + // Land the off->on toggle on a commit even after _overlayActive has dropped. + _bgCommitWindow = true; + bgCommitSettleTimer.restart(); } Connections { @@ -563,10 +569,9 @@ Item { screen: root.surfaceScreen visible: false color: "transparent" - // Skip buffer updates when there's nothing to render. Briefly flipped - // true via _bgCommitWindow when _surfaceBodyW/H changes so the - // contentHoleRect mask carve-out actually commits to the compositor. - updatesEnabled: !root._blurCommitSuppress && (root.overlayContent !== null || root._bgCommitWindow) + // Idle full-screen surface skips buffer updates, else it damages the whole screen + // every frame. Live while an overlay shows or _bgCommitWindow flushes a mask change. + updatesEnabled: !root._blurCommitSuppress && (root._overlayActive || root._bgCommitWindow) WlrLayershell.namespace: root.layerNamespace + ":background" WlrLayershell.layer: root.effectivePopoutLayer diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index 29b10188a..bd8cfec69 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -772,6 +772,34 @@ ], "icon": "display_settings" }, + { + "section": "hoverPopouts", + "label": "Hover Popouts", + "tabIndex": 3, + "category": "Dank Bar", + "keywords": [ + "another", + "bar", + "dank", + "delay", + "hover", + "hovering", + "moving", + "open", + "over", + "panel", + "popout", + "popouts", + "reveal", + "statusbar", + "switches", + "taskbar", + "topbar", + "widget" + ], + "icon": "touch_app", + "description": "Open widget popouts by hovering over the bar. Moving to another widget switches the popout." + }, { "section": "barPosition", "label": "Position",