// The dock — a replacement for Dash-to-Dock configured as: bottom edge, 48px // icons, intellihide against all windows, dot running-indicators, show-apps // button at the leading edge. // // Intellihide means the dock reserves no space at all (exclusiveZone 0) and // floats over windows, appearing when the pointer reaches the bottom edge or // when nothing is in the way. import Quickshell import Quickshell.Wayland import Quickshell.Hyprland import QtQuick import qs.config import qs.services PanelWindow { id: root // Set by Variants when instantiated per-screen. Plain rather than // `required` so the dock can also be created standalone while testing; // Variants injects into the existing property either way. property var modelData: null screen: root.modelData anchors { bottom: true left: true right: true } color: "transparent" // A dock that reserved space would not be intellihiding. exclusiveZone: 0 exclusionMode: ExclusionMode.Ignore // Matched by the `qs-dock` layer rule in hypr/rules.lua — do not rename. WlrLayershell.namespace: "qs-dock" WlrLayershell.layer: WlrLayer.Top WlrLayershell.keyboardFocus: WlrKeyboardFocus.None // ── Geometry ──────────────────────────────────────────────────────────── // Height = room for the tooltip above the bar + the bar + the gap under it. readonly property int revealStripHeight: 3 readonly property int bottomMargin: Theme.barGap readonly property int tooltipSpace: 34 implicitHeight: tooltipSpace + body.implicitHeight + bottomMargin // ── Intellihide ───────────────────────────────────────────────────────── // This instance's own monitor, the same lookup Workspaces.qml uses to // scope a per-screen bar to its own screen. Falls back to null when this // Dock is created standalone (no `screen` set) rather than via Variants. readonly property HyprlandMonitor monitor: root.screen ? Hyprland.monitorFor(root.screen) : null // Hyprland does not expose live toplevel geometry, so exact overlap cannot // be computed. "Is anything on this workspace at all" is the robust proxy, // and it is what Dash-to-Dock's all-windows intellihide felt like in // practice: an empty workspace keeps the dock out. // // Deliberately this instance's own monitor's active workspace, not the // globally-focused one -- with one Dock per screen, keying off the global // focus would make focusing an empty workspace on monitor A hide the dock // on monitor B even though B's own workspace is still busy. readonly property bool workspaceOccupied: { const ws = root.monitor ? root.monitor.activeWorkspace : Hyprland.focusedWorkspace; return !!ws && ws.toplevels.values.length > 0; } readonly property bool wantRevealed: !Settings.dockAutohide || !workspaceOccupied || pointer.hovered property bool revealed: true onWantRevealedChanged: { if (wantRevealed) { hideTimer.stop(); revealTimer.restart(); } else { revealTimer.stop(); hideTimer.restart(); } } Timer { id: revealTimer interval: Settings.dockRevealDelayMs onTriggered: root.revealed = true } Timer { id: hideTimer interval: Settings.dockHideDelayMs onTriggered: root.revealed = false } // Other modules (the bar, the capture overlay) read this. It is one // shared flag but there is one Dock per monitor, so only the instance on // the currently-focused monitor is allowed to write it -- otherwise // whichever instance last changed reveal state would stomp the others, // and a reader would see an arbitrary monitor's value. This scopes the // flag to mean "is the dock revealed on the monitor the user is on", // which is what a capture overlay or the bar actually care about. // (A true per-monitor flag would need ShellState.dockRevealed itself to // become keyed by screen, which is out of scope here -- see the report.) readonly property bool isFocusedMonitorInstance: root.monitor === null || root.monitor === Hyprland.focusedMonitor onRevealedChanged: root._syncShellState() onIsFocusedMonitorInstanceChanged: root._syncShellState() function _syncShellState(): void { if (root.isFocusedMonitorInstance) ShellState.dockRevealed = root.revealed; } // wantRevealed's first evaluation emits no change signal when it lands on // false (the default), so the initial state has to be taken explicitly — // otherwise a shell started on a busy workspace would leave the dock up. Component.onCompleted: { revealed = wantRevealed; root._syncShellState(); } // ── Input region ──────────────────────────────────────────────────────── // Revealed: the bar plus everything below it, so crossing the gap under the // dock does not count as leaving. Hidden: a sliver along the screen edge, // which is the only thing that can still trigger the dock — every other // click passes straight through to the window underneath. mask: Region { item: maskItem } Item { id: surface anchors.fill: parent // Reports the pointer anywhere inside the input region. A HoverHandler // rather than a MouseArea because it keeps reporting while the icons' // own MouseAreas are hovered. HoverHandler { id: pointer } Item { id: maskItem x: root.revealed ? body.x : 0 y: root.revealed ? body.y : surface.height - root.revealStripHeight width: root.revealed ? body.width : surface.width height: root.revealed ? surface.height - body.y : root.revealStripHeight } DockBody { id: body anchors.horizontalCenter: parent.horizontalCenter // Slides off the bottom edge when hidden. y: root.revealed ? root.tooltipSpace : surface.height opacity: root.revealed ? 1 : 0 // Asymmetric on purpose. Revealing is a response to something the // user just did, so it has to feel immediate — any delay there // reads as lag. Hiding is not a response to anything, so it can // take its time and stay calm in peripheral vision. Behavior on y { NumberAnimation { duration: root.revealed ? Theme.durDockReveal : Theme.durNormal easing.type: root.revealed ? Easing.OutQuint : Easing.InCubic } } Behavior on opacity { NumberAnimation { duration: root.revealed ? Theme.durDockReveal : Theme.durNormal easing.type: Easing.OutCubic } } } } }