Files
omarchycn/shell/Ui/Button.qml
T
Ryan Hughes b5f7ba02c2 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.
2026-05-18 12:09:13 -04:00

158 lines
4.8 KiB
QML

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 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 leftAlign: false
// Tooltip palette. Auto-rendered if tooltipText is set.
property color tooltipBackground: Color.background
property color tooltipForeground: foreground
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
border.width: 1
radius: 0
opacity: 0.97
}
contentItem: Text {
text: root.tooltipText
color: root.tooltipForeground
font.family: root.fontFamily
font.pixelSize: Style.font.bodySmall
leftPadding: 10
rightPadding: 10
topPadding: 6
bottomPadding: 6
}
}
Row {
id: row
anchors.verticalCenter: parent.verticalCenter
anchors.left: root.leftAlign ? parent.left : undefined
anchors.leftMargin: root.leftAlign ? root.horizontalPadding : 0
anchors.horizontalCenter: root.leftAlign ? undefined : parent.horizontalCenter
spacing: 8
Text {
visible: root.iconText !== ""
text: root.iconText
color: root.selected ? root.accent : root.foreground
font.family: root.fontFamily
font.pixelSize: root.iconSize
rotation: root.iconRotation
transformOrigin: Item.Center
anchors.verticalCenter: parent.verticalCenter
}
Text {
visible: root.text !== ""
text: root.text
color: root.selected ? root.accent : root.foreground
font.family: root.fontFamily
font.pixelSize: root.fontSize
font.bold: root.selected
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: function(mouse) {
if (root.focusable) root.forceActiveFocus()
if (mouse.button === Qt.RightButton) root.rightClicked()
else root.clicked()
}
}
HoverHandler {
onHoveredChanged: root.hovered(hovered)
}
}