diff --git a/dotfiles/quickshell/hyprchrome/widgets/ChromeBackdrop.qml b/dotfiles/quickshell/hyprchrome/widgets/ChromeBackdrop.qml new file mode 100644 index 0000000..4de1703 --- /dev/null +++ b/dotfiles/quickshell/hyprchrome/widgets/ChromeBackdrop.qml @@ -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 + } + } + } + } +} diff --git a/dotfiles/quickshell/hyprchrome/widgets/HyprChromeBar.qml b/dotfiles/quickshell/hyprchrome/widgets/HyprChromeBar.qml index b407eb3..1177859 100644 --- a/dotfiles/quickshell/hyprchrome/widgets/HyprChromeBar.qml +++ b/dotfiles/quickshell/hyprchrome/widgets/HyprChromeBar.qml @@ -3,6 +3,7 @@ import QtQuick.Shapes import QtQuick.Layouts import Quickshell import Quickshell.Hyprland +import Quickshell.Wayland import qs.hyprchrome.widgets import qs.hyprchrome.widgets.host import qs.hyprchrome.widgets.vitals @@ -45,12 +46,24 @@ Scope { 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 { id: window screen: root.targetScreen 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 // Where the panels will SETTLE, not where they are mid-transition. Binding