dock/pill: fix icon centering, pill z-order, app-mode fullscreen
- dock icons center in the bar (DockApps top-margin was shoving the row down; DockAppButton block now reserves half the dot strip) - pill double-tap fullscreens the real app: routes through dock IPC fullscreen() targeting Hyprland.activeToplevel.address, mode 0, so the pill tap focusing the shell no longer redirects it - pill must be re-created after the dock or it sits under it and drops touch (both on Overlay; z-order = creation order) - HOW-IT-WORKS.md: the concise contract for pill/dock/fullscreen
This commit is contained in:
parent
693df8b55b
commit
0a8e0f7294
6 changed files with 99 additions and 27 deletions
37
surfaces/quickshell/HOW-IT-WORKS.md
Normal file
37
surfaces/quickshell/HOW-IT-WORKS.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue