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 int selectedIndex: 0
|
||||||
property bool cursorActive: false
|
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: {
|
readonly property var visibleSections: {
|
||||||
var list = []
|
var list = []
|
||||||
if (brightnessAvailable) list.push("brightness")
|
if (brightnessAvailable) list.push("brightness")
|
||||||
|
list.push("textsize")
|
||||||
list.push("scale")
|
list.push("scale")
|
||||||
if (displays.length > 1) list.push("monitors")
|
if (displays.length > 1) list.push("monitors")
|
||||||
return list
|
return list
|
||||||
@@ -53,18 +72,19 @@ Panel {
|
|||||||
|
|
||||||
function sectionCount(section) {
|
function sectionCount(section) {
|
||||||
if (section === "brightness") return 0 // only the slider sentinel at -1
|
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 === "scale") return scaleValues.length
|
||||||
if (section === "monitors") return displays.length
|
if (section === "monitors") return displays.length
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function sectionIsSingleRow(section) {
|
function sectionIsSingleRow(section) {
|
||||||
// brightness has only the slider; scale presets sit horizontally.
|
// brightness and text size are lone sliders; scale presets sit horizontally.
|
||||||
return section === "brightness" || section === "scale"
|
return section === "brightness" || section === "textsize" || section === "scale"
|
||||||
}
|
}
|
||||||
|
|
||||||
function sectionFirstIndex(section) {
|
function sectionFirstIndex(section) {
|
||||||
if (section === "brightness") return -1
|
if (section === "brightness" || section === "textsize") return -1
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,8 +157,8 @@ Panel {
|
|||||||
}
|
}
|
||||||
var count = sectionCount(focusSection)
|
var count = sectionCount(focusSection)
|
||||||
if (sectionIsSingleRow(focusSection)) {
|
if (sectionIsSingleRow(focusSection)) {
|
||||||
// brightness uses -1 sentinel; scale clamps into the preset range.
|
// brightness/text size use the -1 sentinel; scale clamps into the presets.
|
||||||
if (focusSection === "brightness") selectedIndex = -1
|
if (focusSection === "brightness" || focusSection === "textsize") selectedIndex = -1
|
||||||
else if (selectedIndex < 0 || selectedIndex >= count) selectedIndex = 0
|
else if (selectedIndex < 0 || selectedIndex >= count) selectedIndex = 0
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -252,6 +272,43 @@ Panel {
|
|||||||
if (!actionProc.running) actionProc.running = true
|
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
|
implicitWidth: button.implicitWidth
|
||||||
implicitHeight: button.implicitHeight
|
implicitHeight: button.implicitHeight
|
||||||
|
|
||||||
@@ -340,6 +397,36 @@ Panel {
|
|||||||
onRunningChanged: if (!running) root.refresh()
|
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 {
|
BarIconButton {
|
||||||
id: button
|
id: button
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -369,6 +456,7 @@ Panel {
|
|||||||
if (dy !== 0) root.moveCursor(dy)
|
if (dy !== 0) root.moveCursor(dy)
|
||||||
else if (dx !== 0) {
|
else if (dx !== 0) {
|
||||||
if (root.focusSection === "brightness") root.adjustBrightness(dx * 5)
|
if (root.focusSection === "brightness") root.adjustBrightness(dx * 5)
|
||||||
|
else if (root.focusSection === "textsize") root.adjustTextSize(dx)
|
||||||
else if (root.focusSection === "scale") root.moveCursorH(dx)
|
else if (root.focusSection === "scale") root.moveCursorH(dx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -506,7 +594,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
HoverHandler {
|
HoverHandler {
|
||||||
onHoveredChanged: if (hovered) {
|
onHoveredChanged: if (hovered && !root.reflowingText) {
|
||||||
root.cursorActive = true
|
root.cursorActive = true
|
||||||
root.focusSection = "brightness"
|
root.focusSection = "brightness"
|
||||||
root.selectedIndex = -1
|
root.selectedIndex = -1
|
||||||
@@ -523,6 +611,77 @@ Panel {
|
|||||||
font.pixelSize: Style.font.bodySmall
|
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 ----------
|
// ---------- Scale ----------
|
||||||
PanelSeparator {
|
PanelSeparator {
|
||||||
foreground: root.bar.foreground
|
foreground: root.bar.foreground
|
||||||
@@ -621,7 +780,7 @@ Panel {
|
|||||||
|
|
||||||
onClicked: root.setScale(scaleValue)
|
onClicked: root.setScale(scaleValue)
|
||||||
onHovered: function(isHovered) {
|
onHovered: function(isHovered) {
|
||||||
if (!isHovered) return
|
if (!isHovered || root.reflowingText) return
|
||||||
root.cursorActive = true
|
root.cursorActive = true
|
||||||
root.focusSection = "scale"
|
root.focusSection = "scale"
|
||||||
root.selectedIndex = pill.scaleIndex
|
root.selectedIndex = pill.scaleIndex
|
||||||
@@ -689,7 +848,7 @@ Panel {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: monitorRow.canToggle ? Qt.PointingHandCursor : Qt.ArrowCursor
|
cursorShape: monitorRow.canToggle ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
onContainsMouseChanged: if (containsMouse) {
|
onContainsMouseChanged: if (containsMouse && !root.reflowingText) {
|
||||||
root.cursorActive = true
|
root.cursorActive = true
|
||||||
root.focusSection = "monitors"
|
root.focusSection = "monitors"
|
||||||
root.selectedIndex = monitorRow.rowIndex
|
root.selectedIndex = monitorRow.rowIndex
|
||||||
|
|||||||
Reference in New Issue
Block a user