From b86e584b029646f114a85cc6534ca6e2fcf17d3a Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sun, 4 Oct 2026 21:58:00 +0200 Subject: [PATCH] fix(quickshell): keep text sharp under zoom and on the lock screen Native-rendered glyphs are rasterised at rest size, so anything moved with `scale` stretched a bitmap and came out blurred and stepped. - Icons render as curves, so their pulse and bump stay smooth. - The lock face is laid out at uiScale instead of being drawn at 1080p size and scaled by up to 1.3, and it sits on whole pixels. Its native labels now land on the pixel grid. - The OSD rises into place instead of scaling in. Co-Authored-By: Claude Opus 5.5 --- quickshell/components/Icon.qml | 6 ++- quickshell/lock/LockFace.qml | 96 ++++++++++++++++++---------------- quickshell/lock/StatusRow.qml | 21 ++++---- quickshell/overlays/Osd.qml | 14 +++-- 4 files changed, 76 insertions(+), 61 deletions(-) diff --git a/quickshell/components/Icon.qml b/quickshell/components/Icon.qml index a519786..87a91a9 100644 --- a/quickshell/components/Icon.qml +++ b/quickshell/components/Icon.qml @@ -13,7 +13,11 @@ Text { color: Theme.primary verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter - renderType: Text.NativeRendering + // Curve glyphs, not native ones. Native glyphs are rasterised once at rest + // size and the bitmap is stretched when `scale` moves, so every pulse and + // bump came out blurred and stepped. Curves are drawn from the outline at + // whatever size the icon is at that frame. + renderType: Text.CurveRendering Behavior on color { ColorAnimation { duration: Theme.durBase } diff --git a/quickshell/lock/LockFace.qml b/quickshell/lock/LockFace.qml index 2e93c24..e01e573 100644 --- a/quickshell/lock/LockFace.qml +++ b/quickshell/lock/LockFace.qml @@ -92,11 +92,12 @@ Item { Column { id: face - x: parent.width * 0.14 - y: parent.height * 0.2 - width: 380 - scale: surface.uiScale - transformOrigin: Item.TopLeft + // Sized by uiScale rather than drawn small and stretched with `scale`: + // a scale transform blurs native text, and every label here is native. + // Whole pixels, too, or the glyphs land between them. + x: Math.round(parent.width * 0.14) + y: Math.round(parent.height * 0.2) + width: Math.round(380 * surface.uiScale) opacity: 0 spacing: 0 @@ -104,42 +105,42 @@ Item { // The house mark, the same porthole the bar carries far left. Porthole { - size: 26 + size: Math.round(26 * surface.uiScale) } Text { - topPadding: Theme.padM + topPadding: Math.round(Theme.padM * surface.uiScale) text: Qt.formatDateTime(clock.date, "HH:mm") color: Theme.accent font.family: Theme.fontDisplay - font.pixelSize: Theme.fsMega * 1.35 + font.pixelSize: Math.round(Theme.fsMega * 1.35 * surface.uiScale) font.weight: Font.Light - font.letterSpacing: 2 - // Scaled with the face, so distance-field glyphs: native hinting - // at this size under a scale transform comes out stepped. - renderType: Text.QtRendering + font.letterSpacing: 2 * surface.uiScale + // Display size, where curves beat hinted bitmaps. + renderType: Text.CurveRendering } Text { - leftPadding: 4 + leftPadding: Math.round(4 * surface.uiScale) text: Qt.formatDateTime(clock.date, "dddd · dd MMMM").toUpperCase() color: Theme.subtext font.family: Theme.fontDisplay - font.pixelSize: Theme.fsBody + font.pixelSize: Math.round(Theme.fsBody * surface.uiScale) font.weight: Theme.weightDisplay - font.letterSpacing: 5 + font.letterSpacing: 5 * surface.uiScale renderType: Text.NativeRendering } - Item { width: 1; height: Theme.padXL } + Item { width: 1; height: Math.round(Theme.padXL * surface.uiScale) } // ------------------------------------------------------ status log Column { width: parent.width - leftPadding: 4 + leftPadding: Math.round(4 * surface.uiScale) StatusRow { - width: parent.width - 4 + width: parent.width - parent.leftPadding + uiScale: surface.uiScale visible: Battery.available label: "Battery" detail: Math.round(Battery.percent) + "%" @@ -148,14 +149,16 @@ Item { } StatusRow { - width: parent.width - 4 + width: parent.width - parent.leftPadding + uiScale: surface.uiScale label: "Network" detail: Sys.netUp ? Net.label : "no link" tone: Sys.netUp ? Theme.accentSoft : Theme.overlay } StatusRow { - width: parent.width - 4 + width: parent.width - parent.leftPadding + uiScale: surface.uiScale visible: Media.hasPlayer label: Media.playing ? "Playing" : "Paused" detail: Media.artist ? Media.title + " — " + Media.artist : Media.title @@ -163,7 +166,8 @@ Item { } StatusRow { - width: parent.width - 4 + width: parent.width - parent.leftPadding + uiScale: surface.uiScale visible: Notifs.count > 0 label: "Notices" detail: Notifs.count + (Notifs.count === 1 ? " message" : " messages") @@ -171,20 +175,20 @@ Item { } } - Item { width: 1; height: Theme.padXL } + Item { width: 1; height: Math.round(Theme.padXL * surface.uiScale) } // ------------------------------------------------------ the field Item { id: authBlock width: parent.width - height: field.height + hint.height + Theme.padS + height: field.height + hint.height + hint.anchors.topMargin transform: Translate { id: nudge } Rectangle { id: field width: parent.width - height: 48 + height: Math.round(48 * surface.uiScale) radius: height / 2 color: Theme.alpha(Theme.hull, 0.86) border.width: 1 @@ -199,23 +203,23 @@ Item { Icon { id: lockGlyph anchors.left: parent.left - anchors.leftMargin: Theme.padL + anchors.leftMargin: Math.round(Theme.padL * surface.uiScale) anchors.verticalCenter: parent.verticalCenter text: "󰌾" color: surface.failed ? Theme.alarm : Theme.muted - font.pixelSize: Theme.fsLarge + font.pixelSize: Math.round(Theme.fsLarge * surface.uiScale) } Text { anchors.left: lockGlyph.right - anchors.leftMargin: Theme.padM + anchors.leftMargin: Math.round(Theme.padM * surface.uiScale) anchors.verticalCenter: parent.verticalCenter visible: surface.entry.length === 0 text: "PASSWORD" color: Theme.muted font.family: Theme.fontDisplay - font.pixelSize: Theme.fsSmall - font.letterSpacing: Theme.tracking * 2 + font.pixelSize: Math.round(Theme.fsSmall * surface.uiScale) + font.letterSpacing: Theme.tracking * 2 * surface.uiScale renderType: Text.NativeRendering } @@ -224,11 +228,11 @@ Item { Row { id: dots anchors.left: lockGlyph.right - anchors.leftMargin: Theme.padM + anchors.leftMargin: Math.round(Theme.padM * surface.uiScale) anchors.right: enter.left - anchors.rightMargin: Theme.padM + anchors.rightMargin: Math.round(Theme.padM * surface.uiScale) anchors.verticalCenter: parent.verticalCenter - spacing: 7 + spacing: Math.round(7 * surface.uiScale) clip: true Repeater { @@ -238,8 +242,8 @@ Item { id: pip required property int index anchors.verticalCenter: parent.verticalCenter - width: 7 - height: 7 + width: Math.round(7 * surface.uiScale) + height: width radius: width / 2 color: surface.failed ? Theme.alarm : Theme.accent opacity: 0 @@ -276,10 +280,10 @@ Item { id: enter readonly property bool ready: surface.entry.length > 0 && !surface.busy anchors.right: parent.right - anchors.rightMargin: 8 + anchors.rightMargin: Math.round(8 * surface.uiScale) anchors.verticalCenter: parent.verticalCenter - width: 32 - height: 32 + width: Math.round(32 * surface.uiScale) + height: width radius: width / 2 color: enter.ready ? Theme.accent : "transparent" border.width: enter.ready ? 0 : 1 @@ -293,7 +297,7 @@ Item { anchors.centerIn: parent text: "󰁔" color: enter.ready ? Theme.ink : Theme.overlay - font.pixelSize: Theme.fsLarge + font.pixelSize: Math.round(Theme.fsLarge * surface.uiScale) } TapHandler { @@ -330,16 +334,16 @@ Item { Row { id: hint anchors.top: field.bottom - anchors.topMargin: Theme.padS + anchors.topMargin: Math.round(Theme.padS * surface.uiScale) anchors.left: parent.left - anchors.leftMargin: Theme.padL - spacing: Theme.padS + anchors.leftMargin: Math.round(Theme.padL * surface.uiScale) + spacing: Math.round(Theme.padS * surface.uiScale) Rectangle { anchors.verticalCenter: parent.verticalCenter - width: 4 - height: 4 - radius: 2 + width: Math.round(4 * surface.uiScale) + height: width + radius: width / 2 color: surface.failed ? Theme.alarm : Theme.overlay } @@ -350,8 +354,8 @@ Item { : (surface.busy ? "VERIFYING" : (Quickshell.env("USER") || "user")) color: surface.failed ? Theme.alarm : Theme.muted font.family: Theme.fontMono - font.pixelSize: Theme.fsMicro - font.letterSpacing: 2 + font.pixelSize: Math.round(Theme.fsMicro * surface.uiScale) + font.letterSpacing: 2 * surface.uiScale renderType: Text.NativeRendering } } diff --git a/quickshell/lock/StatusRow.qml b/quickshell/lock/StatusRow.qml index c7ecd3d..85da813 100644 --- a/quickshell/lock/StatusRow.qml +++ b/quickshell/lock/StatusRow.qml @@ -11,15 +11,18 @@ Item { property string label: "" property string detail: "" property color tone: Theme.primaryDim + // The lock face's size factor. The row is laid out at that size rather + // than scaled, so its native text stays sharp. + property real uiScale: 1 - implicitHeight: visible ? 26 : 0 + implicitHeight: visible ? Math.round(26 * row.uiScale) : 0 Rectangle { id: dot anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - width: Theme.marker - height: Theme.marker + width: Math.round(Theme.marker * row.uiScale) + height: width radius: width / 2 color: row.tone } @@ -27,24 +30,24 @@ Item { Text { id: name anchors.left: dot.right - anchors.leftMargin: Theme.padS + anchors.leftMargin: Math.round(Theme.padS * row.uiScale) anchors.verticalCenter: parent.verticalCenter width: Math.min(implicitWidth, row.width * 0.55) text: row.label.toUpperCase() color: Theme.text elide: Text.ElideRight font.family: Theme.fontDisplay - font.pixelSize: Theme.fsSmall + font.pixelSize: Math.round(Theme.fsSmall * row.uiScale) font.weight: Theme.weightDisplay - font.letterSpacing: Theme.tracking + font.letterSpacing: Theme.tracking * row.uiScale renderType: Text.NativeRendering } Rectangle { anchors.left: name.right - anchors.leftMargin: Theme.padS + anchors.leftMargin: Math.round(Theme.padS * row.uiScale) anchors.right: value.left - anchors.rightMargin: Theme.padS + anchors.rightMargin: Math.round(Theme.padS * row.uiScale) anchors.verticalCenter: parent.verticalCenter height: 1 color: Theme.alpha(Theme.outline, 0.4) @@ -60,7 +63,7 @@ Item { elide: Text.ElideRight horizontalAlignment: Text.AlignRight font.family: Theme.fontMono - font.pixelSize: Theme.fsMicro + font.pixelSize: Math.round(Theme.fsMicro * row.uiScale) renderType: Text.NativeRendering } } diff --git a/quickshell/overlays/Osd.qml b/quickshell/overlays/Osd.qml index 29ce158..8906e90 100644 --- a/quickshell/overlays/Osd.qml +++ b/quickshell/overlays/Osd.qml @@ -113,16 +113,20 @@ PanelWindow { slashWidth: 5 padding: Theme.padM + // It rises a few pixels into place instead of growing: a scale would + // stretch the native text in the panel while it moves. opacity: root.shown ? 1 : 0 - scale: root.shown ? 1 : 0.9 + transform: Translate { + y: root.shown ? 0 : 8 + + Behavior on y { + NumberAnimation { duration: Theme.durSlow; easing.type: Easing.OutCubic } + } + } Behavior on opacity { NumberAnimation { id: fade; duration: Theme.durBase } } - Behavior on scale { - NumberAnimation { duration: Theme.durSlow; easing.type: Easing.OutCubic } - } - Icon { id: glyphIcon anchors.left: parent.left