Watch
1
0
Fork
You've already forked souveraine
0

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:
Fimeg 2026-07-12 13:07:02 -04:00
commit 0a8e0f7294
6 changed files with 99 additions and 27 deletions

View 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.

View file

@ -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

View file

@ -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")

View file

@ -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) {

View file

@ -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
} }

View file

@ -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 })"])
} }
} }
} }