From ad997bd6b13cabfdb97efcb338fba67b50426b06 Mon Sep 17 00:00:00 2001 From: Andre Padez Date: Sat, 15 Aug 2026 17:31:30 +0100 Subject: [PATCH] Brand the panel: yellow IP/DNS line, white toggle - Colour the IP / DNS subtitle under each machine with the logo yellow (#FAE204, sampled as the dominant opaque colour of offscale-logo.png). Stock draws that line through `dim` = Qt.darker(foreground, 1.55), which on yellow shifts the hue to olive (#A19203) rather than quietening it, so it takes the flat accent instead. Everything else keeps the themed foreground: section headers, machine names, account names and all the iconography. - Add OffscaleToggle.qml so the switch reads white when on. The stock ToggleSwitch resolves its checked colours through Style.selectedStateColor / selectedFillFor, which read [controls] selected-color; Ethereal sets that to a literal "#ffcead" and Style.resolveStateColor returns literals verbatim, ignoring the foreground and accent properties entirely. No property on the stock switch can reach it, and overriding the token globally would recolour every selected state in the shell. The copy differs only in the checked state and needs an explicit `import qs.Ui`, which the original gets implicitly by living inside that module. Co-Authored-By: Claude Opus 5 --- OffscaleToggle.qml | 125 +++++++++++++++++++++++++++++++++++++++++++++ Panel.qml | 19 ++++++- 2 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 OffscaleToggle.qml diff --git a/OffscaleToggle.qml b/OffscaleToggle.qml new file mode 100644 index 0000000..778a457 --- /dev/null +++ b/OffscaleToggle.qml @@ -0,0 +1,125 @@ +// Local copy of qs.Ui.ToggleSwitch, changed in exactly one respect: the +// "on" colours come from `foreground` instead of the theme's selected-state +// tokens. +// +// Stock resolves them through Style.selectedStateColor / selectedFillFor, +// which read [controls] selected-color from the active theme. Ethereal sets +// that to a literal "#ffcead", and Style.resolveStateColor returns a literal +// verbatim — the `foreground` and `accent` properties are ignored entirely. +// So no property on the stock switch can make the checked knob white; only +// overriding [controls] selected-color globally would, and that would recolour +// every selected state across the shell (buttons, sliders, and the audio, +// bluetooth, network and clock panels). +// +import QtQuick +import qs.Commons +import qs.Ui // BorderSurface: implicit for the original inside qs.Ui, explicit here + +// Bare on/off switch: a track with a sliding knob and no label. This is the +// switch `Toggle` parks at the end of its labeled row, factored out so panel +// headers and other compact controls render the identical thing. +// +// The caller owns the value: bind `checked` to real state and flip it in +// response to `toggled()`. Services that already track a desired state +// optimistically (see the Tailscale service's `_desired`) get an instant knob +// throw for free, because `checked` is already the optimistic value. +// +// `busy` marks an operation in flight and swallows further clicks, but leaves +// hover, cursor, and tooltips alone so the control does not flicker every time +// a background refresh runs. +// +// The cursor is a ring drawn outside the track rather than a state on the +// track itself: themes give normal chrome a stronger border than hover-cursor +// (0.4 vs 0.25 by default), which is right for controls that are borderless at +// rest but would make a bordered track go *fainter* under the cursor. On the +// panel background the ring reads immediately. `cursorRing` follows +// `interactive` — a switch whose surrounding row owns the click owns the +// cursor too. +// +// `rounded` auto-detects from Style.cornerRadius so the switch follows the +// theme: pill shape when Hyprland corners are rounded, square on sharp. +Item { + id: root + + property bool checked: false + property bool busy: false + + // Off when the surrounding row owns the click, as in `Toggle`. + property bool interactive: true + + // Panel-cursor flag. Same role as Button.hasCursor: panels with their own + // keyboard cursor bind this to drive the highlight separately from hover. + property bool hasCursor: false + + property bool cursorRing: interactive + property int cursorPad: Style.space(6) + property bool rounded: Style.cornerRadius > 0 + property color foreground: Color.foreground + property color accent: Color.accent + + signal toggled() + signal hovered(bool isHovered) + + readonly property alias containsMouse: mouse.containsMouse + readonly property bool hot: hasCursor || mouse.containsMouse + + // `trackHeight` is settable so a compact placement — a switch riding a panel + // section header, say — can ask for a genuinely smaller control instead of + // scaling a big one down, which lands the track and knob on fractional pixels + // and blurs their edges. The derived sizes only carry floors low enough to + // stay out of an override's way; at the default track height each one is + // already above its floor, so nothing about the normal switch changes. + property int trackHeight: Math.max(22, Math.round(Style.spacing.controlHeight * 0.55)) + property int trackWidth: Math.round(trackHeight * 1.9) + property int knobSize: Math.max(6, Math.round(trackHeight * 0.72)) + property int knobInset: Math.max(1, Math.round((trackHeight - knobSize) / 2)) + + readonly property int _pad: cursorRing ? cursorPad : 0 + + implicitWidth: trackWidth + _pad * 2 + implicitHeight: trackHeight + _pad * 2 + + BorderSurface { + anchors.fill: parent + visible: root.cursorRing && root.hot + color: "transparent" + radius: Style.cornerRadius + borderSpec: Border.controlSpec("hover-cursor", root.foreground, root.accent) + } + + BorderSurface { + id: track + width: root.trackWidth + height: root.trackHeight + anchors.centerIn: parent + radius: root.rounded ? height / 2 : 0 + color: root.checked + ? Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.18) + : Style.normalFillFor(root.foreground, root.accent) + borderSpec: Border.controlSpec("normal", root.foreground, root.accent) + + Behavior on color { ColorAnimation { duration: 120 } } + + Rectangle { + width: root.knobSize + height: root.knobSize + radius: root.rounded ? height / 2 : 0 + x: root.checked ? track.width - width - root.knobInset : root.knobInset + anchors.verticalCenter: parent.verticalCenter + color: root.checked ? root.foreground : Qt.darker(root.foreground, 1.25) + + Behavior on x { NumberAnimation { duration: 120; easing.type: Easing.OutCubic } } + Behavior on color { ColorAnimation { duration: 120 } } + } + } + + MouseArea { + id: mouse + anchors.fill: parent + enabled: root.interactive + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onContainsMouseChanged: root.hovered(containsMouse) + onClicked: if (!root.busy) root.toggled() + } +} diff --git a/Panel.qml b/Panel.qml index 1441841..db2205f 100644 --- a/Panel.qml +++ b/Panel.qml @@ -23,9 +23,18 @@ Panel { property bool mullvadPickerOpen: false property string mullvadQuery: "" + // Text stays on the bar's themed colour: section headers, machine names and + // account names read white. readonly property color foreground: bar ? bar.foreground : Color.foreground readonly property color urgent: bar ? bar.urgent : Color.urgent readonly property color dim: Qt.darker(foreground, 1.55) + + // Brand yellow, sampled as the dominant opaque colour of offscale-logo.png. + // Used for exactly one thing: the IP / DNS subtitle under each machine, at + // full strength. Stock renders that line through `dim`, but darkening yellow + // shifts its hue to olive (#A19203) rather than merely quietening it, so it + // gets the flat accent instead. Icons stay on the themed foreground. + readonly property color accent: "#FAE204" readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family readonly property bool showConnections: tailscale.accounts.length > 1 || tailscale.accountsAccessDenied readonly property bool showPeers: tailscale.active && tailscale.peers.length > 0 @@ -499,13 +508,19 @@ Panel { // header's only cursor target. The service already flips `active` // optimistically, so the knob throws the instant you click it. trailingControl: Component { - ToggleSwitch { + OffscaleToggle { id: powerSwitch visible: tailscale.installed checked: tailscale.active busy: tailscale.busy hasCursor: header.ringVisible foreground: hero.foreground + // ToggleSwitch defaults `accent` to Color.accent — the theme + // accent, which is where the off-white/peach knob and border + // came from. The hero only forwards `foreground`, so pin + // accent to it as well and the switch reads white in both the + // connected and disconnected states. + accent: hero.foreground onHovered: function(on) { if (on) header.focusHero() } onToggled: tailscale.toggleOffscale() @@ -998,7 +1013,7 @@ Panel { if (peerRow.peerDns !== "") parts.push(peerRow.peerDns) return parts.join(" · ") } - color: root.dim + color: root.accent font.family: root.fontFamily font.pixelSize: Style.font.caption elide: Text.ElideRight