Files
omarchycn/shell/Ui/ToggleSwitch.qml
14f1bb6c88 Add a Wi-Fi band toggle to the network panel (#6437)
Pins the active connection to 2.4/5/6GHz, or leaves the choice automatic.
The section only appears on Wi-Fi when the network answers on more than one
band, or while a pin is in force so it stays clearable.

bin/omarchy-network-band reports and sets the band. It sets NetworkManager's
802-11-wireless.band rather than pinning a BSSID: NM 1.44+ accepts a third
band value so 5 and 6GHz can be pinned apart, and a band survives an AP
rotating its BSSIDs while leaving roaming between APs intact. It refuses a
band the network does not answer on, and restores the previous setting if
reassociation fails rather than leaving the machine offline.

Under Automatic the pills collapse and the header reads the live band
("WI-FI BAND: 2.4GHZ"); pinning reveals them and the header goes plain. The
height is animated, and the section stays mounted through the reconnect a
band change causes -- otherwise `kind` briefly stops being "wifi" and the
whole segment would tear down and rebuild.

Two fixes to the same panel fall out of this:

- Every stat row stays mounted and reads "--" until it has data, instead of
  appearing a beat after the panel opens and shoving the rows below down.
- The speed test's Run button is scaled to match the band header's controls
  and joins the keyboard cursor chain.

ToggleSwitch gains a settable trackHeight so a compact placement can render a
genuinely small switch instead of scaling one down onto fractional pixels.
Defaults are unchanged.


Claude-Session: https://claude.ai/code/session_01XcKqYe1n5bnRBuAZwikwsr

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 10:11:23 -04:00

112 lines
4.4 KiB
QML

import QtQuick
import qs.Commons
// 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
? Style.selectedFillFor(root.foreground, root.accent)
: Style.normalFillFor(root.foreground, root.accent)
borderSpec: Border.controlSpec(root.checked ? "selected" : "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 ? Style.selectedStateColor(root.foreground, root.accent) : 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()
}
}