Add qs.Ui.Dropdown, SearchableDropdown, Toggle rounded variant

This commit is contained in:
Ryan Hughes
2026-05-18 00:52:59 -04:00
parent 4b4df8b4f5
commit db4fa1f760
6 changed files with 731 additions and 29 deletions
@@ -0,0 +1,210 @@
import QtQuick
import QtQuick.Controls
import qs.Commons
// Themed single-select dropdown. Trigger row paints with the kit's focus
// chrome; the popup anchors below and uses Color.popups.background +
// Color.popups.border so it reads as a panel surface rather than the
// platform-native ComboBox look.
//
// `options` accepts either a plain string[] or an array of
// { value, label } objects (label is what we render; value is what we
// emit). Mixing is fine — each row is interpreted independently.
//
// Keyboard: Tab to focus the trigger, Enter/Space opens, Esc closes,
// j/k or Up/Down walks options inside the open popup, Enter selects.
// A sibling SearchableDropdown reuses the same visuals but adds an
// embedded filter input — keep the two separate so each stays simple.
Item {
id: root
property string label: ""
property string value: ""
property var options: []
property color foreground: Color.foreground
property color background: Color.popups.background
property color popupBorder: Color.popups.border
property color accent: Color.accent
property string fontFamily: "JetBrainsMono Nerd Font"
property int rowHeight: 28
property int popupRowHeight: 28
property bool showLabel: true
signal changed(string value)
function optionValue(o) {
return (o && typeof o === "object") ? String(o.value) : String(o)
}
function optionLabel(o) {
return (o && typeof o === "object") ? String(o.label) : String(o)
}
function currentLabel() {
for (var i = 0; i < options.length; i++) {
if (optionValue(options[i]) === value) return optionLabel(options[i])
}
return value
}
implicitWidth: 240
implicitHeight: showLabel && label !== "" ? rowHeight + 18 : rowHeight
Column {
anchors.fill: parent
spacing: 4
Text {
visible: root.showLabel && root.label !== ""
text: root.label
color: Qt.darker(root.foreground, 1.4)
font.family: root.fontFamily
font.pixelSize: 10
font.bold: true
}
Rectangle {
id: trigger
width: parent.width
height: root.rowHeight
radius: Style.cornerRadius
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b,
trigger.activeFocus ? 0.08 : 0.04)
border.color: trigger.activeFocus
? Style.focusBorderColor
: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.4)
border.width: trigger.activeFocus ? Style.focusBorderWidth : 1
activeFocusOnTab: true
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter
|| event.key === Qt.Key_Space || event.key === Qt.Key_Down) {
popup.opened ? popup.close() : popup.open()
event.accepted = true
} else if (event.key === Qt.Key_Escape && popup.opened) {
popup.close(); event.accepted = true
}
}
Text {
anchors.left: parent.left
anchors.right: chevron.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 10
anchors.rightMargin: 6
text: root.currentLabel()
color: root.foreground
font.family: root.fontFamily
font.pixelSize: 12
elide: Text.ElideRight
}
Text {
id: chevron
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: 8
text: "󰅀"
color: Qt.darker(root.foreground, 1.2)
font.family: root.fontFamily
font.pixelSize: 12
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
trigger.forceActiveFocus()
popup.opened ? popup.close() : popup.open()
}
}
Popup {
id: popup
x: 0
y: trigger.height + 2
width: trigger.width
implicitHeight: Math.min(root.options.length * root.popupRowHeight + 2,
root.popupRowHeight * 8 + 2)
padding: 1
focus: true
background: Rectangle {
color: root.background
border.color: root.popupBorder
border.width: 1
radius: Style.cornerRadius
}
onOpened: optionList.currentIndex = Math.max(0, optionList.indexOfValue(root.value))
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) { popup.close(); event.accepted = true }
else if (event.key === Qt.Key_Down || event.text === "j") {
optionList.currentIndex = Math.min(root.options.length - 1, optionList.currentIndex + 1)
event.accepted = true
} else if (event.key === Qt.Key_Up || event.text === "k") {
optionList.currentIndex = Math.max(0, optionList.currentIndex - 1)
event.accepted = true
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
optionList.selectCurrent(); event.accepted = true
}
}
contentItem: ListView {
id: optionList
implicitHeight: contentHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
model: root.options
currentIndex: -1
function indexOfValue(v) {
for (var i = 0; i < root.options.length; i++)
if (root.optionValue(root.options[i]) === v) return i
return -1
}
function selectCurrent() {
if (currentIndex < 0 || currentIndex >= root.options.length) return
var v = root.optionValue(root.options[currentIndex])
root.value = v
root.changed(v)
popup.close()
}
delegate: Rectangle {
required property var modelData
required property int index
width: optionList.width
height: root.popupRowHeight
color: index === optionList.currentIndex
? Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.14)
: "transparent"
Text {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 10
anchors.rightMargin: 10
text: root.optionLabel(modelData)
color: index === optionList.currentIndex ? root.accent : root.foreground
font.family: root.fontFamily
font.pixelSize: 12
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPositionChanged: optionList.currentIndex = parent.index
onClicked: optionList.selectCurrent()
}
}
}
}
}
}
}
@@ -56,8 +56,10 @@ Grouped by what they're for, not alphabetically.
| `PillButton` | Rounded button with optional icon + label + tooltip. Has `active`, `hasCursor` (keyboard cursor), `focusable` (Tab-focus with accent ring), `bordered` (persistent 1px idle border for primary form buttons), and `enabled`. Hover and keyboard cursor render identically (fill + border) via the shared `hot` state; Tab-focus uses an accent ring that wins over both. |
| `CursorPill` | `PillButton` that participates in a panel's single-cursor model. Adds a `hovered(bool)` signal so the panel can update its cursor state on mouse enter/leave. Use for DNS-pill / header-pill / segmented-choice patterns. |
| `ChoiceButton` | A single button in a mutually-exclusive choice group (segmented control). `selected` uses accent fill+border; focus uses `Style.focusBorderColor` so keyboard nav reads differently from selection. |
| `Toggle` | Title + description + switch. Click anywhere on the row to flip; caller updates `checked` in response. |
| `Toggle` | Title + description + switch. Click anywhere on the row to flip; caller updates `checked` in response. `rounded` auto-detects from `Style.cornerRadius` so the switch is a pill on round-corners themes and square on sharp; override per-instance to force one or the other. |
| `TextField` | Single-line input. Inherits from Qt Quick Controls `TextField` so all of its base API (text, placeholderText, accepted, validator, ...) is available. Adds `password: bool`, `foreground` / `accent` / `selectionTint` color overrides, and `horizontalPadding` / `verticalPadding` size knobs. Focus styling uses `Style.focusBorderColor` to match `Toggle` and `ChoiceButton`. |
| `Dropdown` | Single-select dropdown with a themed popup (no platform-native ComboBox chrome). `options` accepts `string[]` or `[{ value, label }]`. Keyboard: Tab to focus trigger, Enter/Space opens, j/k or arrows walk options, Enter selects. |
| `SearchableDropdown` | `Dropdown` with an embedded search field at the top of the popup that filters options as you type. Use when the option count is high enough that scanning is friction (e.g. bar settings "+ Add widget"). Options can also carry a `description` string that the filter matches against. |
| `PanelActionButton` | 22×22 right-edge action button (confirm, forget, unpair). `hoverColor` swaps between default foreground tint and urgent (red) tint. `focusable: true` enables Tab-focus with an accent ring — used for the bar settings widget-card row controls. |
| `PanelSlider` | Volume/progress slider. Drag, click track, or wheel. `moved(value)` fires per change, `released(value)` once at end. (Named to avoid colliding with `QtQuick.Controls.Slider`.) |
| `WidgetButton` | Bar widget chrome — for the strip itself, not for inside panels. |
@@ -0,0 +1,317 @@
import QtQuick
import QtQuick.Controls
import qs.Commons
// Searchable single-select dropdown. Same trigger shape as Dropdown, but
// the popup leads with an embedded TextField that filters the option
// list in real time. Use for pickers with enough options that scanning
// is friction (e.g. bar settings "+ Add widget").
//
// Filtering is case-insensitive substring against each option's label.
// Options can be string[] or [{ value, label, description? }] — the same
// shape Dropdown accepts. The filter clears whenever the popup closes.
//
// Keyboard: Tab to focus the trigger, Enter/Space opens (search focused
// immediately). Down arrow from the search jumps to the first match;
// Up from the first match returns to the search. Enter selects, Esc
// closes (and clears the filter).
Item {
id: root
property string label: ""
property string value: ""
property var options: []
property string placeholderText: "Search..."
property string emptyText: "No matches"
property color foreground: Color.foreground
property color background: Color.popups.background
property color popupBorder: Color.popups.border
property color accent: Color.accent
property string fontFamily: "JetBrainsMono Nerd Font"
property int rowHeight: 28
property int popupRowHeight: 28
property int popupMinHeight: 220
property bool showLabel: true
signal changed(string value)
function optionValue(o) {
return (o && typeof o === "object") ? String(o.value) : String(o)
}
function optionLabel(o) {
return (o && typeof o === "object") ? String(o.label) : String(o)
}
function optionDescription(o) {
return (o && typeof o === "object" && o.description) ? String(o.description) : ""
}
function currentLabel() {
for (var i = 0; i < options.length; i++) {
if (optionValue(options[i]) === value) return optionLabel(options[i])
}
return value
}
property var filtered: options
function recomputeFiltered() {
var q = searchField.text.toLowerCase()
if (!q) { filtered = options; return }
var out = []
for (var i = 0; i < options.length; i++) {
var lbl = optionLabel(options[i]).toLowerCase()
var desc = optionDescription(options[i]).toLowerCase()
if (lbl.indexOf(q) !== -1 || desc.indexOf(q) !== -1) out.push(options[i])
}
filtered = out
}
onOptionsChanged: recomputeFiltered()
implicitWidth: 260
implicitHeight: showLabel && label !== "" ? rowHeight + 18 : rowHeight
Column {
anchors.fill: parent
spacing: 4
Text {
visible: root.showLabel && root.label !== ""
text: root.label
color: Qt.darker(root.foreground, 1.4)
font.family: root.fontFamily
font.pixelSize: 10
font.bold: true
}
Rectangle {
id: trigger
width: parent.width
height: root.rowHeight
radius: Style.cornerRadius
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b,
trigger.activeFocus ? 0.08 : 0.04)
border.color: trigger.activeFocus
? Style.focusBorderColor
: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.4)
border.width: trigger.activeFocus ? Style.focusBorderWidth : 1
activeFocusOnTab: true
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter
|| event.key === Qt.Key_Space || event.key === Qt.Key_Down) {
popup.opened ? popup.close() : popup.open()
event.accepted = true
} else if (event.key === Qt.Key_Escape && popup.opened) {
popup.close(); event.accepted = true
}
}
Text {
anchors.left: parent.left
anchors.right: chevron.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 10
anchors.rightMargin: 6
text: root.currentLabel() || root.placeholderText
color: root.currentLabel() ? root.foreground : Qt.darker(root.foreground, 1.5)
font.family: root.fontFamily
font.pixelSize: 12
elide: Text.ElideRight
}
Text {
id: chevron
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: 8
text: "󰅀"
color: Qt.darker(root.foreground, 1.2)
font.family: root.fontFamily
font.pixelSize: 12
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
trigger.forceActiveFocus()
popup.opened ? popup.close() : popup.open()
}
}
Popup {
id: popup
x: 0
y: trigger.height + 2
width: trigger.width
implicitHeight: Math.max(root.popupMinHeight,
Math.min(root.filtered.length * root.popupRowHeight + 50,
root.popupRowHeight * 6 + 50))
padding: 1
focus: true
background: Rectangle {
color: root.background
border.color: root.popupBorder
border.width: 1
radius: Style.cornerRadius
}
onOpened: {
searchField.text = ""
root.recomputeFiltered()
Qt.callLater(function() { searchField.forceActiveFocus() })
}
onClosed: searchField.text = ""
contentItem: Column {
spacing: 0
Item {
width: parent.width
height: 38
TextField {
id: searchField
anchors.fill: parent
anchors.margins: 6
placeholderText: root.placeholderText
foreground: root.foreground
accent: root.accent
font.family: root.fontFamily
font.pixelSize: 12
onTextChanged: {
root.recomputeFiltered()
if (resultList.count > 0) resultList.currentIndex = 0
}
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
popup.close(); event.accepted = true
} else if (event.key === Qt.Key_Down) {
if (resultList.count > 0) {
resultList.currentIndex = 0
resultList.forceActiveFocus()
}
event.accepted = true
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
if (resultList.count > 0) {
resultList.currentIndex = 0
resultList.selectCurrent()
}
event.accepted = true
}
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
}
Item {
width: parent.width
height: popup.height - 38 - 2 - 1
Text {
anchors.centerIn: parent
visible: resultList.count === 0
text: root.emptyText
color: Qt.darker(root.foreground, 1.6)
font.family: root.fontFamily
font.pixelSize: 12
}
ListView {
id: resultList
anchors.fill: parent
clip: true
boundsBehavior: Flickable.StopAtBounds
model: root.filtered
currentIndex: -1
keyNavigationEnabled: false
function selectCurrent() {
if (currentIndex < 0 || currentIndex >= root.filtered.length) return
var v = root.optionValue(root.filtered[currentIndex])
root.value = v
root.changed(v)
popup.close()
}
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
popup.close(); event.accepted = true
} else if (event.key === Qt.Key_Down || event.text === "j") {
if (resultList.currentIndex >= resultList.count - 1) {
event.accepted = true; return
}
resultList.currentIndex = resultList.currentIndex + 1
event.accepted = true
} else if (event.key === Qt.Key_Up || event.text === "k") {
if (resultList.currentIndex <= 0) {
searchField.forceActiveFocus()
event.accepted = true; return
}
resultList.currentIndex = resultList.currentIndex - 1
event.accepted = true
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
resultList.selectCurrent(); event.accepted = true
}
}
delegate: Rectangle {
required property var modelData
required property int index
width: resultList.width
height: root.popupRowHeight
color: index === resultList.currentIndex
? Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.14)
: "transparent"
Column {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 10
anchors.rightMargin: 10
spacing: 1
Text {
text: root.optionLabel(modelData)
color: index === resultList.currentIndex ? root.accent : root.foreground
font.family: root.fontFamily
font.pixelSize: 12
elide: Text.ElideRight
width: parent.width
}
Text {
visible: text !== ""
text: root.optionDescription(modelData)
color: Qt.darker(root.foreground, 1.5)
font.family: root.fontFamily
font.pixelSize: 10
elide: Text.ElideRight
width: parent.width
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPositionChanged: resultList.currentIndex = parent.index
onClicked: resultList.selectCurrent()
}
}
}
}
}
}
}
}
}
+10 -2
View File
@@ -9,6 +9,10 @@ import qs.Commons
// Focus styling follows the shared Style tokens (accent border + tinted
// fill on activeFocus) so keyboard nav looks the same here as on
// ChoiceButton and other focusable Ui components.
//
// `rounded` auto-detects from Style.cornerRadius so the switch follows
// the theme: pill shape on round-corners themes, square on sharp.
// Callers can override per-instance.
Rectangle {
id: root
@@ -22,6 +26,10 @@ Rectangle {
// border + tinted fill via Style tokens) so cursor and Tab focus read the same.
property bool hasCursor: false
// Switch shape follows the theme by default: pill on round, square on sharp.
// Override per-instance if a caller wants the opposite.
property bool rounded: Style.cornerRadius > 0
property color foreground: Color.foreground
property color accent: Color.accent
property string fontFamily: "monospace"
@@ -89,7 +97,7 @@ Rectangle {
id: track
width: 42
height: 22
radius: height / 2
radius: root.rounded ? height / 2 : 0
color: root.checked
? Qt.rgba(root.accent.r, root.accent.g, root.accent.b, 0.35)
: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.12)
@@ -104,7 +112,7 @@ Rectangle {
Rectangle {
width: 16
height: 16
radius: 8
radius: root.rounded ? 8 : 0
x: root.checked ? track.width - width - 3 : 3
y: 3
color: root.checked ? root.accent : Qt.darker(root.foreground, 1.25)
@@ -3,6 +3,7 @@ module qs.Ui
ChoiceButton 1.0 ChoiceButton.qml
CursorPill 1.0 CursorPill.qml
CursorSurface 1.0 CursorSurface.qml
Dropdown 1.0 Dropdown.qml
KeyboardPanel 1.0 KeyboardPanel.qml
PanelActionButton 1.0 PanelActionButton.qml
PanelKeyCatcher 1.0 PanelKeyCatcher.qml
@@ -12,6 +13,7 @@ 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
Toggle 1.0 Toggle.qml
WidgetButton 1.0 WidgetButton.qml
@@ -21,7 +21,7 @@ Item {
function open(payloadJson) {
closingFromHost = false
window.visible = true
Qt.callLater(function() { if (scrollArea) scrollArea.forceActiveFocus() })
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
}
// Host-initiated close (`shell hide`). Visibility flips without
@@ -68,6 +68,9 @@ Item {
property int pillDemoIndex: 1
property string choiceDemoValue: "top"
property bool toggleDemoOn: true
property bool toggleSquareOn: false
property string dropdownDemoValue: "calendar"
property string searchableDemoValue: ""
FloatingWindow {
id: window
@@ -86,29 +89,59 @@ Item {
id: focusScope
anchors.fill: parent
focus: true
// Esc + h/l for the cursor demo. Other keys (arrow keys, Page_Down,
// Home/End) propagate down to ScrollView's built-in scroll handling so
// keyboard scrolling works. AfterItem priority means a focused inner
// control would get its keys first — we don't have any here yet.
// Scroll the gallery's ScrollView by `dy` pixels. Bound below 0 and
// above (1 - thumbSize) so the thumb stays on the track when the
// user mashes Page_Down past the end.
function scrollBy(dy) {
var sb = scrollArea.ScrollBar.vertical
if (!sb || scrollArea.contentHeight <= scrollArea.height) return
var newPos = sb.position + dy / scrollArea.contentHeight
sb.position = Math.max(0, Math.min(1 - sb.size, newPos))
}
// Page/Home/End handled here so they bubble up past keyCatcher
// (which only consumes Esc / Enter / j-k-h-l / x / text keys).
Keys.priority: Keys.AfterItem
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
root.requestClose(); event.accepted = true
} else if (event.text === "l") {
root.cursorDemoIndex = Math.min(2, root.cursorDemoIndex + 1)
if (event.key === Qt.Key_PageDown) {
focusScope.scrollBy(300); event.accepted = true
} else if (event.key === Qt.Key_PageUp) {
focusScope.scrollBy(-300); event.accepted = true
} else if (event.key === Qt.Key_Home) {
scrollArea.ScrollBar.vertical.position = 0
event.accepted = true
} else if (event.text === "h") {
root.cursorDemoIndex = Math.max(0, root.cursorDemoIndex - 1)
} else if (event.key === Qt.Key_End) {
var sb = scrollArea.ScrollBar.vertical
if (sb) sb.position = Math.max(0, 1 - sb.size)
event.accepted = true
}
}
ScrollView {
id: scrollArea
// Panel-style key dispatch. j/k scroll the gallery; h/l walk the
// cursor / pill demos; Esc closes. Same component the wifi, audio,
// bluetooth, monitor, and settings panels all use — the gallery
// is meant to demonstrate the standard, so it should USE the
// standard rather than reimplement its own keyhandler.
PanelKeyCatcher {
id: keyCatcher
anchors.fill: parent
anchors.margins: 18
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
onMoveRequested: function(dx, dy) {
if (dy !== 0) {
focusScope.scrollBy(dy * 60)
} else if (dx !== 0) {
root.cursorDemoIndex = Math.max(0, Math.min(2, root.cursorDemoIndex + dx))
root.pillDemoIndex = Math.max(0, Math.min(3, root.pillDemoIndex + dx))
}
}
onCloseRequested: root.requestClose()
ScrollView {
id: scrollArea
anchors.fill: parent
anchors.margins: 18
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
Column {
width: scrollArea.availableWidth
@@ -161,7 +194,7 @@ Item {
width: parent.width
implicitHeight: shCol.implicitHeight + 24
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
radius: 6
radius: Style.cornerRadius
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
border.width: 1
@@ -217,7 +250,7 @@ Item {
width: parent.width
implicitHeight: sepCol.implicitHeight + 24
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
radius: 6
radius: Style.cornerRadius
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
border.width: 1
@@ -262,7 +295,7 @@ Item {
width: parent.width
implicitHeight: csCol.implicitHeight + 24
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
radius: 6
radius: Style.cornerRadius
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
border.width: 1
@@ -343,7 +376,7 @@ Item {
width: parent.width
implicitHeight: pillCol.implicitHeight + 24
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
radius: 6
radius: Style.cornerRadius
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
border.width: 1
@@ -431,7 +464,7 @@ Item {
width: parent.width
implicitHeight: cpRow.implicitHeight + 24
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
radius: 6
radius: Style.cornerRadius
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
border.width: 1
@@ -488,7 +521,7 @@ Item {
width: parent.width
implicitHeight: pabCol.implicitHeight + 24
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
radius: 6
radius: Style.cornerRadius
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
border.width: 1
@@ -619,7 +652,7 @@ Item {
width: parent.width
implicitHeight: sliderRow.implicitHeight + 24
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
radius: 6
radius: Style.cornerRadius
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
border.width: 1
@@ -690,7 +723,7 @@ Item {
width: parent.width
implicitHeight: choiceRow.implicitHeight + 24
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
radius: 6
radius: Style.cornerRadius
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
border.width: 1
@@ -743,7 +776,7 @@ Item {
width: parent.width
implicitHeight: tfCol.implicitHeight + 24
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
radius: 6
radius: Style.cornerRadius
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
border.width: 1
@@ -809,6 +842,135 @@ Item {
checked: root.toggleDemoOn
onClicked: root.toggleDemoOn = !root.toggleDemoOn
}
Toggle {
width: parent.width
label: "Square switch (forced)"
description: "`rounded: false` overrides the theme auto-detect so the switch reads square even when corners are round. Set `rounded: Style.cornerRadius > 0` (the default) to follow the theme."
foreground: root.foreground
accent: root.accent
fontFamily: root.fontFamily
rounded: false
checked: root.toggleSquareOn
onClicked: root.toggleSquareOn = !root.toggleSquareOn
}
}
// ---- Dropdown -----------------------------------------------------
Column {
width: parent.width
spacing: 8
Text {
text: "Dropdown"
color: root.foreground
font.family: root.fontFamily
font.pixelSize: 13
font.bold: true
}
Text {
text: "Themed single-select with a panel-styled popup. Tab to focus the trigger, Enter/Space opens, j/k or arrows walk options, Enter selects. Options can be plain strings or { value, label } objects."
color: Qt.darker(root.foreground, 1.5)
font.family: root.fontFamily
font.pixelSize: 10
width: parent.width
wrapMode: Text.WordWrap
}
Rectangle {
width: parent.width
implicitHeight: ddCol.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
Column {
id: ddCol
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 14
anchors.rightMargin: 14
spacing: 6
Dropdown {
width: 260
label: "Center anchor"
fontFamily: root.fontFamily
options: ["calendar", "weather", "clock", "battery"]
value: root.dropdownDemoValue
onChanged: function(v) { root.dropdownDemoValue = v }
}
}
}
}
// ---- SearchableDropdown -------------------------------------------
Column {
width: parent.width
spacing: 8
Text {
text: "SearchableDropdown"
color: root.foreground
font.family: root.fontFamily
font.pixelSize: 13
font.bold: true
}
Text {
text: "Dropdown with an embedded filter input. Type to narrow the list, Down to jump from the search to the first match, Enter to select. Use this for the bar settings \"+ Add widget\" picker and any other long-list selector."
color: Qt.darker(root.foreground, 1.5)
font.family: root.fontFamily
font.pixelSize: 10
width: parent.width
wrapMode: Text.WordWrap
}
Rectangle {
width: parent.width
implicitHeight: sddCol.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
Column {
id: sddCol
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 14
anchors.rightMargin: 14
spacing: 6
SearchableDropdown {
width: 280
label: "Add widget"
fontFamily: root.fontFamily
placeholderText: "Search widgets..."
options: [
{ value: "clock", label: "Clock", description: "Time + date display" },
{ value: "weather", label: "Weather", description: "Local conditions and forecast" },
{ value: "battery", label: "Battery", description: "Charge level + power profile" },
{ value: "audio", label: "Audio", description: "Output sink + volume" },
{ value: "network", label: "Network", description: "Wi-Fi + ethernet status" },
{ value: "bluetooth", label: "Bluetooth", description: "Paired and nearby devices" },
{ value: "monitor", label: "Monitor", description: "Brightness + scale" },
{ value: "calendar", label: "Calendar", description: "Month grid flyout" },
{ value: "media", label: "Media", description: "Now-playing + transport" },
{ value: "workspaces", label: "Workspaces", description: "Hyprland workspace pills" },
{ value: "system-tray", label: "System tray", description: "StatusNotifierItem icons" },
{ value: "omarchy-menu", label: "Omarchy menu", description: "Launcher / system menu" },
{ value: "power-profiles", label: "Power profiles", description: "Performance / balanced / saver" },
{ value: "hardware", label: "Hardware", description: "CPU, GPU, mem utilization" },
{ value: "notifications", label: "Notifications", description: "Recent notification history" }
]
value: root.searchableDemoValue
onChanged: function(v) { root.searchableDemoValue = v }
}
}
}
}
// ---- Composed example -------------------------------------------
@@ -836,7 +998,7 @@ Item {
width: parent.width
implicitHeight: composedCol.implicitHeight + 24
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
radius: 6
radius: Style.cornerRadius
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
border.width: 1
@@ -965,6 +1127,7 @@ Item {
Item { width: 1; height: 12 }
}
}
}
}
}
}