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:
parent
1f028c129e
commit
ca44b0be2a
2 changed files with 64 additions and 83 deletions
|
|
@ -79,6 +79,24 @@ Scope { // Scope
|
||||||
// glance at the dock while the keyboard is up.
|
// glance at the dock while the keyboard is up.
|
||||||
if (GlobalStates.dockRevealPulse)
|
if (GlobalStates.dockRevealPulse)
|
||||||
return Dock.DockState.Shown;
|
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.
|
// Rail swipe-down dismissed a visible dock; swipe up brings it back.
|
||||||
if (GlobalStates.dockSuppressed)
|
if (GlobalStates.dockSuppressed)
|
||||||
return Dock.DockState.Hidden;
|
return Dock.DockState.Hidden;
|
||||||
|
|
@ -86,9 +104,9 @@ Scope { // Scope
|
||||||
return Dock.DockState.Pinned;
|
return Dock.DockState.Pinned;
|
||||||
if (root.previewShowing)
|
if (root.previewShowing)
|
||||||
return Dock.DockState.Shown;
|
return Dock.DockState.Shown;
|
||||||
// Empty desktop (nothing focused) reveals the dock, unless the OSK
|
// Empty desktop (nothing focused) reveals the dock. The OSK is handled
|
||||||
// took the bottom edge.
|
// above now, for every state rather than only this one.
|
||||||
if (!GlobalStates.oskOpen && !ToplevelManager.activeToplevel?.activated)
|
if (!ToplevelManager.activeToplevel?.activated)
|
||||||
return Dock.DockState.Shown;
|
return Dock.DockState.Shown;
|
||||||
return Dock.DockState.Hidden;
|
return Dock.DockState.Hidden;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,9 @@
|
||||||
// it off for good once the gesture is found. This is the Souveraine analogue
|
// 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.
|
// of Launcher3's AllAppsEduView triple-swipe hint, minus the full overlay.
|
||||||
//
|
//
|
||||||
// When the on-screen keyboard is summoned the rail rides ATOP it (bottom
|
// When the on-screen keyboard is summoned the rail rides ATOP it — the
|
||||||
// margin = the keyboard's measured layer height, probed from
|
// compositor anchors it above the keyboard's exclusive zone, so no height is
|
||||||
// `hyprctl -j layers`, namespace "osk"), and swipe down dismisses the
|
// measured here at all — and swipe down dismisses the
|
||||||
// keyboard first; with no keyboard up, swipe down dismisses a visible dock
|
// keyboard first; with no keyboard up, swipe down dismisses a visible dock
|
||||||
// in any state (GlobalStates.dockSuppressed — pinned included).
|
// in any state (GlobalStates.dockSuppressed — pinned included).
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
|
@ -53,9 +53,29 @@ PanelWindow {
|
||||||
// menu ("keyboard selection not working", 2026-07-21). The pill must stay
|
// menu ("keyboard selection not working", 2026-07-21). The pill must stay
|
||||||
// visible — it is the dismiss handle — just resting exactly at the
|
// visible — it is the dismiss handle — just resting exactly at the
|
||||||
// keyboard's top edge, never over the keys.
|
// keyboard's top edge, never over the keys.
|
||||||
property int oskLift: 200
|
// The compositor places this above the keyboard. Nothing here measures it.
|
||||||
margins.bottom: (GlobalStates.oskOpen || rail.swapping) ? rail.oskLift : 0
|
//
|
||||||
exclusionMode: ExclusionMode.Ignore
|
// 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"
|
color: "transparent"
|
||||||
// Top, not Overlay: stevia's completion bar / layout menu are child
|
// 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
|
// surfaces of the OSK (which lives on Top). On Overlay the pill sat above
|
||||||
|
|
@ -68,76 +88,19 @@ PanelWindow {
|
||||||
WlrLayershell.layer: WlrLayer.Top
|
WlrLayershell.layer: WlrLayer.Top
|
||||||
WlrLayershell.namespace: "souveraine:navigation-rail"
|
WlrLayershell.namespace: "souveraine:navigation-rail"
|
||||||
|
|
||||||
// Measure the keyboard's real layer-surface height. squeekboard maps up
|
// The height probe that used to live here is gone, along with its retry
|
||||||
// to ~1s after SetVisible on a cold start (OnScreenKeyboard.qml sleeps 1
|
// timer and `probeOskHeight()`. It existed to answer "how tall is the
|
||||||
// before the D-Bus call), so a miss retries once.
|
// keyboard right now", including across a swap where stevia (348px) and
|
||||||
property int oskProbeAttempts: 0
|
// squeekboard (315px) differ and `oskOpen` never flips — and getting that
|
||||||
property int oskProbeMax: 2
|
// 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
|
||||||
// Re-measure and re-place the pill.
|
// top edge of 732).
|
||||||
//
|
//
|
||||||
// This used to be triggered by oskOpen going true and nothing else, which
|
// The compositor answers it now, for free and always correctly, because the
|
||||||
// is not enough once the keyboard can be SWAPPED: stevia is 348px tall and
|
// keyboard's exclusive zone is the same fact the probe was reconstructing.
|
||||||
// squeekboard is 315px, and oskOpen does not flip across a swap — the
|
// Deleted rather than ported to a viewtop equivalent: a second measurement
|
||||||
// keyboard stays "open" the whole time, only the daemon changes. So the
|
// of a number the protocol already carries is the drift this class of bug
|
||||||
// lift kept the old keyboard's height and the pill landed 33px inside the
|
// is made of.
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Gesture stages -----------------------------------------------------
|
// --- Gesture stages -----------------------------------------------------
|
||||||
// Upward travel, in px, at which each stage arms. The drag must climb
|
// Upward travel, in px, at which each stage arms. The drag must climb
|
||||||
|
|
@ -327,11 +290,11 @@ PanelWindow {
|
||||||
lastTapAt = -1
|
lastTapAt = -1
|
||||||
rail.swapping = true
|
rail.swapping = true
|
||||||
swapSettle.restart()
|
swapSettle.restart()
|
||||||
// The incoming keyboard is a different height. Keep probing
|
// No re-probe across the swap. The incoming keyboard is a
|
||||||
// across the whole stop/start gap or the pill keeps the old
|
// different height and that used to strand the pill inside the
|
||||||
// one's lift and disappears into the new one's keys.
|
// new one's keys; the compositor re-anchors this surface when
|
||||||
rail.oskProbeMax = 12
|
// the new exclusive zone lands, so the height nobody measures
|
||||||
rail.probeOskHeight()
|
// is the height that cannot be wrong.
|
||||||
Gestures.deliver("osk-swap", Gestures.oskSwapAction)
|
Gestures.deliver("osk-swap", Gestures.oskSwapAction)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue