Keep overlay surfaces mapped so they open sharp (#13419)

The menu, emoji picker, clipboard, OSD, reminder flow and Wi-Fi QR code
mapped a fresh surface on every open. Qt drew its first frames before
Hyprland sent the surface's fractional scale: the pixel ratio stepped
2, 1, then 1.6, so on a 1.6 display each overlay showed blurry for
~350ms before going sharp.

A new OverlayWindow keeps the surface. Hidden, it parks as a 1x1,
input-less layer below windows, off the overlay layer so it never
blocks direct scanout. Showing only resizes and raises it, so the scale
is already settled. Content stays hidden until the surface has grown,
so no 1x1 frame is stretched across the screen, and the window follows
the focused monitor each time it is shown. The image picker's own
fullscreen parking moves onto it too.

Each parked overlay keeps a Qt window alive: at rest the shell holds
~40 MiB more RSS and ~20 MiB more GPU memory.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier HanssonandClaude Opus 5.5 authored and GitHub committed 2026-09-27 18:03:01 +02:00
1 parent 6762769bbf
commit 48de7823da
14 files changed
+164 -96

No files matched your search

+4 -30
View File
@@ -1,11 +1,11 @@
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import Quickshell.Wayland
import QtQuick
import QtQuick.Effects
import QtQuick.Shapes
import qs.Commons
import qs.Ui
import "ImagePickerModel.js" as ImagePickerModel
Item {
@@ -36,9 +36,6 @@ Item {
property string themeRows: ""
property bool themeMode: false
property bool themeOpenPending: false
// The surface outlives each open, so it no longer lands on the focused
// output by itself. Unset until the first open lets the compositor choose.
property var targetScreen: null
// Bound to the central [image-picker] section in shell.toml via Color.qml.
// `dimColor` tints unselected slices and text outlines on top of the scrim;
// it intentionally tracks the foundational background, not a surface role.
@@ -234,7 +231,6 @@ Item {
requestSerial += 1
themeMode = false
themeOpenPending = false
targetScreen = focusedScreen() || targetScreen
imageDirs = nextImageDirs
imageRows = nextImageRows
@@ -278,14 +274,6 @@ Item {
property var imageArray: []
function focusedScreen() {
var monitor = Hyprland.focusedMonitor
var name = monitor ? String(monitor.name || "") : ""
for (var i = 0; i < Quickshell.screens.length; i++) {
if (Quickshell.screens[i].name === name) return Quickshell.screens[i]
}
return null
}
function currentThemePreview() {
var name = String(themeNameFile.text() || "").trim()
@@ -456,25 +444,11 @@ Item {
onExited: root.releaseNextDoneFile()
}
PanelWindow {
OverlayWindow {
id: panel
// Stay mapped between opens. A fresh surface renders its first frames
// before the compositor sends its fractional scale, so the picker flashed
// blurry, and it had to re-upload every thumbnail texture. Closed, it
// waits transparent and input-less on the bottom layer: anything left on
// the overlay layer would block direct scanout for fullscreen apps.
visible: true
screen: root.targetScreen
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
mask: root.opened ? null : closedMask
shown: root.opened
shownKeyboardFocus: root.imagesLoaded ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
WlrLayershell.namespace: "omarchy-image-selector"
WlrLayershell.layer: root.opened ? WlrLayer.Overlay : WlrLayer.Bottom
WlrLayershell.keyboardFocus: root.opened && root.imagesLoaded ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
exclusionMode: ExclusionMode.Ignore
Region { id: closedMask }
Rectangle {
anchors.fill: parent