Watch
1
0
Fork
You've already forked souveraine
0

shell: the keyboard owns the bottom edge, and nothing measures it

Two bugs with one cause — the bottom edge was being reasoned about
twice, by hand, against a compositor that is gone.

The rail set ExclusionMode.Ignore (exclusive zone -1: ignore what
everyone else reserved, anchor to the whole screen) and then computed
its own lift from the keyboard's height, probed by shelling out to
`hyprctl -j layers`. Under viewtop there is no hyprctl, so every probe
threw a JSON.parse on empty output and logged it in a loop every second
or two, forever, while the margin stayed at a hardcoded fallback
measured against a keyboard that is not the one running. A 200px
fallback against some other height is exactly "the pill spawns in the
middle of the keyboard". Zone 0 with Normal — the idiom SidebarRight and
ReloadPopup already use — reserves nothing and respects what others
reserved, so the compositor anchors the rail to the top of the
keyboard's exclusive zone. Correct at any height, any rotation, across a
swap, with nothing to go stale. The probe, its retry timer and
probeOskHeight() are deleted rather than ported: a second measurement of
a number the protocol already carries is what this class of bug is made
of.

The dock consulted oskOpen only in its empty-desktop branch, below
effectivePinned, so a PINNED dock ignored the keyboard and sat under it.
DockManifest.dockState() has always reported "suppressed-by-osk" and
_blockReason() has always refused mutations with "osk-open" — the
manifest promised a suppression the dock never performed, so the agent's
view of the screen and the screen disagreed. The check moves above
pinning and applies in every state, staying below dockRevealPulse
because an explicit reveal is the one thing that should outrank the
keyboard.

Verified on the phone: the probe loop is gone from the journal. The
resulting pill and dock positions want eyes on the glass.
This commit is contained in:
Fimeg 2026-08-03 16:53:06 -04:00
commit ca44b0be2a
2 changed files with 64 additions and 83 deletions

View file

