diff --git a/settings/quickshell.nix b/settings/quickshell.nix index 88d096e..028a3f0 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -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