Standardize shell UI theme tokens

This commit is contained in:
Ryan Hughes
2026-05-18 20:13:12 -04:00
parent 44dff2d23d
commit 8b2bb217d6
43 changed files with 1622 additions and 873 deletions
+36 -36
View File
@@ -6,11 +6,11 @@ import qs.Commons
// 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`
// activeFocus (Tab focus) focus fill + focus border token
// hasCursor || hover hover-cursor fill (+ border if `bordered`)
// selected selected fill + optional selected border
// active selected fill
// idle transparent or normal border if `bordered`
//
// All fills/borders come from `qs.Commons.Style` tokens, so themes
// control the look via [style] in shell.toml.
@@ -41,8 +41,8 @@ Rectangle {
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 real horizontalPadding: Style.spacing.controlPaddingX
property real verticalPadding: Style.spacing.controlPaddingY
property bool leftAlign: false
// Tooltip palette. Auto-rendered if tooltipText is set.
@@ -64,32 +64,32 @@ Rectangle {
readonly property bool hot: mouseArea.containsMouse || hasCursor
readonly property bool _showFocusRing: focusable && activeFocus
readonly property color _selectedColor: Style.selectedStateColor(root.foreground, root.accent)
color: mouseArea.pressed ? Style.pressedFill
: _showFocusRing ? Style.focusFillColor
: selected ? Style.selectedAccentFill
: hot ? Style.hotFill
: active ? Style.selectedFill
color: mouseArea.pressed ? Style.pressedFillFor(root.foreground, root.accent)
: _showFocusRing ? Style.focusFillFor(root.foreground, root.accent)
: hot ? Style.hoverFillFor(root.foreground, root.accent)
: selected ? Style.selectedFillFor(root.foreground, root.accent)
: active ? Style.selectedFillFor(root.foreground, root.accent)
: 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
: selected ? accent
: (bordered && hot) ? Style.focusBorderColor
: bordered ? foreground
: Style.idleBorderColor
// Border follows the same state precedence as fill. Buttons stay
// borderless at rest unless `bordered` is set, but hover-cursor/focus
// always use the shared cursor border so the keyboard target is visible
// and consistent with the rest of the kit. Selected borders are off by
// default for plain buttons; explicitly bordered buttons keep their
// normal border when selected unless selected-border-width opts in to a
// dedicated selected border.
border.color: _showFocusRing ? Style.focusBorderFor(root.foreground, root.accent)
: hot ? Style.hoverBorderFor(root.foreground, root.accent)
: selected ? (Style.selectedBorderWidth > 0 ? Style.selectedBorderFor(root.foreground, root.accent) : Style.normalBorderFor(root.foreground, root.accent))
: bordered ? Style.normalBorderFor(root.foreground, root.accent)
: "transparent"
// 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
: selected ? (hot ? Style.focusBorderWidth : Math.max(Style.borderWidth, 2))
: (bordered && hot) ? Style.focusBorderWidth
: bordered ? Style.borderWidth
: hot ? Style.hoverBorderWidth
: selected ? (Style.selectedBorderWidth > 0 ? Style.selectedBorderWidth : (bordered ? Style.normalBorderWidth : 0))
: bordered ? Style.normalBorderWidth
: 0
Behavior on color { ColorAnimation { duration: 120 } }
@@ -102,7 +102,7 @@ Rectangle {
background: Rectangle {
color: root.tooltipBackground
border.color: root.tooltipForeground
border.width: 1
border.width: Math.max(1, Style.normalBorderWidth)
radius: 0
opacity: 0.97
}
@@ -111,10 +111,10 @@ Rectangle {
color: root.tooltipForeground
font.family: root.fontFamily
font.pixelSize: Style.font.bodySmall
leftPadding: 10
rightPadding: 10
topPadding: 6
bottomPadding: 6
leftPadding: Style.spacing.controlPaddingX
rightPadding: Style.spacing.controlPaddingX
topPadding: Style.spacing.controlPaddingY
bottomPadding: Style.spacing.controlPaddingY
}
}
@@ -124,12 +124,12 @@ Rectangle {
anchors.left: root.leftAlign ? parent.left : undefined
anchors.leftMargin: root.leftAlign ? root.horizontalPadding : 0
anchors.horizontalCenter: root.leftAlign ? undefined : parent.horizontalCenter
spacing: 8
spacing: Style.spacing.controlGap
Text {
visible: root.iconText !== ""
text: root.iconText
color: root.selected ? root.accent : root.foreground
color: root.selected ? root._selectedColor : root.foreground
font.family: root.fontFamily
font.pixelSize: root.iconSize
rotation: root.iconRotation
@@ -140,7 +140,7 @@ Rectangle {
Text {
visible: root.text !== ""
text: root.text
color: root.selected ? root.accent : root.foreground
color: root.selected ? root._selectedColor : root.foreground
font.family: root.fontFamily
font.pixelSize: root.fontSize
font.bold: root.selected
+70 -12
View File
@@ -8,10 +8,18 @@ import qs.Commons
// `options` is either a plain string[] (label == value) or an array of
// { value, label, icon?, tooltip? } objects. Mixing is fine.
//
// Panels with their own keyboard cursor model bind `cursorIndex` to the
// currently-focused option (-1 = no cursor) and listen on `hovered` to
// keep that state synced with the mouse. Forms that don't care about
// the panel cursor model can leave both alone.
// Keyboard navigation. The group itself is a single Tab stop, not one
// stop per chip — so in a form that walks `activeFocusOnTab` items with
// Tab / j / k, the cursor enters the group as a unit. Once focused,
// h / l / Left / Right walks between chips and Enter / Space activates
// the current one. The selected chip is the default landing point so
// users see their existing choice on arrival.
//
// Panel-cursor consumers (the bar widget panels) drive `cursorIndex`
// directly and listen on `hovered` to sync the mouse — Tab focus and
// `cursorIndex` are independent; either one paints the chip's hot
// state, and the bar widget panels never give Tab focus to a
// ButtonGroup so they only see the cursorIndex path.
Row {
id: root
@@ -22,16 +30,24 @@ Row {
property color accent: Color.accent
property string fontFamily: Style.font.family
property real fontSize: Style.font.body
property bool focusable: false
property bool focusable: true
// -1 disables the cursor highlight (the form case). Set from a panel
// to drive Button.hasCursor on the matching index.
// -1 disables the external cursor highlight (the panel-cursor case).
// Driven by a containing panel; the group's own Tab-focus h/l
// tracking is internal and lives in _focusedIndex.
property int cursorIndex: -1
// Internal: which chip h / l / Left / Right is currently sitting on
// when the group itself has Tab focus. Reset to the selected option
// each time focus arrives so the user sees their existing choice.
property int _focusedIndex: -1
signal changed(string value)
signal hovered(int index, bool isHovered)
spacing: 6
spacing: Style.spacing.md
activeFocusOnTab: focusable
function optionValue(o) {
return (o && typeof o === "object") ? String(o.value) : String(o)
@@ -46,6 +62,46 @@ Row {
return (o && typeof o === "object" && o.tooltip) ? String(o.tooltip) : ""
}
function selectedOptionIndex() {
for (var i = 0; i < options.length; i++)
if (optionValue(options[i]) === value) return i
return -1
}
function activateFocused() {
if (_focusedIndex < 0 || _focusedIndex >= options.length) return
var v = optionValue(options[_focusedIndex])
root.changed(v)
}
onActiveFocusChanged: {
if (activeFocus) {
var idx = selectedOptionIndex()
_focusedIndex = idx < 0 ? 0 : idx
} else {
_focusedIndex = -1
}
}
Keys.priority: Keys.BeforeItem
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Left || event.key === Qt.Key_H
|| event.text === "h") {
_focusedIndex = Math.max(0, (_focusedIndex < 0 ? 0 : _focusedIndex) - 1)
event.accepted = true
} else if (event.key === Qt.Key_Right || event.key === Qt.Key_L
|| event.text === "l") {
var max = options.length - 1
var next = (_focusedIndex < 0 ? 0 : _focusedIndex) + 1
_focusedIndex = Math.min(max, next)
event.accepted = true
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter
|| event.key === Qt.Key_Space) {
activateFocused()
event.accepted = true
}
}
Repeater {
model: root.options
@@ -56,17 +112,19 @@ Row {
iconText: root.optionIcon(modelData)
tooltipText: root.optionTooltip(modelData)
selected: root.optionValue(modelData) === root.value
// Chip lights up when either the external panel cursor lands here
// or the group has Tab focus and h/l has walked to this index.
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.
|| (root.activeFocus && root._focusedIndex === index)
// Every chip carries the standard bordered-button chrome so the
// group reads as a row of distinct options. selected / hover-cursor /
// focus are all painted by Button from Style's shared state tokens.
bordered: true
foreground: root.foreground
background: root.background
accent: root.accent
fontFamily: root.fontFamily
fontSize: root.fontSize
focusable: root.focusable
onClicked: root.changed(root.optionValue(modelData))
onHovered: function(h) { root.hovered(index, h) }
}
+21 -17
View File
@@ -7,35 +7,39 @@ import qs.Commons
// `hasCursor` / `current`. That's what guarantees a single highlight on
// screen at any time across both keyboard and mouse interaction.
//
// Two cursor visuals are supported:
//
// default (outline: false) — paint a tinted fill across the row when
// hasCursor is true. Use for narrow text rows (wifi networks, audio
// devices, menu items) where fill reads cleanly.
//
// outline: true — paint an accent border instead of a fill. Use for
// wide content rows where a fill would obscure the row's chrome
// (slider rows in audio / monitor panels). The fill / currentFill
// props are ignored in this mode.
// Cursor paint is always the shared hover-cursor fill plus optional
// hover-cursor border. `outline` remains as a compatibility flag for
// callers that used to request border-only rows, but slider rows still
// receive the same hover-cursor background as every other row.
Rectangle {
id: root
property bool hasCursor: false
property bool current: false
property bool outline: false
property bool bordered: false
property color foreground: Color.foreground
property color fill: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.08)
property color currentFill: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.18)
property color accent: Color.accent
property color fill: Style.hoverFillFor(foreground, accent)
property color currentFill: Style.selectedFillFor(foreground, accent)
radius: Style.cornerRadius
color: root.outline
? "transparent"
: (hasCursor ? fill : (current ? currentFill : "transparent"))
color: hasCursor ? fill : (current ? currentFill : "transparent")
border.color: root.outline && hasCursor ? Style.focusBorderColor : foreground
border.width: root.outline && hasCursor ? Style.focusBorderWidth : 0
border.color: root.hasCursor
? Style.hoverBorderFor(root.foreground, root.accent)
: (root.current
? Style.selectedBorderFor(root.foreground, root.accent)
: (root.bordered
? Style.normalBorderFor(root.foreground, root.accent)
: "transparent"))
border.width: root.hasCursor
? Style.hoverBorderWidth
: (root.current
? Style.selectedBorderWidth
: (root.bordered ? Style.normalBorderWidth : 0))
Behavior on color {
ColorAnimation { duration: 60 }
+35 -26
View File
@@ -27,12 +27,12 @@ Item {
property color popupBorder: Color.popups.border
property color accent: Color.accent
property string fontFamily: Style.font.family
property int rowHeight: 28
property int popupRowHeight: 28
property int rowHeight: Style.spacing.controlHeight
property int popupRowHeight: Style.spacing.popupRowHeight
property bool showLabel: true
// Panel-cursor flag. When true, the trigger renders the same focus ring
// as Tab-focus so a panel's keyboard cursor lands here identically.
// Panel-cursor flag. When true, the trigger renders the shared
// hover-cursor state. Active Qt focus defaults to the same visuals.
// Emits `hovered(bool)` on pointer enter/leave so the panel can keep
// its cursor state in sync with the mouse.
property bool hasCursor: false
@@ -62,12 +62,12 @@ Item {
return value
}
implicitWidth: 240
implicitHeight: showLabel && label !== "" ? rowHeight + 18 : rowHeight
implicitWidth: Style.spacing.dropdownWidth
implicitHeight: showLabel && label !== "" ? rowHeight + Style.spacing.huge : rowHeight
Column {
anchors.fill: parent
spacing: 4
spacing: Style.spacing.labelGap
Text {
visible: root.showLabel && root.label !== ""
@@ -84,18 +84,27 @@ Item {
height: root.rowHeight
radius: Style.cornerRadius
readonly property bool _focused: trigger.activeFocus || root.hasCursor
readonly property bool _focused: trigger.activeFocus
readonly property bool _hot: triggerHover.hovered || root.hasCursor
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b,
trigger._focused ? 0.08 : 0.04)
color: trigger._focused
? Style.focusFillFor(root.foreground, root.accent)
: (trigger._hot
? Style.hoverFillFor(root.foreground, root.accent)
: Style.normalFillFor(root.foreground, root.accent))
border.color: trigger._focused
? Style.focusBorderColor
: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.4)
border.width: trigger._focused ? Style.focusBorderWidth : 1
? Style.focusBorderFor(root.foreground, root.accent)
: (trigger._hot
? Style.hoverBorderFor(root.foreground, root.accent)
: Style.normalBorderFor(root.foreground, root.accent))
border.width: trigger._focused
? Style.focusBorderWidth
: (trigger._hot ? Style.hoverBorderWidth : Style.normalBorderWidth)
activeFocusOnTab: true
HoverHandler {
id: triggerHover
onHoveredChanged: root.hovered(hovered)
}
@@ -113,8 +122,8 @@ Item {
anchors.left: parent.left
anchors.right: chevron.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 10
anchors.rightMargin: 6
anchors.leftMargin: Style.spacing.controlPaddingX
anchors.rightMargin: Style.spacing.md
text: root.currentLabel()
color: root.foreground
font.family: root.fontFamily
@@ -126,7 +135,7 @@ Item {
id: chevron
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: 8
anchors.rightMargin: Style.spacing.controlGap
text: "󰅀"
color: Qt.darker(root.foreground, 1.2)
font.family: root.fontFamily
@@ -145,17 +154,17 @@ Item {
Popup {
id: popup
x: 0
y: trigger.height + 2
y: trigger.height + Style.spacing.xxs
width: trigger.width
implicitHeight: Math.min(root.options.length * root.popupRowHeight + Math.max(0, root.options.length - 1) * 4 + 2,
root.popupRowHeight * 8 + 7 * 4 + 2)
padding: 1
implicitHeight: Math.min(root.options.length * root.popupRowHeight + Math.max(0, root.options.length - 1) * Style.spacing.labelGap + Style.spacing.xxs,
root.popupRowHeight * 8 + 7 * Style.spacing.labelGap + Style.spacing.xxs)
padding: Style.spacing.hairline
focus: true
background: Rectangle {
color: root.background
border.color: root.popupBorder
border.width: 1
border.width: Style.normalBorderWidth
radius: Style.cornerRadius
}
@@ -166,7 +175,7 @@ Item {
contentItem: ListView {
id: optionList
spacing: 4
spacing: Style.spacing.labelGap
Keys.priority: Keys.BeforeItem
Keys.onPressed: function(event) {
@@ -207,17 +216,17 @@ Item {
width: optionList.width
height: root.popupRowHeight
color: index === optionList.currentIndex
? Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.14)
? Style.hoverFillFor(root.foreground, root.accent)
: "transparent"
Text {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 10
anchors.rightMargin: 10
anchors.leftMargin: Style.spacing.controlPaddingX
anchors.rightMargin: Style.spacing.controlPaddingX
text: root.optionLabel(modelData)
color: index === optionList.currentIndex ? root.accent : root.foreground
color: index === optionList.currentIndex ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground
font.family: root.fontFamily
font.pixelSize: Style.font.body
elide: Text.ElideRight
+30 -4
View File
@@ -38,12 +38,12 @@ PanelWindow {
required property Item anchorItem
required property QtObject bar
property var owner: null
property int margin: 10
property int padding: 14
property int margin: Style.gapsOut
property int padding: Style.spacing.popupPadding
property int contentWidth: 280
property int contentHeight: 200
property bool open: false
property int gap: 10 // distance between bar edge and panel
property int gap: Style.gapsOut // distance between bar edge and panel
default property alias contentItem: contentHolder.children
@@ -123,6 +123,32 @@ PanelWindow {
readonly property real anchorH: anchorItem ? anchorItem.height : 0
readonly property real screenW: screen ? screen.width : 0
readonly property real screenH: screen ? screen.height : 0
readonly property real availableCardWidth: screenW > 0
? Math.max(120, screenW - ((barPos === "left" || barPos === "right") ? barW + gap + margin : margin * 2))
: 0
readonly property real availableCardHeight: screenH > 0
? Math.max(120, screenH - ((barPos === "top" || barPos === "bottom") ? barH + gap + margin : margin * 2))
: 0
function fittedContentWidth(width, cap) {
var desired = Math.max(1, Number(width) || 1)
var maxWidth = root.availableCardWidth > 0 ? root.availableCardWidth : desired
if (cap !== undefined && Number(cap) > 0) maxWidth = Math.min(maxWidth, Number(cap))
return Math.round(Math.min(desired, maxWidth))
}
function fittedContentHeight(implicitHeight, cap) {
var desired = Math.max(root.padding * 2, (Number(implicitHeight) || 0) + root.padding * 2)
var maxHeight = root.availableCardHeight > 0 ? root.availableCardHeight : desired
if (cap !== undefined && Number(cap) > 0) maxHeight = Math.min(maxHeight, Number(cap))
return Math.round(Math.min(desired, maxHeight))
}
function cappedContentHeight(height) {
var desired = Math.max(root.padding * 2, Number(height) || root.padding * 2)
var maxHeight = root.availableCardHeight > 0 ? root.availableCardHeight : desired
return Math.round(Math.min(desired, maxHeight))
}
// Desired top-left of the card in screen coordinates. For the
// perpendicular axis (away-from-bar) we anchor to the bar window's edge
@@ -192,7 +218,7 @@ PanelWindow {
height: root.contentHeight
color: Color.popups.background
border.color: Color.popups.border
border.width: 2
border.width: Math.max(1, Style.space(2))
radius: Style.cornerRadius
opacity: root.open ? 1.0 : 0
Behavior on opacity {
+25 -10
View File
@@ -14,14 +14,15 @@ Column {
property color accent: Color.accent
property string fontFamily: Style.font.family
property real fontSize: Style.font.body
property real fieldWidth: 120
property real fieldWidth: Style.spacing.numberFieldWidth
property bool hasCursor: false
property bool _hovered: false
property alias field: spin
signal modified(int value)
signal hovered(bool on)
spacing: 6
spacing: Style.spacing.md
Text {
visible: root.label !== ""
@@ -34,6 +35,7 @@ Column {
QQC.SpinBox {
id: spin
width: root.fieldWidth
implicitHeight: Math.max(Style.spacing.controlHeight, root.fontSize + Style.spacing.controlPaddingY * 2)
from: root.from
to: root.to
stepSize: root.stepSize
@@ -45,16 +47,29 @@ Column {
onValueModified: root.modified(value)
background: Rectangle {
readonly property bool _hot: spin.activeFocus || (root.hasCursor && !spin.activeFocus)
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, _hot ? 0.10 : 0.05)
border.color: _hot
? Style.focusBorderColor
: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.3)
border.width: _hot ? Style.focusBorderWidth : 1
readonly property bool _focused: spin.activeFocus
readonly property bool _hot: root._hovered || root.hasCursor
color: _focused
? Style.focusFillFor(root.foreground, root.accent)
: (_hot
? Style.hoverFillFor(root.foreground, root.accent)
: Style.normalFillFor(root.foreground, root.accent))
border.color: _focused
? Style.focusBorderFor(root.foreground, root.accent)
: (_hot
? Style.hoverBorderFor(root.foreground, root.accent)
: Style.normalBorderFor(root.foreground, root.accent))
border.width: _focused
? Style.focusBorderWidth
: (_hot ? Style.hoverBorderWidth : Style.normalBorderWidth)
radius: Style.cornerRadius
HoverHandler {
onHoveredChanged: root.hovered(hovered)
onHoveredChanged: {
root._hovered = hovered
root.hovered(hovered)
}
}
}
@@ -62,7 +77,7 @@ Column {
text: spin.displayText
font: spin.font
color: root.foreground
selectionColor: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.35)
selectionColor: Style.selectionFillFor(root.foreground, root.accent)
selectedTextColor: root.foreground
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
+17 -8
View File
@@ -13,13 +13,13 @@ import qs.Commons
// here because action buttons are not cursor targets — the row they live
// in is.
//
// Set `focusable: true` to make the button keyboard-tabbable with an
// accent focus ring (Style.focusBorderColor / FillColor / Width). Use this
// Set `focusable: true` to make the button keyboard-tabbable with the
// shared hover-cursor/focus tokens. Use this
// in form contexts (the bar settings widget cards) where Tab walks a list
// of controls; leave it false for the right-edge actions on panel rows
// where the row's CursorSurface owns the keyboard cursor.
//
// Set `hasCursor: true` to have the button render the same fill as a
// Set `hasCursor: true` to have the button render the same hover state as
// mouse hover — so a panel's keyboard cursor lands on it identically.
// Use this when a PanelActionButton is itself the cursor target (rather
// than living inside a CursorSurface row). Emits `hovered(bool)` on
@@ -34,10 +34,11 @@ Rectangle {
property color panelBackground: Color.background
property string fontFamily: Style.font.family
property real fontSize: Style.font.icon
property real size: 22
property real size: Math.max(Style.space(22), fontSize + Style.spacing.sm * 2)
property bool focusable: false
property bool hasCursor: false
property bool bordered: false
signal clicked()
signal hovered(bool isHovered)
@@ -55,12 +56,20 @@ Rectangle {
readonly property bool _hot: (mouse.containsMouse || root.hasCursor) && root.enabled
color: _showFocusRing
? Style.focusFillColor
? Style.focusFillFor(hoverColor, hoverColor)
: (_hot
? Qt.rgba(hoverColor.r, hoverColor.g, hoverColor.b, 0.20)
? Style.hoverFillFor(hoverColor, hoverColor)
: "transparent")
border.width: _showFocusRing ? Style.focusBorderWidth : 0
border.color: _showFocusRing ? Style.focusBorderColor : "transparent"
border.width: _showFocusRing
? Style.focusBorderWidth
: (_hot && bordered
? Style.hoverBorderWidth
: (bordered ? Style.normalBorderWidth : 0))
border.color: _showFocusRing
? Style.focusBorderFor(hoverColor, hoverColor)
: (_hot && bordered
? Style.hoverBorderFor(hoverColor, hoverColor)
: Style.normalBorderFor(foreground, Color.accent))
Behavior on color { ColorAnimation { duration: 60 } }
+10 -8
View File
@@ -1,4 +1,5 @@
import QtQuick
import qs.Commons
Item {
id: root
@@ -9,11 +10,12 @@ Item {
property real maximum: 1
property real step: 0.05
property bool integer: false
property color trackColor: bar ? Qt.rgba(bar.foreground.r, bar.foreground.g, bar.foreground.b, 0.18) : "#333"
property color trackColor: bar ? Style.selectedFillFor(bar.foreground, Color.accent) : "#333"
property color fillColor: bar ? bar.foreground : "#cacccc"
property color knobColor: bar ? bar.foreground : "#cacccc"
property bool dragging: false
property real trackHeight: 4
property real trackHeight: Math.max(4, Math.round(Style.spacing.controlHeight * 0.11))
property real knobSize: Math.max(14, Math.round(Style.spacing.controlHeight * 0.38))
property real liveValue: value
onValueChanged: if (!dragging) liveValue = value
@@ -21,8 +23,8 @@ Item {
signal moved(real value)
signal released(real value)
implicitWidth: 200
implicitHeight: 22
implicitWidth: Style.space(200)
implicitHeight: Math.max(Style.space(22), knobSize + Style.spacing.md)
readonly property real range: Math.max(0.0001, maximum - minimum)
readonly property real progress: Math.max(0, Math.min(1, (liveValue - minimum) / range))
@@ -54,12 +56,12 @@ Item {
Rectangle {
id: knob
width: 14
height: 14
radius: 7
width: root.knobSize
height: root.knobSize
radius: root.knobSize / 2
color: root.knobColor
border.color: root.bar ? root.bar.background : "#101315"
border.width: 2
border.width: Math.max(1, Style.space(2))
anchors.verticalCenter: track.verticalCenter
x: Math.max(0, Math.min(track.width - width, track.width * root.progress - width / 2))
scale: mouseArea.containsMouse || root.dragging ? 1.15 : 1.0
+6 -6
View File
@@ -27,8 +27,8 @@ ToolTip {
background: Rectangle {
color: root.panelBackground
border.color: root.panelForeground
border.width: 1
border.color: Style.normalBorderFor(root.panelForeground, Color.accent)
border.width: Style.normalBorderWidth
radius: 0
opacity: 0.97
}
@@ -38,9 +38,9 @@ ToolTip {
color: root.panelForeground
font.family: root.fontFamily
font.pixelSize: root.fontSize
leftPadding: 10
rightPadding: 10
topPadding: 6
bottomPadding: 6
leftPadding: Style.spacing.controlPaddingX
rightPadding: Style.spacing.controlPaddingX
topPadding: Style.spacing.controlPaddingY
bottomPadding: Style.spacing.controlPaddingY
}
}
+34 -3
View File
@@ -9,8 +9,8 @@ PopupWindow {
required property Item anchorItem
required property QtObject bar
property var owner: null
property int margin: 10
property int padding: 14
property int margin: Style.gapsOut
property int padding: Style.spacing.popupPadding
property int contentWidth: 280
property int contentHeight: 200
property color borderColor: Color.popups.border
@@ -22,7 +22,38 @@ PopupWindow {
readonly property var coordinatorKey: owner || root
readonly property var anchorWindow: anchorItem ? anchorItem.QsWindow.window : null
readonly property var popupScreen: anchorWindow ? anchorWindow.screen : null
readonly property bool containsMouse: cardHover.hovered
readonly property real screenW: popupScreen ? popupScreen.width : 0
readonly property real screenH: popupScreen ? popupScreen.height : 0
readonly property real barW: anchorWindow ? anchorWindow.width : 0
readonly property real barH: anchorWindow ? anchorWindow.height : 0
readonly property real availableCardWidth: screenW > 0
? Math.max(120, screenW - ((bar && (bar.position === "left" || bar.position === "right")) ? barW : 0) - root.margin * 2)
: 0
readonly property real availableCardHeight: screenH > 0
? Math.max(120, screenH - ((bar && (bar.position === "top" || bar.position === "bottom")) ? barH : 0) - root.margin * 2)
: 0
function fittedContentWidth(width, cap) {
var desired = Math.max(1, Number(width) || 1)
var maxWidth = root.availableCardWidth > 0 ? root.availableCardWidth : desired
if (cap !== undefined && Number(cap) > 0) maxWidth = Math.min(maxWidth, Number(cap))
return Math.round(Math.min(desired, maxWidth))
}
function fittedContentHeight(implicitHeight, cap) {
var desired = Math.max(root.padding * 2, (Number(implicitHeight) || 0) + root.padding * 2)
var maxHeight = root.availableCardHeight > 0 ? root.availableCardHeight : desired
if (cap !== undefined && Number(cap) > 0) maxHeight = Math.min(maxHeight, Number(cap))
return Math.round(Math.min(desired, maxHeight))
}
function cappedContentHeight(height) {
var desired = Math.max(root.padding * 2, Number(height) || root.padding * 2)
var maxHeight = root.availableCardHeight > 0 ? root.availableCardHeight : desired
return Math.round(Math.min(desired, maxHeight))
}
function closePopout() {
if (owner && "closePopout" in owner) owner.closePopout()
@@ -119,7 +150,7 @@ PopupWindow {
anchors.fill: parent
color: Color.popups.background
border.color: root.borderColor
border.width: 2
border.width: Math.max(1, Style.space(2))
radius: Style.cornerRadius
opacity: root.open ? 1.0 : 0
+43 -33
View File
@@ -30,13 +30,13 @@ Item {
property color popupBorder: Color.popups.border
property color accent: Color.accent
property string fontFamily: Style.font.family
property int rowHeight: 28
property int popupRowHeight: 28
property int popupMinHeight: 220
property int rowHeight: Style.spacing.controlHeight
property int popupRowHeight: Style.spacing.popupRowHeight
property int popupMinHeight: Style.spacing.searchablePopupMinHeight
property bool showLabel: true
// Panel-cursor flag. When true, the trigger renders the same focus ring
// as Tab-focus so a panel's keyboard cursor lands here identically.
// Panel-cursor flag. When true, the trigger renders the shared
// hover-cursor state. Active Qt focus defaults to the same visuals.
// Emits `hovered(bool)` on pointer enter/leave so the panel can keep
// its cursor state in sync with the mouse.
property bool hasCursor: false
@@ -84,12 +84,12 @@ Item {
onOptionsChanged: recomputeFiltered()
implicitWidth: 260
implicitHeight: showLabel && label !== "" ? rowHeight + 18 : rowHeight
implicitWidth: Style.spacing.searchableDropdownWidth
implicitHeight: showLabel && label !== "" ? rowHeight + Style.spacing.huge : rowHeight
Column {
anchors.fill: parent
spacing: 4
spacing: Style.spacing.labelGap
Text {
visible: root.showLabel && root.label !== ""
@@ -106,18 +106,27 @@ Item {
height: root.rowHeight
radius: Style.cornerRadius
readonly property bool _focused: trigger.activeFocus || root.hasCursor
readonly property bool _focused: trigger.activeFocus
readonly property bool _hot: triggerHover.hovered || root.hasCursor
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b,
trigger._focused ? 0.08 : 0.04)
color: trigger._focused
? Style.focusFillFor(root.foreground, root.accent)
: (trigger._hot
? Style.hoverFillFor(root.foreground, root.accent)
: Style.normalFillFor(root.foreground, root.accent))
border.color: trigger._focused
? Style.focusBorderColor
: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.4)
border.width: trigger._focused ? Style.focusBorderWidth : 1
? Style.focusBorderFor(root.foreground, root.accent)
: (trigger._hot
? Style.hoverBorderFor(root.foreground, root.accent)
: Style.normalBorderFor(root.foreground, root.accent))
border.width: trigger._focused
? Style.focusBorderWidth
: (trigger._hot ? Style.hoverBorderWidth : Style.normalBorderWidth)
activeFocusOnTab: true
HoverHandler {
id: triggerHover
onHoveredChanged: root.hovered(hovered)
}
@@ -135,8 +144,8 @@ Item {
anchors.left: parent.left
anchors.right: chevron.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 10
anchors.rightMargin: 6
anchors.leftMargin: Style.spacing.controlPaddingX
anchors.rightMargin: Style.spacing.md
text: root.currentLabel() || root.triggerLabel || root.placeholderText
color: (root.currentLabel() || root.triggerLabel) ? root.foreground : Qt.darker(root.foreground, 1.5)
font.family: root.fontFamily
@@ -148,7 +157,7 @@ Item {
id: chevron
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: 8
anchors.rightMargin: Style.spacing.controlGap
text: "󰅀"
color: Qt.darker(root.foreground, 1.2)
font.family: root.fontFamily
@@ -167,18 +176,18 @@ Item {
QQC.Popup {
id: popup
x: 0
y: trigger.height + 2
y: trigger.height + Style.spacing.xxs
width: trigger.width
implicitHeight: Math.max(root.popupMinHeight,
Math.min(resultList.contentHeight + 50,
root.popupRowHeight * 6 + 5 * 4 + 50))
padding: 1
Math.min(resultList.contentHeight + Style.space(50),
root.popupRowHeight * 6 + 5 * Style.spacing.labelGap + Style.space(50)))
padding: Style.spacing.hairline
focus: true
background: Rectangle {
color: root.background
border.color: root.popupBorder
border.width: 1
border.width: Style.normalBorderWidth
radius: Style.cornerRadius
}
@@ -193,13 +202,14 @@ Item {
spacing: 0
Item {
id: searchHeader
width: parent.width
height: 38
height: root.popupRowHeight + Style.spacing.controlPaddingX
TextField {
id: searchField
anchors.fill: parent
anchors.margins: 6
anchors.margins: Style.spacing.md
placeholderText: root.placeholderText
foreground: root.foreground
accent: root.accent
@@ -234,12 +244,12 @@ Item {
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
color: Style.alpha(root.foreground, 0.10)
}
Item {
width: parent.width
height: popup.height - 38 - 2 - 1
height: popup.height - searchHeader.height - Style.spacing.xxs - 1
Text {
anchors.centerIn: parent
@@ -253,7 +263,7 @@ Item {
ListView {
id: resultList
anchors.fill: parent
spacing: 4
spacing: Style.spacing.labelGap
clip: true
boundsBehavior: Flickable.StopAtBounds
model: root.filtered
@@ -294,9 +304,9 @@ Item {
required property var modelData
required property int index
width: resultList.width
height: Math.max(root.popupRowHeight, rowContent.implicitHeight + 12)
height: Math.max(root.popupRowHeight, rowContent.implicitHeight + Style.spacing.rowPaddingX)
color: index === resultList.currentIndex
? Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.14)
? Style.hoverFillFor(root.foreground, root.accent)
: "transparent"
Column {
@@ -304,13 +314,13 @@ Item {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 10
anchors.rightMargin: 10
spacing: 2
anchors.leftMargin: Style.spacing.controlPaddingX
anchors.rightMargin: Style.spacing.controlPaddingX
spacing: Style.spacing.xxs
Text {
text: root.optionLabel(modelData)
color: index === resultList.currentIndex ? root.accent : root.foreground
color: index === resultList.currentIndex ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground
font.family: root.fontFamily
font.pixelSize: Style.font.body
elide: Text.ElideRight
+20 -14
View File
@@ -9,9 +9,8 @@ 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 Button paint) so keyboard cursor and form focus
// chrome stay consistent across the shell.
// instance. activeFocus and mouse hover / panel cursor use the same
// hover-cursor defaults, so text inputs match Button, Toggle, and Dropdown.
//
// Sizing is driven by font.pixelSize + verticalPadding. The default 30px
// implicitHeight fits dialog forms; inline callers (wifi's row-embedded
@@ -21,20 +20,20 @@ TextField {
property color foreground: Color.foreground
property color accent: Color.accent
property color selectionTint: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.35)
property color selectionTint: Style.selectionFillFor(foreground, accent)
property bool password: false
property real horizontalPadding: 10
property real verticalPadding: 7
property real horizontalPadding: Style.spacing.controlPaddingX
property real verticalPadding: Style.spacing.inputPaddingY
// Panel-cursor flag. When true (and the field isn't already focused),
// the background paints the same accent ring as activeFocus so the
// panel's keyboard cursor lands here identically to a mouse hover.
// the background paints the shared hover/cursor state.
// For mouse-enter/leave the consumer reads QQC TextField's inherited
// `hovered` property (via onHoveredChanged) — we don't add a sibling
// signal because the inherited property would shadow it.
property bool hasCursor: false
readonly property bool _focused: activeFocus || hasCursor
readonly property bool _focused: activeFocus
readonly property bool _hot: hovered || hasCursor
echoMode: password ? TextInput.Password : TextInput.Normal
font.family: Style.font.family
@@ -50,12 +49,19 @@ TextField {
bottomPadding: verticalPadding
background: Rectangle {
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b,
root._focused ? 0.08 : 0.04)
color: root._focused
? Style.focusFillFor(root.foreground, root.accent)
: (root._hot
? Style.hoverFillFor(root.foreground, root.accent)
: Style.normalFillFor(root.foreground, root.accent))
border.color: root._focused
? Style.focusBorderColor
: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.18)
border.width: root._focused ? Style.focusBorderWidth : 1
? Style.focusBorderFor(root.foreground, root.accent)
: (root._hot
? Style.hoverBorderFor(root.foreground, root.accent)
: Style.normalBorderFor(root.foreground, root.accent))
border.width: root._focused
? Style.focusBorderWidth
: (root._hot ? Style.hoverBorderWidth : Style.normalBorderWidth)
radius: Style.cornerRadius
}
}
+40 -30
View File
@@ -6,10 +6,8 @@ import qs.Commons
// flip `checked` in response (the component is stateless about the actual
// value so it composes cleanly with model-driven UI).
//
// Cursor and focus styling match the rest of the kit: hasCursor (panel
// keyboard cursor / mouse hover) is a fill only, mirroring CursorSurface;
// activeFocus (Tab focus) adds the accent border ring on top via the
// shared Style tokens.
// Cursor and focus styling match the rest of the kit: hasCursor / mouse
// hover and activeFocus share the hover-cursor defaults.
//
// `rounded` auto-detects from Style.cornerRadius so the switch follows
// the theme: pill shape on round-corners themes, square on sharp.
@@ -23,8 +21,7 @@ Rectangle {
// 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).
// separately from activeFocus. Visuals use the same hover-cursor tokens.
property bool hasCursor: false
// Switch shape follows the theme by default: pill on round, square on sharp.
@@ -45,17 +42,30 @@ Rectangle {
Keys.onEnterPressed: root.clicked()
Keys.onSpacePressed: root.clicked()
implicitHeight: Math.max(54, content.implicitHeight + 18)
implicitWidth: 240
readonly property int trackHeight: Math.max(22, Math.round(Style.spacing.controlHeight * 0.55))
readonly property int trackWidth: Math.max(42, Math.round(trackHeight * 1.9))
readonly property int knobSize: Math.max(16, Math.round(trackHeight * 0.72))
readonly property int knobInset: Math.max(2, Math.round((trackHeight - knobSize) / 2))
implicitHeight: Math.max(54, content.implicitHeight + Style.spacing.huge)
implicitWidth: Style.space(240)
radius: Style.cornerRadius
readonly property bool _hot: hasCursor || mouse.containsMouse
color: activeFocus
? Style.focusFillColor
: ((hasCursor || mouse.containsMouse) ? Qt.rgba(foreground.r, foreground.g, foreground.b, 0.08) : Qt.rgba(foreground.r, foreground.g, foreground.b, 0.03))
? Style.focusFillFor(foreground, accent)
: (_hot
? Style.hoverFillFor(foreground, accent)
: Style.normalFillFor(foreground, accent))
border.color: activeFocus
? Style.focusBorderColor
: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.12)
border.width: activeFocus ? Style.focusBorderWidth : 1
? Style.focusBorderFor(foreground, accent)
: (_hot
? Style.hoverBorderFor(foreground, accent)
: Style.normalBorderFor(foreground, accent))
border.width: activeFocus
? Style.focusBorderWidth
: (_hot ? Style.hoverBorderWidth : Style.normalBorderWidth)
Behavior on color { ColorAnimation { duration: 100 } }
@@ -64,13 +74,13 @@ Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 12
anchors.rightMargin: 12
spacing: 12
anchors.leftMargin: Style.spacing.rowPaddingX
anchors.rightMargin: Style.spacing.rowPaddingX
spacing: Style.spacing.rowPaddingX
Column {
width: parent.width - track.width - parent.spacing
spacing: 3
spacing: Style.spacing.xs
anchors.verticalCenter: parent.verticalCenter
Text {
@@ -96,27 +106,27 @@ Rectangle {
Rectangle {
id: track
width: 42
height: 22
width: root.trackWidth
height: root.trackHeight
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)
? Style.selectedFillFor(root.foreground, root.accent)
: Style.normalFillFor(root.foreground, root.accent)
border.color: root.checked
? root.accent
: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.28)
border.width: 1
? Style.selectedBorderFor(root.foreground, root.accent)
: Style.normalBorderFor(root.foreground, root.accent)
border.width: root.checked ? Style.selectedBorderWidth : Style.normalBorderWidth
anchors.verticalCenter: parent.verticalCenter
Behavior on color { ColorAnimation { duration: 120 } }
Rectangle {
width: 16
height: 16
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)
width: root.knobSize
height: root.knobSize
radius: root.rounded ? height / 2 : 0
x: root.checked ? track.width - width - root.knobInset : root.knobInset
y: root.knobInset
color: root.checked ? Style.selectedStateColor(root.foreground, root.accent) : Qt.darker(root.foreground, 1.25)
Behavior on x { NumberAnimation { duration: 120; easing.type: Easing.OutCubic } }
Behavior on color { ColorAnimation { duration: 120 } }
+6 -3
View File
@@ -25,11 +25,14 @@ Item {
readonly property bool vertical: bar ? bar.vertical : false
readonly property int barSize: bar ? bar.barSize : Style.bar.sizeHorizontal
readonly property real scaledHorizontalMargin: Style.spaceReal(horizontalMargin)
readonly property real scaledRightExtraMargin: Style.spaceReal(rightExtraMargin)
readonly property real scaledVerticalPadding: Style.spaceReal(verticalPadding)
visible: text !== "" || keepSpace
opacity: text === "" ? 0 : 1
implicitWidth: fixedWidth > 0 ? fixedWidth : (vertical ? barSize : Math.max(12, label.implicitWidth + horizontalMargin * 2 + rightExtraMargin))
implicitHeight: fixedHeight > 0 ? fixedHeight : (vertical ? Math.max(12, label.implicitHeight + verticalPadding * 2) : barSize)
implicitWidth: fixedWidth > 0 ? fixedWidth : (vertical ? barSize : Math.max(12, label.implicitWidth + scaledHorizontalMargin * 2 + scaledRightExtraMargin))
implicitHeight: fixedHeight > 0 ? fixedHeight : (vertical ? Math.max(12, label.implicitHeight + scaledVerticalPadding * 2) : barSize)
Behavior on opacity {
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
@@ -38,7 +41,7 @@ Item {
Text {
id: label
anchors.centerIn: parent
anchors.horizontalCenterOffset: root.vertical ? 0 : -root.rightExtraMargin / 2
anchors.horizontalCenterOffset: root.vertical ? 0 : -root.scaledRightExtraMargin / 2
text: root.text
color: root.active ? root.activeColor : root.foreground
font.family: root.fontFamily