Compositor: proper monitor scaling detection and display in settings + fixes blurry wallpapers on compositor scaled monitors.

This commit is contained in:
ItsLemmy
2025-10-11 10:29:28 -04:00
parent 656e15f589
commit 54fa04f303
7 changed files with 337 additions and 63 deletions
+1 -1
View File
@@ -1394,7 +1394,7 @@
"system": {
"uptime": "Uptime: {uptime}",
"welcome-back": "Welcome back,",
"monitor-description": "{model} ({width}x{height})",
"monitor-description": "{model} ({width}x{height} @ {scale}x)",
"scaling-percentage": "{percentage}%",
"location-display": "{name} ({coordinates})",
"signal-strength": "{signal}%",
+44 -55
View File
@@ -44,19 +44,6 @@ Variants {
property real fillMode: WallpaperService.getFillModeUniform()
property vector4d fillColor: Qt.vector4d(Settings.data.wallpaper.fillColor.r, Settings.data.wallpaper.fillColor.g, Settings.data.wallpaper.fillColor.b, 1.0)
property int monitoredWidth: modelData.width
property int monitoredHeight: modelData.height
onMonitoredWidthChanged: {
Logger.log("Background", "Screen width changed to:", monitoredWidth, "for", modelData.name)
recalculateImageSizes()
}
onMonitoredHeightChanged: {
Logger.log("Background", "Screen height changed to:", monitoredHeight, "for", modelData.name)
recalculateImageSizes()
}
Component.onCompleted: setWallpaperInitial()
Component.onDestruction: {
@@ -87,6 +74,13 @@ Variants {
}
}
Connections {
target: CompositorService
function onDisplayScalesChanged() {
setWallpaperInitial()
}
}
color: Color.transparent
screen: modelData
WlrLayershell.layer: WlrLayer.Background
@@ -122,38 +116,21 @@ Variants {
cache: false
asynchronous: true
sourceSize: undefined
onStatusChanged: {
if (status === Image.Error) {
Logger.warn("Current wallpaper failed to load:", source)
} else if (status === Image.Ready && !dimensionsCalculated) {
dimensionsCalculated = true
calculateSourceSize()
const optimalSize = calculateOptimalWallpaperSize(implicitWidth, implicitHeight)
if (optimalSize !== false) {
sourceSize = optimalSize
}
}
}
onSourceChanged: {
dimensionsCalculated = false
sourceSize = undefined
}
function calculateSourceSize() {
if (implicitWidth === modelData.width || implicitHeight === modelData.height) {
// Do not resize if one of the dimensions fits perfectly on the screen
return
}
if (implicitWidth > 0 && implicitHeight > 0) {
const imageAspectRatio = implicitWidth / implicitHeight
if (modelData.width >= modelData.height) {
const w = Math.min(modelData.width, implicitWidth)
sourceSize = Qt.size(w, w / imageAspectRatio)
} else {
const h = Math.min(modelData.height, implicitHeight)
sourceSize = Qt.size(h * imageAspectRatio, h)
}
}
}
}
Image {
@@ -168,38 +145,21 @@ Variants {
cache: false
asynchronous: true
sourceSize: undefined
onStatusChanged: {
if (status === Image.Error) {
Logger.warn("Next wallpaper failed to load:", source)
} else if (status === Image.Ready && !dimensionsCalculated) {
dimensionsCalculated = true
calculateSourceSize()
const optimalSize = calculateOptimalWallpaperSize(implicitWidth, implicitHeight)
if (optimalSize !== false) {
sourceSize = optimalSize
}
}
}
onSourceChanged: {
dimensionsCalculated = false
sourceSize = undefined
}
function calculateSourceSize() {
if (implicitWidth === modelData.width || implicitHeight === modelData.height) {
// Do not resize if one of the dimensions fits perfectly on the screen
return
}
if (implicitWidth > 0 && implicitHeight > 0) {
const imageAspectRatio = implicitWidth / implicitHeight
if (modelData.width >= modelData.height) {
const w = Math.min(modelData.width, implicitWidth)
sourceSize = Qt.size(w, w / imageAspectRatio)
} else {
const h = Math.min(modelData.height, implicitHeight)
sourceSize = Qt.size(h * imageAspectRatio, h)
}
}
}
}
// Dynamic shader loader - only loads the active transition shader
@@ -356,6 +316,31 @@ Variants {
}
}
// ------------------------------------------------------
function calculateOptimalWallpaperSize(wpWidth, wpHeight) {
const compositorScale = CompositorService.getDisplayScale(modelData.name)
const screenWidth = modelData.width * compositorScale
const screenHeight = modelData.height * compositorScale
if (wpWidth <= screenWidth || wpHeight <= screenHeight || wpWidth <= 0 || wpHeight <= 0) {
// Do not resize if wallpaper is smaller than one of the screen dimension
return
}
const imageAspectRatio = wpWidth / wpHeight
var dim = Qt.size(0, 0)
if (screenWidth >= screenHeight) {
const w = Math.min(screenWidth, wpWidth)
dim = Qt.size(w, w / imageAspectRatio)
} else {
const h = Math.min(screenHeight, wpHeight)
dim = Qt.size(h * imageAspectRatio, h)
}
Logger.log("Background", `Wallpaper resized on ${modelData.name} ${screenWidth}x${screenHeight} @ ${compositorScale}x`, "src:", wpWidth, wpHeight, "dst:", dim.width, dim.height)
return dim
}
// ------------------------------------------------------
function recalculateImageSizes() {
if (currentWallpaper.status === Image.Ready) {
currentWallpaper.calculateSourceSize()
@@ -365,6 +350,7 @@ Variants {
}
}
// ------------------------------------------------------
function setWallpaperInitial() {
// On startup, defer assigning wallpaper until the service cache is ready, retries every tick
if (!WallpaperService || !WallpaperService.isInitialized) {
@@ -375,6 +361,7 @@ Variants {
setWallpaperImmediate(WallpaperService.getWallpaper(modelData.name))
}
// ------------------------------------------------------
function setWallpaperImmediate(source) {
transitionAnimation.stop()
transitionProgress = 0.0
@@ -388,6 +375,7 @@ Variants {
})
}
// ------------------------------------------------------
function setWallpaperWithTransition(source) {
if (source === currentWallpaper.source) {
return
@@ -421,6 +409,7 @@ Variants {
transitionAnimation.start()
}
// ------------------------------------------------------
// Main method that actually trigger the wallpaper change
function changeWallpaper() {
// Get the transitionType from the settings
+9 -5
View File
@@ -90,11 +90,15 @@ ColumnLayout {
NLabel {
label: modelData.name || "Unknown"
description: I18n.tr("system.monitor-description", {
"model": modelData.model,
"width": modelData.width,
"height": modelData.height
})
description: {
const compositorScale = CompositorService.getDisplayScale(modelData.name)
I18n.tr("system.monitor-description", {
"model": modelData.model,
"width": modelData.width * compositorScale,
"height": modelData.height * compositorScale,
"scale": compositorScale
})
}
}
// Scale
+85
View File
@@ -2,6 +2,7 @@ pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services
@@ -18,6 +19,10 @@ Singleton {
property ListModel windows: ListModel {}
property int focusedWindowIndex: -1
// Display scale data
property var displayScales: ({})
property bool displayScalesLoaded: false
// Generic events
signal workspaceChanged
signal activeWindowChanged
@@ -26,7 +31,18 @@ Singleton {
// Backend service loader
property var backend: null
// Cache file path
property string displayCachePath: ""
Component.onCompleted: {
// Setup cache path (needs Settings to be available)
Qt.callLater(() => {
if (typeof Settings !== 'undefined' && Settings.cacheDir) {
displayCachePath = Settings.cacheDir + "display.json"
displayCacheFileView.path = displayCachePath
}
})
detectCompositor()
}
@@ -69,6 +85,31 @@ Singleton {
}
}
// Cache FileView for display scales
FileView {
id: displayCacheFileView
printErrors: false
watchChanges: false
adapter: JsonAdapter {
id: displayCacheAdapter
property var displays: ({})
}
onLoaded: {
// Load cached display scales
displayScales = displayCacheAdapter.displays || {}
displayScalesLoaded = true
// Logger.log("CompositorService", "Loaded display scales from cache:", JSON.stringify(displayScales))
}
onLoadFailed: {
// Cache doesn't exist yet, will be created on first update
displayScalesLoaded = true
// Logger.log("CompositorService", "No display cache found, will create on first update")
}
}
// Hyprland backend component
Component {
id: hyprlandComponent
@@ -151,6 +192,50 @@ Singleton {
windowListChanged()
}
// Update display scales from backend
function updateDisplayScales() {
if (!backend || !backend.queryDisplayScales) {
Logger.warn("CompositorService", "Backend does not support display scale queries")
return
}
backend.queryDisplayScales()
}
// Called by backend when display scales are ready
function onDisplayScalesUpdated(scales) {
displayScales = scales
saveDisplayScalesToCache()
displayScalesChanged()
Logger.log("CompositorService", "Display scales updated")
}
// Save display scales to cache
function saveDisplayScalesToCache() {
if (!displayCachePath) {
return
}
displayCacheAdapter.displays = displayScales
displayCacheFileView.writeAdapter()
}
// Public function to get scale for a specific display
function getDisplayScale(displayName) {
if (!displayName || !displayScales[displayName]) {
return 1.0
}
return displayScales[displayName].scale || 1.0
}
// Public function to get all display info for a specific display
function getDisplayInfo(displayName) {
if (!displayName || !displayScales[displayName]) {
return null
}
return displayScales[displayName]
}
// Get focused window
function getFocusedWindow() {
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.count) {
+70 -1
View File
@@ -1,6 +1,7 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import qs.Commons
Item {
@@ -15,6 +16,7 @@ Item {
signal workspaceChanged
signal activeWindowChanged
signal windowListChanged
signal displayScalesChanged
// Hyprland-specific properties
property bool initialized: false
@@ -40,6 +42,7 @@ Item {
Qt.callLater(() => {
safeUpdateWorkspaces()
safeUpdateWindows()
queryDisplayScales()
})
initialized = true
Logger.log("HyprlandService", "Initialized successfully")
@@ -48,6 +51,67 @@ Item {
}
}
// Query display scales
function queryDisplayScales() {
hyprlandMonitorsProcess.running = true
}
// Hyprland monitors process for display scale detection
// Hyprland monitors process for display scale detection
Process {
id: hyprlandMonitorsProcess
running: false
command: ["hyprctl", "monitors", "-j"]
property string accumulatedOutput: ""
stdout: SplitParser {
onRead: function (line) {
// Accumulate lines instead of parsing each one
hyprlandMonitorsProcess.accumulatedOutput += line
}
}
onExited: function (exitCode) {
if (exitCode !== 0 || !accumulatedOutput) {
Logger.error("HyprlandService", "Failed to query monitors, exit code:", exitCode)
accumulatedOutput = ""
return
}
try {
const monitorsData = JSON.parse(accumulatedOutput)
const scales = {}
for (const monitor of monitorsData) {
if (monitor.name) {
scales[monitor.name] = {
"name": monitor.name,
"scale": monitor.scale || 1.0,
"width": monitor.width || 0,
"height": monitor.height || 0,
"refresh_rate": monitor.refreshRate || 0,
"x": monitor.x || 0,
"y": monitor.y || 0,
"active_workspace": monitor.activeWorkspace ? monitor.activeWorkspace.id : -1,
"vrr": monitor.vrr || false,
"focused": monitor.focused || false
}
}
}
// Notify CompositorService (it will emit displayScalesChanged)
if (CompositorService && CompositorService.onDisplayScalesUpdated) {
CompositorService.onDisplayScalesUpdated(scales)
}
} catch (e) {
Logger.error("HyprlandService", "Failed to parse monitors:", e)
} finally {
// Clear accumulated output for next query
accumulatedOutput = ""
}
}
}
// Safe update wrapper
function safeUpdate() {
safeUpdateWindows()
@@ -188,7 +252,7 @@ Item {
"id": windowId,
"title": title,
"appId": appId,
"workspaceId": wsId,
"workspaceId": wsId || -1,
"isFocused": focused,
"output": output
}
@@ -268,6 +332,11 @@ Item {
safeUpdateWorkspaces()
workspaceChanged()
updateTimer.restart()
const monitorsEvents = ["configreloaded", "monitoradded", "monitorremoved", "monitoraddedv2", "monitorremovedv2"]
if (monitorsEvents.includes(event.name)) {
Qt.callLater(queryDisplayScales)
}
}
}
+61 -1
View File
@@ -20,12 +20,14 @@ Item {
signal workspaceChanged
signal activeWindowChanged
signal windowListChanged
signal displayScalesChanged
// Initialization
function initialize() {
niriEventStream.running = true
updateWorkspaces()
updateWindows()
queryDisplayScales()
Logger.log("NiriService", "Initialized successfully")
}
@@ -39,6 +41,60 @@ Item {
niriWindowsProcess.running = true
}
// Query display scales
function queryDisplayScales() {
niriOutputsProcess.running = true
}
// Niri outputs process for display scale detection
Process {
id: niriOutputsProcess
running: false
command: ["niri", "msg", "--json", "outputs"]
stdout: SplitParser {
onRead: function (line) {
try {
const outputsData = JSON.parse(line)
const scales = {}
// Niri returns an object with display names as keys
for (const outputName in outputsData) {
const output = outputsData[outputName]
if (output && output.name) {
const logical = output.logical || {}
const currentModeIdx = output.current_mode || 0
const modes = output.modes || []
const currentMode = modes[currentModeIdx] || {}
scales[output.name] = {
"name": output.name,
"scale": logical.scale || 1.0,
"width": logical.width || 0,
"height": logical.height || 0,
"x": logical.x || 0,
"y": logical.y || 0,
"physical_width": (output.physical_size && output.physical_size[0]) || 0,
"physical_height": (output.physical_size && output.physical_size[1]) || 0,
"refresh_rate": currentMode.refresh_rate || 0,
"vrr_supported": output.vrr_supported || false,
"vrr_enabled": output.vrr_enabled || false,
"transform": logical.transform || "Normal"
}
}
}
// Notify CompositorService (it will emit displayScalesChanged)
if (CompositorService && CompositorService.onDisplayScalesUpdated) {
CompositorService.onDisplayScalesUpdated(scales)
}
} catch (e) {
Logger.error("NiriService", "Failed to parse outputs:", e, line)
}
}
}
}
// Niri workspace process
Process {
id: niriWorkspaceProcess
@@ -86,7 +142,7 @@ Item {
}
}
// Niri windows process (for initial load)
// Niri windows process
Process {
id: niriWindowsProcess
running: false
@@ -131,6 +187,10 @@ Item {
handleWindowLayoutsChanged(event.WindowLayoutsChanged)
} else if (event.OverviewOpenedOrClosed) {
handleOverviewOpenedOrClosed(event.OverviewOpenedOrClosed)
} else if (event.OutputsChanged) {
queryDisplayScales()
} else if (event.ConfigLoaded) {
queryDisplayScales()
}
} catch (e) {
Logger.error("NiriService", "Error parsing event stream:", e, data)
+67
View File
@@ -2,6 +2,7 @@ import QtQuick
import Quickshell
import Quickshell.I3
import Quickshell.Wayland
import Quickshell.Io
import qs.Commons
Item {
@@ -16,6 +17,7 @@ Item {
signal workspaceChanged
signal activeWindowChanged
signal windowListChanged
signal displayScalesChanged
// I3-specific properties
property bool initialized: false
@@ -38,6 +40,7 @@ Item {
Qt.callLater(() => {
safeUpdateWorkspaces()
safeUpdateWindows()
queryDisplayScales()
})
initialized = true
Logger.log("SwayService", "Initialized successfully")
@@ -46,6 +49,66 @@ Item {
}
}
// Query display scales
function queryDisplayScales() {
swayOutputsProcess.running = true
}
// Sway outputs process for display scale detection
Process {
id: swayOutputsProcess
running: false
command: ["swaymsg", "-t", "get_outputs", "-r"]
property string accumulatedOutput: ""
stdout: SplitParser {
onRead: function (line) {
swayOutputsProcess.accumulatedOutput += line
}
}
onExited: function (exitCode) {
if (exitCode !== 0 || !accumulatedOutput) {
Logger.error("SwayService", "Failed to query outputs, exit code:", exitCode)
accumulatedOutput = ""
return
}
try {
const outputsData = JSON.parse(accumulatedOutput)
const scales = {}
for (const output of outputsData) {
if (output.name) {
scales[output.name] = {
"name": output.name,
"scale": output.scale || 1.0,
"width": output.current_mode ? output.current_mode.width : 0,
"height": output.current_mode ? output.current_mode.height : 0,
"refresh_rate": output.current_mode ? output.current_mode.refresh : 0,
"x": output.rect ? output.rect.x : 0,
"y": output.rect ? output.rect.y : 0,
"active": output.active || false,
"focused": output.focused || false,
"current_workspace": output.current_workspace || ""
}
}
}
// Notify CompositorService (it will emit displayScalesChanged)
if (CompositorService && CompositorService.onDisplayScalesUpdated) {
CompositorService.onDisplayScalesUpdated(scales)
}
} catch (e) {
Logger.error("SwayService", "Failed to parse outputs:", e)
} finally {
// Clear accumulated output for next query
accumulatedOutput = ""
}
}
}
// Safe update wrapper
function safeUpdate() {
safeUpdateWindows()
@@ -197,6 +260,10 @@ Item {
safeUpdateWorkspaces()
workspaceChanged()
updateTimer.restart()
if (event.type === "output") {
Qt.callLater(queryDisplayScales)
}
}
}