Make Button state composition visible in ButtonGroup and the gallery

Three small fixes around the new Button + ButtonGroup so the cursor is
always visible and the section reads as a single Button showing flag
combinations.

ButtonGroup now exposes cursorIndex + hovered(index, isHovered) so
panels can drive a single Button's hasCursor without reaching past
ButtonGroup's API. Every chip gets bordered: true so the row reads as
a real picker — distinct outlines, not just a tinted fill that vanishes
into the background.

Button's border treatment grows two paths so the cursor is always
visible regardless of other state: bordered + hot recolors the border
to the accent and thickens to the focus-ring width, and selected + hot
thickens the existing accent border to the focus-ring width (so the
cursor landing on the chosen option still reads as cursor presence,
instead of disappearing into the selected fill).

Gallery's Button section pairs each demo button with a small caption
('idle', 'active', 'icon only', 'icon + active', 'bordered +
focusable') so the row reads as one Button with its state flags side
by side rather than five unrelated buttons. The ButtonGroup section
gets the cursor wiring (cursorIndex + onHovered) plus an
onFocusedChanged auto-scroll so the cursor visual is reachable via
j/k h/l from the panel cursor model.
This commit is contained in:
Ryan Hughes
2026-05-18 12:33:24 -04:00
parent 8b858d4093
commit b25dd100e3
3 changed files with 138 additions and 47 deletions
+11 -1
View File
@@ -72,13 +72,23 @@ Rectangle {
: active ? Style.selectedFill : active ? Style.selectedFill
: background : background
// Border color follows the same precedence as fill: focus ring wins,
// then selected, then cursor on bordered (paints accent so the chip
// structure clearly reads as "cursor is here"), then plain bordered
// (foreground), then nothing.
border.color: _showFocusRing ? Style.focusBorderColor border.color: _showFocusRing ? Style.focusBorderColor
: selected ? accent : selected ? accent
: (bordered && hot) ? Style.focusBorderColor
: bordered ? foreground : bordered ? foreground
: Style.idleBorderColor : Style.idleBorderColor
// selected+hot thickens to the focus-ring width so the cursor remains
// visible on the chosen option (otherwise selected's accent fill+border
// masks any hot fill). bordered+hot also thickens so the chip cursor
// reads as a deliberate state change rather than a faint tint.
border.width: _showFocusRing ? Style.focusBorderWidth border.width: _showFocusRing ? Style.focusBorderWidth
: selected ? Math.max(Style.borderWidth, 2) : selected ? (hot ? Style.focusBorderWidth : Math.max(Style.borderWidth, 2))
: (bordered && hot) ? Style.focusBorderWidth
: bordered ? Style.borderWidth : bordered ? Style.borderWidth
: 0 : 0
+16 -4
View File
@@ -8,10 +8,10 @@ import qs.Commons
// `options` is either a plain string[] (label == value) or an array of // `options` is either a plain string[] (label == value) or an array of
// { value, label, icon?, tooltip? } objects. Mixing is fine. // { value, label, icon?, tooltip? } objects. Mixing is fine.
// //
// For panel-cursor-driven selection (where j/k walks a row), use bare // Panels with their own keyboard cursor model bind `cursorIndex` to the
// `Button { hasCursor: ... }` instances in a Row — ButtonGroup is the // currently-focused option (-1 = no cursor) and listen on `hovered` to
// convenience for non-cursor form contexts where you just need // keep that state synced with the mouse. Forms that don't care about
// "selected: value === optionValue" wiring. // the panel cursor model can leave both alone.
Row { Row {
id: root id: root
@@ -24,7 +24,12 @@ Row {
property real fontSize: Style.font.body property real fontSize: Style.font.body
property bool focusable: false property bool focusable: false
// -1 disables the cursor highlight (the form case). Set from a panel
// to drive Button.hasCursor on the matching index.
property int cursorIndex: -1
signal changed(string value) signal changed(string value)
signal hovered(int index, bool isHovered)
spacing: 6 spacing: 6
@@ -46,10 +51,16 @@ Row {
delegate: Button { delegate: Button {
required property var modelData required property var modelData
required property int index
text: root.optionLabel(modelData) text: root.optionLabel(modelData)
iconText: root.optionIcon(modelData) iconText: root.optionIcon(modelData)
tooltipText: root.optionTooltip(modelData) tooltipText: root.optionTooltip(modelData)
selected: root.optionValue(modelData) === root.value selected: root.optionValue(modelData) === root.value
hasCursor: root.cursorIndex === index
// Every chip carries an idle border so the group reads as a row of
// distinct options. selected paints accent; the cursor recolors the
// chip's border to accent via Button's bordered+hot path.
bordered: true
foreground: root.foreground foreground: root.foreground
background: root.background background: root.background
accent: root.accent accent: root.accent
@@ -57,6 +68,7 @@ Row {
fontSize: root.fontSize fontSize: root.fontSize
focusable: root.focusable focusable: root.focusable
onClicked: root.changed(root.optionValue(modelData)) onClicked: root.changed(root.optionValue(modelData))
onHovered: function(h) { root.hovered(index, h) }
} }
} }
} }
+111 -42
View File
@@ -833,62 +833,121 @@ Item {
anchors.left: parent.left anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 14 anchors.leftMargin: 14
spacing: 6 spacing: 16
Button { // Each demo Button is paired with a caption labeling the
text: "DHCP" // state(s) it exercises so the section reads as one Button
tooltipText: "Use DNS from DHCP" // showing its flag combinations side by side.
hasCursor: root.focusSection === "button" && root.selectedIndex === 0
onHovered: function(h) { Column {
if (h) { root.focusSection = "button"; root.selectedIndex = 0 } spacing: 6
Button {
anchors.horizontalCenter: parent.horizontalCenter
text: "DHCP"
tooltipText: "Use DNS from DHCP"
hasCursor: root.focusSection === "button" && root.selectedIndex === 0
onHovered: function(h) {
if (h) { root.focusSection = "button"; root.selectedIndex = 0 }
}
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "idle"
color: Qt.darker(root.foreground, 1.5)
font.family: root.fontFamily
font.pixelSize: Style.font.caption
} }
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
} }
Button { Column {
text: "Cloudflare" spacing: 6
tooltipText: "Set DNS to Cloudflare" Button {
active: true anchors.horizontalCenter: parent.horizontalCenter
hasCursor: root.focusSection === "button" && root.selectedIndex === 1 text: "Cloudflare"
onHovered: function(h) { tooltipText: "Set DNS to Cloudflare"
if (h) { root.focusSection = "button"; root.selectedIndex = 1 } active: true
hasCursor: root.focusSection === "button" && root.selectedIndex === 1
onHovered: function(h) {
if (h) { root.focusSection = "button"; root.selectedIndex = 1 }
}
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "active"
color: Qt.darker(root.foreground, 1.5)
font.family: root.fontFamily
font.pixelSize: Style.font.caption
} }
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
} }
Button { Column {
iconText: "󰑐" spacing: 6
tooltipText: "Refresh" Button {
horizontalPadding: 8 anchors.horizontalCenter: parent.horizontalCenter
verticalPadding: 4 iconText: "󰑐"
hasCursor: root.focusSection === "button" && root.selectedIndex === 2 tooltipText: "Refresh"
onHovered: function(h) { horizontalPadding: 8
if (h) { root.focusSection = "button"; root.selectedIndex = 2 } verticalPadding: 4
hasCursor: root.focusSection === "button" && root.selectedIndex === 2
onHovered: function(h) {
if (h) { root.focusSection = "button"; root.selectedIndex = 2 }
}
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "icon only"
color: Qt.darker(root.foreground, 1.5)
font.family: root.fontFamily
font.pixelSize: Style.font.caption
} }
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
} }
Button { Column {
iconText: "󰂯" spacing: 6
text: "On" Button {
tooltipText: "Turn Bluetooth off" anchors.horizontalCenter: parent.horizontalCenter
active: true iconText: "󰂯"
hasCursor: root.focusSection === "button" && root.selectedIndex === 3 text: "On"
onHovered: function(h) { tooltipText: "Turn Bluetooth off"
if (h) { root.focusSection = "button"; root.selectedIndex = 3 } active: true
hasCursor: root.focusSection === "button" && root.selectedIndex === 3
onHovered: function(h) {
if (h) { root.focusSection = "button"; root.selectedIndex = 3 }
}
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "icon + active"
color: Qt.darker(root.foreground, 1.5)
font.family: root.fontFamily
font.pixelSize: Style.font.caption
} }
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
} }
Button { Column {
text: "Apply" spacing: 6
focusable: true Button {
bordered: true anchors.horizontalCenter: parent.horizontalCenter
hasCursor: root.focusSection === "button" && root.selectedIndex === 4 text: "Apply"
onHovered: function(h) { focusable: true
if (h) { root.focusSection = "button"; root.selectedIndex = 4 } bordered: true
hasCursor: root.focusSection === "button" && root.selectedIndex === 4
onHovered: function(h) {
if (h) { root.focusSection = "button"; root.selectedIndex = 4 }
}
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "bordered + focusable"
color: Qt.darker(root.foreground, 1.5)
font.family: root.fontFamily
font.pixelSize: Style.font.caption
} }
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
} }
} }
} }
@@ -896,8 +955,11 @@ Item {
// ---- ButtonGroup ------------------------------------------------- // ---- ButtonGroup -------------------------------------------------
Column { Column {
id: buttonGroupSection
width: parent.width width: parent.width
spacing: 8 spacing: 8
readonly property bool focused: root.focusSection === "button-group"
onFocusedChanged: if (focused) root.ensureCursorVisible(this)
Text { Text {
text: "ButtonGroup" text: "ButtonGroup"
@@ -930,10 +992,17 @@ Item {
anchors.leftMargin: 14 anchors.leftMargin: 14
options: ["top", "right", "bottom", "left"] options: ["top", "right", "bottom", "left"]
value: root.choiceDemoValue value: root.choiceDemoValue
cursorIndex: root.focusSection === "button-group" ? root.selectedIndex : -1
onChanged: function(v) { onChanged: function(v) {
root.focusSection = "button-group" root.focusSection = "button-group"
root.choiceDemoValue = v root.choiceDemoValue = v
} }
onHovered: function(index, isHovered) {
if (isHovered) {
root.focusSection = "button-group"
root.selectedIndex = index
}
}
} }
} }
} }