Add a notched text-size slider to the display panel
A macOS-style slider snapping to curated stops (9-20px), applied via omarchy-display-text-size. Suppresses hover-driven focus changes while a size change reflows the panel, so h/l on the slider can't let a shifted row steal keyboard focus. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
84ad03b8d2
commit
ab49ec3787
@@ -43,9 +43,28 @@ Panel {
|
||||
property int selectedIndex: 0
|
||||
property bool cursorActive: false
|
||||
|
||||
// Text size slider — curated macOS-style notches (px). The panel snaps to
|
||||
// these stops; the CLI (omarchy-display-text-size) accepts any integer in range.
|
||||
readonly property var textSizeStops: [9, 10, 11, 12, 14, 16, 20]
|
||||
// While a change is in flight, the chosen stop index overrides the live
|
||||
// base-size so the knob doesn't snap back during the file round-trip. -1 =
|
||||
// no pending change; follow Style.font.baseSize.
|
||||
property int textSizePreviewIndex: -1
|
||||
|
||||
// A text-size change reflows the whole panel (both font and spacing scale),
|
||||
// which slides rows under a stationary pointer and fires synthetic hover.
|
||||
// While true, hover is not allowed to hijack the keyboard focus section —
|
||||
// otherwise h/l on the text-size slider can jump focus to another row.
|
||||
property bool reflowingText: false
|
||||
function markReflowing() {
|
||||
root.reflowingText = true
|
||||
reflowSettle.restart()
|
||||
}
|
||||
|
||||
readonly property var visibleSections: {
|
||||
var list = []
|
||||
if (brightnessAvailable) list.push("brightness")
|
||||
list.push("textsize")
|
||||
list.push("scale")
|
||||
if (displays.length > 1) list.push("monitors")
|
||||
return list
|
||||
@@ -53,18 +72,19 @@ Panel {
|
||||
|
||||
function sectionCount(section) {
|
||||
if (section === "brightness") return 0 // only the slider sentinel at -1
|
||||
if (section === "textsize") return 0 // slider sentinel at -1, like brightness
|
||||
if (section === "scale") return scaleValues.length
|
||||
if (section === "monitors") return displays.length
|
||||
return 0
|
||||
}
|
||||
|
||||
function sectionIsSingleRow(section) {
|
||||
// brightness has only the slider; scale presets sit horizontally.
|
||||
return section === "brightness" || section === "scale"
|
||||
// brightness and text size are lone sliders; scale presets sit horizontally.
|
||||
return section === "brightness" || section === "textsize" || section === "scale"
|
||||
}
|
||||
|
||||
function sectionFirstIndex(section) {
|
||||
if (section === "brightness") return -1
|
||||
if (section === "brightness" || section === "textsize") return -1
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -137,8 +157,8 @@ Panel {
|
||||
}
|
||||
var count = sectionCount(focusSection)
|
||||
if (sectionIsSingleRow(focusSection)) {
|
||||
// brightness uses -1 sentinel; scale clamps into the preset range.
|
||||
if (focusSection === "brightness") selectedIndex = -1
|
||||
// brightness/text size use the -1 sentinel; scale clamps into the presets.
|
||||
if (focusSection === "brightness" || focusSection === "textsize") selectedIndex = -1
|
||||
else if (selectedIndex < 0 || selectedIndex >= count) selectedIndex = 0
|
||||
return
|
||||
}
|
||||
@@ -252,6 +272,43 @@ Panel {
|
||||
if (!actionProc.running) actionProc.running = true
|
||||
}
|
||||
|
||||
// ---- Text size (shell base font + GTK text-scaling, via one CLI) ----
|
||||
function nearestTextStop(px) {
|
||||
var best = 0
|
||||
var bestDist = 1e9
|
||||
for (var i = 0; i < textSizeStops.length; i++) {
|
||||
var d = Math.abs(textSizeStops[i] - px)
|
||||
if (d < bestDist) { bestDist = d; best = i }
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
// Effective stop index: the pending choice while a change is in flight,
|
||||
// otherwise whatever Style's live base-size rounds to.
|
||||
function currentTextIndex() {
|
||||
return textSizePreviewIndex >= 0 ? textSizePreviewIndex : nearestTextStop(Style.font.baseSize)
|
||||
}
|
||||
|
||||
// px shown in the header: the pending stop if any, else the true base-size
|
||||
// (which may be an off-notch value set from the CLI).
|
||||
function displayedTextPx() {
|
||||
return textSizePreviewIndex >= 0 ? textSizeStops[textSizePreviewIndex] : Style.font.baseSize
|
||||
}
|
||||
|
||||
function setTextSize(px) {
|
||||
textScaleProc.command = ["omarchy-display-text-size", String(px)]
|
||||
if (!textScaleProc.running) textScaleProc.running = true
|
||||
}
|
||||
|
||||
function adjustTextSize(deltaSteps) {
|
||||
var idx = currentTextIndex() + deltaSteps
|
||||
if (idx < 0) idx = 0
|
||||
if (idx > textSizeStops.length - 1) idx = textSizeStops.length - 1
|
||||
markReflowing()
|
||||
textSizePreviewIndex = idx
|
||||
setTextSize(textSizeStops[idx])
|
||||
}
|
||||
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
@@ -340,6 +397,36 @@ Panel {
|
||||
onRunningChanged: if (!running) root.refresh()
|
||||
}
|
||||
|
||||
// Applies text size via the CLI, which rewrites the shell override file;
|
||||
// Style picks the new base-size up through its own file watch, so there's
|
||||
// nothing to refresh here.
|
||||
Process {
|
||||
id: textScaleProc
|
||||
stdout: StdioCollector { waitForEnd: true }
|
||||
}
|
||||
|
||||
// Clears the hover-suppression flag once the reflow triggered by a text-size
|
||||
// change has settled.
|
||||
Timer {
|
||||
id: reflowSettle
|
||||
interval: 300
|
||||
repeat: false
|
||||
onTriggered: root.reflowingText = false
|
||||
}
|
||||
|
||||
// Once Style's base-size catches up to the pending choice, drop the preview
|
||||
// so the slider tracks the live value again. The change itself reflows the
|
||||
// panel, so suppress hover for a beat while it lands.
|
||||
Connections {
|
||||
target: Style
|
||||
function onFontBaseSizeChanged() {
|
||||
root.markReflowing()
|
||||
if (root.textSizePreviewIndex >= 0
|
||||
&& root.nearestTextStop(Style.font.baseSize) === root.textSizePreviewIndex)
|
||||
root.textSizePreviewIndex = -1
|
||||
}
|
||||
}
|
||||
|
||||
BarIconButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
@@ -369,6 +456,7 @@ Panel {
|
||||
if (dy !== 0) root.moveCursor(dy)
|
||||
else if (dx !== 0) {
|
||||
if (root.focusSection === "brightness") root.adjustBrightness(dx * 5)
|
||||
else if (root.focusSection === "textsize") root.adjustTextSize(dx)
|
||||
else if (root.focusSection === "scale") root.moveCursorH(dx)
|
||||
}
|
||||
}
|
||||
@@ -506,7 +594,7 @@ Panel {
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
onHoveredChanged: if (hovered) {
|
||||
onHoveredChanged: if (hovered && !root.reflowingText) {
|
||||
root.cursorActive = true
|
||||
root.focusSection = "brightness"
|
||||
root.selectedIndex = -1
|
||||
@@ -523,6 +611,77 @@ Panel {
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
|
||||
// ---------- Text size ----------
|
||||
PanelSeparator {
|
||||
foreground: root.bar.foreground
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Style.space(6)
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
implicitHeight: Math.max(textSizeHeader.implicitHeight, textSizePx.implicitHeight)
|
||||
|
||||
PanelSectionHeader {
|
||||
id: textSizeHeader
|
||||
text: "TEXT SIZE"
|
||||
foreground: root.bar.foreground
|
||||
fontFamily: root.bar.fontFamily
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
id: textSizePx
|
||||
text: (textSizeSlider.dragging
|
||||
? root.textSizeStops[Math.round(textSizeSlider.liveValue)]
|
||||
: root.displayedTextPx()) + "px"
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.caption
|
||||
font.bold: true
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Style.space(6)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
CursorSurface {
|
||||
id: textSizeRow
|
||||
width: parent.width
|
||||
height: textSizeSlider.implicitHeight + Style.spacing.controlGap
|
||||
hasCursor: root.cursorActive && root.focusSection === "textsize" && root.selectedIndex === -1
|
||||
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(textSizeRow)
|
||||
foreground: root.bar.foreground
|
||||
outline: true
|
||||
|
||||
PanelSlider {
|
||||
id: textSizeSlider
|
||||
bar: root.bar
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.space(6)
|
||||
anchors.rightMargin: Style.space(6)
|
||||
minimum: 0
|
||||
maximum: root.textSizeStops.length - 1
|
||||
step: 1
|
||||
integer: true
|
||||
tickCount: root.textSizeStops.length
|
||||
value: root.currentTextIndex()
|
||||
onReleased: function(v) { root.setTextSize(root.textSizeStops[Math.round(v)]) }
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
onHoveredChanged: if (hovered && !root.reflowingText) {
|
||||
root.cursorActive = true
|
||||
root.focusSection = "textsize"
|
||||
root.selectedIndex = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Scale ----------
|
||||
PanelSeparator {
|
||||
foreground: root.bar.foreground
|
||||
@@ -621,7 +780,7 @@ Panel {
|
||||
|
||||
onClicked: root.setScale(scaleValue)
|
||||
onHovered: function(isHovered) {
|
||||
if (!isHovered) return
|
||||
if (!isHovered || root.reflowingText) return
|
||||
root.cursorActive = true
|
||||
root.focusSection = "scale"
|
||||
root.selectedIndex = pill.scaleIndex
|
||||
@@ -689,7 +848,7 @@ Panel {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: monitorRow.canToggle ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onContainsMouseChanged: if (containsMouse) {
|
||||
onContainsMouseChanged: if (containsMouse && !root.reflowingText) {
|
||||
root.cursorActive = true
|
||||
root.focusSection = "monitors"
|
||||
root.selectedIndex = monitorRow.rowIndex
|
||||
|
||||
Reference in New Issue
Block a user