Decode launcher and tray icons at physical resolution

Quickshell's IconImage decodes at logical size, so on HiDPI displays
PNG icons were rendered from a texture at half the needed resolution
and looked blurry next to SVG icons. Use plain Image with sourceSize
scaled by Screen.devicePixelRatio, as the notification widgets
already do.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-07-22 17:02:52 -07:00
co-authored by Claude Fable 5
parent 4c38afeac3
commit 6a047462e8
2 changed files with 18 additions and 9 deletions
+12 -5
View File
@@ -2,7 +2,6 @@ import Quickshell
import QtQuick import QtQuick
import QtQuick.Effects import QtQuick.Effects
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
import Quickshell.Widgets
import qs.Commons import qs.Commons
import qs.Ui import qs.Ui
import "TrayModel.js" as TrayModel import "TrayModel.js" as TrayModel
@@ -502,15 +501,19 @@ BarWidget {
font.pixelSize: Style.font.bodySmall font.pixelSize: Style.font.bodySmall
} }
IconImage { Image {
id: menuIcon id: menuIcon
visible: !menuRow.modelData.isSeparator && String(menuRow.modelData.icon || "") !== "" visible: !menuRow.modelData.isSeparator && String(menuRow.modelData.icon || "") !== ""
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Style.space(24) anchors.leftMargin: Style.space(24)
implicitSize: Style.space(16)
width: Style.space(16) width: Style.space(16)
height: Style.space(16) height: Style.space(16)
fillMode: Image.PreserveAspectFit
// Decode at physical pixels: IconImage uses the logical size,
// which leaves PNG icons upscaled and blurry on HiDPI displays.
sourceSize.width: width * Screen.devicePixelRatio
sourceSize.height: height * Screen.devicePixelRatio
source: menuRow.modelData.icon source: menuRow.modelData.icon
} }
@@ -569,10 +572,14 @@ BarWidget {
required property var icon required property var icon
readonly property bool symbolic: root.iconIsSymbolic(icon) readonly property bool symbolic: root.iconIsSymbolic(icon)
IconImage { Image {
id: trayIconImage id: trayIconImage
anchors.fill: parent anchors.fill: parent
implicitSize: Math.round(Math.min(parent.width, parent.height)) fillMode: Image.PreserveAspectFit
// Decode at physical pixels: IconImage uses the logical size,
// which leaves PNG icons upscaled and blurry on HiDPI displays.
sourceSize.width: Math.round(Math.min(width, height) * Screen.devicePixelRatio)
sourceSize.height: Math.round(Math.min(width, height) * Screen.devicePixelRatio)
source: root.trayIconSource(trayIconRoot.icon) source: root.trayIconSource(trayIconRoot.icon)
// Kept as a hidden layer so the effect can sample it as a texture. // Kept as a hidden layer so the effect can sample it as a texture.
visible: !trayIconRoot.symbolic visible: !trayIconRoot.symbolic
+6 -4
View File
@@ -1,7 +1,6 @@
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Widgets
import QtQuick import QtQuick
import qs.Commons import qs.Commons
import qs.Ui import qs.Ui
@@ -591,15 +590,18 @@ Item {
width: root.iconSlotWidth width: root.iconSlotWidth
height: parent.height height: parent.height
IconImage { Image {
id: appIcon id: appIcon
anchors.centerIn: parent anchors.centerIn: parent
implicitSize: root.iconSize
width: root.iconSize width: root.iconSize
height: root.iconSize height: root.iconSize
fillMode: Image.PreserveAspectFit
// Decode at physical pixels: IconImage uses the logical size,
// which leaves PNG icons upscaled and blurry on HiDPI displays.
sourceSize.width: root.iconSize * Screen.devicePixelRatio
sourceSize.height: root.iconSize * Screen.devicePixelRatio
source: root.iconSource(row.icon) source: root.iconSource(row.icon)
asynchronous: true asynchronous: true
mipmap: true
} }
Text { Text {