quickshell: SDF shader chrome — true smooth-min liquid junctions with distance-band borders
Bar, frame, panel and toast are one signed-distance field (compiled to qsb at build time); ears, border gaps and the parametric melt are all emergent from the math. PanelShape and all melt machinery deleted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
724cdded4c
commit
a514893f3e
1 changed files with 94 additions and 217 deletions
|
|
@ -47,6 +47,69 @@ in
|
||||||
nmcli = "${pkgs.networkmanager}/bin/nmcli";
|
nmcli = "${pkgs.networkmanager}/bin/nmcli";
|
||||||
# Follow stylix's monospace choice so a font swap propagates to the bar
|
# Follow stylix's monospace choice so a font swap propagates to the bar
|
||||||
monoFont = osConfig.stylix.fonts.monospace.name;
|
monoFont = osConfig.stylix.fonts.monospace.name;
|
||||||
|
# Shell chrome fragment shader: the bar, screen frame, dropdown panel
|
||||||
|
# and toast are one signed-distance field merged with a circular
|
||||||
|
# smooth-min (caelestia-style liquid junctions); the 2px border is the
|
||||||
|
# distance band just inside the surface, so it follows every fillet.
|
||||||
|
# Qt 6 requires shaders precompiled to .qsb — done here at build time.
|
||||||
|
chromeFragSrc = pkgs.writeText "shell-chrome.frag" ''
|
||||||
|
#version 440
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 qt_TexCoord0;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
layout(std140, binding = 0) uniform buf {
|
||||||
|
mat4 qt_Matrix;
|
||||||
|
float qt_Opacity;
|
||||||
|
vec4 cutout; // cx, cy, hw, hh — rounded inner screen cutout
|
||||||
|
vec4 panel; // cx, cy, hw, hh — dropdown panel (hw <= 0: none)
|
||||||
|
vec4 toast; // cx, cy, hw, hh — toast (hw <= 0: none)
|
||||||
|
vec4 fillColor; // straight (non-premultiplied) rgba
|
||||||
|
vec4 borderColor;
|
||||||
|
vec2 res;
|
||||||
|
float cutoutR;
|
||||||
|
float panelR;
|
||||||
|
float meltK;
|
||||||
|
float borderW;
|
||||||
|
};
|
||||||
|
|
||||||
|
float sdRoundedBox(vec2 p, vec2 center, vec2 halfSize, float r) {
|
||||||
|
vec2 d = abs(p - center) - halfSize + vec2(r);
|
||||||
|
return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0) - r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Circular smooth min: the blend fillet is a true circular arc of
|
||||||
|
// radius k tangent to both surfaces.
|
||||||
|
float smin(float a, float b, float k) {
|
||||||
|
return max(k, min(a, b)) - length(max(vec2(k) - vec2(a, b), vec2(0.0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 p = qt_TexCoord0 * res;
|
||||||
|
|
||||||
|
// Shell = bar band + frame band = everything outside the cutout
|
||||||
|
float d = -sdRoundedBox(p, cutout.xy, cutout.zw, cutoutR);
|
||||||
|
|
||||||
|
if (panel.z > 0.5)
|
||||||
|
d = smin(d, sdRoundedBox(p, panel.xy, panel.zw, panelR), meltK);
|
||||||
|
if (toast.z > 0.5)
|
||||||
|
d = smin(d, sdRoundedBox(p, toast.xy, toast.zw, panelR), meltK);
|
||||||
|
|
||||||
|
float fw = fwidth(d);
|
||||||
|
// 1 inside the union, 0 outside (antialiased)
|
||||||
|
float edge = 1.0 - smoothstep(-fw, fw, d);
|
||||||
|
// 1 deeper than the border band, 0 within it
|
||||||
|
float inner = 1.0 - smoothstep(-borderW - fw, -borderW + fw, d);
|
||||||
|
|
||||||
|
vec4 c = mix(borderColor, fillColor, inner);
|
||||||
|
fragColor = vec4(c.rgb * c.a, c.a) * edge * qt_Opacity;
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
chromeShader = pkgs.runCommand "shell-chrome.frag.qsb" {
|
||||||
|
nativeBuildInputs = [ pkgs.qt6.qtshadertools ];
|
||||||
|
} ''
|
||||||
|
qsb --glsl "300 es,330" --hlsl 50 --msl 12 -o $out ${chromeFragSrc}
|
||||||
|
'';
|
||||||
# 7-day forecast JSON from Open-Meteo (no API key). Location is
|
# 7-day forecast JSON from Open-Meteo (no API key). Location is
|
||||||
# auto-detected by IP via ipinfo.io, falling back to London.
|
# auto-detected by IP via ipinfo.io, falling back to London.
|
||||||
weatherFetchScript = pkgs.writeShellScript "weather-fetch" ''
|
weatherFetchScript = pkgs.writeShellScript "weather-fetch" ''
|
||||||
|
|
@ -461,121 +524,45 @@ in
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Item {
|
||||||
id: barBgRect
|
id: barBgRect
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
height: 30
|
height: 30
|
||||||
color: Theme.barBg
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Screen frame: a bar-coloured band around the monitor.
|
// ── Shell chrome: bar, frame, panel and toast rendered as
|
||||||
// Fill is one donut path (screen rect minus rounded inner
|
// ONE signed-distance field (caelestia-style). Surfaces merge
|
||||||
// cutout); the inner border is one continuous open path from
|
// via circular smooth-min, and the 2px border is the distance
|
||||||
// the top-left corner the long way around to the bottom-right
|
// band just inside the boundary — borders flow through every
|
||||||
// corner. The right column's border is separate so it can
|
// junction fillet by construction, so all of the previous
|
||||||
// open up where a flush-right dropdown merges into it.
|
// ears / border-gaps / melt geometry lives in the math now.
|
||||||
Shape {
|
ShaderEffect {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
preferredRendererType: Shape.CurveRenderer
|
readonly property real panelLeft: chrome.x + 8
|
||||||
|
readonly property real panelRight: chrome.x + chrome.width + (chrome.flushRight ? 4 : -8)
|
||||||
ShapePath {
|
property vector4d cutout: Qt.vector4d(
|
||||||
fillColor: Theme.barBg
|
bar.width / 2,
|
||||||
strokeWidth: -1
|
(30 + bar.height - Theme.frameWidth) / 2,
|
||||||
fillRule: ShapePath.OddEvenFill
|
bar.width / 2 - Theme.frameWidth,
|
||||||
startX: 0; startY: 30
|
(bar.height - Theme.frameWidth - 30) / 2)
|
||||||
PathLine { x: bar.width; y: 30 }
|
property vector4d panel: chrome.visible
|
||||||
PathLine { x: bar.width; y: bar.height }
|
? Qt.vector4d((panelLeft + panelRight) / 2, 26 + chrome.height / 2,
|
||||||
PathLine { x: 0; y: bar.height }
|
(panelRight - panelLeft) / 2, 4 + chrome.height / 2)
|
||||||
PathLine { x: 0; y: 30 }
|
: Qt.vector4d(0, 0, 0, 0)
|
||||||
PathMove { x: Theme.frameWidth + 8; y: 30 }
|
property vector4d toast: toastItem.visible && _toastRect.height > 0.5
|
||||||
PathLine { x: bar.width - Theme.frameWidth - 8; y: 30 }
|
? Qt.vector4d(toastItem.x + 8 + _toastRect.width / 2, 26 + _toastRect.height / 2,
|
||||||
PathArc { x: bar.width - Theme.frameWidth; y: 38; radiusX: 8; radiusY: 8; direction: PathArc.Clockwise }
|
_toastRect.width / 2, 4 + _toastRect.height / 2)
|
||||||
PathLine { x: bar.width - Theme.frameWidth; y: bar.height - Theme.frameWidth - 8 }
|
: Qt.vector4d(0, 0, 0, 0)
|
||||||
PathArc { x: bar.width - Theme.frameWidth - 8; y: bar.height - Theme.frameWidth; radiusX: 8; radiusY: 8; direction: PathArc.Clockwise }
|
property vector4d fillColor: Qt.vector4d(Theme.barBg.r, Theme.barBg.g, Theme.barBg.b, Theme.barBg.a)
|
||||||
PathLine { x: Theme.frameWidth + 8; y: bar.height - Theme.frameWidth }
|
property vector4d borderColor: Qt.vector4d(Theme.base03.r, Theme.base03.g, Theme.base03.b, 1)
|
||||||
PathArc { x: Theme.frameWidth; y: bar.height - Theme.frameWidth - 8; radiusX: 8; radiusY: 8; direction: PathArc.Clockwise }
|
property vector2d res: Qt.vector2d(width, height)
|
||||||
PathLine { x: Theme.frameWidth; y: 38 }
|
property real cutoutR: 8
|
||||||
PathArc { x: Theme.frameWidth + 8; y: 30; radiusX: 8; radiusY: 8; direction: PathArc.Clockwise }
|
property real panelR: 8
|
||||||
}
|
property real meltK: 12
|
||||||
|
property real borderW: Theme.borderWidth
|
||||||
ShapePath {
|
fragmentShader: "file://${chromeShader}"
|
||||||
fillColor: "transparent"
|
|
||||||
strokeColor: Theme.base03
|
|
||||||
strokeWidth: Theme.borderWidth
|
|
||||||
capStyle: ShapePath.FlatCap
|
|
||||||
startX: Theme.frameWidth + 8; startY: 30
|
|
||||||
PathArc { x: Theme.frameWidth; y: 38; radiusX: 8; radiusY: 8; direction: PathArc.Counterclockwise }
|
|
||||||
PathLine { x: Theme.frameWidth; y: bar.height - Theme.frameWidth - 8 }
|
|
||||||
PathArc { x: Theme.frameWidth + 8; y: bar.height - Theme.frameWidth; radiusX: 8; radiusY: 8; direction: PathArc.Counterclockwise }
|
|
||||||
PathLine { x: bar.width - Theme.frameWidth - 8; y: bar.height - Theme.frameWidth }
|
|
||||||
PathArc { x: bar.width - Theme.frameWidth; y: bar.height - Theme.frameWidth - 8; radiusX: 8; radiusY: 8; direction: PathArc.Counterclockwise }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Frame right-column inner border — opens up over a merged
|
|
||||||
// panel (a border must not slice an open junction) and
|
|
||||||
// resumes exactly at the panel's bottom flare. Driven by the
|
|
||||||
// melt progress so it follows the morph smoothly.
|
|
||||||
Rectangle {
|
|
||||||
x: bar.width - Theme.frameWidth - Theme.borderWidth / 2
|
|
||||||
y: 38 + chrome.height * chrome.mergeP
|
|
||||||
width: Theme.borderWidth
|
|
||||||
height: Math.max(0, bar.height - Theme.frameWidth - 8 - y)
|
|
||||||
color: Theme.base03
|
|
||||||
}
|
|
||||||
|
|
||||||
// Frame top-right inner corner — fades in sync with the
|
|
||||||
// panel's geometric melt into the column.
|
|
||||||
Shape {
|
|
||||||
opacity: 1 - chrome.mergeP
|
|
||||||
visible: opacity > 0.01
|
|
||||||
preferredRendererType: Shape.CurveRenderer
|
|
||||||
ShapePath {
|
|
||||||
fillColor: "transparent"
|
|
||||||
strokeColor: Theme.base03
|
|
||||||
strokeWidth: Theme.borderWidth
|
|
||||||
capStyle: ShapePath.FlatCap
|
|
||||||
startX: bar.width - Theme.frameWidth - 8; startY: 30
|
|
||||||
PathArc { x: bar.width - Theme.frameWidth; y: 38; radiusX: 8; radiusY: 8; direction: PathArc.Clockwise }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The "gap source" for the bar border — the morphing chrome
|
|
||||||
// panel takes priority, then the toast. Tracking the animated
|
|
||||||
// chrome means the border gap follows the morph.
|
|
||||||
property bool hasGap: chrome.visible
|
|
||||||
|| (toastItem.visible && _toastRect.height > 0)
|
|
||||||
property real gapLeft: chrome.visible
|
|
||||||
? chrome.x
|
|
||||||
: toastItem.visible && _toastRect.height > 0
|
|
||||||
? toastItem.x : 0
|
|
||||||
property real gapRight: chrome.visible
|
|
||||||
? chrome.x + chrome.width
|
|
||||||
: toastItem.visible && _toastRect.height > 0
|
|
||||||
? toastItem.x + toastItem.width : 0
|
|
||||||
|
|
||||||
// Bar bottom border — left segment (up to gap). Centered on
|
|
||||||
// y=30 so it runs into the panel's edge-centered border stroke.
|
|
||||||
Rectangle {
|
|
||||||
id: barBorderLeft
|
|
||||||
x: Theme.frameWidth + 8
|
|
||||||
y: 30 - Theme.borderWidth / 2
|
|
||||||
width: Math.max(0, (bar.hasGap ? bar.gapLeft : bar.width - Theme.frameWidth - 8) - x)
|
|
||||||
height: Theme.borderWidth
|
|
||||||
color: Theme.base03
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bar bottom border — right segment (after gap)
|
|
||||||
Rectangle {
|
|
||||||
id: barBorderRight
|
|
||||||
visible: bar.hasGap
|
|
||||||
x: bar.gapRight
|
|
||||||
y: 30 - Theme.borderWidth / 2
|
|
||||||
width: Math.max(0, bar.width - Theme.frameWidth - 8 - x)
|
|
||||||
height: Theme.borderWidth
|
|
||||||
color: Theme.base03
|
|
||||||
}
|
}
|
||||||
|
|
||||||
property var activeDropdown: null
|
property var activeDropdown: null
|
||||||
|
|
@ -1206,87 +1193,6 @@ in
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Panel silhouette: fill and border are each ONE continuous
|
|
||||||
// path — concave ear, side, rounded bottom corners, side,
|
|
||||||
// ear — so there are no seams or junctions at all. The border
|
|
||||||
// path is open at the top where the panel joins the bar.
|
|
||||||
//
|
|
||||||
// `merge` (0..1) continuously morphs the RIGHT side between
|
|
||||||
// floating (concave top ear, convex bottom corner at an 8px
|
|
||||||
// inset) and merged-into-the-frame-column (edge flush, ear
|
|
||||||
// collapsed, bottom corner flipped into a concave flare).
|
|
||||||
// The right side is built from cubics (kappa 0.5523) because
|
|
||||||
// they degrade gracefully through the zero-radius midpoint,
|
|
||||||
// where arcs would degenerate.
|
|
||||||
component PanelShape: Shape {
|
|
||||||
id: pshape
|
|
||||||
property real merge: 0
|
|
||||||
readonly property real ear: 8
|
|
||||||
readonly property real rr: Math.min(8, Math.max(1, height / 2))
|
|
||||||
// Right-side morph geometry
|
|
||||||
readonly property real xr: width - ear * (1 - merge) // right edge x
|
|
||||||
readonly property real re: ear * (1 - merge) // top ear radius
|
|
||||||
readonly property real rek: re * 0.5523
|
|
||||||
readonly property real bey: height - ear + 2 * ear * merge // bottom feature endpoint y
|
|
||||||
readonly property real bck: ear * 0.5523 * (1 - 2 * merge) // endpoint control offset; sign flips at the melt point
|
|
||||||
preferredRendererType: Shape.CurveRenderer
|
|
||||||
|
|
||||||
ShapePath {
|
|
||||||
fillColor: Theme.barBg
|
|
||||||
strokeWidth: -1
|
|
||||||
startX: 0; startY: 0
|
|
||||||
PathArc { x: pshape.ear; y: Math.min(pshape.ear, pshape.height); radiusX: pshape.ear; radiusY: pshape.ear; direction: PathArc.Clockwise }
|
|
||||||
PathLine { x: pshape.ear; y: pshape.height - pshape.rr }
|
|
||||||
PathArc { x: pshape.ear + pshape.rr; y: pshape.height; radiusX: pshape.rr; radiusY: pshape.rr; direction: PathArc.Counterclockwise }
|
|
||||||
PathLine { x: pshape.xr - pshape.ear; y: pshape.height }
|
|
||||||
PathCubic {
|
|
||||||
x: pshape.xr; y: pshape.bey
|
|
||||||
control1X: pshape.xr - pshape.ear + 4.42; control1Y: pshape.height
|
|
||||||
control2X: pshape.xr; control2Y: pshape.bey + pshape.bck
|
|
||||||
}
|
|
||||||
PathLine { x: pshape.xr; y: Math.min(pshape.re, pshape.height) }
|
|
||||||
PathCubic {
|
|
||||||
x: pshape.xr + pshape.re; y: 0
|
|
||||||
control1X: pshape.xr; control1Y: Math.max(0, Math.min(pshape.re, pshape.height) - pshape.rek)
|
|
||||||
control2X: pshape.xr + pshape.re - pshape.rek; control2Y: 0
|
|
||||||
}
|
|
||||||
PathLine { x: 0; y: 0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
ShapePath {
|
|
||||||
fillColor: "transparent"
|
|
||||||
strokeColor: Theme.base03
|
|
||||||
strokeWidth: Theme.borderWidth
|
|
||||||
capStyle: ShapePath.FlatCap
|
|
||||||
startX: 0; startY: 0
|
|
||||||
PathArc { x: pshape.ear; y: Math.min(pshape.ear, pshape.height); radiusX: pshape.ear; radiusY: pshape.ear; direction: PathArc.Clockwise }
|
|
||||||
PathLine { x: pshape.ear; y: pshape.height - pshape.rr }
|
|
||||||
PathArc { x: pshape.ear + pshape.rr; y: pshape.height; radiusX: pshape.rr; radiusY: pshape.rr; direction: PathArc.Counterclockwise }
|
|
||||||
PathLine { x: pshape.xr - pshape.ear; y: pshape.height }
|
|
||||||
PathCubic {
|
|
||||||
x: pshape.xr; y: pshape.bey
|
|
||||||
control1X: pshape.xr - pshape.ear + 4.42; control1Y: pshape.height
|
|
||||||
control2X: pshape.xr; control2Y: pshape.bey + pshape.bck
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Right edge + ear stroke, fading out with the melt — a
|
|
||||||
// merged junction must not be sliced by its own border.
|
|
||||||
ShapePath {
|
|
||||||
fillColor: "transparent"
|
|
||||||
strokeColor: Qt.alpha(Theme.base03, 1 - pshape.merge)
|
|
||||||
strokeWidth: Theme.borderWidth
|
|
||||||
capStyle: ShapePath.FlatCap
|
|
||||||
startX: pshape.xr; startY: pshape.bey
|
|
||||||
PathLine { x: pshape.xr; y: Math.min(pshape.re, pshape.height) }
|
|
||||||
PathCubic {
|
|
||||||
x: pshape.xr + pshape.re; y: 0
|
|
||||||
control1X: pshape.xr; control1Y: Math.max(0, Math.min(pshape.re, pshape.height) - pshape.rek)
|
|
||||||
control2X: pshape.xr + pshape.re - pshape.rek; control2Y: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The shared morphing panel: follows the active dropdown's
|
// The shared morphing panel: follows the active dropdown's
|
||||||
// geometry with animation (the caelestia-style morph), snaps
|
// geometry with animation (the caelestia-style morph), snaps
|
||||||
// instantly when opening from closed.
|
// instantly when opening from closed.
|
||||||
|
|
@ -1299,11 +1205,6 @@ in
|
||||||
property real openH: bar.activeDropdown ? tH : 0
|
property real openH: bar.activeDropdown ? tH : 0
|
||||||
property bool snap: false
|
property bool snap: false
|
||||||
readonly property real stubW: 32
|
readonly property real stubW: 32
|
||||||
// Merged whenever the (latched) target is a flush dropdown
|
|
||||||
// and the panel is on screen — flush panels are born,
|
|
||||||
// live and close merged; the melt only animates for
|
|
||||||
// morphs between flush and centered dropdowns.
|
|
||||||
readonly property bool mergedRight: visible && flushRight
|
|
||||||
|
|
||||||
// Grow-from / shrink-to the widget that owns the dropdown:
|
// Grow-from / shrink-to the widget that owns the dropdown:
|
||||||
// the panel opens as a small stub on the button and
|
// the panel opens as a small stub on the button and
|
||||||
|
|
@ -1371,24 +1272,6 @@ in
|
||||||
NumberAnimation { duration: 280; easing.type: Easing.OutExpo }
|
NumberAnimation { duration: 280; easing.type: Easing.OutExpo }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continuous geometric melt instead of a shape swap: on
|
|
||||||
// dock the ear collapses, the edge slides the last 8px
|
|
||||||
// into the column and the bottom corner flips into the
|
|
||||||
// flare — one shape the whole way.
|
|
||||||
property real mergeP: mergedRight ? 1 : 0
|
|
||||||
Behavior on mergeP {
|
|
||||||
// Only animate while the panel is on screen — flush
|
|
||||||
// panels opening from closed are born merged, no
|
|
||||||
// visible melt-in at birth.
|
|
||||||
enabled: chrome.height > 0.5
|
|
||||||
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
|
|
||||||
}
|
|
||||||
|
|
||||||
PanelShape {
|
|
||||||
width: chrome.width
|
|
||||||
height: chrome.height
|
|
||||||
merge: chrome.mergeP
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context menu
|
// Context menu
|
||||||
|
|
@ -2544,12 +2427,6 @@ in
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Same single-path silhouette as the dropdown chrome
|
|
||||||
PanelShape {
|
|
||||||
width: toastItem.width
|
|
||||||
height: _toastRect.height
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: _toastRect
|
id: _toastRect
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue