Unify PillButton, CursorPill, ChoiceButton into qs.Ui.Button + ButtonGroup

The three components were three takes on the same shape \u2014 a clickable
rectangle with text/icon, a hot/hover state, an optional persistent
border, and an optional 'selected' or 'active' highlight. CursorPill
was a 30-line PillButton wrapper that added a hovered() signal;
ChoiceButton was effectively PillButton with selected: bool painting
an accent fill+border.

Collapse them into a single qs.Ui.Button. State flags compose
independently:

  hasCursor / hover         hot fill
  active                    persistent foreground-tint fill
  selected                  accent fill + accent border
  bordered: true            persistent 1px idle border (form primaries)
  focusable: true           Tab focus paints the accent ring
  pressed                   pressed fill

The hovered(bool) signal is now built-in, so CursorPill's wrapper is
unnecessary. ButtonGroup wraps a Row+Repeater for the form-style
'pick one of N' pattern; panel-cursor-driven cases still compose
Buttons directly in a Row with per-instance hasCursor wiring.

Theme tokens move into a new [style] section in shell.toml:

  border-width        = 1
  focus-border-width  = 3
  idle-border-alpha   = 0.4
  hot-fill-alpha      = 0.08
  selected-fill-alpha = 0.18
  pressed-fill-alpha  = 0.22
  focus-fill-alpha    = 0.22

Style.qml parses these out of the same shell.toml [font] / [bar]
already reads, and exposes pre-computed Style.hotFill / selectedFill /
pressedFill / idleBorderColor / selectedAccentFill / borderWidth +
the existing focusBorder* tokens. Themes that don't ship a [style]
section get the previous defaults unchanged.

