A chamfer with no neighbour behind it now carries the corner it removed, put back OUTSIDE the panel as a detached accent triangle. capGap is the perpendicular distance from the cut, hence the per-axis shift of capGap over root 2: the cap moves along the cut's normal, not along an axis. The trace joining two caps belongs to the RAIL, not the panel — the run it draws is the gap BETWEEN two panels, which no panel can see. HyprChromeBar filters the row down to panels (a slack Item has no `rightChamfer`, so spacers drop out, and dropping them is exactly what makes a trace span them), then joins each right cap to the next left cap: straight, one 45° step at the midpoint of the gap, straight. It meets the middle of each cap's outward face rather than its tip. TestPanel is a staging slot between two spacers. It counts seconds since load, which is the cheapest proof the panel is live, and standing alone it exercises a cap and a trace at both ends — something a rail of butted panels never does. Restructures the module in the same commit, since the moves and the edits above land in the same files: folders are PascalCase, the bar and its widgets moved under Widgets/Bar, and DebugWindow takes the HyprChrome Theme instead of the legacy one. The two singletons are identical today, so that is not a visual fix — it is a palette edit reaching the debug stage in future. Rename detection needs -M40% to follow HyprChromeBar, which grew past the default similarity threshold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U19X5LGTxtq4pb4jdNVivv
264 lines
12 KiB
QML
264 lines
12 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import Quickshell
|
|
import Quickshell.Wayland
|
|
import QtQuick
|
|
import qs.HyprChrome.Theme
|
|
|
|
// Scrim behind the bar: dims the desktop and lays the dense bar's drafting grid
|
|
// over it. It is always on screen — only how far DOWN it reaches changes.
|
|
// Expanded, it covers the whole output; collapsed, it shrinks to the band the
|
|
// rail itself occupies, so the bar keeps its backing without the desktop being
|
|
// dimmed. That collapsed band ends in a fade rather than a cut, so there is no
|
|
// hard line across the wallpaper; expanded there is nothing to fade against —
|
|
// the scrim runs to the bottom of the output.
|
|
//
|
|
// Its layer arrives from HyprChromeShell, which derives it together with the
|
|
// bar's so the two stay exactly one level apart — see that file for why the
|
|
// pair cannot be split. Collapsed that puts it on BACKGROUND, shared with the
|
|
// wallpaper (hyprpaper), where order IS creation order: if the wallpaper is
|
|
// restarted under a running shell it comes up on top and the collapsed band
|
|
// goes with it. A `layerrule = order` in the Hyprland config is the fix if that
|
|
// ever bites.
|
|
//
|
|
// 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
|
|
|
|
// Which layer to sit on. An int rather than a private decision: it is half
|
|
// of a pair with the bar's, so the shell derives both.
|
|
property int wlrLayer: WlrLayer.Top
|
|
|
|
property real dim: 0.75
|
|
property int gridSpacing: 60
|
|
|
|
// Height of the collapsed rail, including its margins — the band the scrim
|
|
// stays behind while the bar is collapsed. Driven by the host, which is the
|
|
// only thing that knows what the panels currently measure.
|
|
property real barHeight: 0
|
|
|
|
// Share of the COLLAPSED band spent fading to nothing at the bottom, as a
|
|
// fraction rather than a pixel length so the tail scales with whatever the
|
|
// rail currently measures. Expanded there is no fade at all: the scrim runs
|
|
// to the bottom of the output, where the screen edge ends it.
|
|
property real fade: 0.5
|
|
readonly property real fadeAmount: Math.max(0, Math.min(0.95, backdrop.fade))
|
|
|
|
// The share actually in force. Animated rather than switched, so expanding
|
|
// shrinks the tail away as the scrim grows instead of dropping a hard edge
|
|
// onto the desktop the moment the state flips.
|
|
property real fadeSpan: backdrop.active ? 0 : backdrop.fadeAmount
|
|
// Position, in fractions of revealHeight, where the falloff starts. 1 while
|
|
// expanded, i.e. no falloff.
|
|
readonly property real fadeStart: 1 - backdrop.fadeSpan
|
|
|
|
// Collapsed, the SOLID part is the bar band and the tail hangs below it,
|
|
// hence the division: barHeight is what must survive the fade, not what the
|
|
// whole scrim measures. Sized off the static fadeAmount, not the animated
|
|
// fadeSpan — the height and the ramp have to animate independently or each
|
|
// would be chasing the other. Bound rather than readonly so the Behavior
|
|
// below can animate the state change.
|
|
property real revealHeight: backdrop.active
|
|
? backdrop.height
|
|
: Math.min(backdrop.height, backdrop.barHeight / (1 - backdrop.fadeAmount))
|
|
|
|
// Both matched to the bar's own collapse so the scrim and the panels
|
|
// resolve together rather than one trailing the other.
|
|
Behavior on revealHeight {
|
|
NumberAnimation {
|
|
duration: 200
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
|
|
Behavior on fadeSpan {
|
|
NumberAnimation {
|
|
duration: 200
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
|
|
// 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.15
|
|
|
|
// 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.45
|
|
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)
|
|
|
|
// Fade targets keep the source RGB and drop only the alpha: interpolating
|
|
// toward a plain "transparent" would run the gradient through black.
|
|
readonly property color gridSolid: Qt.rgba(backdrop.gridColor.r, backdrop.gridColor.g,
|
|
backdrop.gridColor.b, backdrop.gridOpacity)
|
|
readonly property color gridClear: Qt.rgba(backdrop.gridColor.r, backdrop.gridColor.g,
|
|
backdrop.gridColor.b, 0)
|
|
readonly property color dimSolid: Qt.rgba(Theme.surface.r, Theme.surface.g,
|
|
Theme.surface.b, backdrop.dim)
|
|
readonly property color dimClear: Qt.rgba(Theme.surface.r, Theme.surface.g,
|
|
Theme.surface.b, 0)
|
|
|
|
// What the gradients END on. With no fade the ramp has zero length, so its
|
|
// start stop and its end stop sit on the SAME position — and Qt sorts stops
|
|
// with an unstable sort, leaving which of the two wins undefined. It picked
|
|
// the transparent one, which turned "no fade" into a ramp across the entire
|
|
// scrim. Ending on the solid color instead makes the degenerate case
|
|
// unambiguous: all three stops match and the fill is flat.
|
|
readonly property color gridEnd: backdrop.fadeSpan > 0 ? backdrop.gridClear : backdrop.gridSolid
|
|
readonly property color dimEnd: backdrop.fadeSpan > 0 ? backdrop.dimClear : backdrop.dimSolid
|
|
|
|
// Same ramp as the gradients above, for the marks that are placed at a
|
|
// single y and so cannot carry a gradient of their own. Reads revealHeight
|
|
// and fadeStart, so bindings that call it re-evaluate when either changes.
|
|
function fadeAt(y: real): real {
|
|
const start = backdrop.revealHeight * backdrop.fadeStart;
|
|
if (y <= start)
|
|
return 1;
|
|
if (y >= backdrop.revealHeight)
|
|
return 0;
|
|
return 1 - (y - start) / (backdrop.revealHeight - start);
|
|
}
|
|
|
|
visible: backdrop.revealHeight > 0
|
|
|
|
WlrLayershell.namespace: "hyprchrome-scrim"
|
|
WlrLayershell.layer: backdrop.wlrLayer
|
|
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
|
|
|
|
// Only as tall as the scrim currently reaches; everything inside is
|
|
// laid out against this, so shrinking it scopes the whole drawing
|
|
// rather than just clipping it.
|
|
width: backdrop.width
|
|
height: backdrop.revealHeight
|
|
clip: true
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
gradient: Gradient {
|
|
GradientStop { position: 0; color: backdrop.dimSolid }
|
|
GradientStop { position: backdrop.fadeStart; color: backdrop.dimSolid }
|
|
GradientStop { position: 1; color: backdrop.dimEnd }
|
|
}
|
|
}
|
|
|
|
// Faint drafting grid; no gradient and deliberately subordinate to
|
|
// whatever is showing through it — except at the bottom, where it has to
|
|
// fade with the scrim it sits on.
|
|
Repeater {
|
|
model: Math.ceil(scrim.width / backdrop.gridSpacing)
|
|
|
|
Rectangle {
|
|
required property int index
|
|
|
|
x: index * backdrop.gridSpacing
|
|
width: 1
|
|
height: scrim.height
|
|
gradient: Gradient {
|
|
GradientStop { position: 0; color: backdrop.gridSolid }
|
|
GradientStop { position: backdrop.fadeStart; color: backdrop.gridSolid }
|
|
GradientStop { position: 1; color: backdrop.gridEnd }
|
|
}
|
|
}
|
|
}
|
|
|
|
Repeater {
|
|
// Modelled against the whole output, not the current reveal, so a
|
|
// collapse fades the rules out where they stand instead of
|
|
// restocking the Repeater on every animation frame.
|
|
model: Math.ceil(backdrop.height / backdrop.gridSpacing)
|
|
|
|
Rectangle {
|
|
required property int index
|
|
readonly property real line: index * backdrop.gridSpacing
|
|
|
|
y: line
|
|
width: scrim.width
|
|
height: 1
|
|
color: backdrop.gridColor
|
|
opacity: backdrop.gridOpacity * backdrop.fadeAt(line)
|
|
}
|
|
}
|
|
|
|
// 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
|
|
// Sampled at the intersection the mark registers against, not at
|
|
// its own top edge, so a cross fades as one piece.
|
|
opacity: backdrop.crossOpacity * backdrop.fadeAt(row * backdrop.gridSpacing * 2)
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|