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 <noreply@anthropic.com>
This commit is contained in:
nvrlandClaude Opus 5.5 committed 2026-10-04 21:58:00 +02:00
1 parent f1d16e5175
commit b86e584b02
4 files changed
+76 -61

No files matched your search

+5 -1
View File
@@ -13,7 +13,11 @@ Text {
color: Theme.primary color: Theme.primary
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter 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 { Behavior on color {
ColorAnimation { duration: Theme.durBase } ColorAnimation { duration: Theme.durBase }
+50 -46
View File
@@ -92,11 +92,12 @@ Item {
Column { Column {
id: face id: face
x: parent.width * 0.14 // Sized by uiScale rather than drawn small and stretched with `scale`:
y: parent.height * 0.2 // a scale transform blurs native text, and every label here is native.
width: 380 // Whole pixels, too, or the glyphs land between them.
scale: surface.uiScale x: Math.round(parent.width * 0.14)
transformOrigin: Item.TopLeft y: Math.round(parent.height * 0.2)
width: Math.round(380 * surface.uiScale)
opacity: 0 opacity: 0
spacing: 0 spacing: 0
@@ -104,42 +105,42 @@ Item {
// The house mark, the same porthole the bar carries far left. // The house mark, the same porthole the bar carries far left.
Porthole { Porthole {
size: 26 size: Math.round(26 * surface.uiScale)
} }
Text { Text {
topPadding: Theme.padM topPadding: Math.round(Theme.padM * surface.uiScale)
text: Qt.formatDateTime(clock.date, "HH:mm") text: Qt.formatDateTime(clock.date, "HH:mm")
color: Theme.accent color: Theme.accent
font.family: Theme.fontDisplay 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.weight: Font.Light
font.letterSpacing: 2 font.letterSpacing: 2 * surface.uiScale
// Scaled with the face, so distance-field glyphs: native hinting // Display size, where curves beat hinted bitmaps.
// at this size under a scale transform comes out stepped. renderType: Text.CurveRendering
renderType: Text.QtRendering
} }
Text { Text {
leftPadding: 4 leftPadding: Math.round(4 * surface.uiScale)
text: Qt.formatDateTime(clock.date, "dddd · dd MMMM").toUpperCase() text: Qt.formatDateTime(clock.date, "dddd · dd MMMM").toUpperCase()
color: Theme.subtext color: Theme.subtext
font.family: Theme.fontDisplay font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody font.pixelSize: Math.round(Theme.fsBody * surface.uiScale)
font.weight: Theme.weightDisplay font.weight: Theme.weightDisplay
font.letterSpacing: 5 font.letterSpacing: 5 * surface.uiScale
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
Item { width: 1; height: Theme.padXL } Item { width: 1; height: Math.round(Theme.padXL * surface.uiScale) }
// ------------------------------------------------------ status log // ------------------------------------------------------ status log
Column { Column {
width: parent.width width: parent.width
leftPadding: 4 leftPadding: Math.round(4 * surface.uiScale)
StatusRow { StatusRow {
width: parent.width - 4 width: parent.width - parent.leftPadding
uiScale: surface.uiScale
visible: Battery.available visible: Battery.available
label: "Battery" label: "Battery"
detail: Math.round(Battery.percent) + "%" detail: Math.round(Battery.percent) + "%"
@@ -148,14 +149,16 @@ Item {
} }
StatusRow { StatusRow {
width: parent.width - 4 width: parent.width - parent.leftPadding
uiScale: surface.uiScale
label: "Network" label: "Network"
detail: Sys.netUp ? Net.label : "no link" detail: Sys.netUp ? Net.label : "no link"
tone: Sys.netUp ? Theme.accentSoft : Theme.overlay tone: Sys.netUp ? Theme.accentSoft : Theme.overlay
} }
StatusRow { StatusRow {
width: parent.width - 4 width: parent.width - parent.leftPadding
uiScale: surface.uiScale
visible: Media.hasPlayer visible: Media.hasPlayer
label: Media.playing ? "Playing" : "Paused" label: Media.playing ? "Playing" : "Paused"
detail: Media.artist ? Media.title + " — " + Media.artist : Media.title detail: Media.artist ? Media.title + " — " + Media.artist : Media.title
@@ -163,7 +166,8 @@ Item {
} }
StatusRow { StatusRow {
width: parent.width - 4 width: parent.width - parent.leftPadding
uiScale: surface.uiScale
visible: Notifs.count > 0 visible: Notifs.count > 0
label: "Notices" label: "Notices"
detail: Notifs.count + (Notifs.count === 1 ? " message" : " messages") 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 // ------------------------------------------------------ the field
Item { Item {
id: authBlock id: authBlock
width: parent.width width: parent.width
height: field.height + hint.height + Theme.padS height: field.height + hint.height + hint.anchors.topMargin
transform: Translate { id: nudge } transform: Translate { id: nudge }
Rectangle { Rectangle {
id: field id: field
width: parent.width width: parent.width
height: 48 height: Math.round(48 * surface.uiScale)
radius: height / 2 radius: height / 2
color: Theme.alpha(Theme.hull, 0.86) color: Theme.alpha(Theme.hull, 0.86)
border.width: 1 border.width: 1
@@ -199,23 +203,23 @@ Item {
Icon { Icon {
id: lockGlyph id: lockGlyph
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Theme.padL anchors.leftMargin: Math.round(Theme.padL * surface.uiScale)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "󰌾" text: "󰌾"
color: surface.failed ? Theme.alarm : Theme.muted color: surface.failed ? Theme.alarm : Theme.muted
font.pixelSize: Theme.fsLarge font.pixelSize: Math.round(Theme.fsLarge * surface.uiScale)
} }
Text { Text {
anchors.left: lockGlyph.right anchors.left: lockGlyph.right
anchors.leftMargin: Theme.padM anchors.leftMargin: Math.round(Theme.padM * surface.uiScale)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: surface.entry.length === 0 visible: surface.entry.length === 0
text: "PASSWORD" text: "PASSWORD"
color: Theme.muted color: Theme.muted
font.family: Theme.fontDisplay font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall font.pixelSize: Math.round(Theme.fsSmall * surface.uiScale)
font.letterSpacing: Theme.tracking * 2 font.letterSpacing: Theme.tracking * 2 * surface.uiScale
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
@@ -224,11 +228,11 @@ Item {
Row { Row {
id: dots id: dots
anchors.left: lockGlyph.right anchors.left: lockGlyph.right
anchors.leftMargin: Theme.padM anchors.leftMargin: Math.round(Theme.padM * surface.uiScale)
anchors.right: enter.left anchors.right: enter.left
anchors.rightMargin: Theme.padM anchors.rightMargin: Math.round(Theme.padM * surface.uiScale)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: 7 spacing: Math.round(7 * surface.uiScale)
clip: true clip: true
Repeater { Repeater {
@@ -238,8 +242,8 @@ Item {
id: pip id: pip
required property int index required property int index
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: 7 width: Math.round(7 * surface.uiScale)
height: 7 height: width
radius: width / 2 radius: width / 2
color: surface.failed ? Theme.alarm : Theme.accent color: surface.failed ? Theme.alarm : Theme.accent
opacity: 0 opacity: 0
@@ -276,10 +280,10 @@ Item {
id: enter id: enter
readonly property bool ready: surface.entry.length > 0 && !surface.busy readonly property bool ready: surface.entry.length > 0 && !surface.busy
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 8 anchors.rightMargin: Math.round(8 * surface.uiScale)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: 32 width: Math.round(32 * surface.uiScale)
height: 32 height: width
radius: width / 2 radius: width / 2
color: enter.ready ? Theme.accent : "transparent" color: enter.ready ? Theme.accent : "transparent"
border.width: enter.ready ? 0 : 1 border.width: enter.ready ? 0 : 1
@@ -293,7 +297,7 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
text: "󰁔" text: "󰁔"
color: enter.ready ? Theme.ink : Theme.overlay color: enter.ready ? Theme.ink : Theme.overlay
font.pixelSize: Theme.fsLarge font.pixelSize: Math.round(Theme.fsLarge * surface.uiScale)
} }
TapHandler { TapHandler {
@@ -330,16 +334,16 @@ Item {
Row { Row {
id: hint id: hint
anchors.top: field.bottom anchors.top: field.bottom
anchors.topMargin: Theme.padS anchors.topMargin: Math.round(Theme.padS * surface.uiScale)
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Theme.padL anchors.leftMargin: Math.round(Theme.padL * surface.uiScale)
spacing: Theme.padS spacing: Math.round(Theme.padS * surface.uiScale)
Rectangle { Rectangle {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: 4 width: Math.round(4 * surface.uiScale)
height: 4 height: width
radius: 2 radius: width / 2
color: surface.failed ? Theme.alarm : Theme.overlay color: surface.failed ? Theme.alarm : Theme.overlay
} }
@@ -350,8 +354,8 @@ Item {
: (surface.busy ? "VERIFYING" : (Quickshell.env("USER") || "user")) : (surface.busy ? "VERIFYING" : (Quickshell.env("USER") || "user"))
color: surface.failed ? Theme.alarm : Theme.muted color: surface.failed ? Theme.alarm : Theme.muted
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Math.round(Theme.fsMicro * surface.uiScale)
font.letterSpacing: 2 font.letterSpacing: 2 * surface.uiScale
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
} }
+12 -9
View File
@@ -11,15 +11,18 @@ Item {
property string label: "" property string label: ""
property string detail: "" property string detail: ""
property color tone: Theme.primaryDim 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 { Rectangle {
id: dot id: dot
anchors.left: parent.left anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: Theme.marker width: Math.round(Theme.marker * row.uiScale)
height: Theme.marker height: width
radius: width / 2 radius: width / 2
color: row.tone color: row.tone
} }
@@ -27,24 +30,24 @@ Item {
Text { Text {
id: name id: name
anchors.left: dot.right anchors.left: dot.right
anchors.leftMargin: Theme.padS anchors.leftMargin: Math.round(Theme.padS * row.uiScale)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: Math.min(implicitWidth, row.width * 0.55) width: Math.min(implicitWidth, row.width * 0.55)
text: row.label.toUpperCase() text: row.label.toUpperCase()
color: Theme.text color: Theme.text
elide: Text.ElideRight elide: Text.ElideRight
font.family: Theme.fontDisplay font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall font.pixelSize: Math.round(Theme.fsSmall * row.uiScale)
font.weight: Theme.weightDisplay font.weight: Theme.weightDisplay
font.letterSpacing: Theme.tracking font.letterSpacing: Theme.tracking * row.uiScale
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
Rectangle { Rectangle {
anchors.left: name.right anchors.left: name.right
anchors.leftMargin: Theme.padS anchors.leftMargin: Math.round(Theme.padS * row.uiScale)
anchors.right: value.left anchors.right: value.left
anchors.rightMargin: Theme.padS anchors.rightMargin: Math.round(Theme.padS * row.uiScale)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
height: 1 height: 1
color: Theme.alpha(Theme.outline, 0.4) color: Theme.alpha(Theme.outline, 0.4)
@@ -60,7 +63,7 @@ Item {
elide: Text.ElideRight elide: Text.ElideRight
horizontalAlignment: Text.AlignRight horizontalAlignment: Text.AlignRight
font.family: Theme.fontMono font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro font.pixelSize: Math.round(Theme.fsMicro * row.uiScale)
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
} }
+9 -5
View File
@@ -113,16 +113,20 @@ PanelWindow {
slashWidth: 5 slashWidth: 5
padding: Theme.padM 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 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 { Behavior on opacity {
NumberAnimation { id: fade; duration: Theme.durBase } NumberAnimation { id: fade; duration: Theme.durBase }
} }
Behavior on scale {
NumberAnimation { duration: Theme.durSlow; easing.type: Easing.OutCubic }
}
Icon { Icon {
id: glyphIcon id: glyphIcon
anchors.left: parent.left anchors.left: parent.left