diff --git a/shell/plugins/panels/monitor/Panel.qml b/shell/plugins/panels/monitor/Panel.qml index be0c346e..2a972ad8 100644 --- a/shell/plugins/panels/monitor/Panel.qml +++ b/shell/plugins/panels/monitor/Panel.qml @@ -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