@ -79,6 +79,24 @@ Scope { // Scope
// glance at the dock while the keyboard is up.
if (GlobalStates.dockRevealPulse)
return Dock.DockState.Shown;
// The keyboard owns the bottom edge, pinned or not.
//
// This check used to live below `effectivePinned` and inside the
// empty-desktop branch only, so a PINNED dock ignored the OSK entirely
// and sat under the keyboard "the dock is still always showing with
// keyboard". Meanwhile DockManifest.dockState() has always reported
// "suppressed-by-osk" whenever oskOpen, and _blockReason() refuses
// mutations with "osk-open": the manifest promised a suppression the
// dock did not perform, so the agent's view of the screen and the
// screen disagreed. Same shape as the pill layerrule in
// INTERFACE-ARCHITECTURE a documented guarantee the code does not
// enforce.
//
// Below the pulse deliberately: an explicit reveal is the one thing
// that outranks the keyboard, because glancing at the dock while
// typing is exactly what it is for.
if (GlobalStates.oskOpen)
return Dock.DockState.Hidden;
// Rail swipe-down dismissed a visible dock; swipe up brings it back.
if (GlobalStates.dockSuppressed)
return Dock.DockState.Hidden;
@ -86,9 +104,9 @@ Scope { // Scope
return Dock.DockState.Pinned;
if (root.previewShowing)
return Dock.DockState.Shown;
// Empty desktop (nothing focused) reveals the dock, unless the OSK
// took the bottom edge.
if (!GlobalStates.oskOpen && !ToplevelManager.activeToplevel?.activated)
// Empty desktop (nothing focused) reveals the dock. The OSK is handled
// above now, for every state rather than only this one.
if (!ToplevelManager.activeToplevel?.activated)
return Dock.DockState.Shown;
return Dock.DockState.Hidden;
}

View file

@ -22,9 +22,9 @@
// it off for good once the gesture is found. This is the Souveraine analogue
// of Launcher3's AllAppsEduView triple-swipe hint, minus the full overlay.
//
// When the on-screen keyboard is summoned the rail rides ATOP it (bottom
// margin = the keyboard's measured layer height, probed from
// `hyprctl -j layers`, namespace "osk"), and swipe down dismisses the
// When the on-screen keyboard is summoned the rail rides ATOP it the
// compositor anchors it above the keyboard's exclusive zone, so no height is
// measured here at all and swipe down dismisses the
// keyboard first; with no keyboard up, swipe down dismisses a visible dock
// in any state (GlobalStates.dockSuppressed pinned included).
import QtQuick
@ -53,9 +53,29 @@ PanelWindow {
// menu ("keyboard selection not working", 2026-07-21). The pill must stay
// visible it is the dismiss handle just resting exactly at the
// keyboard's top edge, never over the keys.
property int oskLift: 200
margins.bottom: (GlobalStates.oskOpen || rail.swapping) ? rail.oskLift : 0
exclusionMode: ExclusionMode.Ignore
// The compositor places this above the keyboard. Nothing here measures it.
//
// This used to be `ExclusionMode.Ignore` layer-shell's exclusive zone
// -1, "ignore what everyone else reserved and anchor to the whole screen"
// and then hand-computed a bottom margin from the keyboard's height, probed
// by shelling out to `hyprctl -j layers`. Under viewtop there is no
// hyprctl, so the probe threw on every attempt (a JSON.parse of an empty
// string, logged in a loop every second or two forever) and the margin
// stayed at a hardcoded fallback measured against a keyboard that is no
// longer the one running. A fallback that is 200 while the keyboard is some
// other height is exactly "the pill spawns in the middle of the keyboard".
//
// Zone 0 with Normal is the shell's own idiom for this (SidebarRight,
// SidebarLeft unpinned, ReloadPopup): reserve nothing, but respect what
// others reserved. The keyboard already declares an exclusive zone, so the
// compositor anchors this to the top of it correct for any keyboard, any
// height, any rotation, and across a swap, with no measurement to go stale.
//
// START-HERE §3: a control that worked under Hyprland and does nothing now
// is almost always a hyprctl or a hyprland.lua binding, and the fix is
// never to re-add the binding.
exclusionMode: ExclusionMode.Normal
exclusiveZone: 0
color: "transparent"
// Top, not Overlay: stevia's completion bar / layout menu are child
// surfaces of the OSK (which lives on Top). On Overlay the pill sat above
@ -68,76 +88,19 @@ PanelWindow {
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "souveraine:navigation-rail"
// Measure the keyboard's real layer-surface height. squeekboard maps up
// to ~1s after SetVisible on a cold start (OnScreenKeyboard.qml sleeps 1
// before the D-Bus call), so a miss retries once.
property int oskProbeAttempts: 0
property int oskProbeMax: 2
// Re-measure and re-place the pill.
// The height probe that used to live here is gone, along with its retry
// timer and `probeOskHeight()`. It existed to answer "how tall is the
// keyboard right now", including across a swap where stevia (348px) and
// squeekboard (315px) differ and `oskOpen` never flips and getting that
// answer wrong put the pill 33px inside the new keyboard's top rows, where
// it reads as simply gone (observed 2026-07-29: rail y=733 against an osk
// top edge of 732).
//
// This used to be triggered by oskOpen going true and nothing else, which
// is not enough once the keyboard can be SWAPPED: stevia is 348px tall and
// squeekboard is 315px, and oskOpen does not flip across a swap the
// keyboard stays "open" the whole time, only the daemon changes. So the
// lift kept the old keyboard's height and the pill landed 33px inside the
// new one's top rows, behind the keys, where it reads as simply gone.
// (Observed 2026-07-29: rail y=733 against an osk top edge of 732.)
// Any path that can change the keyboard's height must call this.
function probeOskHeight(): void {
rail.oskProbeAttempts = 0;
oskProbeRetry.stop();
oskProbe.running = true;
}
Connections {
target: GlobalStates
function onOskOpenChanged() {
if (GlobalStates.oskOpen) {
rail.oskProbeMax = 2;
rail.probeOskHeight();
}
}
}
Process {
id: oskProbe
command: ["hyprctl", "-j", "layers"]
stdout: StdioCollector {
onStreamFinished: {
try {
const monitors = JSON.parse(text);
for (const mon of Object.values(monitors)) {
for (const surfaces of Object.values(mon.levels)) {
for (const s of surfaces) {
if (s.namespace === "osk" && s.h > 0) {
rail.oskLift = s.h;
return;
}
}
}
}
} catch (e) {
console.log("[rail] osk layer probe parse failed:", e);
}
// Keyboard not mapped yet (cold start takes ~1s). Retry a
// couple of times, then keep the last known height.
if (GlobalStates.oskOpen && rail.oskProbeAttempts < rail.oskProbeMax)
oskProbeRetry.restart();
}
}
}
Timer {
id: oskProbeRetry
// Cold start is slow, but a swap has a real gap where NO osk layer is
// mapped at all (one unit stopped, the next not up yet), so poll harder
// while swapping otherwise the pill wears the old height for over a
// second after every swap.
interval: rail.swapping ? 400 : 1200
onTriggered: {
rail.oskProbeAttempts += 1;
if (GlobalStates.oskOpen) oskProbe.running = true;
}
}
// The compositor answers it now, for free and always correctly, because the
// keyboard's exclusive zone is the same fact the probe was reconstructing.
// Deleted rather than ported to a viewtop equivalent: a second measurement
// of a number the protocol already carries is the drift this class of bug
// is made of.
// --- Gesture stages -----------------------------------------------------
// Upward travel, in px, at which each stage arms. The drag must climb
@ -327,11 +290,11 @@ PanelWindow {
lastTapAt = -1
rail.swapping = true
swapSettle.restart()
// The incoming keyboard is a different height. Keep probing
// across the whole stop/start gap or the pill keeps the old
// one's lift and disappears into the new one's keys.
rail.oskProbeMax = 12
rail.probeOskHeight()
// No re-probe across the swap. The incoming keyboard is a
// different height and that used to strand the pill inside the
// new one's keys; the compositor re-anchors this surface when
// the new exclusive zone lands, so the height nobody measures
// is the height that cannot be wrong.
Gestures.deliver("osk-swap", Gestures.oskSwapAction)
return
}