feat(quickshell): dim the desktop behind the expanded hyprchrome bar
A full-screen scrim carrying the dense bar's drafting grid, shown while the rail is expanded and faded out with it. It sits on the TOP layer while the bar moves to OVERLAY. Both on one layer would stack by surface creation order, which is not something to rely on; a layer apart makes "above windows, below the bar" a guarantee. The mask is an empty Region, so the scrim takes no clicks and reserves nothing. The grid is the dense bar's, at twice the spacing and with registration crosses on every other intersection. Its 0.018 opacity was tuned against a near-black panel and is invisible over a scrim on lit windows, so grid and crosses are both properties rather than constants, and the lines take a desaturated accent derived from the palette instead of plain text colour. Cross geometry rounds with Math.floor on both the mark's offset and the bars inside it. anchors.*Center halves the box unfloored, which put an even-sized mark half a pixel off the 1px rule it registers against. crossThickness is in steps for the same reason: 1 -> 1px, 2 -> 3px, 3 -> 5px, since only an odd width straddles a rule symmetrically. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HEtXvseVb5gwAtKNhFx2PU
This commit is contained in:
@@ -0,0 +1,162 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import QtQuick
|
||||||
|
import qs.hyprchrome.theme
|
||||||
|
|
||||||
|
// Full-screen scrim behind the bar: dims the desktop and lays the dense bar's
|
||||||
|
// drafting grid over it while the rail is expanded.
|
||||||
|
//
|
||||||
|
// Sits on the TOP layer while the bar itself is on OVERLAY. Two surfaces on the
|
||||||
|
// same layer stack by creation order, which is not something to rely on; one
|
||||||
|
// layer apart is a guarantee — above ordinary windows, below the bar.
|
||||||
|
//
|
||||||
|
// It reserves nothing and takes no input: the mask is an empty Region, so
|
||||||
|
// clicks land on whatever is underneath rather than on the scrim.
|
||||||
|
PanelWindow {
|
||||||
|
id: backdrop
|
||||||
|
|
||||||
|
property bool active: true
|
||||||
|
property real dim: 0.55
|
||||||
|
property int gridSpacing: 120
|
||||||
|
|
||||||
|
// The dense bar drew this grid at 0.018 against its own near-black panel.
|
||||||
|
// Over a 55% scrim on top of lit windows that is invisible, so it is a
|
||||||
|
// knob rather than a constant.
|
||||||
|
property real gridOpacity: 0.1
|
||||||
|
|
||||||
|
// The accent with its saturation pulled back: warm enough to read as part
|
||||||
|
// of the palette, not so loud that a full-screen grid competes with the
|
||||||
|
// bar. Derived rather than a literal so it tracks a palette change.
|
||||||
|
// Registration crosses sit on every other intersection of the grid.
|
||||||
|
property color crossColor: Theme.muted
|
||||||
|
property real crossOpacity: 0.35
|
||||||
|
property int crossSize: 20
|
||||||
|
|
||||||
|
// Thickness in STEPS, not pixels: 1 -> 1px, 2 -> 3px, 3 -> 5px. Only odd
|
||||||
|
// widths can straddle a 1px rule symmetrically, so an even pixel count
|
||||||
|
// would push every mark half a pixel off the grid it registers against.
|
||||||
|
property int crossThickness: 2
|
||||||
|
readonly property int crossWeight: Math.max(1, backdrop.crossThickness) * 2 - 1
|
||||||
|
|
||||||
|
property color gridColor: Qt.hsla(Theme.accent.hslHue,
|
||||||
|
Theme.accent.hslSaturation * 0.45,
|
||||||
|
Theme.accent.hslLightness,
|
||||||
|
1)
|
||||||
|
|
||||||
|
visible: scrim.opacity > 0
|
||||||
|
|
||||||
|
WlrLayershell.layer: WlrLayer.Top
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||||
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
readonly property int crossColumns: Math.ceil(backdrop.width / (backdrop.gridSpacing * 2)) + 1
|
||||||
|
readonly property int crossRows: Math.ceil(backdrop.height / (backdrop.gridSpacing * 2)) + 1
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: true
|
||||||
|
left: true
|
||||||
|
right: true
|
||||||
|
bottom: true
|
||||||
|
}
|
||||||
|
|
||||||
|
mask: Region {}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: scrim
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
opacity: backdrop.active ? 1 : 0
|
||||||
|
|
||||||
|
// Matched to the bar's own collapse so the scrim and the panels resolve
|
||||||
|
// together rather than one trailing the other.
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 200
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Theme.surface
|
||||||
|
opacity: backdrop.dim
|
||||||
|
}
|
||||||
|
|
||||||
|
// Faint drafting grid; no gradient and deliberately subordinate to
|
||||||
|
// whatever is showing through it.
|
||||||
|
Repeater {
|
||||||
|
model: Math.ceil(scrim.width / backdrop.gridSpacing)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
x: index * backdrop.gridSpacing
|
||||||
|
width: 1
|
||||||
|
height: scrim.height
|
||||||
|
color: backdrop.gridColor
|
||||||
|
opacity: backdrop.gridOpacity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Math.ceil(scrim.height / backdrop.gridSpacing)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
y: index * backdrop.gridSpacing
|
||||||
|
width: scrim.width
|
||||||
|
height: 1
|
||||||
|
color: backdrop.gridColor
|
||||||
|
opacity: backdrop.gridOpacity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register marks on every other line, so they land on a 2x grid rather
|
||||||
|
// than on every crossing — sparse enough to read as drafting registration
|
||||||
|
// rather than as texture.
|
||||||
|
Repeater {
|
||||||
|
model: backdrop.crossColumns * backdrop.crossRows
|
||||||
|
|
||||||
|
Item {
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
readonly property int column: index % backdrop.crossColumns
|
||||||
|
readonly property int row: Math.floor(index / backdrop.crossColumns)
|
||||||
|
|
||||||
|
// Marks on every other rule in both directions, so they line up
|
||||||
|
// in columns as well as rows. Both offsets are whole multiples of
|
||||||
|
// gridSpacing, so every mark lands on a real intersection.
|
||||||
|
//
|
||||||
|
// Math.floor, not /2: the item offset and the bars inside it must
|
||||||
|
// round the same way, or an even crossSize sits half a pixel off the
|
||||||
|
// rule it marks.
|
||||||
|
x: column * backdrop.gridSpacing * 2 - Math.floor(backdrop.crossSize / 2)
|
||||||
|
y: row * backdrop.gridSpacing * 2 - Math.floor(backdrop.crossSize / 2)
|
||||||
|
width: backdrop.crossSize
|
||||||
|
height: backdrop.crossSize
|
||||||
|
opacity: backdrop.crossOpacity
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
// Placed with the same Math.floor the item's own offset uses.
|
||||||
|
// anchors.verticalCenter halves the height unfloored, so an even
|
||||||
|
// crossSize put the 1px bar half a pixel off the rule it marks.
|
||||||
|
y: Math.floor(backdrop.crossSize / 2) - Math.floor(backdrop.crossWeight / 2)
|
||||||
|
width: parent.width
|
||||||
|
height: backdrop.crossWeight
|
||||||
|
color: backdrop.crossColor
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
x: Math.floor(backdrop.crossSize / 2) - Math.floor(backdrop.crossWeight / 2)
|
||||||
|
width: backdrop.crossWeight
|
||||||
|
height: parent.height
|
||||||
|
color: backdrop.crossColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import QtQuick.Shapes
|
|||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Hyprland
|
import Quickshell.Hyprland
|
||||||
|
import Quickshell.Wayland
|
||||||
import qs.hyprchrome.widgets
|
import qs.hyprchrome.widgets
|
||||||
import qs.hyprchrome.widgets.host
|
import qs.hyprchrome.widgets.host
|
||||||
import qs.hyprchrome.widgets.vitals
|
import qs.hyprchrome.widgets.vitals
|
||||||
@@ -45,12 +46,24 @@ Scope {
|
|||||||
onPressed: root.toggle()
|
onPressed: root.toggle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scrim first: it is a layer below the bar, so stacking does not depend on
|
||||||
|
// creation order, but keeping the declaration order the same as the visual
|
||||||
|
// order costs nothing.
|
||||||
|
ChromeBackdrop {
|
||||||
|
screen: root.targetScreen
|
||||||
|
active: root.expanded
|
||||||
|
}
|
||||||
|
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: window
|
id: window
|
||||||
|
|
||||||
screen: root.targetScreen
|
screen: root.targetScreen
|
||||||
visible: root.targetScreen !== null
|
visible: root.targetScreen !== null
|
||||||
|
|
||||||
|
// One layer above the scrim, so the stacking is guaranteed rather than
|
||||||
|
// dependent on surface creation order. See ChromeBackdrop.
|
||||||
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
|
||||||
property int margin: 12
|
property int margin: 12
|
||||||
|
|
||||||
// Where the panels will SETTLE, not where they are mid-transition. Binding
|
// Where the panels will SETTLE, not where they are mid-transition. Binding
|
||||||
|
|||||||
Reference in New Issue
Block a user