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 {
|
function reveal(): void {
|
||||||
GlobalStates.pulseDockReveal();
|
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
|
// Settings-surface stub (entry point b). souveraine-settings doesn't
|
||||||
|
|
@ -226,16 +250,13 @@ Scope { // Scope
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: dockRow
|
id: dockRow
|
||||||
// Match the visible rectangle's insets so the icon
|
// Anchored to the visible bar. Split top/bottom
|
||||||
// row lands INSIDE the thin pill, not drooping
|
// so the icons ride UP inside the bar instead of
|
||||||
// below it. The bar stays thin; only the icons move.
|
// drooping out the bottom (uniform margins left
|
||||||
anchors {
|
// them low; less top + more bottom lifts them).
|
||||||
top: parent.top
|
anchors.fill: dockVisualBackground
|
||||||
bottom: parent.bottom
|
anchors.topMargin: 0
|
||||||
horizontalCenter: parent.horizontalCenter
|
anchors.bottomMargin: 8
|
||||||
topMargin: Appearance.sizes.elevationMargin
|
|
||||||
bottomMargin: Appearance.sizes.hyprlandGapsOut
|
|
||||||
}
|
|
||||||
spacing: 3
|
spacing: 3
|
||||||
property real padding: 5
|
property real padding: 5
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -292,14 +292,22 @@ DockButton {
|
||||||
active: !isSeparator
|
active: !isSeparator
|
||||||
sourceComponent: Item {
|
sourceComponent: Item {
|
||||||
anchors.centerIn: parent
|
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 {
|
Loader {
|
||||||
id: iconImageLoader
|
id: iconImageLoader
|
||||||
anchors {
|
anchors {
|
||||||
left: parent.left
|
left: parent.left
|
||||||
right: parent.right
|
right: parent.right
|
||||||
verticalCenter: parent.verticalCenter
|
top: parent.top
|
||||||
}
|
}
|
||||||
|
height: root.iconSize
|
||||||
active: !root.isSeparator
|
active: !root.isSeparator
|
||||||
sourceComponent: IconImage {
|
sourceComponent: IconImage {
|
||||||
source: Quickshell.iconPath(AppSearch.guessIcon(appToplevel.appId), "image-missing")
|
source: Quickshell.iconPath(AppSearch.guessIcon(appToplevel.appId), "image-missing")
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,9 @@ Item {
|
||||||
property string dragTargetStackId: ""
|
property string dragTargetStackId: ""
|
||||||
|
|
||||||
Layout.fillHeight: true
|
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
|
implicitWidth: listView.implicitWidth
|
||||||
|
|
||||||
function popupCenterXForButton(button) {
|
function popupCenterXForButton(button) {
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,11 @@ import QtQuick.Layouts
|
||||||
|
|
||||||
RippleButton {
|
RippleButton {
|
||||||
Layout.fillHeight: true
|
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
|
implicitWidth: implicitHeight - topInset - bottomInset
|
||||||
buttonRadius: Appearance.rounding.normal
|
buttonRadius: Appearance.rounding.normal
|
||||||
|
|
||||||
background.implicitHeight: 50
|
background.implicitHeight: 44
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,12 @@ ShellRoot {
|
||||||
Behavior on width { NumberAnimation { duration: 120 } }
|
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 {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
property real startY: 0
|
property real startY: 0
|
||||||
|
|
@ -47,10 +53,10 @@ ShellRoot {
|
||||||
if (fired) return
|
if (fired) return
|
||||||
if (startY - mouse.y > 35) {
|
if (startY - mouse.y > 35) {
|
||||||
fired = true
|
fired = true
|
||||||
// Pulse the dock (brings it on top for a few seconds, or
|
// Pulse the dock (up for a few seconds, or escalate to
|
||||||
// escalates to overview if already visible). Mirrors the
|
// overview if already showing). Mirrors the bottom-edge
|
||||||
// bottom-edge hyprgrass swipe-up so there's one dock
|
// hyprgrass swipe-up so there's one dock gesture from
|
||||||
// gesture from either surface.
|
// either edge.
|
||||||
Quickshell.execDetached(["qs", "-c", "ii", "ipc", "call", "dock", "swipeUp"])
|
Quickshell.execDetached(["qs", "-c", "ii", "ipc", "call", "dock", "swipeUp"])
|
||||||
} else if (mouse.y - startY > 35) {
|
} else if (mouse.y - startY > 35) {
|
||||||
fired = true
|
fired = true
|
||||||
|
|
@ -59,15 +65,11 @@ ShellRoot {
|
||||||
}
|
}
|
||||||
onDoubleClicked: {
|
onDoubleClicked: {
|
||||||
fired = true
|
fired = true
|
||||||
// "App mode": fullscreen mode 1 = maximize — gaps and window
|
// App-mode fullscreen = mode 0. Whole tiled area, no gaps,
|
||||||
// chrome gone, but the qs bar and pill keep their layer
|
// no border. Route through ii's "dock" IPC (not hyprctl):
|
||||||
// space. Mode 0 (true fullscreen) covers even the Overlay
|
// tapping the pill focuses the shell, so hyprctl would act on
|
||||||
// layer on this build and strands you with no pill; app
|
// the pill; ii's fullscreen() targets the real app toplevel.
|
||||||
// mode is the phone default, real fullscreen stays an
|
Quickshell.execDetached(["qs", "-c", "ii", "ipc", "call", "dock", "fullscreen"])
|
||||||
// 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 })"])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue