Two causes, both outside the panel animation itself. bodyBottom and bodyEndPadding are animated reals, so their sum spends the tail of every transition on a fraction. A layout rounds that up, then drops a pixel the moment the animation lands on its exact value — a 1px hop after the motion has visibly finished. implicitHeight and targetHeight now round. The rest was the surface. Hyprland animates layer-surface resizes (animations enabled, `layers` left at its default), and the deferred shrink put that resize exactly where the panel motion ended. The bar is now sized once to the expanded rail via the new BarPanel.expandedHeight and never resizes; only exclusiveZone tracks the state, so the desktop still reflows once per toggle, at the start. That retires barHeight, the content-height handler and the 340ms shrink timer. A surface that stays tall would swallow clicks across the screen while the rail is collapsed, so input is masked to the panel row. Instrumenting BarPanel per frame ruled the panels themselves out first: slug and summary hold the same absolute y through an entire collapse. The bar and scrim also get their own layer namespaces. Nothing depends on them yet; they are the handle for a layerrule that would exempt the rail from compositor animations without catching the launchers, which share the default "quickshell" namespace and do want their fade. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HEtXvseVb5gwAtKNhFx2PU
164 lines
5.9 KiB
QML
164 lines
5.9 KiB
QML
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.namespace: "hyprchrome-scrim"
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|