Fix: Never pair exclusionMode with exclusiveZone
Quickshell's exclusiveZone setter flips exclusionMode back to Normal as a side effect, so a window declaring both is at the mercy of which property the QML engine applies last. The 2026-09-14 Qt update changed that order and every full-screen overlay slid under the bar. Each overlay now declares ExclusionMode.Ignore alone. The capture picker was the visible failure: it started under the bar, the full-output freeze frame was squeezed into a shorter box, and every selection landed one bar-height off in the real capture. tests/quickshell/exclusion-idiom-contract fails any file that pairs them.
This commit is contained in:
@@ -250,7 +250,7 @@ docs/ Settings reference, and the design specs behind the work
|
|||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
189 of them, under `tests/`. `tests/contracts.manifest` classifies every
|
190 of them, under `tests/`. `tests/contracts.manifest` classifies every
|
||||||
contract by the capabilities it needs. Run the hermetic set, or grant a
|
contract by the capabilities it needs. Run the hermetic set, or grant a
|
||||||
specific external capability when automation needs it:
|
specific external capability when automation needs it:
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,13 @@ PanelWindow {
|
|||||||
right: true
|
right: true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fullscreen overlays must not reserve space, or every window on the
|
// Ignore, and nothing else: reserve no space (or every window on the
|
||||||
// workspace gets resized as the picker opens and closes.
|
// workspace resizes as the picker opens and closes) and respect nobody's
|
||||||
exclusiveZone: 0
|
// (or the window starts under the bar, the full-output freeze frame is
|
||||||
|
// squeezed into a shorter box, and every selection lands one bar-height
|
||||||
|
// off in the real capture). Never pair this with exclusiveZone; see
|
||||||
|
// tests/quickshell/exclusion-idiom-contract.
|
||||||
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
WlrLayershell.namespace: "qs-capture" // matched by a layerrule in hypr/rules.lua
|
WlrLayershell.namespace: "qs-capture" // matched by a layerrule in hypr/rules.lua
|
||||||
WlrLayershell.layer: WlrLayer.Overlay
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ PanelWindow {
|
|||||||
|
|
||||||
anchors { top: true; bottom: true; left: true; right: true }
|
anchors { top: true; bottom: true; left: true; right: true }
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
exclusiveZone: 0
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
// The `^qs-popover` prefix rule in hypr/rules.lua blurs what is behind
|
// The `^qs-popover` prefix rule in hypr/rules.lua blurs what is behind
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ PanelWindow {
|
|||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
// A dock that reserved space would not be intellihiding.
|
// A dock that reserved space would not be intellihiding.
|
||||||
exclusiveZone: 0
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
// Matched by the `qs-dock` layer rule in hypr/rules.lua — do not rename.
|
// Matched by the `qs-dock` layer rule in hypr/rules.lua — do not rename.
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ PanelWindow {
|
|||||||
|
|
||||||
anchors { top: true; bottom: true; left: true; right: true }
|
anchors { top: true; bottom: true; left: true; right: true }
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
exclusiveZone: 0
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
// Blurred by the `^qs-popover` rule in hypr/rules.lua; the scrim is painted
|
// Blurred by the `^qs-popover` rule in hypr/rules.lua; the scrim is painted
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ PanelWindow {
|
|||||||
// Reserve nothing and respect nothing: the glow is drawn over the whole
|
// Reserve nothing and respect nothing: the glow is drawn over the whole
|
||||||
// output including under the bar and the dock, which is what makes it
|
// output including under the bar and the dock, which is what makes it
|
||||||
// visible from wherever the eyes happen to be.
|
// visible from wherever the eyes happen to be.
|
||||||
exclusiveZone: 0
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ PanelWindow {
|
|||||||
screen: root.modelData
|
screen: root.modelData
|
||||||
anchors.bottom: true
|
anchors.bottom: true
|
||||||
margins.bottom: Theme.dockIconSize + Theme.dockPadding * 2 + Theme.barGap * 3
|
margins.bottom: Theme.dockIconSize + Theme.dockPadding * 2 + Theme.barGap * 3
|
||||||
exclusiveZone: 0
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
implicitWidth: root.desiredWidth
|
implicitWidth: root.desiredWidth
|
||||||
implicitHeight: 64
|
implicitHeight: 64
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
exclusiveZone: 0
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
// Matched by the `qs-overlay` layer rule in hypr/rules.lua — do not rename.
|
// Matched by the `qs-overlay` layer rule in hypr/rules.lua — do not rename.
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ Variants {
|
|||||||
implicitWidth: 260
|
implicitWidth: 260
|
||||||
implicitHeight: 172
|
implicitHeight: 172
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
exclusiveZone: 0
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
mask: Region {}
|
mask: Region {}
|
||||||
|
|
||||||
|
|||||||
@@ -383,6 +383,13 @@ Lua-configured Hyprland, prints the refusal to stdout, and exits 0. `eval` exits
|
|||||||
0 on syntax and runtime errors too. The only trustworthy signal that a write
|
0 on syntax and runtime errors too. The only trustworthy signal that a write
|
||||||
landed is reading the value back.
|
landed is reading the value back.
|
||||||
|
|
||||||
|
**`exclusionMode: ExclusionMode.Ignore` goes alone.** Quickshell's `exclusiveZone`
|
||||||
|
setter flips `exclusionMode` back to `Normal` as a side effect, so a window that
|
||||||
|
declares both is at the mercy of which property the QML engine applies last.
|
||||||
|
The 2026-09-14 Qt update changed that order and every full-screen overlay slid
|
||||||
|
under the bar. `tests/quickshell/exclusion-idiom-contract` fails any file that
|
||||||
|
pairs them.
|
||||||
|
|
||||||
**The Settings window is tiled.** `implicitWidth` is a hint; the layout decides,
|
**The Settings window is tiled.** `implicitWidth` is a hint; the layout decides,
|
||||||
and it ranges from a half-screen split to the full display. `SliderRow` stacks
|
and it ranges from a half-screen split to the full display. `SliderRow` stacks
|
||||||
its control under the label below 520px. Test narrow.
|
its control under the label below 520px. Test narrow.
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ PanelWindow {
|
|||||||
|
|
||||||
anchors { top: true; bottom: true; left: true; right: true }
|
anchors { top: true; bottom: true; left: true; right: true }
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
exclusiveZone: 0
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
WlrLayershell.namespace: "qs-popover-welcome"
|
WlrLayershell.namespace: "qs-popover-welcome"
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ live-desktop tests/quickshell/dock-position-contract
|
|||||||
# Reads Hyprland option descriptions from the live compositor to verify enum
|
# Reads Hyprland option descriptions from the live compositor to verify enum
|
||||||
# mappings.
|
# mappings.
|
||||||
live-compositor tests/quickshell/enum-hypr-map-contract
|
live-compositor tests/quickshell/enum-hypr-map-contract
|
||||||
|
hermetic tests/quickshell/exclusion-idiom-contract
|
||||||
hermetic tests/quickshell/fingerprint-contract
|
hermetic tests/quickshell/fingerprint-contract
|
||||||
# Reads the host firewall state through the production firewall helper.
|
# Reads the host firewall state through the production firewall helper.
|
||||||
live-host tests/quickshell/firewall-contract
|
live-host tests/quickshell/firewall-contract
|
||||||
|
|||||||
Executable
+45
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# A window that wants the whole output must ask for ExclusionMode.Ignore and
|
||||||
|
# nothing else. Quickshell's exclusiveZone setter forces exclusionMode back to
|
||||||
|
# Normal as a side effect, so a window declaring both
|
||||||
|
#
|
||||||
|
# exclusiveZone: 0
|
||||||
|
# exclusionMode: ExclusionMode.Ignore
|
||||||
|
#
|
||||||
|
# ends up with whichever property the QML engine applied last, and that order
|
||||||
|
# is not ours to control: the 2026-09-14 Qt/Quickshell update flipped it, every
|
||||||
|
# full-screen overlay slid down under the bar, and the screenshot picker's
|
||||||
|
# frozen frame stopped lining up with the screen it was a picture of. This pins
|
||||||
|
# the order-independent idiom: Ignore alone, never paired with exclusiveZone.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
qs="$repo_dir/config/dot/quickshell"
|
||||||
|
|
||||||
|
fail() { printf 'exclusion idiom contract: %s\n' "$1" >&2; exit 1; }
|
||||||
|
[[ -d "$qs/modules" ]] || fail "missing $qs/modules"
|
||||||
|
|
||||||
|
python3 - "$qs" <<'PY'
|
||||||
|
import re, sys, pathlib
|
||||||
|
qs = pathlib.Path(sys.argv[1])
|
||||||
|
problems = []
|
||||||
|
ignore = re.compile(r'\bexclusionMode\s*:\s*ExclusionMode\.Ignore\b')
|
||||||
|
zone = re.compile(r'^\s*(?:WlrLayershell\.)?exclusiveZone\s*:', re.M)
|
||||||
|
checked = 0
|
||||||
|
for path in sorted(qs.rglob('*.qml')):
|
||||||
|
text = path.read_text()
|
||||||
|
if not ignore.search(text):
|
||||||
|
continue
|
||||||
|
checked += 1
|
||||||
|
for m in zone.finditer(text):
|
||||||
|
line = text.count('\n', 0, m.start()) + 1
|
||||||
|
problems.append(f"{path.relative_to(qs)}:{line}: exclusiveZone set in a file that uses "
|
||||||
|
"ExclusionMode.Ignore; drop it, Ignore already means -1")
|
||||||
|
if checked == 0:
|
||||||
|
problems.append("no window uses ExclusionMode.Ignore; the overlays this pins are gone")
|
||||||
|
if problems:
|
||||||
|
print('\n'.join(problems), file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
PY
|
||||||
Reference in New Issue
Block a user