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";
|
||||
# Follow stylix's monospace choice so a font swap propagates to the bar
|
||||
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
|
||||
# auto-detected by IP via ipinfo.io, falling back to London.
|
||||
weatherFetchScript = pkgs.writeShellScript "weather-fetch" ''
|
||||
|
|
@ -461,121 +524,45 @@ in
|
|||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Item {
|
||||
id: barBgRect
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 30
|
||||
color: Theme.barBg
|
||||
}
|
||||
|
||||
// ── Screen frame: a bar-coloured band around the monitor.
|
||||
// Fill is one donut path (screen rect minus rounded inner
|
||||
// cutout); the inner border is one continuous open path from
|
||||
// the top-left corner the long way around to the bottom-right
|
||||
// corner. The right column's border is separate so it can
|
||||
// open up where a flush-right dropdown merges into it.
|
||||
Shape {
|
||||
// ── Shell chrome: bar, frame, panel and toast rendered as
|
||||
// ONE signed-distance field (caelestia-style). Surfaces merge
|
||||
// via circular smooth-min, and the 2px border is the distance
|
||||
// band just inside the boundary — borders flow through every
|
||||
// junction fillet by construction, so all of the previous
|
||||
// ears / border-gaps / melt geometry lives in the math now.
|
||||
ShaderEffect {
|
||||
anchors.fill: parent
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: Theme.barBg
|
||||
strokeWidth: -1
|
||||
fillRule: ShapePath.OddEvenFill
|
||||
startX: 0; startY: 30
|
||||
PathLine { x: bar.width; y: 30 }
|
||||
PathLine { x: bar.width; y: bar.height }
|
||||
PathLine { x: 0; y: bar.height }
|
||||
PathLine { x: 0; y: 30 }
|
||||
PathMove { x: Theme.frameWidth + 8; y: 30 }
|
||||
PathLine { x: bar.width - Theme.frameWidth - 8; y: 30 }
|
||||
PathArc { x: bar.width - Theme.frameWidth; y: 38; radiusX: 8; radiusY: 8; direction: PathArc.Clockwise }
|
||||
PathLine { x: bar.width - Theme.frameWidth; y: bar.height - Theme.frameWidth - 8 }
|
||||
PathArc { x: bar.width - Theme.frameWidth - 8; y: bar.height - Theme.frameWidth; radiusX: 8; radiusY: 8; direction: PathArc.Clockwise }
|
||||
PathLine { x: Theme.frameWidth + 8; y: bar.height - Theme.frameWidth }
|
||||
PathArc { x: Theme.frameWidth; y: bar.height - Theme.frameWidth - 8; radiusX: 8; radiusY: 8; direction: PathArc.Clockwise }
|
||||
PathLine { x: Theme.frameWidth; y: 38 }
|
||||
PathArc { x: Theme.frameWidth + 8; y: 30; radiusX: 8; radiusY: 8; direction: PathArc.Clockwise }
|
||||
}
|
||||
|
||||
ShapePath {
|
||||
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
|
||||
readonly property real panelLeft: chrome.x + 8
|
||||
readonly property real panelRight: chrome.x + chrome.width + (chrome.flushRight ? 4 : -8)
|
||||
property vector4d cutout: Qt.vector4d(
|
||||
bar.width / 2,
|
||||
(30 + bar.height - Theme.frameWidth) / 2,
|
||||
bar.width / 2 - Theme.frameWidth,
|
||||
(bar.height - Theme.frameWidth - 30) / 2)
|
||||
property vector4d panel: chrome.visible
|
||||
? Qt.vector4d((panelLeft + panelRight) / 2, 26 + chrome.height / 2,
|
||||
(panelRight - panelLeft) / 2, 4 + chrome.height / 2)
|
||||
: Qt.vector4d(0, 0, 0, 0)
|
||||
property vector4d toast: toastItem.visible && _toastRect.height > 0.5
|
||||
? Qt.vector4d(toastItem.x + 8 + _toastRect.width / 2, 26 + _toastRect.height / 2,
|
||||
_toastRect.width / 2, 4 + _toastRect.height / 2)
|
||||
: Qt.vector4d(0, 0, 0, 0)
|
||||
property vector4d fillColor: Qt.vector4d(Theme.barBg.r, Theme.barBg.g, Theme.barBg.b, Theme.barBg.a)
|
||||
property vector4d borderColor: Qt.vector4d(Theme.base03.r, Theme.base03.g, Theme.base03.b, 1)
|
||||
property vector2d res: Qt.vector2d(width, height)
|
||||
property real cutoutR: 8
|
||||
property real panelR: 8
|
||||
property real meltK: 12
|
||||
property real borderW: Theme.borderWidth
|
||||
fragmentShader: "file://${chromeShader}"
|
||||
}
|
||||
|
||||
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
|
||||
// geometry with animation (the caelestia-style morph), snaps
|
||||
// instantly when opening from closed.
|
||||
|
|
@ -1299,11 +1205,6 @@ in
|
|||
property real openH: bar.activeDropdown ? tH : 0
|
||||
property bool snap: false
|
||||
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:
|
||||
// the panel opens as a small stub on the button and
|
||||
|
|
@ -1371,24 +1272,6 @@ in
|
|||
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
|
||||
|
|
@ -2544,12 +2427,6 @@ in
|
|||
}
|
||||
}
|
||||
|
||||
// Same single-path silhouette as the dropdown chrome
|
||||
PanelShape {
|
||||
width: toastItem.width
|
||||
height: _toastRect.height
|
||||
}
|
||||
|
||||
Item {
|
||||
id: _toastRect
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue