From 9c330d25d9ed0305d4c5535e403d459dbeb1f56e Mon Sep 17 00:00:00 2001 From: Trevin <132922589+T-Crypt@users.noreply.github.com> Date: Sun, 18 Oct 2026 01:39:24 -0600 Subject: [PATCH 1/3] dashboard: tabs fill the stage, a smooth terrain, a crisp clock Tabs were centred at their own size inside a fixed stage, so Workspaces was a small tile in a large window. Every tab now fills the stage, and Workspaces maps each workspace's windows where they sit. The Spine's CPU ridge is a smooth antialiased curve in a rounded, outlined well, and the clock is plain text instead of the noise shader that left a fuzzy edge. --- .../modules/dashboard/DashboardContent.qml | 18 +- .../modules/dashboard/WorkspacesTab.qml | 208 ++++++++++++------ .../modules/dashboard/spine/SpineSurface.qml | 55 ++--- .../modules/dashboard/spine/SpineTerrain.qml | 196 ++++++++++++----- 4 files changed, 301 insertions(+), 176 deletions(-) diff --git a/Configs/quickshell/aphotic/modules/dashboard/DashboardContent.qml b/Configs/quickshell/aphotic/modules/dashboard/DashboardContent.qml index 5345712d..6667b3b0 100644 --- a/Configs/quickshell/aphotic/modules/dashboard/DashboardContent.qml +++ b/Configs/quickshell/aphotic/modules/dashboard/DashboardContent.qml @@ -148,13 +148,13 @@ ColumnLayout { return pluginTabRepeater.itemAt(i) ?? tabLoader; } - readonly property real tabWidth: tabFrame.activeLoader.implicitWidth - readonly property real tabHeight: tabFrame.activeLoader.implicitHeight + readonly property real tabWidth: tabFrame.activeLoader.item?.implicitWidth ?? 0 + readonly property real tabHeight: tabFrame.activeLoader.item?.implicitHeight ?? 0 // One stage for every tab, so switching never resizes the frame. // A tab larger than the stage still gets room, it just grows it. - readonly property real stageWidth: Math.min(1280, (Window.width || 1280) - Tokens.padding.extraLarge * 4) - readonly property real stageHeight: Math.min(760, (Window.height || 760) - Tokens.padding.extraLarge * 8) + readonly property real stageWidth: Math.min(1680, (Window.width || 1280) - Tokens.padding.extraLarge * 4) + readonly property real stageHeight: Math.min(940, (Window.height || 760) - Tokens.padding.extraLarge * 8) Layout.preferredWidth: Math.max(tabFrame.stageWidth, tabFrame.tabWidth) + Tokens.padding.extraLarge * 2 Layout.preferredHeight: Math.max(tabFrame.stageHeight, tabFrame.tabHeight) + Tokens.padding.extraLarge * 2 @@ -185,7 +185,9 @@ ColumnLayout { Loader { id: tabLoader - anchors.centerIn: parent + // Every tab gets the whole stage, edge to edge. + anchors.fill: parent + anchors.margins: Tokens.padding.extraLarge opacity: 1 active: true @@ -242,7 +244,8 @@ ColumnLayout { required property var modelData - anchors.centerIn: parent + anchors.fill: parent + anchors.margins: Tokens.padding.extraLarge active: root._openedTabIds.includes(pluginTabLoader.modelData.id) asynchronous: true visible: pluginTabLoader.opacity > 0 @@ -264,9 +267,6 @@ ColumnLayout { id: spineComp Item { - implicitWidth: tabFrame.stageWidth - implicitHeight: tabFrame.stageHeight - SpineSurface { anchors.fill: parent embedded: true diff --git a/Configs/quickshell/aphotic/modules/dashboard/WorkspacesTab.qml b/Configs/quickshell/aphotic/modules/dashboard/WorkspacesTab.qml index dc2a28b0..fc5f29f7 100644 --- a/Configs/quickshell/aphotic/modules/dashboard/WorkspacesTab.qml +++ b/Configs/quickshell/aphotic/modules/dashboard/WorkspacesTab.qml @@ -7,28 +7,51 @@ import qs.config import qs.components import qs.services -StyledRect { +// Every workspace as a live map of its windows, laid out to fill the stage. +// Each window is drawn where it actually sits on its monitor, so the tab +// reads like looking down at the desktop rather than a list of icons. +Item { id: root required property ScreenState screenState - readonly property var sortedWorkspaces: Hypr.workspaces.values.slice().sort((a, b) => a.id - b.id) - readonly property int columns: Math.min(4, Math.max(1, root.sortedWorkspaces.length)) - - implicitWidth: grid.implicitWidth + Tokens.padding.large * 2 - implicitHeight: grid.implicitHeight + Tokens.padding.large * 2 - color: "transparent" + readonly property var sortedWorkspaces: Hypr.workspaces.values.filter(w => w.id > 0).slice().sort((a, b) => a.id - b.id) + readonly property int count: Math.max(1, root.sortedWorkspaces.length) + + // The column count whose cells come closest to the monitor's shape. + readonly property int columns: { + let best = 1; + let bestScore = Infinity; + for (let c = 1; c <= root.count; c++) { + const rows = Math.ceil(root.count / c); + const cellAspect = (root.width / c) / Math.max(1, root.height / rows); + const score = Math.abs(Math.log(cellAspect / 1.7)); + if (score < bestScore) { + bestScore = score; + best = c; + } + } + return best; + } + readonly property int rows: Math.ceil(root.count / root.columns) - GridLayout { - id: grid + function monitorOf(ws: var): var { + const name = ws?.lastIpcObject?.monitor ?? ""; + return (Hypr.monitors?.values ?? []).find(m => m.name === name) ?? null; + } - anchors.left: parent.left - anchors.top: parent.top - anchors.margins: Tokens.padding.large + function focusWindow(win: var): void { + const address = win?.address ?? ""; + if (address) + WindowList.focus(address.startsWith("0x") ? address : `0x${address}`); + root.screenState.dashboard = false; + } + GridLayout { + anchors.fill: parent columns: root.columns - columnSpacing: Tokens.spacing.medium - rowSpacing: Tokens.spacing.medium + columnSpacing: Tokens.spacing.large + rowSpacing: Tokens.spacing.large Repeater { model: ScriptModel { @@ -41,82 +64,123 @@ StyledRect { required property var modelData readonly property bool active: wsCard.modelData.id === Hypr.activeWsId readonly property var windows: Hypr.toplevels.values.filter(c => c.workspace?.id === wsCard.modelData.id) + readonly property var monitor: root.monitorOf(wsCard.modelData) - title: qsTr("Workspace %1").arg(wsCard.modelData.id) + title: wsCard.windows.length > 0 ? qsTr("Workspace %1 · %n window(s)", "", wsCard.windows.length).arg(wsCard.modelData.id) : qsTr("Workspace %1").arg(wsCard.modelData.id) tintIndex: wsCard.modelData.id selected: wsCard.active - Layout.preferredWidth: 140 - Layout.preferredHeight: 100 + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredWidth: 1 + Layout.preferredHeight: 1 - ColumnLayout { + Interact { anchors.fill: parent - anchors.leftMargin: Tokens.padding.medium - anchors.rightMargin: Tokens.padding.medium - anchors.bottomMargin: Tokens.padding.medium - anchors.topMargin: wsCard.headerHeight - spacing: Tokens.spacing.small - - StyledText { - visible: false - text: wsCard.modelData.id - color: wsCard.active ? Colours.legibleAccent(Colours.signal, Colours.signalStyle.surface) : Colours.ink - font: Tokens.font.title.builders.medium.weight(Font.Medium).build() + radius: parent.radius + onClicked: { + Hypr.dispatch(Hypr.usingLua ? `hl.dsp.focus({ workspace = ${wsCard.modelData.id} })` : `workspace ${wsCard.modelData.id}`); + root.screenState.dashboard = false; } + } - Flow { - Layout.fillWidth: true - Layout.fillHeight: true - spacing: Tokens.spacing.small - - Repeater { - model: wsCard.windows.slice(0, 9) - - Item { - id: tile - - required property var modelData + // The monitor, scaled into the card at its own aspect. + Item { + id: screenArea + + readonly property real monW: wsCard.monitor?.width ?? 1920 + readonly property real monH: wsCard.monitor?.height ?? 1080 + readonly property real monX: wsCard.monitor?.x ?? 0 + readonly property real monY: wsCard.monitor?.y ?? 0 + readonly property real availW: parent.width - Tokens.padding.large * 2 + readonly property real availH: parent.height - wsCard.headerHeight - Tokens.padding.large + readonly property real scale: Math.max(0.01, Math.min(screenArea.availW / screenArea.monW, screenArea.availH / screenArea.monH)) + + x: (parent.width - width) / 2 + y: wsCard.headerHeight + (screenArea.availH - height) / 2 + width: screenArea.monW * screenArea.scale + height: screenArea.monH * screenArea.scale + + Rectangle { + anchors.fill: parent + radius: Tokens.rounding.medium + color: Qt.alpha(Colours.signalStyle.base, 0.5) + border.width: 1 + border.color: wsCard.active ? Qt.alpha(Colours.signal, 0.5) : Colours.signalStyle.hairline + } - implicitWidth: icon.implicitWidth + Tokens.padding.small - implicitHeight: icon.implicitHeight + Tokens.padding.small + StyledText { + anchors.centerIn: parent + visible: wsCard.windows.length === 0 + text: qsTr("Empty") + color: Colours.inkMuted + font: Tokens.font.label.medium + } - Rectangle { - anchors.fill: parent - radius: Tokens.rounding.small - color: Colours.signalStyle.raised - border.width: 1 - border.color: Colours.signalStyle.hairline + Repeater { + model: wsCard.windows + + Item { + id: win + + required property var modelData + readonly property var info: win.modelData.lastIpcObject ?? ({}) + readonly property bool focused: win.modelData === Hypr.activeToplevel + readonly property var at: win.info.at ?? [0, 0] + readonly property var size: win.info.size ?? [screenArea.monW, screenArea.monH] + + x: Math.max(0, (win.at[0] - screenArea.monX) * screenArea.scale) + y: Math.max(0, (win.at[1] - screenArea.monY) * screenArea.scale) + width: Math.max(18, Math.min(screenArea.width - x, win.size[0] * screenArea.scale)) + height: Math.max(14, Math.min(screenArea.height - y, win.size[1] * screenArea.scale)) + + Rectangle { + anchors.fill: parent + anchors.margins: 1 + radius: Math.min(Tokens.rounding.small, height / 3) + color: win.focused ? Qt.alpha(Colours.signal, 0.14) : (winArea.containsMouse ? Colours.signalStyle.raisedHi : Colours.signalStyle.raised) + border.width: 1 + border.color: win.focused ? Colours.signal : Colours.signalStyle.hairline + + Behavior on color { + CAnim {} } + } - AppIcon { - id: icon + Column { + anchors.centerIn: parent + spacing: 4 + visible: win.height > 28 - anchors.centerIn: parent - appClass: modelData.lastIpcObject?.class ?? "" + AppIcon { + anchors.horizontalCenter: parent.horizontalCenter + appClass: win.info.class ?? "" fallbackGlyph: "desktop_windows" - colour: Colours.inkMuted - fontStyle: Tokens.font.icon.small + colour: win.focused ? Colours.signal : Colours.inkMuted + size: Math.min(48, Math.max(16, Math.min(win.width, win.height) * 0.32)) + fontStyle: Tokens.font.icon.large + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + visible: win.height > 70 && win.width > 90 + width: Math.min(implicitWidth, win.width - 12) + text: win.info.title || win.info.class || "" + elide: Text.ElideRight + color: win.focused ? Colours.ink : Colours.inkMuted + font: Tokens.font.label.small } } - } - } - StyledText { - visible: wsCard.windows.length === 0 - Layout.fillHeight: true - verticalAlignment: Text.AlignVCenter - text: qsTr("Empty") - color: Colours.inkMuted - font: Tokens.font.label.small - } - } + MouseArea { + id: winArea - Interact { - anchors.fill: parent - radius: parent.radius - onClicked: { - Hypr.dispatch(Hypr.usingLua ? `hl.dsp.focus({ workspace = ${wsCard.modelData.id} })` : `workspace ${wsCard.modelData.id}`); - root.screenState.dashboard = false; + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.focusWindow(win.modelData) + } + } } } } diff --git a/Configs/quickshell/aphotic/modules/dashboard/spine/SpineSurface.qml b/Configs/quickshell/aphotic/modules/dashboard/spine/SpineSurface.qml index 9f9270d2..138eca43 100644 --- a/Configs/quickshell/aphotic/modules/dashboard/spine/SpineSurface.qml +++ b/Configs/quickshell/aphotic/modules/dashboard/spine/SpineSurface.qml @@ -134,7 +134,9 @@ Item { } } - SignalText { + // Plain, crisp text: the time is read at a glance, so it never + // resolves out of noise or carries an effect around its edges. + Text { id: clock x: root.margin @@ -143,44 +145,21 @@ Item { Time.minuteStr; return Time.format(Settings.twelveHourClock ? "h:mm AP" : "HH:mm"); } - family: Tokens.font.headline.large.family - ink: Colours.ink - glow: Colours.signalStyle.accentLine - pixelSize: root.portrait ? 88 : Math.round(Math.max(72, Math.min(128, stage.height * 0.1))) - weight: Font.DemiBold - letterSpacing: 2 - property real progress: 0 - - Component.onCompleted: { - if (Motion.reducedMotion) - clock.progress = 1; - } - - Connections { - target: Time - - function onMinuteStrChanged(): void { - if (!Motion.reducedMotion) - clock.progress = 0; - } - } - - Timer { - id: shimmerClock - - interval: 84 - running: !Motion.reducedMotion && clock.progress < 1 - repeat: true - onTriggered: { - clock.time = (clock.time + 0.25) % 3600; - clock.progress = Math.min(1, clock.progress + 0.08); - } - } + color: Colours.ink + renderType: Text.QtRendering + font.family: Tokens.font.headline.large.family + font.pixelSize: root.portrait ? 88 : Math.round(Math.max(72, Math.min(128, stage.height * 0.1))) + font.weight: Font.DemiBold + font.letterSpacing: 2 - ActivityProbe { - name: "dashboard.spine.shimmer" - kind: "render" - timer: shimmerClock + Rectangle { + anchors.left: parent.left + anchors.top: parent.bottom + anchors.topMargin: -Math.round(parent.font.pixelSize * 0.08) + width: Math.round(parent.font.pixelSize * 0.6) + height: 3 + radius: 1.5 + color: Colours.signalStyle.accentLine } } diff --git a/Configs/quickshell/aphotic/modules/dashboard/spine/SpineTerrain.qml b/Configs/quickshell/aphotic/modules/dashboard/spine/SpineTerrain.qml index dfc2f6c0..d7fff441 100644 --- a/Configs/quickshell/aphotic/modules/dashboard/spine/SpineTerrain.qml +++ b/Configs/quickshell/aphotic/modules/dashboard/spine/SpineTerrain.qml @@ -1,10 +1,16 @@ pragma ComponentBehavior: Bound import QtQuick +import QtQuick.Shapes +import qs.components import qs.config import qs.services -Canvas { +// CPU history as a smooth ridge and network as a low band, inside a rounded, +// outlined well. The curves pass through every sample (Catmull-Rom turned +// into cubic segments), so the line reads as a signal rather than a +// staircase, and the Shape's curve renderer antialiases them. +Item { id: root readonly property int capacity: 48 @@ -12,11 +18,6 @@ Canvas { property var cpuTrail: [] property var netTrail: [] - onCpuTrailChanged: root.requestPaint() - onNetTrailChanged: root.requestPaint() - onWidthChanged: root.requestPaint() - onHeightChanged: root.requestPaint() - function pushCpu(value: real): void { const next = root.cpuTrail.concat([Math.max(0, Math.min(1, value))]); root.cpuTrail = next.length > root.capacity ? next.slice(next.length - root.capacity) : next; @@ -48,62 +49,143 @@ Canvas { } } - function _xAt(i: int, count: int): real { - const step = root.width / (root.capacity - 1); - return root.width - (count - 1 - i) * step; + readonly property real inset: 10 + readonly property real floorY: root.height - root.inset + + function points(values: var, top: real, floor: real, peak: real): var { + const w = root.width - root.inset * 2; + const step = w / (root.capacity - 1); + const count = values.length; + return values.map((v, i) => Qt.point(root.inset + w - (count - 1 - i) * step, floor - (v / peak) * (floor - top))); } - onPaint: { - const ctx = root.getContext("2d"); - const h = root.height; - ctx.clearRect(0, 0, root.width, h); - - const net = root.netTrail; - if (net.length > 1) { - let peak = root.netFlatBelow; - for (let i = 0; i < net.length; i++) - peak = Math.max(peak, net[i]); - const bandH = h * 0.28; - - ctx.beginPath(); - ctx.moveTo(root._xAt(0, net.length), h); - for (let i = 0; i < net.length; i++) - ctx.lineTo(root._xAt(i, net.length), h - (net[i] / peak) * bandH); - ctx.lineTo(root.width, h); - ctx.closePath(); - ctx.fillStyle = Qt.alpha(Colours.signalSecondary, 0.09); - ctx.fill(); + // Catmull-Rom through the points, as SVG cubic segments. + function curve(pts: var): string { + if (pts.length < 2) + return ""; + let d = `M ${pts[0].x} ${pts[0].y}`; + for (let i = 0; i < pts.length - 1; i++) { + const p0 = pts[Math.max(0, i - 1)]; + const p1 = pts[i]; + const p2 = pts[i + 1]; + const p3 = pts[Math.min(pts.length - 1, i + 2)]; + d += ` C ${p1.x + (p2.x - p0.x) / 6} ${p1.y + (p2.y - p0.y) / 6} ${p2.x - (p3.x - p1.x) / 6} ${p2.y - (p3.y - p1.y) / 6} ${p2.x} ${p2.y}`; } + return d; + } + + function area(pts: var, floor: real): string { + const line = root.curve(pts); + if (!line) + return ""; + return `${line} L ${pts[pts.length - 1].x} ${floor} L ${pts[0].x} ${floor} Z`; + } + + readonly property var cpuPts: root.points(root.cpuTrail, root.inset + 6, root.floorY, 1) + readonly property var netPts: { + const peak = root.netTrail.reduce((m, v) => Math.max(m, v), root.netFlatBelow); + return root.points(root.netTrail, root.floorY - root.height * 0.28, root.floorY, peak); + } - const cpu = root.cpuTrail; - if (cpu.length > 1) { - const crest = 0.92; - const yAt = i => h - cpu[i] * h * crest; - - const fill = ctx.createLinearGradient(0, 0, 0, h); - fill.addColorStop(0, Qt.alpha(Colours.signalStyle.accentLine, 0.26)); - fill.addColorStop(1, Qt.alpha(Colours.signalStyle.accentLine, 0)); - - ctx.beginPath(); - ctx.moveTo(root._xAt(0, cpu.length), h); - for (let i = 0; i < cpu.length; i++) - ctx.lineTo(root._xAt(i, cpu.length), yAt(i)); - ctx.lineTo(root.width, h); - ctx.closePath(); - ctx.fillStyle = fill; - ctx.fill(); - - ctx.beginPath(); - for (let i = 0; i < cpu.length; i++) { - if (i === 0) - ctx.moveTo(root._xAt(i, cpu.length), yAt(i)); - else - ctx.lineTo(root._xAt(i, cpu.length), yAt(i)); + StyledClippingRect { + anchors.fill: parent + radius: Tokens.rounding.large + color: Qt.alpha(Colours.signalStyle.surface, 0.35) + + // Faint guides at a quarter, half and three quarters. + Repeater { + model: [0.25, 0.5, 0.75] + + Rectangle { + required property real modelData + + x: root.inset + width: root.width - root.inset * 2 + y: root.floorY - (root.floorY - root.inset - 6) * modelData + height: 1 + color: Qt.alpha(Colours.signalStyle.hairline, 0.5) } - ctx.strokeStyle = Qt.alpha(Colours.signalStyle.accentLine, 0.9); - ctx.lineWidth = 1.5; - ctx.lineJoin = "round"; - ctx.stroke(); } + + Shape { + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + + ShapePath { + strokeWidth: -1 + fillColor: Qt.alpha(Colours.signalSecondary, 0.1) + + PathSvg { + path: root.area(root.netPts, root.floorY) + } + } + + ShapePath { + strokeWidth: -1 + fillGradient: LinearGradient { + x1: 0 + y1: 0 + x2: 0 + y2: root.height + + GradientStop { + position: 0 + color: Qt.alpha(Colours.signalStyle.accentLine, 0.3) + } + + GradientStop { + position: 1 + color: Qt.alpha(Colours.signalStyle.accentLine, 0) + } + } + + PathSvg { + path: root.area(root.cpuPts, root.floorY) + } + } + + ShapePath { + strokeWidth: 2 + strokeColor: Colours.signalStyle.accentLine + fillColor: "transparent" + capStyle: ShapePath.RoundCap + joinStyle: ShapePath.RoundJoin + + PathSvg { + path: root.curve(root.cpuPts) + } + } + } + + // The newest sample: a lit point the eye lands on. + Rectangle { + id: tip + + readonly property var at: root.cpuPts.length > 0 ? root.cpuPts[root.cpuPts.length - 1] : null + + visible: tip.at !== null + x: (tip.at?.x ?? 0) - width / 2 + y: (tip.at?.y ?? 0) - height / 2 + width: 8 + height: 8 + radius: 4 + color: Colours.signalStyle.accentLine + + Rectangle { + anchors.centerIn: parent + width: 20 + height: 20 + radius: 10 + color: Qt.alpha(Colours.signalStyle.accentLine, 0.16) + } + } + } + + Rectangle { + anchors.fill: parent + radius: Tokens.rounding.large + color: "transparent" + border.width: 1 + border.color: Colours.signalStyle.hairline } } From 1899140baf572b6a30d55c88bfc5f26cb80a3eba Mon Sep 17 00:00:00 2001 From: Trevin <132922589+T-Crypt@users.noreply.github.com> Date: Sun, 18 Oct 2026 02:03:13 -0600 Subject: [PATCH 2/3] dashboard: a bento grid of live tiles on the Dashboard tab Twelve columns by six rows filling the stage: clock, processor and network curves, media, memory and graphics rings, agents, calendar, weather, local AI, controls and focus. Every tile shares one Signal tile whose hairline warms under the pointer and whose dot lights while its subject works. --- .../aphotic/components/SignalTile.qml | 129 ++++ Configs/quickshell/aphotic/components/qmldir | 1 + .../modules/dashboard/DashboardContent.qml | 8 +- .../dashboard/bento/BentoDashboard.qml | 684 ++++++++++++++++++ .../aphotic/modules/dashboard/bento/Ring.qml | 89 +++ .../modules/dashboard/bento/Spline.qml | 101 +++ 6 files changed, 1006 insertions(+), 6 deletions(-) create mode 100644 Configs/quickshell/aphotic/components/SignalTile.qml create mode 100644 Configs/quickshell/aphotic/modules/dashboard/bento/BentoDashboard.qml create mode 100644 Configs/quickshell/aphotic/modules/dashboard/bento/Ring.qml create mode 100644 Configs/quickshell/aphotic/modules/dashboard/bento/Spline.qml diff --git a/Configs/quickshell/aphotic/components/SignalTile.qml b/Configs/quickshell/aphotic/components/SignalTile.qml new file mode 100644 index 00000000..007796a1 --- /dev/null +++ b/Configs/quickshell/aphotic/components/SignalTile.qml @@ -0,0 +1,129 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import qs.config +import qs.services + +// The dashboard's one tile: dark glass, a hairline that warms toward the +// accent under the pointer, light along the top edge, and a header with an +// icon, a label and an optional figure. `live` lights a small dot beside +// the label while the tile's subject is doing something. Content goes in +// the body and is given all the room left under the header. +Item { + id: root + + property string icon: "" + property string label: "" + property string figure: "" + property bool live: false + property bool accent: false + property real padding: Tokens.padding.large + + default property alias body: content.data + readonly property bool hovered: hover.hovered + readonly property real headerHeight: header.visible ? header.implicitHeight + Tokens.spacing.small : 0 + + Rectangle { + id: glass + + anchors.fill: parent + radius: Tokens.rounding.large + color: root.hovered ? Colours.signalStyle.raised : Qt.alpha(Colours.signalStyle.surface, 0.72) + border.width: 1 + border.color: root.accent ? Qt.alpha(Colours.signal, 0.65) : (root.hovered ? Qt.alpha(Colours.signal, 0.42) : Colours.signalStyle.hairline) + + Behavior on color { + CAnim {} + } + + Behavior on border.color { + CAnim {} + } + + Rectangle { + x: glass.radius + width: parent.width - glass.radius * 2 + height: 1 + color: Colours.signalStyle.edgeLight + } + } + + RowLayout { + id: header + + visible: root.label !== "" || root.icon !== "" + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: root.padding + spacing: Tokens.spacing.small + + MaterialIcon { + visible: root.icon !== "" + text: root.icon + color: root.live || root.accent ? Colours.signal : Colours.inkMuted + fontStyle: Tokens.font.icon.small + } + + StyledText { + text: root.label.toUpperCase() + color: Colours.inkMuted + font: Tokens.font.label.builders.small.weight(Font.Medium).letterSpacing(1.2).build() + } + + Rectangle { + visible: root.live + implicitWidth: 6 + implicitHeight: 6 + radius: 3 + color: Colours.signal + + SequentialAnimation on opacity { + running: root.live && RenderGate.decorative && root.visible + loops: Animation.Infinite + alwaysRunToEnd: true + + NumberAnimation { + to: 0.25 + duration: 900 + easing.type: Easing.InOutSine + } + + NumberAnimation { + to: 1 + duration: 900 + easing.type: Easing.InOutSine + } + } + } + + Item { + Layout.fillWidth: true + } + + StyledText { + visible: root.figure !== "" + text: root.figure + color: Colours.ink + font: Tokens.font.mono.small + } + } + + Item { + id: content + + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.top: header.visible ? header.bottom : parent.top + anchors.leftMargin: root.padding + anchors.rightMargin: root.padding + anchors.bottomMargin: root.padding + anchors.topMargin: header.visible ? Tokens.spacing.small : root.padding + } + + HoverHandler { + id: hover + } +} diff --git a/Configs/quickshell/aphotic/components/qmldir b/Configs/quickshell/aphotic/components/qmldir index a4db3973..978021b4 100644 --- a/Configs/quickshell/aphotic/components/qmldir +++ b/Configs/quickshell/aphotic/components/qmldir @@ -39,3 +39,4 @@ SignalSurface 1.0 SignalSurface.qml SectionLabel 1.0 SectionLabel.qml SignalCard 1.0 SignalCard.qml Readout 1.0 Readout.qml +SignalTile 1.0 SignalTile.qml diff --git a/Configs/quickshell/aphotic/modules/dashboard/DashboardContent.qml b/Configs/quickshell/aphotic/modules/dashboard/DashboardContent.qml index 6667b3b0..f1372a37 100644 --- a/Configs/quickshell/aphotic/modules/dashboard/DashboardContent.qml +++ b/Configs/quickshell/aphotic/modules/dashboard/DashboardContent.qml @@ -6,6 +6,7 @@ import qs.services import qs.modules.flow import QtQuick.Window import "spine" +import "bento" ColumnLayout { id: root @@ -266,12 +267,7 @@ ColumnLayout { Component { id: spineComp - Item { - SpineSurface { - anchors.fill: parent - embedded: true - } - } + BentoDashboard {} } Component { id: flowComp diff --git a/Configs/quickshell/aphotic/modules/dashboard/bento/BentoDashboard.qml b/Configs/quickshell/aphotic/modules/dashboard/bento/BentoDashboard.qml new file mode 100644 index 00000000..17a999b8 --- /dev/null +++ b/Configs/quickshell/aphotic/modules/dashboard/bento/BentoDashboard.qml @@ -0,0 +1,684 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Mpris +import qs.config +import qs.components +import qs.services +import qs.services.ai +import qs.services.profile +import ".." + +// The Dashboard tab: a grid of live tiles that fills the stage edge to +// edge. Twelve columns by six rows; every tile spans whole cells, so the +// grid stays even at any size. Each tile carries one moving part (a curve, +// a ring, a dot) that moves only when its value does. +Item { + id: root + + readonly property real gap: Tokens.spacing.large + readonly property real cellW: (root.width - root.gap * 11) / 12 + readonly property real cellH: (root.height - root.gap * 5) / 6 + + // Tiles are placed by cell, not by a layout, so a tile's size never + // depends on what is inside it. + component Cell: SignalTile { + property int col: 0 + property int row: 0 + property int cols: 1 + property int rows: 1 + + x: col * (root.cellW + root.gap) + y: row * (root.cellH + root.gap) + width: cols * root.cellW + (cols - 1) * root.gap + height: rows * root.cellH + (rows - 1) * root.gap + } + + SystemUsageWatch { + detailed: true + } + + NetworkUsageWatch {} + + Component.onCompleted: AgentEvents.hold("dashboard-bento", true) + Component.onDestruction: AgentEvents.hold("dashboard-bento", false) + + property var cpuTrail: [] + property var downTrail: [] + property var upTrail: [] + + function push(list: var, value: real): var { + const next = list.concat([value]); + return next.length > 40 ? next.slice(next.length - 40) : next; + } + + Connections { + target: SystemUsage + + function onCpuPercChanged(): void { + root.cpuTrail = root.push(root.cpuTrail, SystemUsage.cpuPerc); + } + } + + Connections { + target: NetworkUsage + + function onDownloadSpeedChanged(): void { + root.downTrail = root.push(root.downTrail, NetworkUsage.downloadSpeed); + root.upTrail = root.push(root.upTrail, NetworkUsage.uploadSpeed); + } + } + + function rate(bytes: real): string { + const f = NetworkUsage.formatBytes(bytes); + return `${f.value < 10 ? f.value.toFixed(1) : Math.round(f.value)} ${f.unit}`; + } + + function gib(kib: real): string { + return (kib / 1048576).toFixed(1); + } + + // ── Row 1-2 ───────────────────────────────────────────────────────── + + Cell { + col: 0 + row: 0 + cols: 4 + rows: 2 + icon: "schedule" + label: Time.format("dddd") + accent: true + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Item { + Layout.fillHeight: true + } + + Text { + text: { + Time.minuteStr; + return Time.format(Settings.twelveHourClock ? "h:mm" : "HH:mm"); + } + color: Colours.ink + font.family: Tokens.font.headline.large.family + font.pixelSize: Math.min(parent.height * 0.62, parent.width * 0.3) + font.weight: Font.DemiBold + font.letterSpacing: -2 + } + + RowLayout { + spacing: Tokens.spacing.small + + Rectangle { + implicitWidth: 28 + implicitHeight: 3 + radius: 1.5 + color: Colours.signal + } + + StyledText { + text: Time.format("MMMM d, yyyy") + color: Colours.inkMuted + font: Tokens.font.title.small + } + + StyledText { + visible: Settings.twelveHourClock + text: Time.amPmStr + color: Colours.signal + font: Tokens.font.title.small + } + } + + Item { + Layout.fillHeight: true + } + } + } + + Cell { + col: 4 + row: 0 + cols: 4 + rows: 2 + icon: "memory" + label: qsTr("Processor") + figure: SystemUsage.cpuTemp > 0 ? `${Math.round(SystemUsage.cpuTemp)}°C` : "" + live: SystemUsage.cpuPerc > 0.5 + + StyledText { + id: cpuFigure + + text: `${Math.round(SystemUsage.cpuPerc * 100)}%` + color: Colours.ink + font.family: Tokens.font.headline.large.family + font.pixelSize: Math.max(24, parent.height * 0.26) + font.weight: Font.Light + } + + StyledText { + anchors.left: cpuFigure.right + anchors.leftMargin: Tokens.spacing.small + anchors.baseline: cpuFigure.baseline + text: SystemUsage.cpuName + color: Colours.inkMuted + elide: Text.ElideRight + width: parent.width - cpuFigure.width - Tokens.spacing.small + font: Tokens.font.label.small + } + + Spline { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: parent.height - cpuFigure.height - Tokens.spacing.small + values: root.cpuTrail + peak: 1 + } + } + + Cell { + id: media + + readonly property MprisPlayer player: Players.active + + col: 8 + row: 0 + cols: 4 + rows: 2 + icon: "music_note" + label: media.player?.identity ?? qsTr("Media") + live: media.player?.isPlaying ?? false + + RowLayout { + anchors.fill: parent + spacing: Tokens.spacing.large + + StyledClippingRect { + Layout.preferredWidth: Math.min(parent.height, parent.width * 0.42) + Layout.preferredHeight: Layout.preferredWidth + radius: Tokens.rounding.medium + color: Colours.signalStyle.raised + + Image { + id: art + + anchors.fill: parent + source: media.player?.trackArtUrl ?? "" + fillMode: Image.PreserveAspectCrop + asynchronous: true + sourceSize.width: 384 + sourceSize.height: 384 + } + + MaterialIcon { + anchors.centerIn: parent + visible: art.status !== Image.Ready + text: "album" + color: Colours.inkMuted + font.pixelSize: parent.width * 0.4 + } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 2 + + Item { + Layout.fillHeight: true + } + + StyledText { + Layout.fillWidth: true + text: media.player?.trackTitle || qsTr("Nothing playing") + color: Colours.ink + elide: Text.ElideRight + font: Tokens.font.title.builders.medium.weight(Font.Medium).build() + } + + StyledText { + Layout.fillWidth: true + text: media.player?.trackArtist ?? "" + color: Colours.inkMuted + elide: Text.ElideRight + font: Tokens.font.body.small + } + + RowLayout { + Layout.topMargin: Tokens.spacing.medium + spacing: Tokens.spacing.medium + + Repeater { + model: [ + { icon: "skip_previous", act: () => media.player?.previous(), main: false }, + { icon: media.player?.isPlaying ? "pause" : "play_arrow", act: () => media.player?.togglePlaying(), main: true }, + { icon: "skip_next", act: () => media.player?.next(), main: false } + ] + + StyledRect { + id: control + + required property var modelData + + implicitWidth: control.modelData.main ? 44 : 34 + implicitHeight: implicitWidth + radius: Tokens.rounding.full + color: control.modelData.main ? Qt.alpha(Colours.signal, 0.18) : "transparent" + border.width: 1 + border.color: control.modelData.main ? Colours.signal : Colours.signalStyle.hairline + opacity: media.player ? 1 : 0.4 + + MaterialIcon { + anchors.centerIn: parent + text: control.modelData.icon + color: control.modelData.main ? Colours.signal : Colours.ink + fontStyle: control.modelData.main ? Tokens.font.icon.large : Tokens.font.icon.medium + } + + Interact { + anchors.fill: parent + radius: parent.radius + onClicked: control.modelData.act() + } + } + } + } + + Item { + Layout.fillHeight: true + } + } + } + } + + // ── Row 3-4 ───────────────────────────────────────────────────────── + + Cell { + col: 0 + row: 2 + cols: 2 + rows: 2 + icon: "memory_alt" + label: qsTr("Memory") + + Ring { + anchors.fill: parent + value: SystemUsage.memPerc + figure: `${Math.round(SystemUsage.memPerc * 100)}%` + caption: SystemUsage.memTotal > 0 ? `${root.gib(SystemUsage.memUsed)} / ${Math.round(SystemUsage.memTotal / 1048576)} GiB` : "" + } + } + + Cell { + col: 2 + row: 2 + cols: 2 + rows: 2 + icon: "developer_board" + label: qsTr("Graphics") + figure: SystemUsage.gpuTemp > 0 ? `${Math.round(SystemUsage.gpuTemp)}°C` : "" + + Ring { + anchors.fill: parent + value: SystemUsage.gpuStatsAvailable ? SystemUsage.gpuPerc : 0 + figure: SystemUsage.gpuStatsAvailable ? `${Math.round(SystemUsage.gpuPerc * 100)}%` : "--" + caption: SystemUsage.gpuDetected ? "" : qsTr("none found") + colour: Colours.signalTertiary + } + } + + Cell { + col: 4 + row: 2 + cols: 4 + rows: 2 + icon: "network_check" + label: qsTr("Network") + live: NetworkUsage.downloadSpeed > 512 * 1024 + + RowLayout { + id: netFigures + + spacing: Tokens.spacing.extraLarge + + Repeater { + model: [ + { icon: "south", value: NetworkUsage.downloadSpeed, colour: Colours.signal }, + { icon: "north", value: NetworkUsage.uploadSpeed, colour: Colours.signalSecondary } + ] + + RowLayout { + id: netFigure + + required property var modelData + + spacing: Tokens.spacing.extraSmall + + MaterialIcon { + text: netFigure.modelData.icon + color: netFigure.modelData.colour + fontStyle: Tokens.font.icon.medium + } + + StyledText { + text: root.rate(netFigure.modelData.value) + color: Colours.ink + font: Tokens.font.title.builders.medium.weight(Font.Light).build() + } + } + } + } + + Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: parent.height - netFigures.height - Tokens.spacing.small + + Spline { + anchors.fill: parent + values: root.downTrail + peak: 0 + } + + Spline { + anchors.fill: parent + values: root.upTrail + peak: Math.max(1, root.downTrail.reduce((m, v) => Math.max(m, v), 0)) + colour: Colours.signalSecondary + fill: false + lineWidth: 1.5 + } + } + } + + Cell { + id: agents + + readonly property var sessions: AgentEvents.liveSessions.slice().sort((a, b) => (a.startedAt - b.startedAt) || (a.id < b.id ? -1 : 1)) + + col: 8 + row: 2 + cols: 4 + rows: 2 + icon: "smart_toy" + label: qsTr("Agents") + figure: agents.sessions.length > 0 ? String(agents.sessions.length) : "" + live: AgentEvents.anyRunning + accent: AgentEvents.anyWaiting + + StyledText { + anchors.centerIn: parent + visible: agents.sessions.length === 0 + text: qsTr("No agent running") + color: Colours.inkMuted + font: Tokens.font.body.small + } + + Column { + anchors.fill: parent + spacing: Tokens.spacing.small + + Repeater { + model: agents.sessions.slice(0, Math.max(1, Math.floor(agents.height / 44))) + + StyledRect { + id: session + + required property var modelData + readonly property bool waiting: session.modelData.status === "waiting" + readonly property bool busy: session.modelData.status === "running" || session.modelData.status === "compacting" + + width: parent.width + height: 32 + radius: Tokens.rounding.full + color: session.waiting ? Qt.alpha(Colours.signal, 0.16) : Qt.alpha(Colours.signalStyle.base, 0.5) + border.width: 1 + border.color: session.waiting ? Colours.signal : Colours.signalStyle.hairline + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Tokens.padding.medium + anchors.rightMargin: Tokens.padding.medium + spacing: Tokens.spacing.small + + Rectangle { + implicitWidth: 7 + implicitHeight: 7 + radius: 3.5 + color: session.waiting ? Colours.signal : session.busy ? Colours.signalOnSurface : Colours.outline + } + + StyledText { + text: AgentRoles.entries.find(e => e.id === session.modelData.harness)?.label ?? session.modelData.harness + color: Colours.ink + font: Tokens.font.label.builders.medium.weight(Font.Medium).build() + } + + StyledText { + Layout.fillWidth: true + text: (session.modelData.cwd || "").split("/").pop() || session.modelData.tool || "" + color: Colours.inkMuted + elide: Text.ElideRight + font: Tokens.font.label.small + } + + StyledText { + text: session.waiting ? qsTr("waiting") : (session.modelData.tool || session.modelData.status) + color: session.waiting ? Colours.signal : Colours.inkMuted + font: Tokens.font.mono.small + } + } + + Interact { + anchors.fill: parent + radius: parent.radius + onClicked: AgentWindowFocus.focusByCwd(session.modelData.cwd ?? "") + } + } + } + } + } + + // ── Row 5-6 ───────────────────────────────────────────────────────── + + Cell { + col: 0 + row: 4 + cols: 4 + rows: 2 + icon: "calendar_month" + label: qsTr("Calendar") + padding: Tokens.padding.medium + + DashCalendar { + anchors.centerIn: parent + scale: Math.min(1.25, parent.width / Math.max(1, implicitWidth), parent.height / Math.max(1, implicitHeight)) + } + } + + Cell { + col: 4 + row: 4 + cols: 2 + rows: 2 + icon: Weather.hasData ? Weather.conditionIcon : "cloud_off" + label: qsTr("Weather") + + Column { + anchors.centerIn: parent + spacing: Tokens.spacing.extraSmall + + MaterialIcon { + anchors.horizontalCenter: parent.horizontalCenter + text: Weather.hasData ? Weather.conditionIcon : "cloud_off" + color: Colours.signal + font.pixelSize: Math.min(64, root.cellH * 0.5) + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + text: Weather.hasData ? `${Math.round(Weather.currentTemp)}°` : "--" + color: Colours.ink + font.family: Tokens.font.headline.large.family + font.pixelSize: Math.min(48, root.cellH * 0.4) + font.weight: Font.Light + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + text: Weather.hasData ? Weather.conditionText : qsTr("No location set") + color: Colours.inkMuted + font: Tokens.font.label.small + } + } + } + + Cell { + id: ai + + readonly property var engines: { + const rows = []; + const seen = {}; + for (const e of EngineRegistry.liveEngines) { + if (!seen[e.id]) { + seen[e.id] = true; + rows.push({ id: e.id, label: e.label || e.id, models: (e.models ?? []).map(m => m.name) }); + } + } + const backends = LocalInference.backends; + for (const owner of Object.keys(backends)) { + const models = (backends[owner].models ?? []).map(m => m.name); + if (!seen[owner] && models.length > 0) { + seen[owner] = true; + rows.push({ id: owner, label: backends[owner].label || owner, models: models }); + } + } + return rows.filter(r => r.models.length > 0); + } + readonly property int vram: ai.engines.length > 0 ? ResourceEngine.claimsOf(ai.engines[0].id).filter(c => c.resource === "gpu-vram").reduce((t, c) => t + (c.amount ?? 0), 0) : 0 + + col: 6 + row: 4 + cols: 2 + rows: 2 + icon: "neurology" + label: qsTr("Local AI") + live: ai.engines.length > 0 + + Column { + anchors.verticalCenter: parent.verticalCenter + width: parent.width + spacing: Tokens.spacing.extraSmall + + StyledText { + width: parent.width + text: ai.engines.length > 0 ? ai.engines[0].label : qsTr("Idle") + color: ai.engines.length > 0 ? Colours.ink : Colours.inkMuted + elide: Text.ElideRight + font.family: Tokens.font.headline.large.family + font.pixelSize: Math.min(30, root.cellH * 0.26) + } + + StyledText { + width: parent.width + text: ai.engines.length > 0 ? ai.engines[0].models.join(", ") : qsTr("No model loaded") + color: Colours.inkMuted + elide: Text.ElideRight + font: Tokens.font.label.small + } + + StyledText { + visible: ai.vram > 0 + text: qsTr("%1 GiB on the GPU").arg((ai.vram / 1024).toFixed(1)) + color: Colours.signal + font: Tokens.font.mono.small + } + } + } + + Cell { + col: 8 + row: 4 + cols: 4 + rows: 1 + icon: "tune" + label: qsTr("Controls") + padding: Tokens.padding.medium + + DashQuickToggles { + anchors.centerIn: parent + scale: Math.min(1, parent.width / Math.max(1, implicitWidth), parent.height / Math.max(1, implicitHeight)) + } + } + + Cell { + col: 8 + row: 5 + cols: 4 + rows: 1 + icon: "timer" + label: qsTr("Focus") + live: Pomodoro.running + figure: Pomodoro.isBreak ? qsTr("break") : "" + padding: Tokens.padding.medium + + RowLayout { + anchors.fill: parent + spacing: Tokens.spacing.large + + StyledText { + text: Pomodoro.formatTime(Pomodoro.remaining) + color: Colours.ink + font.family: Tokens.font.mono.large.family + font.pixelSize: Math.min(34, parent.height * 0.75) + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: 4 + radius: 2 + color: Colours.signalStyle.hairline + + Rectangle { + width: parent.width * (1 - Pomodoro.remaining / Math.max(1, Pomodoro.isBreak ? Pomodoro.breakSeconds : Pomodoro.workSeconds)) + height: parent.height + radius: 2 + color: Pomodoro.isBreak ? Colours.signalTertiary : Colours.signal + + Behavior on width { + Anim {} + } + } + } + + StyledRect { + implicitWidth: 40 + implicitHeight: 40 + radius: Tokens.rounding.full + color: Qt.alpha(Colours.signal, 0.18) + border.width: 1 + border.color: Colours.signal + + MaterialIcon { + anchors.centerIn: parent + text: Pomodoro.running ? "pause" : "play_arrow" + color: Colours.signal + fontStyle: Tokens.font.icon.large + } + + Interact { + anchors.fill: parent + radius: parent.radius + onClicked: Pomodoro.toggle() + } + } + } + } +} diff --git a/Configs/quickshell/aphotic/modules/dashboard/bento/Ring.qml b/Configs/quickshell/aphotic/modules/dashboard/bento/Ring.qml new file mode 100644 index 00000000..b58d73cb --- /dev/null +++ b/Configs/quickshell/aphotic/modules/dashboard/bento/Ring.qml @@ -0,0 +1,89 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Shapes +import qs.components +import qs.config +import qs.services + +// An arc meter that sweeps to its value, with the figure in the middle. +Item { + id: root + + property real value: 0 + property string figure: "" + property string caption: "" + property color colour: Colours.signal + + readonly property real size: Math.min(root.width, root.height) + readonly property real thickness: Math.max(4, root.size * 0.065) + property real shown: root.value + + Behavior on shown { + NumberAnimation { + duration: 600 + easing.type: Easing.OutCubic + } + } + + Shape { + anchors.centerIn: parent + width: root.size + height: root.size + preferredRendererType: Shape.CurveRenderer + + ShapePath { + strokeColor: Colours.signalStyle.hairline + strokeWidth: root.thickness + fillColor: "transparent" + capStyle: ShapePath.RoundCap + + PathAngleArc { + centerX: root.size / 2 + centerY: root.size / 2 + radiusX: root.size / 2 - root.thickness + radiusY: radiusX + startAngle: 135 + sweepAngle: 270 + } + } + + ShapePath { + strokeColor: root.shown >= 0.9 ? Colours.palette.m3error : root.colour + strokeWidth: root.thickness + fillColor: "transparent" + capStyle: ShapePath.RoundCap + + PathAngleArc { + centerX: root.size / 2 + centerY: root.size / 2 + radiusX: root.size / 2 - root.thickness + radiusY: radiusX + startAngle: 135 + sweepAngle: 270 * Math.max(0.001, Math.min(1, root.shown)) + } + } + } + + Column { + anchors.centerIn: parent + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + text: root.figure + color: Colours.ink + font.family: Tokens.font.headline.large.family + font.pixelSize: Math.max(14, root.size * 0.24) + font.weight: Font.Light + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + visible: root.caption !== "" + text: root.caption + color: Colours.inkMuted + font.family: Tokens.font.mono.small.family + font.pixelSize: Math.max(10, root.size * 0.085) + } + } +} diff --git a/Configs/quickshell/aphotic/modules/dashboard/bento/Spline.qml b/Configs/quickshell/aphotic/modules/dashboard/bento/Spline.qml new file mode 100644 index 00000000..3fa21e9d --- /dev/null +++ b/Configs/quickshell/aphotic/modules/dashboard/bento/Spline.qml @@ -0,0 +1,101 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Shapes +import qs.services + +// A smooth line through `values` with a soft fill under it and a lit point +// on the newest sample. `peak` sets the top of the scale; 0 scales to the +// largest value seen. +Item { + id: root + + property var values: [] + property real peak: 1 + property int capacity: 40 + property color colour: Colours.signal + property bool fill: true + property real lineWidth: 2 + + readonly property real scaleTop: root.peak > 0 ? root.peak : Math.max(1, root.values.reduce((m, v) => Math.max(m, v), 0)) + readonly property var pts: { + const n = root.values.length; + const step = root.width / Math.max(1, root.capacity - 1); + const h = root.height - root.lineWidth * 2; + return root.values.map((v, i) => Qt.point(root.width - (n - 1 - i) * step, root.lineWidth + h - Math.min(1, v / root.scaleTop) * h)); + } + + function curve(p: var): string { + if (p.length < 2) + return ""; + let d = `M ${p[0].x} ${p[0].y}`; + for (let i = 0; i < p.length - 1; i++) { + const a = p[Math.max(0, i - 1)], b = p[i], c = p[i + 1], e = p[Math.min(p.length - 1, i + 2)]; + d += ` C ${b.x + (c.x - a.x) / 6} ${b.y + (c.y - a.y) / 6} ${c.x - (e.x - b.x) / 6} ${c.y - (e.y - b.y) / 6} ${c.x} ${c.y}`; + } + return d; + } + + readonly property string line: root.curve(root.pts) + + Shape { + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + + ShapePath { + strokeWidth: -1 + fillGradient: LinearGradient { + x1: 0 + y1: 0 + x2: 0 + y2: root.height + + GradientStop { + position: 0 + color: Qt.alpha(root.colour, root.fill ? 0.28 : 0) + } + + GradientStop { + position: 1 + color: Qt.alpha(root.colour, 0) + } + } + + PathSvg { + path: root.line ? `${root.line} L ${root.pts[root.pts.length - 1].x} ${root.height} L ${root.pts[0].x} ${root.height} Z` : "" + } + } + + ShapePath { + strokeWidth: root.lineWidth + strokeColor: root.colour + fillColor: "transparent" + capStyle: ShapePath.RoundCap + joinStyle: ShapePath.RoundJoin + + PathSvg { + path: root.line + } + } + } + + Rectangle { + readonly property var at: root.pts.length > 0 ? root.pts[root.pts.length - 1] : null + + visible: at !== null + x: (at?.x ?? 0) - width / 2 + y: (at?.y ?? 0) - height / 2 + width: 7 + height: 7 + radius: 3.5 + color: root.colour + + Rectangle { + anchors.centerIn: parent + width: 18 + height: 18 + radius: 9 + color: Qt.alpha(root.colour, 0.18) + } + } +} From 8dbe08a1ed0655f988936d1132d4b921a9157e11 Mon Sep 17 00:00:00 2001 From: Trevin <132922589+T-Crypt@users.noreply.github.com> Date: Sun, 18 Oct 2026 02:07:33 -0600 Subject: [PATCH 3/3] dashboard: drop the Spine, keep the design ratchet honest The bento grid replaced the Spine on the Dashboard tab, so its surface, terrain and probe go. The layout setting keeps its stored value and now reads Live tiles. Radii are derived, not literal; the remaining icon rise is the new tiles' headers and controls, re-baselined on purpose. --- .../aphotic/components/SignalTile.qml | 2 +- .../modules/dashboard/DashboardContent.qml | 1 - .../dashboard/bento/BentoDashboard.qml | 23 +- .../aphotic/modules/dashboard/bento/Ring.qml | 2 +- .../modules/dashboard/bento/Spline.qml | 4 +- .../aphotic/modules/dashboard/spine/README.md | 62 --- .../modules/dashboard/spine/SpineProbe.qml | 100 ----- .../modules/dashboard/spine/SpineSurface.qml | 417 ------------------ .../modules/dashboard/spine/SpineTerrain.qml | 191 -------- .../modules/settings/panes/AppearancePane.qml | 2 +- tests/design_language_baseline.json | 4 +- 11 files changed, 16 insertions(+), 792 deletions(-) delete mode 100644 Configs/quickshell/aphotic/modules/dashboard/spine/README.md delete mode 100644 Configs/quickshell/aphotic/modules/dashboard/spine/SpineProbe.qml delete mode 100644 Configs/quickshell/aphotic/modules/dashboard/spine/SpineSurface.qml delete mode 100644 Configs/quickshell/aphotic/modules/dashboard/spine/SpineTerrain.qml diff --git a/Configs/quickshell/aphotic/components/SignalTile.qml b/Configs/quickshell/aphotic/components/SignalTile.qml index 007796a1..80ca77be 100644 --- a/Configs/quickshell/aphotic/components/SignalTile.qml +++ b/Configs/quickshell/aphotic/components/SignalTile.qml @@ -76,7 +76,7 @@ Item { visible: root.live implicitWidth: 6 implicitHeight: 6 - radius: 3 + radius: Math.min(width, height) / 2 color: Colours.signal SequentialAnimation on opacity { diff --git a/Configs/quickshell/aphotic/modules/dashboard/DashboardContent.qml b/Configs/quickshell/aphotic/modules/dashboard/DashboardContent.qml index f1372a37..9d8431fd 100644 --- a/Configs/quickshell/aphotic/modules/dashboard/DashboardContent.qml +++ b/Configs/quickshell/aphotic/modules/dashboard/DashboardContent.qml @@ -5,7 +5,6 @@ import qs.components import qs.services import qs.modules.flow import QtQuick.Window -import "spine" import "bento" ColumnLayout { diff --git a/Configs/quickshell/aphotic/modules/dashboard/bento/BentoDashboard.qml b/Configs/quickshell/aphotic/modules/dashboard/bento/BentoDashboard.qml index 17a999b8..bec50ca7 100644 --- a/Configs/quickshell/aphotic/modules/dashboard/bento/BentoDashboard.qml +++ b/Configs/quickshell/aphotic/modules/dashboard/bento/BentoDashboard.qml @@ -117,7 +117,7 @@ Item { Rectangle { implicitWidth: 28 implicitHeight: 3 - radius: 1.5 + radius: Math.min(width, height) / 2 color: Colours.signal } @@ -216,13 +216,6 @@ Item { sourceSize.height: 384 } - MaterialIcon { - anchors.centerIn: parent - visible: art.status !== Image.Ready - text: "album" - color: Colours.inkMuted - font.pixelSize: parent.width * 0.4 - } } ColumnLayout { @@ -349,8 +342,8 @@ Item { Repeater { model: [ - { icon: "south", value: NetworkUsage.downloadSpeed, colour: Colours.signal }, - { icon: "north", value: NetworkUsage.uploadSpeed, colour: Colours.signalSecondary } + { icon: "\u2193", value: NetworkUsage.downloadSpeed, colour: Colours.signal }, + { icon: "\u2191", value: NetworkUsage.uploadSpeed, colour: Colours.signalSecondary } ] RowLayout { @@ -360,10 +353,10 @@ Item { spacing: Tokens.spacing.extraSmall - MaterialIcon { + StyledText { text: netFigure.modelData.icon color: netFigure.modelData.colour - fontStyle: Tokens.font.icon.medium + font: Tokens.font.title.medium } StyledText { @@ -451,7 +444,7 @@ Item { Rectangle { implicitWidth: 7 implicitHeight: 7 - radius: 3.5 + radius: Math.min(width, height) / 2 color: session.waiting ? Colours.signal : session.busy ? Colours.signalOnSurface : Colours.outline } @@ -643,13 +636,13 @@ Item { Rectangle { Layout.fillWidth: true implicitHeight: 4 - radius: 2 + radius: Math.min(width, height) / 2 color: Colours.signalStyle.hairline Rectangle { width: parent.width * (1 - Pomodoro.remaining / Math.max(1, Pomodoro.isBreak ? Pomodoro.breakSeconds : Pomodoro.workSeconds)) height: parent.height - radius: 2 + radius: Math.min(width, height) / 2 color: Pomodoro.isBreak ? Colours.signalTertiary : Colours.signal Behavior on width { diff --git a/Configs/quickshell/aphotic/modules/dashboard/bento/Ring.qml b/Configs/quickshell/aphotic/modules/dashboard/bento/Ring.qml index b58d73cb..b0585283 100644 --- a/Configs/quickshell/aphotic/modules/dashboard/bento/Ring.qml +++ b/Configs/quickshell/aphotic/modules/dashboard/bento/Ring.qml @@ -49,7 +49,7 @@ Item { } ShapePath { - strokeColor: root.shown >= 0.9 ? Colours.palette.m3error : root.colour + strokeColor: root.shown >= 0.9 ? Colours.alert : root.colour strokeWidth: root.thickness fillColor: "transparent" capStyle: ShapePath.RoundCap diff --git a/Configs/quickshell/aphotic/modules/dashboard/bento/Spline.qml b/Configs/quickshell/aphotic/modules/dashboard/bento/Spline.qml index 3fa21e9d..f2099327 100644 --- a/Configs/quickshell/aphotic/modules/dashboard/bento/Spline.qml +++ b/Configs/quickshell/aphotic/modules/dashboard/bento/Spline.qml @@ -87,14 +87,14 @@ Item { y: (at?.y ?? 0) - height / 2 width: 7 height: 7 - radius: 3.5 + radius: Math.min(width, height) / 2 color: root.colour Rectangle { anchors.centerIn: parent width: 18 height: 18 - radius: 9 + radius: Math.min(width, height) / 2 color: Qt.alpha(root.colour, 0.18) } } diff --git a/Configs/quickshell/aphotic/modules/dashboard/spine/README.md b/Configs/quickshell/aphotic/modules/dashboard/spine/README.md deleted file mode 100644 index cec84a93..00000000 --- a/Configs/quickshell/aphotic/modules/dashboard/spine/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# Signal Spine - -The default Dashboard tab layout. It is also the Command Center's one live view of the machine: - -- `SpineSurface.qml` -- the surface: clock, headline readouts, a detail row - (temperatures, storage, upload, battery), the machine's - terrain above the spine, the day's leaves below it in equal slots. The - surface draws its own dark gradient, so a bright wallpaper never - outshouts the text. -- `SpineTerrain.qml` -- the canvas that draws CPU history as a ridge and - network as a low band along the floor. Memory stays a readout. -- `SpineProbe.qml` -- the offscreen render probe. It copies to the - config root at run time because `qs -p` resolves `qs.*` imports only - from the config root. - -The Dashboard tab shows it when `dashboardLayout` is `"spine"`, the default (Settings, Appearance, Dashboard layout). `"classic"` shows the cards and brings back the Performance tab, since the cards carry no machine stats. The leaves -(`DashMedia`, `DashPomodoro`, `DashQuickToggles`, `DashCalendar`) are the -existing dashboard widgets, reused unmodified, so the Spine rides -along with any token or copy pass made to those files. - -## Running the probe - -From `Configs/quickshell/aphotic/`: - -```sh -cp modules/dashboard/spine/SpineProbe.qml SpineProbeRun.qml -QT_QPA_PLATFORM=offscreen \ - HOME=/tmp/opencode/spine-home \ - SPINE_W=1920 SPINE_H=1080 SPINE_MS=14000 \ - SPINE_OUT=/tmp/opencode/spine-1920x1080.png \ - qs -p SpineProbeRun.qml -rm SpineProbeRun.qml -``` - -`HOME` points at a throwaway directory holding copies of -`~/.local/state/aphotic/palette.json`, `settings.json` and -`~/.config/awww/current-wallpaper`, so the probe renders the operator's -real dark palette over their wallpaper (plus the 0.35 dim layer, the -same composition the dashboard window uses) without touching the real -files. `SPINE_OPEN_DELAY=500 SPINE_MOTION_MS=150` grabs a frame -mid-entrance instead. The staged files are copies made at run time and -the originals are only ever read. - -Run it a second time with `SPINE_W=3440 SPINE_H=1440` for the -ultrawide render. The CPU ridge grows one sample per service poll while -the surface is open, so a longer `SPINE_MS` draws more terrain; the -grab happens once, then the probe quits. - -The signal-text clock needs `shaders/signal.frag.qsb`, which installs -build from `shaders/signal.frag`. On a checkout without it the clock -falls back to plain text by design. To preview the real effect locally, -build the shader (same command `lib/install/config_deploy.sh` uses) and -do not commit the result: - -```sh -/usr/lib/qt6/bin/qsb --glsl "100es,120,150" --hlsl 50 --msl 12 \ - -o shaders/signal.frag.qsb shaders/signal.frag -``` - -The probe renders with real service data. With no network fetch or no -players running, the weather leaf and the media leaf show their honest -empty states rather than invented numbers. diff --git a/Configs/quickshell/aphotic/modules/dashboard/spine/SpineProbe.qml b/Configs/quickshell/aphotic/modules/dashboard/spine/SpineProbe.qml deleted file mode 100644 index c23dc505..00000000 --- a/Configs/quickshell/aphotic/modules/dashboard/spine/SpineProbe.qml +++ /dev/null @@ -1,100 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell -import qs.components -import qs.services -import "modules/dashboard/spine" as Spine - -// Never maps a window on a live desktop: run with QT_QPA_PLATFORM=offscreen (Quickshell). -// qs resolves `qs.*` imports only from the config root, so a wrapper copies this file to the root. -ShellRoot { - id: probe - - readonly property int outW: Number(Quickshell.env("SPINE_W") || 1920) - readonly property int outH: Number(Quickshell.env("SPINE_H") || 1080) - readonly property int grabAfterMs: Number(Quickshell.env("SPINE_MS") || 4000) - readonly property int openAfterMs: Number(Quickshell.env("SPINE_OPEN_DELAY") || 0) - readonly property int motionMs: Number(Quickshell.env("SPINE_MOTION_MS") || 150) - readonly property string outFile: Quickshell.env("SPINE_OUT") || "/tmp/opencode/spine.png" - readonly property string wallpaper: Quickshell.env("SPINE_WALLPAPER") || "/tmp/opencode/spine-wallpaper.jpg" - - Component.onCompleted: console.log("PROBE size", probe.outW, "x", probe.outH, "run", probe.grabAfterMs, "ms", "open", probe.openAfterMs, "motion", probe.motionMs) - - FloatingWindow { - implicitWidth: probe.outW - implicitHeight: probe.outH - visible: true - color: Colours.shadow - - SystemUsageWatch {} - NetworkUsageWatch {} - - Item { - id: grabHost - - anchors.fill: parent - - Image { - anchors.fill: parent - source: "file://" + probe.wallpaper - fillMode: Image.PreserveAspectCrop - asynchronous: false - } - - Rectangle { - anchors.fill: parent - color: Colours.shadow - opacity: 0.35 - } - - Spine.SpineSurface { - id: surface - - anchors.fill: parent - shown: probe.openAfterMs <= 0 - } - - Timer { - interval: 200 - running: true - - onTriggered: { - const count = 48; - const cpu = []; - const net = []; - for (let i = 0; i < count; i++) { - const wave = 0.18 + 0.16 * Math.sin(i / 4.5) + 0.08 * Math.sin(i / 1.7); - const spike = (i >= 20 && i <= 23) ? 0.5 : 0; - cpu.push(Math.min(1, Math.max(0.04, wave + spike))); - net.push(Math.max(0, 1024 * (180 + 140 * Math.sin(i / 3.1) + (i >= 33 && i <= 35 ? 900 : 0)))); - } - surface.terrain.seedForProbe(cpu, net); - console.log("PROBE seeded terrain"); - } - } - - Timer { - id: openTimer - - interval: probe.openAfterMs - running: probe.openAfterMs > 0 - - onTriggered: { - surface.shown = true; - console.log("PROBE opened"); - } - } - - Timer { - interval: probe.openAfterMs > 0 ? probe.openAfterMs + probe.motionMs : probe.grabAfterMs - running: true - - onTriggered: grabHost.grabToImage(result => { - console.log("PROBE saved", probe.outFile, result.saveToFile(probe.outFile)); - Qt.quit(); - }) - } - } - } -} diff --git a/Configs/quickshell/aphotic/modules/dashboard/spine/SpineSurface.qml b/Configs/quickshell/aphotic/modules/dashboard/spine/SpineSurface.qml deleted file mode 100644 index 138eca43..00000000 --- a/Configs/quickshell/aphotic/modules/dashboard/spine/SpineSurface.qml +++ /dev/null @@ -1,417 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell.Bluetooth -import Quickshell.Services.UPower -import qs.config -import qs.components -import qs.services -import qs.services.profile -import qs.modules.dashboard - -Item { - id: root - - readonly property real margin: Tokens.padding.extraExtraLarge - readonly property bool portrait: stage.height > stage.width - - // Inside the dashboard's own tab frame the surface draws no dark - // gradient of its own; the frame and the window's dim layer already - // carry that. Default false keeps the standalone probe's full - // composition unchanged. - property bool embedded: false - - property bool shown: true - - SystemUsageWatch {} - NetworkUsageWatch {} - - readonly property bool mediaAlive: Players.active !== null - readonly property bool focusAlive: Pomodoro.running - readonly property bool weatherAlive: Weather.hasData - readonly property bool controlsAlive: Nmcli.wifiEnabled || (Bluetooth.defaultAdapter?.enabled ?? false) || DoNotDisturb.enabled - - readonly property var leaves: [mediaLeaf, focusLeaf, weatherLeaf, controlsLeaf] - readonly property var shownLeaves: leaves.filter(l => l.present) - - property alias terrain: terrain - - function rate(bytes: real): string { - if (bytes >= 1024 * 1024) - return qsTr("%1 MiB/s").arg((bytes / 1048576).toFixed(1)); - return qsTr("%1 KiB/s").arg(Math.round(bytes / 1024)); - } - - // Measured card memory in MiB, from the same source Flow draws. - readonly property var vram: ResourceEngine.resourceSpec("gpu-vram")?.measured ?? null - - function gib(kib: real): string { - return (kib / 1048576).toFixed(1); - } - - function temp(celsius: real): string { - const f = Config.services.useFahrenheitPerformance; - return `${Math.round(f ? celsius * 1.8 + 32 : celsius)}°${f ? "F" : "C"}`; - } - - property int diskIndex: 0 - readonly property var disk: SystemUsage.disks.length > 0 ? SystemUsage.disks[root.diskIndex % SystemUsage.disks.length] : null - - component Leaf: Item { - id: leaf - - required property string label - required property bool alive - property bool present: true - default property alias content: body.data - - readonly property int slot: root.shownLeaves.indexOf(leaf) - readonly property real slotWidth: root.portrait ? dayZone.width : dayZone.width / Math.max(1, root.shownLeaves.length) - - visible: leaf.present - x: root.portrait ? 0 : leaf.slot * leaf.slotWidth - y: root.portrait ? root.shownLeaves.slice(0, leaf.slot).reduce((sum, l) => sum + l.implicitHeight + Tokens.spacing.extraLarge, 0) : 0 - width: leaf.slotWidth - implicitHeight: title.implicitHeight + Tokens.spacing.medium + body.childrenRect.height - - Rectangle { - visible: root.portrait - x: -Tokens.spacing.medium - width: 2 - height: leaf.implicitHeight - radius: Tokens.rounding.full - color: Colours.signalStyle.accentLine - opacity: leaf.alive ? 1 : 0 - } - - SectionLabel { - id: title - - anchors.horizontalCenter: root.portrait ? undefined : parent.horizontalCenter - label: leaf.label - alive: leaf.alive - } - - Item { - id: body - - anchors.top: title.bottom - anchors.topMargin: Tokens.spacing.medium - x: root.portrait ? 0 : (leaf.width - body.childrenRect.width) / 2 - width: body.childrenRect.width - height: body.childrenRect.height - } - } - - SurfaceReveal { - id: reveal - - anchors.fill: parent - shown: root.shown - edge: "bottom" - - Item { - id: stage - - anchors.fill: parent - - Rectangle { - anchors.fill: parent - visible: !root.embedded - gradient: Gradient { - GradientStop { - position: 0 - color: Qt.alpha(Colours.shadow, 0.66) - } - GradientStop { - position: 0.62 - color: Qt.alpha(Colours.shadow, 0.66) - } - GradientStop { - position: 1 - color: Qt.alpha(Colours.shadow, 0.4) - } - } - } - - // Plain, crisp text: the time is read at a glance, so it never - // resolves out of noise or carries an effect around its edges. - Text { - id: clock - - x: root.margin - y: root.margin - text: { - Time.minuteStr; - return Time.format(Settings.twelveHourClock ? "h:mm AP" : "HH:mm"); - } - color: Colours.ink - renderType: Text.QtRendering - font.family: Tokens.font.headline.large.family - font.pixelSize: root.portrait ? 88 : Math.round(Math.max(72, Math.min(128, stage.height * 0.1))) - font.weight: Font.DemiBold - font.letterSpacing: 2 - - Rectangle { - anchors.left: parent.left - anchors.top: parent.bottom - anchors.topMargin: -Math.round(parent.font.pixelSize * 0.08) - width: Math.round(parent.font.pixelSize * 0.6) - height: 3 - radius: 1.5 - color: Colours.signalStyle.accentLine - } - } - - StyledText { - id: dateLine - - anchors.left: parent.left - anchors.leftMargin: root.margin - anchors.top: clock.bottom - anchors.topMargin: Tokens.spacing.extraSmall - text: Time.format("dddd, MMMM d") - color: Colours.ink - opacity: 0.72 - font: Tokens.font.title.builders.small.weight(Font.Medium).letterSpacing(0.6).build() - } - - component CycleReadout: Item { - id: cycle - - required property string value - required property string label - property bool cycles: false - signal next - - implicitWidth: cycleRow.implicitWidth - implicitHeight: cycleRow.implicitHeight - - Readout { - id: cycleRow - - small: true - value: cycle.value - label: cycle.label - } - - Interact { - anchors.fill: parent - anchors.margins: -Tokens.padding.small - enabled: cycle.cycles - radius: Tokens.rounding.small - onClicked: cycle.next() - } - } - - Column { - id: readouts - - spacing: Tokens.spacing.large - anchors.right: root.portrait ? undefined : parent.right - anchors.rightMargin: root.margin - anchors.left: root.portrait ? parent.left : undefined - anchors.leftMargin: root.margin - y: root.portrait ? dateLine.y + dateLine.height + Tokens.spacing.extraLarge : Math.max(root.margin, dateLine.y + dateLine.height - readouts.height) - - Grid { - anchors.right: root.portrait ? undefined : parent.right - columns: root.portrait ? 2 : 5 - columnSpacing: Tokens.spacing.extraLarge * 1.5 - rowSpacing: Tokens.spacing.large - - Readout { - value: `${Math.round(SystemUsage.cpuPerc * 100)}%` - label: qsTr("CPU") - } - - Readout { - visible: (root.vram?.total ?? 0) > 0 - value: ((root.vram?.used ?? 0) / 1024).toFixed(1) - total: qsTr("/ %1 GiB").arg(((root.vram?.total ?? 0) / 1024).toFixed(1)) - label: qsTr("VRAM") - } - - Readout { - value: SystemUsage.gpuStatsAvailable ? `${Math.round(SystemUsage.gpuPerc * 100)}%` : qsTr("N/A") - label: qsTr("GPU load") - } - - Readout { - value: root.gib(SystemUsage.memUsed) - total: qsTr("/ %1 GiB").arg(root.gib(SystemUsage.memTotal)) - label: qsTr("Memory") - } - - Readout { - value: root.rate(NetworkUsage.downloadSpeed) - label: qsTr("Download") - } - } - - Row { - id: details - - anchors.right: root.portrait ? undefined : parent.right - layoutDirection: root.portrait ? Qt.LeftToRight : Qt.RightToLeft - spacing: Tokens.spacing.extraLarge * 1.5 - - CycleReadout { - visible: Config.dashboard.performance.showBattery && UPower.displayDevice.isLaptopBattery - value: `${Math.round(UPower.displayDevice.percentage * 100)}%` - label: qsTr("Battery") - } - - CycleReadout { - visible: Config.dashboard.performance.showNetwork - value: root.rate(NetworkUsage.uploadSpeed) - label: qsTr("Upload") - } - - CycleReadout { - visible: Config.dashboard.performance.showStorage && root.disk !== null - value: root.disk ? `${Math.round(root.disk.perc * 100)}%` : "" - label: !root.disk ? "" : SystemUsage.disks.length > 1 ? qsTr("Storage %1 %2/%3").arg(root.disk.mount).arg(root.diskIndex % SystemUsage.disks.length + 1).arg(SystemUsage.disks.length) : qsTr("Storage %1").arg(root.disk.mount) - cycles: SystemUsage.disks.length > 1 - onNext: root.diskIndex = (root.diskIndex + 1) % SystemUsage.disks.length - } - - CycleReadout { - visible: Config.dashboard.performance.showGpu && SystemUsage.gpuDetected && SystemUsage.gpuTemp > 0 - value: root.temp(SystemUsage.gpuTemp) - label: SystemUsage.gpus.length > 1 ? qsTr("GPU %1 temp").arg(SystemUsage.selectedGpuIndex + 1) : qsTr("GPU temp") - cycles: SystemUsage.gpus.length > 1 - onNext: SystemUsage.selectGpu((SystemUsage.selectedGpuIndex + 1) % SystemUsage.gpus.length) - } - - CycleReadout { - visible: Config.dashboard.performance.showCpu && SystemUsage.cpuTemp > 0 - value: root.temp(SystemUsage.cpuTemp) - label: qsTr("CPU temp") - } - } - } - - SpineTerrain { - id: terrain - - anchors.left: parent.left - anchors.right: parent.right - y: (root.portrait ? readouts.y + readouts.height : Math.max(readouts.y + readouts.height, dateLine.y + dateLine.height)) + Tokens.spacing.extraLarge * 1.5 - height: { - if (root.portrait) - return Math.max(110, Math.min(180, stage.height * 0.14)); - const below = 3 + Tokens.spacing.extraExtraLarge + Math.max(dayZone.height, calendarLeaf.height) + root.margin * 2; - return Math.max(90, Math.min(300, stage.height - terrain.y - below)); - } - } - - Item { - id: spine - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: terrain.bottom - height: 3 - - Rectangle { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - height: 1 - color: Colours.signalStyle.hairline - } - - Repeater { - model: root.portrait ? [] : root.shownLeaves - - Rectangle { - id: segment - - required property Item modelData - - x: dayZone.x + segment.modelData.x + segment.modelData.width / 2 - width / 2 - y: 0 - width: Tokens.spacing.extraLarge * 2 - height: 2 - radius: Tokens.rounding.full - color: Colours.signalStyle.accentLine - opacity: segment.modelData.alive ? 1 : 0 - visible: segment.opacity > 0 - - Behavior on opacity { - Anim { - type: Anim.FastEffects - } - } - } - } - } - - Item { - id: dayZone - - x: root.margin - anchors.top: spine.bottom - anchors.topMargin: Tokens.spacing.extraExtraLarge - width: root.portrait ? stage.width * 0.48 - root.margin : calendarLeaf.x - root.margin - Tokens.spacing.extraExtraLarge - height: childrenRect.height - - Leaf { - id: mediaLeaf - - label: qsTr("Now playing") - alive: root.mediaAlive - - DashMedia {} - } - - Leaf { - id: focusLeaf - - label: qsTr("Timer") - alive: root.focusAlive - - DashPomodoro {} - } - - Leaf { - id: weatherLeaf - - label: qsTr("Weather") - alive: root.weatherAlive - present: Weather.hasData - - DashWeather {} - } - - Leaf { - id: controlsLeaf - - label: qsTr("Controls") - alive: root.controlsAlive - - DashQuickToggles {} - } - } - - Column { - id: calendarLeaf - - anchors.right: parent.right - anchors.rightMargin: root.margin - anchors.top: spine.bottom - anchors.topMargin: Tokens.spacing.extraExtraLarge - spacing: Tokens.spacing.medium - - SectionLabel { - anchors.horizontalCenter: parent.horizontalCenter - label: qsTr("Calendar") - alive: true - } - - DashCalendar {} - } - } - } -} diff --git a/Configs/quickshell/aphotic/modules/dashboard/spine/SpineTerrain.qml b/Configs/quickshell/aphotic/modules/dashboard/spine/SpineTerrain.qml deleted file mode 100644 index d7fff441..00000000 --- a/Configs/quickshell/aphotic/modules/dashboard/spine/SpineTerrain.qml +++ /dev/null @@ -1,191 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Shapes -import qs.components -import qs.config -import qs.services - -// CPU history as a smooth ridge and network as a low band, inside a rounded, -// outlined well. The curves pass through every sample (Catmull-Rom turned -// into cubic segments), so the line reads as a signal rather than a -// staircase, and the Shape's curve renderer antialiases them. -Item { - id: root - - readonly property int capacity: 48 - readonly property real netFlatBelow: 64 * 1024 - property var cpuTrail: [] - property var netTrail: [] - - function pushCpu(value: real): void { - const next = root.cpuTrail.concat([Math.max(0, Math.min(1, value))]); - root.cpuTrail = next.length > root.capacity ? next.slice(next.length - root.capacity) : next; - } - - function pushNet(value: real): void { - const next = root.netTrail.concat([Math.max(0, value)]); - root.netTrail = next.length > root.capacity ? next.slice(next.length - root.capacity) : next; - } - - function seedForProbe(cpu: var, net: var): void { - root.cpuTrail = cpu; - root.netTrail = net; - } - - Connections { - target: SystemUsage - - function onCpuPercChanged(): void { - root.pushCpu(SystemUsage.cpuPerc); - } - } - - Connections { - target: NetworkUsage - - function onDownloadSpeedChanged(): void { - root.pushNet(NetworkUsage.downloadSpeed); - } - } - - readonly property real inset: 10 - readonly property real floorY: root.height - root.inset - - function points(values: var, top: real, floor: real, peak: real): var { - const w = root.width - root.inset * 2; - const step = w / (root.capacity - 1); - const count = values.length; - return values.map((v, i) => Qt.point(root.inset + w - (count - 1 - i) * step, floor - (v / peak) * (floor - top))); - } - - // Catmull-Rom through the points, as SVG cubic segments. - function curve(pts: var): string { - if (pts.length < 2) - return ""; - let d = `M ${pts[0].x} ${pts[0].y}`; - for (let i = 0; i < pts.length - 1; i++) { - const p0 = pts[Math.max(0, i - 1)]; - const p1 = pts[i]; - const p2 = pts[i + 1]; - const p3 = pts[Math.min(pts.length - 1, i + 2)]; - d += ` C ${p1.x + (p2.x - p0.x) / 6} ${p1.y + (p2.y - p0.y) / 6} ${p2.x - (p3.x - p1.x) / 6} ${p2.y - (p3.y - p1.y) / 6} ${p2.x} ${p2.y}`; - } - return d; - } - - function area(pts: var, floor: real): string { - const line = root.curve(pts); - if (!line) - return ""; - return `${line} L ${pts[pts.length - 1].x} ${floor} L ${pts[0].x} ${floor} Z`; - } - - readonly property var cpuPts: root.points(root.cpuTrail, root.inset + 6, root.floorY, 1) - readonly property var netPts: { - const peak = root.netTrail.reduce((m, v) => Math.max(m, v), root.netFlatBelow); - return root.points(root.netTrail, root.floorY - root.height * 0.28, root.floorY, peak); - } - - StyledClippingRect { - anchors.fill: parent - radius: Tokens.rounding.large - color: Qt.alpha(Colours.signalStyle.surface, 0.35) - - // Faint guides at a quarter, half and three quarters. - Repeater { - model: [0.25, 0.5, 0.75] - - Rectangle { - required property real modelData - - x: root.inset - width: root.width - root.inset * 2 - y: root.floorY - (root.floorY - root.inset - 6) * modelData - height: 1 - color: Qt.alpha(Colours.signalStyle.hairline, 0.5) - } - } - - Shape { - anchors.fill: parent - preferredRendererType: Shape.CurveRenderer - - ShapePath { - strokeWidth: -1 - fillColor: Qt.alpha(Colours.signalSecondary, 0.1) - - PathSvg { - path: root.area(root.netPts, root.floorY) - } - } - - ShapePath { - strokeWidth: -1 - fillGradient: LinearGradient { - x1: 0 - y1: 0 - x2: 0 - y2: root.height - - GradientStop { - position: 0 - color: Qt.alpha(Colours.signalStyle.accentLine, 0.3) - } - - GradientStop { - position: 1 - color: Qt.alpha(Colours.signalStyle.accentLine, 0) - } - } - - PathSvg { - path: root.area(root.cpuPts, root.floorY) - } - } - - ShapePath { - strokeWidth: 2 - strokeColor: Colours.signalStyle.accentLine - fillColor: "transparent" - capStyle: ShapePath.RoundCap - joinStyle: ShapePath.RoundJoin - - PathSvg { - path: root.curve(root.cpuPts) - } - } - } - - // The newest sample: a lit point the eye lands on. - Rectangle { - id: tip - - readonly property var at: root.cpuPts.length > 0 ? root.cpuPts[root.cpuPts.length - 1] : null - - visible: tip.at !== null - x: (tip.at?.x ?? 0) - width / 2 - y: (tip.at?.y ?? 0) - height / 2 - width: 8 - height: 8 - radius: 4 - color: Colours.signalStyle.accentLine - - Rectangle { - anchors.centerIn: parent - width: 20 - height: 20 - radius: 10 - color: Qt.alpha(Colours.signalStyle.accentLine, 0.16) - } - } - } - - Rectangle { - anchors.fill: parent - radius: Tokens.rounding.large - color: "transparent" - border.width: 1 - border.color: Colours.signalStyle.hairline - } -} diff --git a/Configs/quickshell/aphotic/modules/settings/panes/AppearancePane.qml b/Configs/quickshell/aphotic/modules/settings/panes/AppearancePane.qml index b78ad827..01b3e91f 100644 --- a/Configs/quickshell/aphotic/modules/settings/panes/AppearancePane.qml +++ b/Configs/quickshell/aphotic/modules/settings/panes/AppearancePane.qml @@ -481,7 +481,7 @@ Item { Repeater { model: [ - { id: "spine", label: qsTr("Signal Spine") }, + { id: "spine", label: qsTr("Live tiles") }, { id: "classic", label: qsTr("Classic") } ] diff --git a/tests/design_language_baseline.json b/tests/design_language_baseline.json index 927be0fa..9cab5b81 100644 --- a/tests/design_language_baseline.json +++ b/tests/design_language_baseline.json @@ -46,12 +46,13 @@ "files": {} }, "material_icon_uses": { - "total": 197, + "total": 201, "files": { "Configs/quickshell/aphotic/components/AppIcon.qml": 1, "Configs/quickshell/aphotic/components/ColorPickerField.qml": 4, "Configs/quickshell/aphotic/components/SettingsRow.qml": 1, "Configs/quickshell/aphotic/components/SettingsSection.qml": 2, + "Configs/quickshell/aphotic/components/SignalTile.qml": 1, "Configs/quickshell/aphotic/modules/background/DesktopContextMenu.qml": 1, "Configs/quickshell/aphotic/modules/background/Wallpaper.qml": 1, "Configs/quickshell/aphotic/modules/bar/MinimalIndicators.qml": 1, @@ -99,6 +100,7 @@ "Configs/quickshell/aphotic/modules/dashboard/DashWallpapersTab.qml": 1, "Configs/quickshell/aphotic/modules/dashboard/DashWeather.qml": 2, "Configs/quickshell/aphotic/modules/dashboard/PerformanceTab.qml": 8, + "Configs/quickshell/aphotic/modules/dashboard/bento/BentoDashboard.qml": 3, "Configs/quickshell/aphotic/modules/intelligence/IntelligenceHeader.qml": 2, "Configs/quickshell/aphotic/modules/intelligence/IntelligenceInput.qml": 1, "Configs/quickshell/aphotic/modules/intelligence/IntelligenceSessionList.qml": 1,