Watch
1
0
Fork
You've already forked souveraine
0
souveraine/surfaces/quickshell/modules/ii/dock/Dock.qml
Fimeg ec7c4e8e8c quickshell: dock geometry/icon-centering, GlobalStates, ConflictKiller service
Dock + pill z-order and icon centering pass; GlobalStates.qml and a
ConflictKiller service for the ii sidebar.
2026-07-12 14:56:56 -04:00

317 lines
15 KiB
QML

// Pixel3Arch patch to ii's stock Dock.qml (2026-07-07).
//
// Problem: the dock (pinned, exclusiveZone claiming real screen space) and
// wvkbd (the on-screen keyboard, see OnScreenKeyboard.qml) both anchor to
// the bottom edge and both claim exclusive zones — their reservations
// stack instead of one giving way, so the keyboard/its focus-grab shield
// drift out of alignment with each other depending on what else is
// reserving space. Simplest real fix: the dock gets out of the way
// entirely while the keyboard is open, instead of trying to make the
// keyboard-side math account for wherever the dock happens to be.
//
// GlobalStates.oskOpen suppresses both reveal-when-idle and the pinned
// exclusive zone. Double-tapping the pill (see
// overlays/quickshell-pill/shell.qml) still pulses the dock visible for a
// few seconds via GlobalStates.dockRevealPulse, for reaching a dock app
// without first closing the keyboard.
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell.Io
import Quickshell.Hyprland
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Hyprland
Scope { // Scope
id: root
property bool pinned: Config.options?.dock.pinnedOnStartup ?? false
// Dock visibility as one explicit state instead of five OR'd booleans
// (Phosh's lesson: a small enum beats an overlapping-boolean knot).
// HIDDEN - fully tucked below the edge
// PEEK - hover strip / empty-desktop / pill-pulse showing it briefly
// SHOWN - visible but not claiming exclusive space
// PINNED - visible AND reserving an exclusive zone
// OSK-open and previewPopup-hover are *inputs* to this, not states.
enum DockState { Hidden, Peek, Shown, Pinned }
// The real pin, suppressed while the OSK is open unless the user is mid
// pill-pulse. Kept as a helper the state below reads.
property bool effectivePinned: root.pinned && (!GlobalStates.oskOpen || GlobalStates.dockRevealPulse)
// requestDockShow (previewPopup hover) is threaded up from DockApps via
// this alias so the state computation can see it in one place.
property bool previewShowing: false
// App mode: any fullscreen window on the focused monitor owns the
// display, so the dock hides. ii's own screenCorners/Background use this
// same scan (activeToplevel.wayland.fullscreen is NOT reliable); mirror
// it verbatim. dockRevealPulse still overrides so swipe-up can flash it.
readonly property var focusedWorkspaces: Hyprland.workspaces.values.filter(
ws => ws.monitor && ws.monitor.name === Hyprland.focusedMonitor?.name)
readonly property var activeFullscreenWorkspace: root.focusedWorkspaces.filter(
ws => ws.active && ws.toplevels.values.filter(w => w.wayland?.fullscreen)[0] !== undefined)[0]
readonly property bool activeMonitorHasFullscreen: root.activeFullscreenWorkspace !== undefined
function computeDockState() {
if (root.activeMonitorHasFullscreen && !GlobalStates.dockRevealPulse)
return Dock.DockState.Hidden;
if (root.effectivePinned)
return Dock.DockState.Pinned;
if (GlobalStates.dockRevealPulse || root.previewShowing)
return Dock.DockState.Peek;
// Empty desktop (nothing focused) reveals the dock, unless the OSK
// took the bottom edge.
if (!GlobalStates.oskOpen && !ToplevelManager.activeToplevel?.activated)
return Dock.DockState.Peek;
return Dock.DockState.Hidden;
}
property int dockState: computeDockState()
// Two-stage bottom-edge swipe (hyprgrass edge:d:u -> `qs -c ii ipc call
// dock swipeUp`): first swipe pulses the dock visible for a few
// seconds; swiping again while it's showing (or when it's pinned
// anyway) escalates to the overview. Swipe with the overview open
// closes it again.
IpcHandler {
target: "dock"
function swipeUp(): void {
if (GlobalStates.overviewOpen) {
GlobalStates.overviewOpen = false;
return;
}
// Dock already visible (Peek/Shown/Pinned) -> escalate to overview.
if (root.dockState !== Dock.DockState.Hidden) {
GlobalStates.dockRevealPulse = false;
GlobalStates.overviewOpen = true;
} else {
GlobalStates.pulseDockReveal();
}
}
// Swipe down while the dock is showing dismisses it: clears the
// pulse and, if the overview is up, closes that instead.
function swipeDown(): void {
if (GlobalStates.overviewOpen) {
GlobalStates.overviewOpen = false;
return;
}
GlobalStates.dockRevealPulse = false;
}
function reveal(): void {
GlobalStates.pulseDockReveal();
}
// Toggle app-mode fullscreen on the REAL active window. The pill can't
// use `hyprctl dispatch fullscreen` because tapping the pill makes the
// shell (org.quickshell) the focused surface, so hyprctl maximized the
// pill, not the app. ToplevelManager.activeToplevel is the last real
// app toplevel — a layer-shell tap never becomes activeToplevel — and
// its .wayland.fullscreen is writable (Quickshell ToplevelHandle::
// setFullscreen). Same ii-side-sees-the-real-window trick the dock
// buttons use with .activate().
function fullscreen(): void {
// App-mode fullscreen = mode 0 (whole display, no gaps/border).
// Hyprland.activeToplevel is Hyprland's real active APP window and
// its .address is a stable window handle — a layer-shell pill tap
// never becomes a Hyprland toplevel, so this stays the app even
// after the tap focuses the shell. Dispatch AT that address so we
// fullscreen the app, not whatever hyprctl thinks is focused.
const raw = Hyprland.activeToplevel?.address;
if (!raw) return;
// .address may or may not carry the 0x prefix; normalize to exactly
// one. Selector "address:0x..." is verified working on this fork.
const addr = raw.startsWith("0x") ? raw : "0x" + raw;
Quickshell.execDetached(["hyprctl", "dispatch",
`hl.dsp.window.fullscreen({ window = "address:${addr}", mode = 0 })`]);
}
}
// Settings-surface stub (entry point b). souveraine-settings doesn't
// exist yet; the long-hold "App settings…" menu calls dockSettings.openApp
// here. For now it just pulses the dock and logs, so nothing errors and
// the future settings app has a stable IPC name to take over.
IpcHandler {
target: "dockSettings"
function openApp(appId: string): void {
console.log("[dockSettings] openApp stub for", appId,
"- souveraine-settings not yet installed");
GlobalStates.pulseDockReveal();
}
function open(): void {
console.log("[dockSettings] open stub - souveraine-settings not yet installed");
GlobalStates.pulseDockReveal();
}
}
Variants {
// For each monitor
model: Quickshell.screens
PanelWindow {
id: dockRoot
// Window
required property var modelData
screen: modelData
// The dock window maps only when it should paint. This is what
// actually hides it over a fullscreen app (a Hidden dockState
// unmounts the layer entirely) — the earlier `visible` was
// always-true and only `reveal` flipped, so the dock kept
// painting over fullscreen. hoverToReveal (mouse, off by default)
// keeps the strip alive for desktop pointer use.
property bool reveal: root.dockState !== Dock.DockState.Hidden
|| (Config.options?.dock.hoverToReveal && dockMouseArea.containsMouse)
visible: !GlobalStates.screenLocked && reveal
anchors {
bottom: true
left: true
right: true
}
exclusiveZone: root.dockState === Dock.DockState.Pinned ? implicitHeight - (Appearance.sizes.hyprlandGapsOut) - (Appearance.sizes.elevationMargin - Appearance.sizes.hyprlandGapsOut) : 0
implicitWidth: dockBackground.implicitWidth
WlrLayershell.namespace: "quickshell:dock"
// Overlay, not Top: fullscreen windows render above the Top
// layer, and the whole point of the edge swipe is to reach the
// dock from a fullscreen app.
WlrLayershell.layer: WlrLayer.Overlay
color: "transparent"
// Content-driven: tall enough for one 64px dock button + the
// row's 8px bottom margin + the 32px pill strip, with Config
// dock.height as a floor. A fixed config height (72) left the
// visible bar ~36px for 64px buttons — icons poked out the
// bottom and count dots landed under the bar. Size off the
// BUTTON's implicit height, NOT dockRow.implicitHeight: the
// separator's Layout margins inflate the row's implicit and
// made the bar overshoot (content then top-aligned with a dead
// band underneath).
implicitHeight: Math.max(Config.options?.dock.height ?? 70,
overviewButton.implicitHeight + 8 + 32)
+ Appearance.sizes.elevationMargin + Appearance.sizes.hyprlandGapsOut
mask: Region {
item: dockMouseArea
}
MouseArea {
id: dockMouseArea
height: parent.height
anchors {
top: parent.top
topMargin: dockRoot.reveal ? 0 : Config.options?.dock.hoverToReveal ? (dockRoot.implicitHeight - Config.options.dock.hoverRegionHeight) : (dockRoot.implicitHeight + 1)
horizontalCenter: parent.horizontalCenter
}
implicitWidth: dockHoverRegion.implicitWidth + Appearance.sizes.elevationMargin * 2
hoverEnabled: true
Behavior on anchors.topMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Item {
id: dockHoverRegion
anchors.fill: parent
implicitWidth: dockBackground.implicitWidth
Item { // Wrapper for the dock background
id: dockBackground
anchors {
top: parent.top
// Reserve the pill strip: bottom-anchor short of
// the window's bottom by thePillZone so the dock
// floats above the pill and never covers it.
bottom: parent.bottom
bottomMargin: 32
horizontalCenter: parent.horizontalCenter
}
implicitWidth: dockRow.implicitWidth + 5 * 2
height: parent.height - Appearance.sizes.elevationMargin - Appearance.sizes.hyprlandGapsOut - 32
StyledRectangularShadow {
target: dockVisualBackground
}
Rectangle { // The real rectangle that is visible
id: dockVisualBackground
property real margin: Appearance.sizes.elevationMargin
anchors.fill: parent
anchors.topMargin: Appearance.sizes.elevationMargin
anchors.bottomMargin: Appearance.sizes.hyprlandGapsOut
color: Appearance.colors.colLayer0
border.width: 1
border.color: Appearance.colors.colLayer0Border
radius: Appearance.rounding.large
}
RowLayout {
id: dockRow
// Anchored to the visible bar. Split top/bottom
// so the icons ride UP inside the bar instead of
// drooping out the bottom (uniform margins left
// them low; less top + more bottom lifts them).
anchors.fill: dockVisualBackground
anchors.topMargin: 0
anchors.bottomMargin: 8
// The background is built 2*padding wider than the
// row (dockBackground.implicitWidth); inset the row
// so that width actually becomes side padding —
// without it the first icon sat ON the rounded corner.
anchors.leftMargin: padding
anchors.rightMargin: padding
spacing: 3
property real padding: 5
DockApps {
id: dockApps
buttonPadding: dockRow.padding
// Keep the whole dock on-screen: the app list
// may take at most what's left after the fixed
// separator + overview button + paddings. Past
// that it scrolls horizontally.
maxWidth: dockRoot.width
- Appearance.sizes.elevationMargin * 2
- dockSeparator.implicitWidth
- overviewButton.implicitWidth
- dockRow.spacing * 2 - 5 * 2
onRequestDockShowChanged: root.previewShowing = requestDockShow
}
DockSeparator {
id: dockSeparator
}
DockButton {
id: overviewButton
Layout.fillHeight: true
onClicked: GlobalStates.overviewOpen = !GlobalStates.overviewOpen
topInset: Appearance.sizes.hyprlandGapsOut + dockRow.padding
bottomInset: Appearance.sizes.hyprlandGapsOut + dockRow.padding
contentItem: MaterialSymbol {
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
font.pixelSize: parent.width / 2
text: "apps"
color: Appearance.colors.colOnLayer0
}
}
}
}
}
}
}
}
}