From b5f7ba02c26ed324269e2b602954111eae406e3b Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Mon, 18 May 2026 12:09:13 -0400 Subject: [PATCH] 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. --- default/themed/shell.toml.tpl | 11 ++ shell/Commons/Style.qml | 66 +++++-- shell/Ui/{PillButton.qml => Button.qml} | 123 ++++++------ shell/Ui/ButtonGroup.qml | 62 ++++++ shell/Ui/ChoiceButton.qml | 80 -------- shell/Ui/CursorPill.qml | 28 --- shell/Ui/TextField.qml | 2 +- shell/Ui/Toggle.qml | 2 +- shell/Ui/qmldir | 5 +- shell/plugins/bar/Bar.qml | 4 +- shell/plugins/bar/widgets/bluetoothPanel.qml | 6 +- shell/plugins/bar/widgets/calendar.qml | 4 +- shell/plugins/bar/widgets/media.qml | 6 +- shell/plugins/bar/widgets/monitorPanel.qml | 4 +- shell/plugins/bar/widgets/networkPanel.qml | 6 +- shell/plugins/dev-gallery/GalleryPanel.qml | 190 +++++-------------- shell/plugins/settings/SettingsPanel.qml | 8 +- 17 files changed, 264 insertions(+), 343 deletions(-) rename shell/Ui/{PillButton.qml => Button.qml} (60%) create mode 100644 shell/Ui/ButtonGroup.qml delete mode 100644 shell/Ui/ChoiceButton.qml delete mode 100644 shell/Ui/CursorPill.qml diff --git a/default/themed/shell.toml.tpl b/default/themed/shell.toml.tpl index d57393aa..8c5bec3a 100644 --- a/default/themed/shell.toml.tpl +++ b/default/themed/shell.toml.tpl @@ -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. # derives from it (e.g. body = base, subtitle ≈ base * 1.083, diff --git a/shell/Commons/Style.qml b/shell/Commons/Style.qml index e514bfb0..8bb51117 100644 --- a/shell/Commons/Style.qml +++ b/shell/Commons/Style.qml @@ -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 { diff --git a/shell/Ui/PillButton.qml b/shell/Ui/Button.qml similarity index 60% rename from shell/Ui/PillButton.qml rename to shell/Ui/Button.qml index 3812b867..98d38dcc 100644 --- a/shell/Ui/PillButton.qml +++ b/shell/Ui/Button.qml @@ -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) + } } diff --git a/shell/Ui/ButtonGroup.qml b/shell/Ui/ButtonGroup.qml new file mode 100644 index 00000000..f9e946dc --- /dev/null +++ b/shell/Ui/ButtonGroup.qml @@ -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)) + } + } +} diff --git a/shell/Ui/ChoiceButton.qml b/shell/Ui/ChoiceButton.qml deleted file mode 100644 index ac8005c2..00000000 --- a/shell/Ui/ChoiceButton.qml +++ /dev/null @@ -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) - } -} diff --git a/shell/Ui/CursorPill.qml b/shell/Ui/CursorPill.qml deleted file mode 100644 index da299455..00000000 --- a/shell/Ui/CursorPill.qml +++ /dev/null @@ -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) - } -} diff --git a/shell/Ui/TextField.qml b/shell/Ui/TextField.qml index 8a68710d..2e5692e5 100644 --- a/shell/Ui/TextField.qml +++ b/shell/Ui/TextField.qml @@ -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 diff --git a/shell/Ui/Toggle.qml b/shell/Ui/Toggle.qml index 81f9703a..e4019ca5 100644 --- a/shell/Ui/Toggle.qml +++ b/shell/Ui/Toggle.qml @@ -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). diff --git a/shell/Ui/qmldir b/shell/Ui/qmldir index 6d44f333..66afd990 100644 --- a/shell/Ui/qmldir +++ b/shell/Ui/qmldir @@ -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 diff --git a/shell/plugins/bar/Bar.qml b/shell/plugins/bar/Bar.qml index 21971725..360e9570 100644 --- a/shell/plugins/bar/Bar.qml +++ b/shell/plugins/bar/Bar.qml @@ -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 diff --git a/shell/plugins/bar/widgets/bluetoothPanel.qml b/shell/plugins/bar/widgets/bluetoothPanel.qml index b9b5fb78..3ee6d4d3 100644 --- a/shell/plugins/bar/widgets/bluetoothPanel.qml +++ b/shell/plugins/bar/widgets/bluetoothPanel.qml @@ -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 diff --git a/shell/plugins/bar/widgets/calendar.qml b/shell/plugins/bar/widgets/calendar.qml index 0cac7919..45503505 100644 --- a/shell/plugins/bar/widgets/calendar.qml +++ b/shell/plugins/bar/widgets/calendar.qml @@ -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 diff --git a/shell/plugins/bar/widgets/media.qml b/shell/plugins/bar/widgets/media.qml index 2742d720..4bc52110 100644 --- a/shell/plugins/bar/widgets/media.qml +++ b/shell/plugins/bar/widgets/media.qml @@ -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 diff --git a/shell/plugins/bar/widgets/monitorPanel.qml b/shell/plugins/bar/widgets/monitorPanel.qml index 7c676232..7e4ff853 100644 --- a/shell/plugins/bar/widgets/monitorPanel.qml +++ b/shell/plugins/bar/widgets/monitorPanel.qml @@ -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 diff --git a/shell/plugins/bar/widgets/networkPanel.qml b/shell/plugins/bar/widgets/networkPanel.qml index e8fe13d6..6679e42c 100644 --- a/shell/plugins/bar/widgets/networkPanel.qml +++ b/shell/plugins/bar/widgets/networkPanel.qml @@ -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 diff --git a/shell/plugins/dev-gallery/GalleryPanel.qml b/shell/plugins/dev-gallery/GalleryPanel.qml index 627531f2..55710816 100644 --- a/shell/plugins/dev-gallery/GalleryPanel.qml +++ b/shell/plugins/dev-gallery/GalleryPanel.qml @@ -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 diff --git a/shell/plugins/settings/SettingsPanel.qml b/shell/plugins/settings/SettingsPanel.qml index c329dd35..47c8c674 100644 --- a/shell/plugins/settings/SettingsPanel.qml +++ b/shell/plugins/settings/SettingsPanel.qml @@ -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