diff --git a/surfaces/quickshell/HOW-IT-WORKS.md b/surfaces/quickshell/HOW-IT-WORKS.md new file mode 100644 index 0000000..9b4b6d3 --- /dev/null +++ b/surfaces/quickshell/HOW-IT-WORKS.md @@ -0,0 +1,37 @@ +# How the phone shell works + +Three surfaces: **bar** (top), **dock** (bottom, above pill), **pill** (bottom edge). +Edit here → `./deploy.sh --phone` → restart. Deployed via symlink; live edits = git-tracked. + +## Pill (`pill/shell.qml`) — the always-on gesture bar + +- **Always visible.** `WlrLayer.Overlay` + `ExclusionMode.Ignore` + `margins.bottom:0`. Survives fullscreen, dock, OSK. Never lower the layer. +- **Must sit ABOVE the dock in z-order** (both on Overlay; later-created wins). Restart pill LAST, or it gets buried and stops taking touch. +- Gestures (MouseArea — pointer handlers don't get touch here): + - **double-tap** → fullscreen active app, mode 0 (whole display, no border/gaps). Routes via `dock` IPC `fullscreen()`, which targets `Hyprland.activeToplevel.address` — NOT hyprctl on "active window" (the tap focuses the shell). + - **swipe up** → `dock` IPC `swipeUp` (pulse dock; again = overview). + - **swipe down** → `dock` IPC `swipeDown` (dismiss). +- **No keyboard on the pill.** OSK = 3-finger hyprgrass swipe only. + +## Dock (`modules/ii/dock/Dock.qml`) + +- **On `WlrLayer.Overlay`** so the swipe-pulse reaches it over fullscreen apps. +- **Hidden = layer unmounted** (`visible:false`), not just tucked — else it paints over fullscreen. +- **Visibility** (`computeDockState`, first match wins): + 1. fullscreen app on focused monitor → **Hidden** (unless mid-pulse) + 2. pinned → **Pinned** (only state that reserves exclusive zone) + 3. pulse or preview-hover → **Peek** + 4. empty desktop / no focused app → **Peek** + 5. else (normal app focused) → **Hidden** +- **Reserves 32px pill strip** at the bottom so it never covers the pill. +- Depends on **`GlobalStates.dockRevealPulse` + `pulseDockReveal()`** — MUST be in the surface tree + deploy manifest (currently drift-only on phone; see gap below). + +## Fullscreen modes (Hyprland 0.55, lua dispatch) + +- Dispatch: `hl.dsp.window.fullscreen({ window="address:0x…", mode=N })`. Classic `fullscreen 1` errors. +- **mode 0** = app-mode: whole display, no border/gaps, bar+pill stay (Overlay outranks it). ← the pill uses this. +- **mode 1** = maximize but keeps 20px gaps + 2px border. + +## Known gap + +- `GlobalStates.qml` (the pulse patch) is NOT in the surface tree or `deploy.sh` manifest — only hand-patched live on the phone. A clean deploy ships a dock whose swipe/pulse IPC no-ops. Adopt it into the tree + manifest. diff --git a/surfaces/quickshell/modules/ii/dock/Dock.qml b/surfaces/quickshell/modules/ii/dock/Dock.qml index 51945d7..b508130 100644 --- a/surfaces/quickshell/modules/ii/dock/Dock.qml +++ b/surfaces/quickshell/modules/ii/dock/Dock.qml @@ -111,6 +111,30 @@ Scope { // Scope 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 @@ -226,16 +250,13 @@ Scope { // Scope RowLayout { id: dockRow - // Match the visible rectangle's insets so the icon - // row lands INSIDE the thin pill, not drooping - // below it. The bar stays thin; only the icons move. - anchors { - top: parent.top - bottom: parent.bottom - horizontalCenter: parent.horizontalCenter - topMargin: Appearance.sizes.elevationMargin - bottomMargin: Appearance.sizes.hyprlandGapsOut - } + // 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 spacing: 3 property real padding: 5 diff --git a/surfaces/quickshell/modules/ii/dock/DockAppButton.qml b/surfaces/quickshell/modules/ii/dock/DockAppButton.qml index f2c5299..34245b0 100644 --- a/surfaces/quickshell/modules/ii/dock/DockAppButton.qml +++ b/surfaces/quickshell/modules/ii/dock/DockAppButton.qml @@ -292,14 +292,22 @@ DockButton { active: !isSeparator sourceComponent: Item { anchors.centerIn: parent + // Center the icon+dots block. Reserve only HALF the dot strip + // below the icon: reserving the full strip pushed the block's + // center below the icon's center, so centerIn left extra gap + // above the icon (icons read a few px high). Half-reserve splits + // the difference so the glyph sits visually centered. + width: root.iconSize + height: root.iconSize + (root.countDotHeight + 2) / 2 Loader { id: iconImageLoader anchors { left: parent.left right: parent.right - verticalCenter: parent.verticalCenter + top: parent.top } + height: root.iconSize active: !root.isSeparator sourceComponent: IconImage { source: Quickshell.iconPath(AppSearch.guessIcon(appToplevel.appId), "image-missing") diff --git a/surfaces/quickshell/modules/ii/dock/DockApps.qml b/surfaces/quickshell/modules/ii/dock/DockApps.qml index 231f1d0..bee270d 100644 --- a/surfaces/quickshell/modules/ii/dock/DockApps.qml +++ b/surfaces/quickshell/modules/ii/dock/DockApps.qml @@ -31,7 +31,9 @@ Item { property string dragTargetStackId: "" Layout.fillHeight: true - Layout.topMargin: Appearance.sizes.hyprlandGapsOut + // No top-only margin: it shoved the whole icon list DOWN inside the bar + // with nothing balancing it — the actual cause of icons drooping below + // the dock. fillHeight + the dockRow centering handle vertical placement. implicitWidth: listView.implicitWidth function popupCenterXForButton(button) { diff --git a/surfaces/quickshell/modules/ii/dock/DockButton.qml b/surfaces/quickshell/modules/ii/dock/DockButton.qml index 4decdae..d56c08d 100644 --- a/surfaces/quickshell/modules/ii/dock/DockButton.qml +++ b/surfaces/quickshell/modules/ii/dock/DockButton.qml @@ -5,9 +5,11 @@ import QtQuick.Layouts RippleButton { Layout.fillHeight: true - Layout.topMargin: Appearance.sizes.elevationMargin - Appearance.sizes.hyprlandGapsOut + // No top-only Layout.topMargin: it shoved every button DOWN with nothing + // balancing it, so icons drooped below the bar. The dockRow already + // centers the row in the visible bar; let fillHeight do the centering. implicitWidth: implicitHeight - topInset - bottomInset buttonRadius: Appearance.rounding.normal - background.implicitHeight: 50 + background.implicitHeight: 44 } diff --git a/surfaces/quickshell/pill/shell.qml b/surfaces/quickshell/pill/shell.qml index a191f25..9355297 100644 --- a/surfaces/quickshell/pill/shell.qml +++ b/surfaces/quickshell/pill/shell.qml @@ -35,6 +35,12 @@ ShellRoot { Behavior on width { NumberAnimation { duration: 120 } } } + // MouseArea, not pointer handlers: every reference Quickshell shell + // (dots-hyprland, belanasaikiran's dock) drives touch through + // MouseArea — TapHandler/DragHandler in a layer-shell window did not + // receive touch here at all (no grow, no fire). onDoubleClicked works + // on this touch stack; the fullscreen dispatch string below is + // verified to maximize the active window (fullscreen 0->1). MouseArea { anchors.fill: parent property real startY: 0 @@ -47,10 +53,10 @@ ShellRoot { if (fired) return if (startY - mouse.y > 35) { fired = true - // Pulse the dock (brings it on top for a few seconds, or - // escalates to overview if already visible). Mirrors the - // bottom-edge hyprgrass swipe-up so there's one dock - // gesture from either surface. + // Pulse the dock (up for a few seconds, or escalate to + // overview if already showing). Mirrors the bottom-edge + // hyprgrass swipe-up so there's one dock gesture from + // either edge. Quickshell.execDetached(["qs", "-c", "ii", "ipc", "call", "dock", "swipeUp"]) } else if (mouse.y - startY > 35) { fired = true @@ -59,15 +65,11 @@ ShellRoot { } onDoubleClicked: { fired = true - // "App mode": fullscreen mode 1 = maximize — gaps and window - // chrome gone, but the qs bar and pill keep their layer - // space. Mode 0 (true fullscreen) covers even the Overlay - // layer on this build and strands you with no pill; app - // mode is the phone default, real fullscreen stays an - // app-side decision (videos etc). - // No-op with nothing focused. Lua-fork dispatcher syntax — - // classic "fullscreen 1" errors out on this build. - Quickshell.execDetached(["hyprctl", "dispatch", "hl.dsp.window.fullscreen({ mode = 1 })"]) + // App-mode fullscreen = mode 0. Whole tiled area, no gaps, + // no border. Route through ii's "dock" IPC (not hyprctl): + // tapping the pill focuses the shell, so hyprctl would act on + // the pill; ii's fullscreen() targets the real app toplevel. + Quickshell.execDetached(["qs", "-c", "ii", "ipc", "call", "dock", "fullscreen"]) } } }