mirror of
https://github.com/arthur-pbty/arthur-os.git
synced 2026-08-02 04:37:49 +02:00
Themes now drive typography the same way they drive colors: one [font] base-size in shell.toml is the rem root, and every Style.font.<token> (caption, bodySmall, body, subtitle, title, heading, display, displayLarge, iconSmall, icon, iconLarge) derives from it via a fixed multiplier. Themes can also pin individual tokens for stylistic emphasis. base-size is clamped 11..13 until row-height tokens exist. Bar dimensions move to the same singleton: [bar] size-horizontal / size-vertical replace the hardcoded 26/28 in Bar.qml, exposed as Style.bar.sizeHorizontal / sizeVertical. Style.qml also resolves the fontconfig 'monospace' alias via fc-match and exposes Style.font.resolvedFamily so panels can display the concrete family. Watches ~/.config/fontconfig/fonts.conf so it tracks 'omarchy font set <name>'. The qs.Ui kit (PillButton, Dropdown, Toggle, TextField, etc.) and every first-party plugin (bar widgets, settings, menu, clipboard, emoji, polkit, notifications, osd, image-picker, dev-gallery) now bind to Style.font.* instead of pixel literals. Only three deliberate display-scale outliers remain: the notification empty-state glyph and the weather flyout's hero temperature pair, all commented. Background plugin's applyTheme IPC fast-path also pushes shell.toml to Style so theme swaps update typography and bar size without waiting for inotify debounce. Dev gallery (omarchy dev ui-preview) now ships a Typography section that renders the full scale and theme tokens live, and its summon command is fixed (omarchy-shell-ipc -> omarchy-shell).
239 lines
7.9 KiB
QML
239 lines
7.9 KiB
QML
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: Style.font.family
|
|
property int rowHeight: 28
|
|
property int popupRowHeight: 28
|
|
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.
|
|
// Emits `hovered(bool)` on pointer enter/leave so the panel can keep
|
|
// its cursor state in sync with the mouse.
|
|
property bool hasCursor: false
|
|
|
|
// popupOpen + open()/close()/toggle() let a parent panel know when the
|
|
// dropdown owns keys (its embedded ListView is active) and suspend its
|
|
// own keyCatcher so j/k inside the popup don't double-drive the panel
|
|
// cursor.
|
|
readonly property bool popupOpen: popup.opened
|
|
function open() { popup.open() }
|
|
function close() { popup.close() }
|
|
function toggle() { popup.opened ? popup.close() : popup.open() }
|
|
|
|
signal changed(string value)
|
|
signal hovered(bool isHovered)
|
|
|
|
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: Style.font.caption
|
|
font.bold: true
|
|
}
|
|
|
|
Rectangle {
|
|
id: trigger
|
|
width: parent.width
|
|
height: root.rowHeight
|
|
radius: Style.cornerRadius
|
|
|
|
readonly property bool _focused: trigger.activeFocus || root.hasCursor
|
|
|
|
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b,
|
|
trigger._focused ? 0.08 : 0.04)
|
|
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
|
|
|
|
activeFocusOnTab: true
|
|
|
|
HoverHandler {
|
|
onHoveredChanged: root.hovered(hovered)
|
|
}
|
|
|
|
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: Style.font.body
|
|
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: Style.font.body
|
|
}
|
|
|
|
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 + Math.max(0, root.options.length - 1) * 4 + 2,
|
|
root.popupRowHeight * 8 + 7 * 4 + 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))
|
|
optionList.forceActiveFocus()
|
|
}
|
|
|
|
contentItem: ListView {
|
|
id: optionList
|
|
spacing: 4
|
|
|
|
Keys.priority: Keys.BeforeItem
|
|
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
|
|
}
|
|
}
|
|
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: Style.font.body
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onPositionChanged: optionList.currentIndex = parent.index
|
|
onClicked: optionList.selectCurrent()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|