Dev gallery consolidates three sections (PillButton, CursorPill,
ChoiceButton) into Button + ButtonGroup, with the cursor model
sections renamed accordingly.
This commit is contained in:
Ryan Hughes
2026-05-18 12:09:13 -04:00
parent 42dee4d399
commit b5f7ba02c2
17 changed files with 264 additions and 343 deletions
+11
View File
@@ -12,6 +12,17 @@ active = "{{ color1 }}"
size-horizontal = 26
size-vertical = 28
[style]
# State alphas used by every interactive surface in the kit (Button,
# Toggle, TextField, etc.). Foreground-tinted unless noted.
border-width = 1 # idle 1px border on inputs and bordered buttons
focus-border-width = 3 # accent ring on Tab focus
idle-border-alpha = 0.4 # alpha for the idle 1px foreground border
hot-fill-alpha = 0.08 # cursor / hover fill
selected-fill-alpha = 0.18 # selected / active / current fill
pressed-fill-alpha = 0.22 # button pressed
focus-fill-alpha = 0.22 # accent fill behind Tab focus ring
[font]
# base-size is the rem root for the type scale. Every Style.font.<token>
# derives from it (e.g. body = base, subtitle ≈ base * 1.083,
+52 -14
View File
@@ -25,18 +25,48 @@ QtObject {
property int cornerRadius: 0
// ---------------------------------------------------------- state alphas
//
// Foreground-tinted fills + border alphas used by every interactive
// surface in the kit. Themes can override these via [style] in
// shell.toml; otherwise the kit defaults below apply.
property var styleOverrides: ({})
function styleNum(key, fallback) {
var v = styleOverrides[key]
var n = Number(v)
return isFinite(n) ? n : fallback
}
readonly property int borderWidth: Math.max(0, Math.round(styleNum("border-width", 1)))
readonly property int focusBorderWidth: Math.max(1, Math.round(styleNum("focus-border-width", 3)))
readonly property real idleBorderAlpha: styleNum("idle-border-alpha", 0.4)
readonly property real hotFillAlpha: styleNum("hot-fill-alpha", 0.08)
readonly property real selectedFillAlpha: styleNum("selected-fill-alpha", 0.18)
readonly property real pressedFillAlpha: styleNum("pressed-fill-alpha", 0.22)
readonly property real focusFillAlpha: styleNum("focus-fill-alpha", 0.22)
function alphaForeground(a) {
return Qt.rgba(Color.foreground.r, Color.foreground.g, Color.foreground.b, a)
}
function alphaAccent(a) {
return Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, a)
}
// Focus affordances. Deliberately distinct from "selected" (which uses an
// accent fill) so the keyboard cursor never reads as the chosen value.
// The settings panel originated these tokens; promoting them here means
// every Ui component picks them up uniformly.
readonly property color focusBorderColor: Color.accent
readonly property color focusFillColor: Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.22)
readonly property int focusBorderWidth: 3
readonly property color focusFillColor: alphaAccent(focusFillAlpha)
// Convenience fills used by panel rows and pills. Hot is hover/keyboard
// cursor; selected is the persistent chosen/current item state.
readonly property color hotFill: Qt.rgba(Color.foreground.r, Color.foreground.g, Color.foreground.b, 0.08)
readonly property color selectedFill: Qt.rgba(Color.foreground.r, Color.foreground.g, Color.foreground.b, 0.18)
// cursor; selected/active is the persistent chosen/current state.
readonly property color hotFill: alphaForeground(hotFillAlpha)
readonly property color selectedFill: alphaForeground(selectedFillAlpha)
readonly property color pressedFill: alphaForeground(pressedFillAlpha)
readonly property color idleBorderColor: alphaForeground(idleBorderAlpha)
readonly property color selectedAccentFill: alphaAccent(selectedFillAlpha)
// ---------------------------------------------------------- typography
//
@@ -57,7 +87,8 @@ QtObject {
property int fontBaseSize: 12
// Parsed maps populated by loadShell. Keep them as plain dicts so
// reassigning the whole property fires reactive bindings.
// reassigning the whole property fires reactive bindings. styleOverrides
// is declared up top next to the helpers that consume it.
property var fontOverrides: ({})
property var barOverrides: ({})
@@ -117,10 +148,12 @@ QtObject {
// Parse [font] base-size + per-token overrides and [bar] size-* keys
// out of shell.toml. Color.qml owns the quoted-string side of the same
// file; we only care about the unquoted integer values here.
// file; we handle the unquoted numeric tokens for [font], [bar] sizes,
// and [style] (alphas + border widths).
function loadShell(raw) {
var fontOut = {}
var barOut = {}
var styleOut = {}
var nextBase = 12
var text = String(raw || "")
if (text) {
@@ -131,15 +164,19 @@ QtObject {
if (!line || line.charAt(0) === "#") continue
var sectionMatch = line.match(/^\[([A-Za-z0-9_-]+)\]\s*(#.*)?$/)
if (sectionMatch) { section = sectionMatch[1]; continue }
var kv = line.match(/^([A-Za-z0-9_-]+)\s*=\s*(-?\d+)\s*(#.*)?$/)
// Accept ints OR floats (alphas are 0..1).
var kv = line.match(/^([A-Za-z0-9_-]+)\s*=\s*(-?\d+(?:\.\d+)?)\s*(#.*)?$/)
if (!kv) continue
var key = kv[1]
var val = parseInt(kv[2], 10)
var raw = kv[2]
if (section === "font") {
if (key === "base-size") nextBase = val
else fontOut[key] = val
var ival = parseInt(raw, 10)
if (key === "base-size") nextBase = ival
else fontOut[key] = ival
} else if (section === "bar" && (key === "size-horizontal" || key === "size-vertical")) {
barOut[key] = val
barOut[key] = parseInt(raw, 10)
} else if (section === "style") {
styleOut[key] = parseFloat(raw)
}
}
}
@@ -150,6 +187,7 @@ QtObject {
fontBaseSize = nextBase
fontOverrides = fontOut
barOverrides = barOut
styleOverrides = styleOut
}
property Process hyprctlProc: Process {
+66 -57
View File
@@ -2,60 +2,93 @@ import QtQuick
import QtQuick.Controls
import qs.Commons
// The button. One component for every clickable thing in the kit.
// States compose independently and are applied in priority order:
//
// pressed (mouse down) pressed fill
// activeFocus (Tab focus) accent ring + accent fill
// selected accent fill + accent border
// active foreground tint fill (highlighted)
// hasCursor || hover hot fill
// idle transparent or 1px border if `bordered`
//
// All fills/borders come from `qs.Commons.Style` tokens, so themes
// control the look via [style] in shell.toml.
//
// Emits `hovered(bool)` so panels with their own keyboard cursor model
// can update state on mouse enter/leave.
Rectangle {
id: root
property string text: ""
property string iconText: ""
property string tooltipText: ""
// State flags (see comment above for paint priority).
property bool selected: false
property bool active: false
property bool hasCursor: false
property bool focusable: false
property bool bordered: false
// Colors. Defaults track the theme; per-instance overrides are honored.
property color foreground: Color.foreground
property color background: "transparent"
property color hoverBackground: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.08)
property color pressedBackground: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.22)
property color tooltipBackground: Color.background
property color tooltipForeground: foreground
property color accent: Color.accent
// Sizing.
property string fontFamily: Style.font.family
property real fontSize: Style.font.body
property real iconSize: Style.font.icon
property real iconRotation: 0
property real horizontalPadding: 10
property real verticalPadding: 6
property bool active: false
property bool leftAlign: false
property color activeBackground: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.18)
// Keyboard cursor flag. When true, the pill renders the same fill + border
// as a mouse hover so j/k navigation and pointer hover are visually
// indistinguishable. Default false; bind from a panel's cursor state.
property bool hasCursor: false
// Tooltip palette. Auto-rendered if tooltipText is set.
property color tooltipBackground: Color.background
property color tooltipForeground: foreground
// Tab-focusable form-button mode. Enables activeFocusOnTab and
// Enter/Return/Space activation, and uses Style.focusBorderColor / FillColor
// for the focus ring (distinct from the panel-cursor `hot` state). Set
// true for settings buttons (Save, Cancel, Reset); leave false for the
// panel-cursor pills (DNS picker, header actions).
property bool focusable: false
// Persistent 1px foreground border at idle. Use for "primary" form buttons
// (Save, Apply, + Add widget) so they read as buttons before the cursor
// hits them.
//
// The hot/cursor state is a fill only, matching CursorSurface's canonical
// chrome and the other panel primitives (Toggle, PanelActionButton). The
// accent border ring is reserved for Tab focus on `focusable` buttons.
property bool bordered: false
signal clicked()
signal rightClicked()
signal hovered(bool isHovered)
activeFocusOnTab: focusable
Keys.onReturnPressed: if (focusable) root.clicked()
Keys.onEnterPressed: if (focusable) root.clicked()
Keys.onSpacePressed: if (focusable) root.clicked()
implicitWidth: row.implicitWidth + horizontalPadding * 2
implicitHeight: row.implicitHeight + verticalPadding * 2
radius: Style.cornerRadius
readonly property bool hot: mouseArea.containsMouse || hasCursor
readonly property bool _showFocusRing: focusable && activeFocus
color: mouseArea.pressed ? Style.pressedFill
: _showFocusRing ? Style.focusFillColor
: selected ? Style.selectedAccentFill
: hot ? Style.hotFill
: active ? Style.selectedFill
: background
border.color: _showFocusRing ? Style.focusBorderColor
: selected ? accent
: bordered ? foreground
: Style.idleBorderColor
border.width: _showFocusRing ? Style.focusBorderWidth
: selected ? Math.max(Style.borderWidth, 2)
: bordered ? Style.borderWidth
: 0
Behavior on color { ColorAnimation { duration: 120 } }
ToolTip {
visible: root.tooltipText !== "" && mouseArea.containsMouse
text: root.tooltipText
delay: 400
padding: 0
background: Rectangle {
color: root.tooltipBackground
border.color: root.tooltipForeground
@@ -63,7 +96,6 @@ Rectangle {
radius: 0
opacity: 0.97
}
contentItem: Text {
text: root.tooltipText
color: root.tooltipForeground
@@ -76,34 +108,6 @@ Rectangle {
}
}
signal clicked()
signal rightClicked()
implicitWidth: row.implicitWidth + horizontalPadding * 2
implicitHeight: row.implicitHeight + verticalPadding * 2
radius: Style.cornerRadius
// Hot = mouse hover OR keyboard cursor. Pressed wins over both; otherwise
// hot beats `active` (so a cursor on an already-active pill still reads
// as hovered, matching CursorSurface's behaviour for navigable rows).
readonly property bool hot: mouseArea.containsMouse || hasCursor
// Tab-focus styling wins over hot the accent ring is the strongest
// signal and shouldn't be masked by a hover landing on the focused item.
readonly property bool _showFocusRing: focusable && activeFocus
color: mouseArea.pressed ? pressedBackground
: _showFocusRing ? Style.focusFillColor
: hot ? hoverBackground
: (active ? activeBackground : background)
border.width: _showFocusRing ? Style.focusBorderWidth
: (bordered ? 1 : 0)
border.color: _showFocusRing ? Style.focusBorderColor : foreground
Behavior on color {
ColorAnimation { duration: 120 }
}
Row {
id: row
anchors.verticalCenter: parent.verticalCenter
@@ -115,7 +119,7 @@ Rectangle {
Text {
visible: root.iconText !== ""
text: root.iconText
color: root.foreground
color: root.selected ? root.accent : root.foreground
font.family: root.fontFamily
font.pixelSize: root.iconSize
rotation: root.iconRotation
@@ -126,9 +130,10 @@ Rectangle {
Text {
visible: root.text !== ""
text: root.text
color: root.foreground
color: root.selected ? root.accent : root.foreground
font.family: root.fontFamily
font.pixelSize: root.fontSize
font.bold: root.selected
anchors.verticalCenter: parent.verticalCenter
}
}
@@ -145,4 +150,8 @@ Rectangle {
else root.clicked()
}
}
HoverHandler {
onHoveredChanged: root.hovered(hovered)
}
}
+62
View File
@@ -0,0 +1,62 @@
import QtQuick
import qs.Commons
// Mutually-exclusive row of Buttons — the form-style "pick one of N"
// pattern (bar position top/right/bottom/left, theme preset chips, etc.).
// Emits `changed(value)` when the user activates a different option.
//
// `options` is either a plain string[] (label == value) or an array of
// { value, label, icon?, tooltip? } objects. Mixing is fine.
//
// For panel-cursor-driven selection (where j/k walks a row), use bare
// `Button { hasCursor: ... }` instances in a Row — ButtonGroup is the
// convenience for non-cursor form contexts where you just need
// "selected: value === optionValue" wiring.
Row {
id: root
property var options: []
property string value: ""
property color foreground: Color.foreground
property color background: Color.background
property color accent: Color.accent
property string fontFamily: Style.font.family
property real fontSize: Style.font.body
property bool focusable: false
signal changed(string value)
spacing: 6
function optionValue(o) {
return (o && typeof o === "object") ? String(o.value) : String(o)
}
function optionLabel(o) {
return (o && typeof o === "object" && o.label !== undefined) ? String(o.label) : String(o)
}
function optionIcon(o) {
return (o && typeof o === "object" && o.icon) ? String(o.icon) : ""
}
function optionTooltip(o) {
return (o && typeof o === "object" && o.tooltip) ? String(o.tooltip) : ""
}
Repeater {
model: root.options
delegate: Button {
required property var modelData
text: root.optionLabel(modelData)
iconText: root.optionIcon(modelData)
tooltipText: root.optionTooltip(modelData)
selected: root.optionValue(modelData) === root.value
foreground: root.foreground
background: root.background
accent: root.accent
fontFamily: root.fontFamily
fontSize: root.fontSize
focusable: root.focusable
onClicked: root.changed(root.optionValue(modelData))
}
}
}
-80
View File
@@ -1,80 +0,0 @@
import QtQuick
import qs.Commons
// A single button in a mutually-exclusive choice group (a Row of these
// makes a "segmented control"). Distinct from PillButton because it has
// a real `selected` state semantic — used for picking between options
// (bar position: top/bottom/left/right), not for momentary actions.
//
// Selected styling uses the accent fill+border; focus styling uses the
// Style.focusBorderColor outline so keyboard nav can land on a non-selected
// option without it reading as the chosen one. This separation matters in
// the settings panel and any future "pick one" UI.
Rectangle {
id: root
property string text: ""
property bool selected: false
// Panel-cursor flag. Same role as PillButton.hasCursor: panels that own
// their own cursor state bind this to drive the keyboard highlight
// separately from real activeFocus. Cursor renders as a fill only —
// CursorSurface is the canonical chrome — while Tab focus adds the
// accent border ring on top.
property bool hasCursor: false
property bool borderlessHighlight: false
property color foreground: Color.foreground
property color background: Color.background
property color accent: Color.accent
property string fontFamily: Style.font.family
property real fontSize: Style.font.body
signal clicked()
signal hovered(bool isHovered)
activeFocusOnTab: true
Keys.onReturnPressed: root.clicked()
Keys.onEnterPressed: root.clicked()
Keys.onSpacePressed: root.clicked()
implicitWidth: Math.max(56, label.implicitWidth + 22)
implicitHeight: 28
radius: Style.cornerRadius
color: selected
? Qt.rgba(accent.r, accent.g, accent.b, 0.18)
: ((mouse.containsMouse || hasCursor) ? Qt.rgba(foreground.r, foreground.g, foreground.b, 0.08) : background)
border.color: selected
? accent
: (activeFocus ? foreground : Qt.rgba(foreground.r, foreground.g, foreground.b, 0.4))
border.width: borderlessHighlight ? (activeFocus ? 2 : 0)
: (selected ? 2 : (activeFocus ? 2 : 1))
Behavior on color { ColorAnimation { duration: 100 } }
Text {
id: label
anchors.centerIn: parent
text: root.text
color: root.selected ? root.accent : root.foreground
font.family: root.fontFamily
font.pixelSize: root.fontSize
font.bold: root.selected
}
MouseArea {
id: mouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.forceActiveFocus()
root.clicked()
}
}
HoverHandler {
onHoveredChanged: root.hovered(hovered)
}
}
-28
View File
@@ -1,28 +0,0 @@
import QtQuick
// PillButton that participates in a panel's single-cursor model. Use
// inside a row of pills (DNS providers, bluetooth header actions, choice
// chips) where mouse hover and keyboard cursor should land in the same
// place.
//
// Caller binds `hasCursor` to the panel's cursor state and listens to
// `hovered(bool)` to update that state when the mouse enters or leaves.
// This is structurally a wrapper around PillButton with one extra
// HoverHandler — but extracting it lets every panel use the same wiring
// idiom and lets plugin authors drop into the same cursor model without
// touching internals.
//
// Why HoverHandler instead of a MouseArea overlay: HoverHandler doesn't
// steal pointer events from PillButton's internal click MouseArea, so
// clicks still reach the underlying button. An overlay MouseArea with
// acceptedButtons: Qt.NoButton works but is fragile around tooltip
// timing and event propagation.
PillButton {
id: root
signal hovered(bool isHovered)
HoverHandler {
onHoveredChanged: root.hovered(hovered)
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ import qs.Commons
// Defaults bind to qs.Commons.Color so a caller with no theme overrides
// just works; foreground / accent / selectionTint can be overridden per
// instance. Focus styling uses Style.focusBorderColor (the same accent
// ring Toggle and ChoiceButton paint) so keyboard cursor and form-focus
// ring Toggle and Button paint) so keyboard cursor and form focus
// chrome stay consistent across the shell.
//
// Sizing is driven by font.pixelSize + verticalPadding. The default 30px
+1 -1
View File
@@ -21,7 +21,7 @@ Rectangle {
property string description: ""
property bool checked: false
// Panel-cursor flag. Same role as PillButton.hasCursor / ChoiceButton.hasCursor:
// Panel-cursor flag. Same role as Button.hasCursor:
// panels with their own keyboard cursor bind this to drive the highlight
// separately from activeFocus. Renders as a tinted fill only — the accent
// border ring is reserved for Tab focus (activeFocus).
+2 -3
View File
@@ -1,7 +1,7 @@
module qs.Ui
ChoiceButton 1.0 ChoiceButton.qml
CursorPill 1.0 CursorPill.qml
Button 1.0 Button.qml
ButtonGroup 1.0 ButtonGroup.qml
CursorSurface 1.0 CursorSurface.qml
Dropdown 1.0 Dropdown.qml
KeyboardPanel 1.0 KeyboardPanel.qml
@@ -12,7 +12,6 @@ PanelSectionHeader 1.0 PanelSectionHeader.qml
PanelSeparator 1.0 PanelSeparator.qml
PanelSlider 1.0 PanelSlider.qml
PanelToolTip 1.0 PanelToolTip.qml
PillButton 1.0 PillButton.qml
PopupCard 1.0 PopupCard.qml
SearchableDropdown 1.0 SearchableDropdown.qml
TextField 1.0 TextField.qml
+2 -2
View File
@@ -1551,7 +1551,7 @@ Item {
elide: Text.ElideRight
}
PillButton {
Button {
id: rowPinBtn
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
@@ -1565,7 +1565,7 @@ Item {
onClicked: trayRoot.togglePin(rowRoot.itemId)
}
PillButton {
Button {
id: rowHideBtn
anchors.verticalCenter: parent.verticalCenter
anchors.right: rowPinBtn.left
+3 -3
View File
@@ -512,11 +512,11 @@ Item {
}
}
// Header pill: a CursorPill bound into the panel's "header" cursor
// section. CursorPill collapses what used to be a PillButton subclass +
// Header pill: a Button bound into the panel's "header" cursor
// section. Button collapses what used to be a Button subclass +
// overlay MouseArea into one component; we keep the pillIndex / activated
// shim here so the three header pill instantiations stay readable.
component HeaderPill: CursorPill {
component HeaderPill: Button {
id: pill
required property int pillIndex
property bool pillEnabled: true
+2 -2
View File
@@ -95,7 +95,7 @@ Item {
width: parent.width
implicitHeight: 28
PillButton {
Button {
id: prevButton
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
@@ -115,7 +115,7 @@ Item {
font.bold: true
}
PillButton {
Button {
id: nextButton
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
+3 -3
View File
@@ -198,7 +198,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 6
PillButton {
Button {
iconText: "󰒮"
foreground: root.bar.foreground
horizontalPadding: 10
@@ -208,7 +208,7 @@ Item {
onClicked: if (root.activePlayer) root.activePlayer.previous()
}
PillButton {
Button {
iconText: root.activePlayer && root.activePlayer.isPlaying ? "󰏤" : "󰐊"
foreground: root.bar.foreground
horizontalPadding: 14
@@ -219,7 +219,7 @@ Item {
onClicked: if (root.activePlayer) root.activePlayer.togglePlaying()
}
PillButton {
Button {
iconText: "󰒭"
foreground: root.bar.foreground
horizontalPadding: 10
+2 -2
View File
@@ -30,7 +30,7 @@ Item {
// "brightness" - single slider row, selectedIndex = -1 sentinel
// (mirrors audioPanel's slider rows). Only present if a
// controllable backlight was detected.
// "scale" - 6 ChoiceButton scale presets; treated as a single
// "scale" - 6 Button scale presets; treated as a single
// horizontal row from j/k's perspective. h/l moves
// between presets, identical to bluetooth's header.
// "monitors" - vertical Toggle list for enabling/disabling displays;
@@ -499,7 +499,7 @@ Item {
Repeater {
model: root.scaleValues
CursorPill {
Button {
required property string modelData
required property int index
+3 -3
View File
@@ -697,7 +697,7 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
}
}
PillButton {
Button {
id: refreshBtn
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
@@ -915,7 +915,7 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
// One DNS provider pill. The cursor + current visuals come entirely from
// CursorSurface; this component just binds them to the panel's cursor
// state and renders the label/tooltip/click target.
component DnsProviderPill: CursorPill {
component DnsProviderPill: Button {
id: pill
required property string provider
required property int index
@@ -928,7 +928,7 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
horizontalPadding: 10
verticalPadding: 6
// Map the panel's domain semantics onto CursorPill's structural props:
// Map the panel's domain semantics onto Button's structural props:
// `current DNS` is the pill's `active` fill; the keyboard cursor lights
// up `hasCursor`.
active: root.dnsProvider === provider
+50 -140
View File
@@ -95,20 +95,19 @@ Item {
property int numberDemoValue: 15
readonly property var visibleSections: [
"cursor-surface", "pill-button", "cursor-pill", "panel-action-button",
"panel-tool-tip", "slider", "choice-button", "text-field", "number-field",
"cursor-surface", "button", "button-group", "panel-action-button",
"panel-tool-tip", "slider", "text-field", "number-field",
"toggle", "dropdown", "searchable-dropdown", "composed"
]
function sectionCount(section) {
switch (section) {
case "cursor-surface": return 3
case "pill-button": return 5
case "cursor-pill": return 4
case "button": return 5
case "button-group": return 4
case "panel-action-button": return 4
case "panel-tool-tip": return 1
case "slider": return 1
case "choice-button": return 4
case "text-field": return 2
case "number-field": return 1
case "toggle": return 2
@@ -120,13 +119,11 @@ Item {
}
// True for sections whose primitives lay out horizontally (a row of
// pills, choice buttons, etc.) j/k jumps to the next/prev section,
// h/l walks within the row.
// buttons) j/k jumps to the next/prev section, h/l walks within the row.
function sectionIsHorizontal(section) {
return section === "pill-button"
|| section === "cursor-pill"
return section === "button"
|| section === "button-group"
|| section === "panel-action-button"
|| section === "choice-button"
}
// True for sections where h/l should adjust a value rather than walk.
@@ -191,7 +188,7 @@ Item {
}
function activateCursor() {
if (focusSection === "choice-button") {
if (focusSection === "button-group") {
var opts = ["top", "right", "bottom", "left"]
if (selectedIndex >= 0 && selectedIndex < opts.length)
root.choiceDemoValue = opts[selectedIndex]
@@ -802,20 +799,20 @@ Item {
}
}
// ---- PillButton --------------------------------------------------
// ---- Button ------------------------------------------------------
Column {
width: parent.width
spacing: 8
Text {
text: "PillButton"
text: "Button"
color: root.foreground
font.family: root.fontFamily
font.pixelSize: Style.font.subtitle
font.bold: true
}
Text {
text: "Compact rounded button with optional icon, label, tooltip, and `active` highlight. Used inside panels for inline actions (Refresh, DNS pills, Bluetooth header)."
text: "The kit's only button. State flags compose: hasCursor (panel cursor / hover) paints a tinted fill; active adds a persistent highlight; bordered draws a 1px idle ring for primary form buttons; focusable enables Tab focus with the accent ring. Click below or press h/l to walk the demo cursor."
color: Qt.darker(root.foreground, 1.5)
font.family: root.fontFamily
font.pixelSize: Style.font.caption
@@ -825,100 +822,92 @@ Item {
Rectangle {
width: parent.width
implicitHeight: pillCol.implicitHeight + 24
implicitHeight: buttonRow.implicitHeight + 24
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
radius: Style.cornerRadius
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
border.width: 1
Row {
id: pillCol
id: buttonRow
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 14
spacing: 6
PillButton {
Button {
text: "DHCP"
tooltipText: "Use DNS from DHCP"
tooltipBackground: root.background
tooltipForeground: root.foreground
foreground: root.foreground
fontFamily: root.fontFamily
hasCursor: root.focusSection === "pill-button" && root.selectedIndex === 0
hasCursor: root.focusSection === "button" && root.selectedIndex === 0
onHovered: function(h) {
if (h) { root.focusSection = "button"; root.selectedIndex = 0 }
}
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
HoverHandler { onHoveredChanged: if (hovered) { root.focusSection = "pill-button"; root.selectedIndex = 0 } }
}
PillButton {
Button {
text: "Cloudflare"
tooltipText: "Set DNS to Cloudflare"
tooltipBackground: root.background
tooltipForeground: root.foreground
foreground: root.foreground
fontFamily: root.fontFamily
active: true
hasCursor: root.focusSection === "pill-button" && root.selectedIndex === 1
hasCursor: root.focusSection === "button" && root.selectedIndex === 1
onHovered: function(h) {
if (h) { root.focusSection = "button"; root.selectedIndex = 1 }
}
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
HoverHandler { onHoveredChanged: if (hovered) { root.focusSection = "pill-button"; root.selectedIndex = 1 } }
}
PillButton {
Button {
iconText: "󰑐"
tooltipText: "Refresh"
tooltipBackground: root.background
tooltipForeground: root.foreground
foreground: root.foreground
fontFamily: root.fontFamily
horizontalPadding: 8
verticalPadding: 4
hasCursor: root.focusSection === "pill-button" && root.selectedIndex === 2
hasCursor: root.focusSection === "button" && root.selectedIndex === 2
onHovered: function(h) {
if (h) { root.focusSection = "button"; root.selectedIndex = 2 }
}
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
HoverHandler { onHoveredChanged: if (hovered) { root.focusSection = "pill-button"; root.selectedIndex = 2 } }
}
PillButton {
Button {
iconText: "󰂯"
text: "On"
tooltipText: "Turn Bluetooth off"
tooltipBackground: root.background
tooltipForeground: root.foreground
foreground: root.foreground
fontFamily: root.fontFamily
active: true
hasCursor: root.focusSection === "pill-button" && root.selectedIndex === 3
hasCursor: root.focusSection === "button" && root.selectedIndex === 3
onHovered: function(h) {
if (h) { root.focusSection = "button"; root.selectedIndex = 3 }
}
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
HoverHandler { onHoveredChanged: if (hovered) { root.focusSection = "pill-button"; root.selectedIndex = 3 } }
}
PillButton {
Button {
text: "Apply"
foreground: root.foreground
fontFamily: root.fontFamily
focusable: true
bordered: true
hasCursor: root.focusSection === "pill-button" && root.selectedIndex === 4
hasCursor: root.focusSection === "button" && root.selectedIndex === 4
onHovered: function(h) {
if (h) { root.focusSection = "button"; root.selectedIndex = 4 }
}
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
HoverHandler { onHoveredChanged: if (hovered) { root.focusSection = "pill-button"; root.selectedIndex = 4 } }
}
}
}
}
// ---- CursorPill --------------------------------------------------
// ---- ButtonGroup -------------------------------------------------
Column {
width: parent.width
spacing: 8
Text {
text: "CursorPill"
text: "ButtonGroup"
color: root.foreground
font.family: root.fontFamily
font.pixelSize: Style.font.subtitle
font.bold: true
}
Text {
text: "PillButton with panel-cursor wiring. Bind hasCursor to your cursor state and onHovered to update it on mouse enter; clicks come from PillButton's clicked() signal. Use this for any \"pick one in a row\" UI (wifi DNS pills, bluetooth header actions). Click below or press h/l to walk the demo cursor."
text: "Mutually-exclusive row of Buttons. Selected option paints the accent fill+border so the chosen value stands out from non-selected options the cursor may pass through. Click to pick or press h/l + Enter."
color: Qt.darker(root.foreground, 1.5)
font.family: root.fontFamily
font.pixelSize: Style.font.caption
@@ -928,38 +917,22 @@ Item {
Rectangle {
width: parent.width
implicitHeight: cpRow.implicitHeight + 24
implicitHeight: choiceRow.implicitHeight + 24
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
radius: Style.cornerRadius
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
border.width: 1
Row {
id: cpRow
ButtonGroup {
id: choiceRow
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 14
spacing: 6
Repeater {
model: ["DHCP", "Cloudflare", "Google", "Custom"]
CursorPill {
required property string modelData
required property int index
text: modelData
foreground: root.foreground
tooltipBackground: root.background
tooltipForeground: root.foreground
fontFamily: root.fontFamily
tooltipText: "Pick " + modelData
hasCursor: root.focusSection === "cursor-pill" && root.selectedIndex === index
active: modelData === "Cloudflare"
onHovered: function(h) {
if (h) { root.focusSection = "cursor-pill"; root.selectedIndex = index }
}
onClicked: { root.focusSection = "cursor-pill"; root.selectedIndex = index }
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
}
options: ["top", "right", "bottom", "left"]
value: root.choiceDemoValue
onChanged: function(v) {
root.focusSection = "button-group"
root.choiceDemoValue = v
}
}
}
@@ -1210,69 +1183,6 @@ Item {
}
}
// ---- ChoiceButton --------------------------------------------------
Column {
width: parent.width
spacing: 8
Text {
text: "ChoiceButton"
color: root.foreground
font.family: root.fontFamily
font.pixelSize: Style.font.subtitle
font.bold: true
}
Text {
text: "A single button in a mutually-exclusive choice group. Selected styling uses the accent fill+border; focus styling uses the Style.focusBorderColor outline so keyboard nav can land on a non-selected option without it reading as the chosen one."
color: Qt.darker(root.foreground, 1.5)
font.family: root.fontFamily
font.pixelSize: Style.font.caption
width: parent.width
wrapMode: Text.WordWrap
}
Rectangle {
width: parent.width
implicitHeight: choiceRow.implicitHeight + 24
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
radius: Style.cornerRadius
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
border.width: 1
Row {
id: choiceRow
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 14
spacing: 6
Repeater {
model: ["top", "right", "bottom", "left"]
ChoiceButton {
required property string modelData
required property int index
text: modelData
foreground: root.foreground
background: root.background
accent: root.accent
fontFamily: root.fontFamily
selected: root.choiceDemoValue === modelData
hasCursor: root.focusSection === "choice-button" && root.selectedIndex === index
onClicked: {
root.focusSection = "choice-button"
root.selectedIndex = index
root.choiceDemoValue = modelData
}
onHovered: function(h) {
if (h) { root.focusSection = "choice-button"; root.selectedIndex = index }
}
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
}
}
}
}
}
// ---- TextField -----------------------------------------------------
Column {
width: parent.width
@@ -1421,7 +1331,7 @@ Item {
font.bold: true
}
Text {
text: "Title + description + switch. Click anywhere on the row to flip; caller updates `checked` in response. Same focus tokens as ChoiceButton."
text: "Title + description + switch. Click anywhere on the row to flip; caller updates `checked` in response. Same focus tokens as Button."
color: Qt.darker(root.foreground, 1.5)
font.family: root.fontFamily
font.pixelSize: Style.font.caption
+4 -4
View File
@@ -680,14 +680,14 @@ Item {
Row {
Layout.alignment: Qt.AlignRight
spacing: 8
PillButton {
Button {
text: "Cancel"
foreground: root.foreground
fontFamily: root.fontFamily
focusable: true
onClicked: root.discardWidgetSettings()
}
PillButton {
Button {
text: "Apply"
foreground: root.foreground
fontFamily: root.fontFamily
@@ -788,7 +788,7 @@ Item {
Row {
Layout.alignment: Qt.AlignRight
PillButton {
Button {
text: "Reset bar to defaults"
foreground: root.urgent
fontFamily: root.fontFamily
@@ -829,7 +829,7 @@ Item {
Repeater {
model: positionGroup.positions
delegate: ChoiceButton {
delegate: Button {
required property string modelData
text: modelData