From 7c89aa438af5b72338fe83292d5168e1381d81e6 Mon Sep 17 00:00:00 2001 From: Ahmed Alghali Date: Mon, 12 Oct 2026 18:24:26 +0300 Subject: [PATCH] Fix the background snapping to a darker, saturated look when opening The blurred wallpaper was only requested when the window opened and rendered in the background (for aerial wallpapers, by decoding a video frame). Until it arrived the system blur showed instead; then the wallpaper replaced it in a single frame, with a saturation boost on top. - Render the blurred wallpaper ahead of time: at launch, on Space and display changes, and when the wallpaper store changes - On a cold start, wait up to 0.25 s for it before fading in; if it's later, cross-fade it in instead of swapping - Use the system blur only when the wallpaper can't be read - Drop the saturation boost; Launchpad only blurred and dimmed the wallpaper --- Sources/Relaunch/AppDelegate.swift | 62 +++++++++++++++++++++++++++--- Sources/Relaunch/Views.swift | 5 ++- Sources/Relaunch/Wallpaper.swift | 15 +++++++- 3 files changed, 75 insertions(+), 7 deletions(-) diff --git a/Sources/Relaunch/AppDelegate.swift b/Sources/Relaunch/AppDelegate.swift index cae30bf..6da8ef4 100644 --- a/Sources/Relaunch/AppDelegate.swift +++ b/Sources/Relaunch/AppDelegate.swift @@ -53,6 +53,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { installHotKey() installHotCorner() watchApplicationFolders() + watchWallpaper() let nc = NotificationCenter.default nc.addObserver(forName: NSApplication.didResignActiveNotification, object: nil, queue: .main) { [weak self] _ in @@ -61,9 +62,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate { self.model.hide() } nc.addObserver(forName: NSApplication.didChangeScreenParametersNotification, object: nil, queue: .main) { [weak self] _ in + Wallpaper.prewarm() guard let self, self.isShown, let screen = self.window.screen else { return } self.place(on: screen) } + // Each Space can have its own wallpaper. + NSWorkspace.shared.notificationCenter.addObserver( + forName: NSWorkspace.activeSpaceDidChangeNotification, object: nil, queue: .main) { _ in + Wallpaper.prewarm() + } if !launchedAtLogin { show() } } @@ -83,15 +90,37 @@ final class AppDelegate: NSObject, NSApplicationDelegate { let mouse = NSEvent.mouseLocation let screen = NSScreen.screens.first { NSMouseInRect(mouse, $0.frame, false) } ?? NSScreen.main! place(on: screen) - Wallpaper.blurred(for: screen) { [weak self] image in self?.model.wallpaper = image } model.prepareToShow() + + // The background must be final before the fade starts. Normally it was + // rendered ahead of time; on a cold start, wait briefly for it, and if + // it's still late, cross-fade it in rather than swapping it. + var started = false + let start = { [weak self] in + guard let self, !started else { return } + started = true + self.model.animateIn() + debugLog("animate in, wallpaper ready=\(self.model.wallpaper != nil) key=\(self.window.isKeyWindow)") + } + if let ready = Wallpaper.cached(for: screen) { + model.wallpaper = ready + DispatchQueue.main.async(execute: start) + } else { + Wallpaper.blurred(for: screen) { [weak self] image in + guard let self else { return } + if started { + withAnimation(.easeInOut(duration: 0.35)) { self.model.wallpaper = image } + } else { + self.model.wallpaper = image + DispatchQueue.main.async(execute: start) + } + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: start) + } + NSApp.unhide(nil) window.makeKeyAndOrderFront(nil) NSApp.activate(ignoringOtherApps: true) - DispatchQueue.main.async { - self.model.animateIn() - debugLog("next tick key=\(self.window.isKeyWindow) active=\(NSApp.isActive)") - } debugLog("show on \(screen.frame) visible=\(window.isVisible) key=\(window.isKeyWindow) active=\(NSApp.isActive)") } @@ -156,6 +185,29 @@ final class AppDelegate: NSObject, NSApplicationDelegate { if !hit { cornerArmed = true } } + // MARK: Wallpaper + + /// Re-renders the blurred wallpaper when the user picks a new one, so the + /// next open never has to wait for it. + private func watchWallpaper() { + Wallpaper.prewarm() + let store = FileManager.default.homeDirectoryForCurrentUser + .appendingPathComponent("Library/Application Support/com.apple.wallpaper/Store") + let fd = open(store.path, O_EVTONLY) + guard fd >= 0 else { return } + let src = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fd, eventMask: [.write, .rename], queue: .main) + var pending: DispatchWorkItem? + src.setEventHandler { + pending?.cancel() + let work = DispatchWorkItem { Wallpaper.prewarm() } + pending = work + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: work) + } + src.setCancelHandler { close(fd) } + src.resume() + watchers.append(src) + } + // MARK: Installs and removals private func watchApplicationFolders() { diff --git a/Sources/Relaunch/Views.swift b/Sources/Relaunch/Views.swift index 75ee84e..ae1fc55 100644 --- a/Sources/Relaunch/Views.swift +++ b/Sources/Relaunch/Views.swift @@ -51,13 +51,16 @@ struct BackgroundView: View { var body: some View { ZStack { - VisualEffectBackground() + // Fallback only, for wallpapers that can't be read as an image: it + // blurs the windows behind us, which Launchpad never showed. + if model.wallpaper == nil { VisualEffectBackground() } if let img = model.wallpaper { Image(nsImage: img) .resizable() .aspectRatio(contentMode: .fill) .frame(width: model.size.width, height: model.size.height) .clipped() + .transition(.opacity) } Color.black.opacity(0.2) } diff --git a/Sources/Relaunch/Wallpaper.swift b/Sources/Relaunch/Wallpaper.swift index 8382b92..5a2e542 100644 --- a/Sources/Relaunch/Wallpaper.swift +++ b/Sources/Relaunch/Wallpaper.swift @@ -33,6 +33,18 @@ enum Wallpaper { } } + /// The blurred wallpaper if it's already rendered. Opening must never wait + /// on rendering: swapping the background in mid-animation is what made the + /// first open flash from a soft blur to a dark, saturated one. + static func cached(for screen: NSScreen) -> NSImage? { + current(for: screen).flatMap { cache[$0.url.path] } + } + + /// Renders ahead of time, so the next open finds it in `cached(for:)`. + static func prewarm() { + for screen in NSScreen.screens { blurred(for: screen) { _ in } } + } + /// `desktopImageURL` only knows classic still pictures. Since macOS 14 the /// real choice lives in the wallpaper agent's store, and for the default /// dynamic wallpapers (aerials) the API just returns DefaultDesktop.heic. @@ -77,8 +89,9 @@ enum Wallpaper { let output = input.clampedToExtent() .applyingGaussianBlur(sigma: 18) .cropped(to: input.extent) + // Dim slightly but keep the wallpaper's own colours: Launchpad + // blurred and darkened the desktop picture, it didn't saturate it. .applyingFilter("CIColorControls", parameters: [ - kCIInputSaturationKey: 1.15, kCIInputBrightnessKey: -0.04, ]) guard let out = context.createCGImage(output, from: input.extent) else { return nil }