From 23fbc01a0ec95edf195d045a726ea534bbabcf64 Mon Sep 17 00:00:00 2001 From: lwouis Date: Sat, 24 Jan 2026 21:21:22 +0100 Subject: [PATCH] fix: app icons could sometimes be invisible (closes #5226) --- resources/l10n/ko.lproj/Localizable.strings | 2 +- src/logic/Application.swift | 23 +++++++++++++++------ src/logic/Screens.swift | 6 ++++-- src/ui/App.swift | 4 ++-- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/resources/l10n/ko.lproj/Localizable.strings b/resources/l10n/ko.lproj/Localizable.strings index 6220d065..ab7525b6 100644 --- a/resources/l10n/ko.lproj/Localizable.strings +++ b/resources/l10n/ko.lproj/Localizable.strings @@ -188,7 +188,7 @@ "Show standard tabs as windows" = "표준 탭을 윈도우로 보이기"; "Show standard tabs as windows:" = "표준 탭을 윈도우로 보여주기:"; "Show titles" = "제목 표시"; -"Show windows from applications" = "윈도우을 표시할 앱"; +"Show windows from applications" = "윈도우를 표시할 앱"; "Show windows from screens" = "윈도우를 표시할 화면"; "Show windows from Spaces" = "윈도우를 표시할 Space"; "Show windows from:" = "창을 표시할 항목:"; diff --git a/src/logic/Application.swift b/src/logic/Application.swift index 826624d5..2ea59593 100644 --- a/src/logic/Application.swift +++ b/src/logic/Application.swift @@ -45,18 +45,29 @@ class Application: NSObject { return 0 }() + /// Converting NSImage to CGImage may seem simple, but it's actually very tricky. Lots of time has been put to make it work robustly + /// The RunningApplication.icon can have store bitmaps or vectors. We have to rasterize into pixels. This is not easy as there are many APIs: + /// * icon.cgImage(forProposedRect:) > context.draw -> only API which works + /// * icon.cgImage(forProposedRect:) > cgImage.draw -> returns nil for some users (could never reproduce it locally) + /// * icon.draw() -> returns nil for some users (could never reproduce it locally) + /// * icon.bestRepresentation() > bestRep.draw(in:) -> returns nil for some users (could never reproduce it locally) + /// MacOS Big Sur also introduced a constant padding around app icons. It was later increased with Tahoe. We have to crop it static func appIconWithoutPadding(_ icon: NSImage?) -> CGImage? { guard let icon else { return nil } let finalWidth = max(ThumbnailsPanel.maxPossibleAppIconSize.width, ThumbnailsPanel.maxPossibleAppIconSize.height) + // we hardcode cropping values based on a reference 1024 icon, and depending on the macOS version let padding = appIconPadding * (finalWidth / (1024 - appIconPadding * 2)) + // we need a bigger image size, since we'll crop to reach finalWidth let sourceWidth = finalWidth + padding * 2 - guard let context = CGContext(data: nil, width: Int(finalWidth), height: Int(finalWidth), bitsPerComponent: 8, bytesPerRow: 0, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { return nil } + // we ask the NSImage for the closest image it has to our desired size. It's likely to return a 1024x1024 or 512x512 image; whichever is closest + var proposedRect = CGRect(origin: .zero, size: NSSize(width: sourceWidth, height: sourceWidth)) + guard let cgImage = icon.cgImage(forProposedRect: &proposedRect, context: nil, hints: [.interpolation: NSImageInterpolation.high]) else { return nil } + // we have to crop this image; let's scale our intended padding, given the image size we got + let paddingScaled = padding * (CGFloat(cgImage.width) / sourceWidth) + guard let image = cgImage.cropping(to: CGRect(x: paddingScaled, y: paddingScaled, width: CGFloat(cgImage.width) - paddingScaled * 2, height: CGFloat(cgImage.height) - paddingScaled * 2).integral), + let context = CGContext(data: nil, width: Int(finalWidth), height: Int(finalWidth), bitsPerComponent: 8, bytesPerRow: 0, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue).union(.byteOrder32Little).rawValue) else { return nil } context.interpolationQuality = .high - let drawRect = CGRect(x: -padding, y: -padding, width: sourceWidth, height: sourceWidth) - NSGraphicsContext.saveGraphicsState() - NSGraphicsContext.current = NSGraphicsContext(cgContext: context, flipped: false) - icon.draw(in: drawRect, from: .zero, operation: .copy, fraction: 1.0, respectFlipped: false, hints: nil) - NSGraphicsContext.restoreGraphicsState() + context.draw(image, in: CGRect(origin: .zero, size: NSSize(width: finalWidth, height: finalWidth))) return context.makeImage() } diff --git a/src/logic/Screens.swift b/src/logic/Screens.swift index 0cb622cc..1f6293e9 100644 --- a/src/logic/Screens.swift +++ b/src/logic/Screens.swift @@ -13,7 +13,7 @@ class Screens { } extension NSScreen { - static var preferred: NSScreen = detectPreferred() ?? NSScreen.screens.first! + static var preferred = NSScreen.screens.first! static func updatePreferred() { preferred = detectPreferred() ?? NSScreen.screens.first! @@ -35,7 +35,9 @@ extension NSScreen { /// we find the screen with the key window ourselves manually static func active() -> NSScreen? { guard let frontmostPid = NSWorkspace.shared.frontmostApplication?.processIdentifier, - let frontmostApp = Applications.findOrCreate(frontmostPid) else { return nil } + // we avoid Applications.findOrCreate() here, because it active() is called very early during launch + // we are not ready to create applications yet + let frontmostApp = (Applications.list.first { $0.pid == frontmostPid }) else { return nil } guard let focusedWindow = frontmostApp.focusedWindow else { return NSScreen.withActiveMenubar() } // on the very first summon, this window may not have its spaces updated, which may land the wrong active screen focusedWindow.updateSpacesAndScreen() diff --git a/src/ui/App.swift b/src/ui/App.swift index f17a49d1..8f20cfad 100644 --- a/src/ui/App.swift +++ b/src/ui/App.swift @@ -299,6 +299,8 @@ extension App: NSApplicationDelegate { BackgroundWork.start() NSScreen.updatePreferred() Appearance.update() + ThumbnailsPanel.updateMaxPossibleThumbnailSize() + ThumbnailsPanel.updateMaxPossibleAppIconSize() Menubar.initialize() MainMenu.loadFromXib() self.thumbnailsPanel = ThumbnailsPanel() @@ -307,8 +309,6 @@ extension App: NSApplicationDelegate { Screens.refresh() SpacesEvents.observe() ScreensEvents.observe() - ThumbnailsPanel.updateMaxPossibleThumbnailSize() - ThumbnailsPanel.updateMaxPossibleAppIconSize() SystemAppearanceEvents.observe() SystemScrollerStyleEvents.observe() Applications.initialDiscovery()