feat: improve memory image of showing app icons (closes #5144)

This commit is contained in:
lwouis committed 2026-01-05 23:12:36 +01:00
1 parent 5c4199ec74
commit bf58335bbe
14 files changed
+65 -38

No files matched your search

-5
View File
@@ -194,11 +194,6 @@ extension DispatchQoS {
}
extension NSImage {
func appIconFixedSize(_ size: NSSize) -> CGImage? {
var rect = NSRect(origin: .zero, size: size)
return cgImage(forProposedRect: &rect, context: nil, hints: nil)
}
// NSImage(named) caches/reuses NSImage objects; we force separate instances of images by using copy()
static func initCopy(_ name: String) -> NSImage {
return NSImage(named: name)!.copy() as! NSImage
+12 -9
View File
@@ -45,16 +45,19 @@ class Application: NSObject {
return 0
}()
private static func appIconWithoutPadding(_ icon: NSImage?) -> CGImage? {
static func appIconWithoutPadding(_ icon: NSImage?) -> CGImage? {
guard let icon else { return nil }
// we can render the icon quite big (e.g. windowless app icon), so we store it high-res
let iconWidth = CGFloat(1024)
// NSRunningApplication.icon returns icons with padding; we remove it manually
let croppedSize = iconWidth - appIconPadding * 2
return icon
.appIconFixedSize(NSSize(width: iconWidth, height: iconWidth))?
.cropping(to: CGRect(x: appIconPadding, y: appIconPadding, width: croppedSize, height: croppedSize).integral)
let finalWidth = max(ThumbnailsPanel.maxPossibleAppIconSize.width, ThumbnailsPanel.maxPossibleAppIconSize.height)
let padding = appIconPadding * (finalWidth / (1024 - appIconPadding * 2))
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 }
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()
return context.makeImage()
}
init(_ runningApplication: NSRunningApplication) {
+12
View File
@@ -161,4 +161,16 @@ class Applications {
static func find(_ pid: pid_t?) -> Application? {
return list.first { $0.pid == pid }
}
static func updateAppIcons() {
for app in list {
BackgroundWork.screenshotsQueue.addOperation { [weak app] in
guard let app else { return }
let r = Application.appIconWithoutPadding(app.runningApplication.icon)
DispatchQueue.main.async { [weak app] in
app?.icon = r
}
}
}
}
}
+1 -1
View File
@@ -102,7 +102,7 @@ class Window {
if !view.thumbnail.isHidden {
let thumbnailSize = ThumbnailView.thumbnailSize(screenshot.size(), false)
let newSize = thumbnailSize.width != view.thumbnail.frame.width || thumbnailSize.height != view.thumbnail.frame.height
view.thumbnail.updateWithResizedCopy(screenshot, thumbnailSize)
view.thumbnail.updateContents(screenshot, thumbnailSize)
// if the thumbnail size has changed, we need to refresh the open UI
if newSize {
App.app.refreshOpenUi([], .refreshOnlyThumbnailsAfterShowUi)
-1
View File
@@ -8,7 +8,6 @@ class SpacesEvents {
@objc private static func handleEvent(_ notification: Notification) {
ScreensEvents.debouncerScreenAndSpace.debounce(.spaceEvent) {
Logger.debug { notification.name.rawValue }
ThumbnailsPanel.updateMaxPossibleThumbnailSize()
// Workaround for Safari full-screen videos
// when full-screening a video, Safari spawns a second full-screen window called "Safari"
// this window doesn't emit resize/move events. It doesn't pass isActualWindow on creation. It's added on focusedWindowChanged
+1
View File
@@ -306,6 +306,7 @@ extension App: NSApplicationDelegate {
SpacesEvents.observe()
ScreensEvents.observe()
ThumbnailsPanel.updateMaxPossibleThumbnailSize()
ThumbnailsPanel.updateMaxPossibleAppIconSize()
SystemAppearanceEvents.observe()
SystemScrollerStyleEvents.observe()
Applications.initialDiscovery()
+1 -1
View File
@@ -29,7 +29,7 @@ class FeedbackWindow: NSWindow {
private func setupView() {
let appIcon = LightImageView()
appIcon.updateWithResizedCopy(.cgImage(App.appIcon), NSSize(width: 80, height: 80))
appIcon.updateContents(.cgImage(App.appIcon), NSSize(width: 80, height: 80))
appIcon.fit(80, 80)
let appText = StackView([
BoldLabel(NSLocalizedString("Share improvement ideas, or report bugs", comment: "")),
@@ -27,12 +27,7 @@ class LightImageView: NSView {
layerContentsRedrawPolicy = .never
}
func updateWithResizedCopy(_ caLayerContents: CALayerContents, _ size: NSSize) {
let scaleFactor = NSScreen.preferred.backingScaleFactor
// alternatively, we could set layer!.contentsGravity to .center, and use the lines bellow to resize ourselves
// let scaledSize = NSSize(width: size.width * scaleFactor, height: size.height * scaleFactor)
// layer!.contents = image.resizedCopyWithCoreGraphics(scaledSize, fixBitmapInfo)
// it would produce subjectively better quality, but the resizing would be done on the CPU so poor performance
func updateContents(_ caLayerContents: CALayerContents, _ size: NSSize) {
switch caLayerContents {
case .cgImage(let image?):
layer!.contents = image
@@ -40,7 +35,6 @@ class LightImageView: NSView {
layer!.contents = CVPixelBufferGetIOSurface(pixelBuffer)?.takeUnretainedValue()
default: break
}
layer!.contentsScale = scaleFactor
if frame.size != size {
frame.size = size
}
+2 -2
View File
@@ -30,7 +30,7 @@ class PreviewPanel: NSPanel {
func show(_ id: CGWindowID, _ preview: CALayerContents, _ position: CGPoint, _ size: CGSize) {
if id != currentId {
repositionAndResize(position, size)
previewView.updateWithResizedCopy(preview, size)
previewView.updateContents(preview, size)
}
if id != currentId || !isVisible {
if Preferences.previewFadeInAnimation {
@@ -54,7 +54,7 @@ class PreviewPanel: NSPanel {
func updateIfShowing(_ id: CGWindowID?, _ preview: CALayerContents, _ position: CGPoint, _ size: CGSize) {
if isVisible && id == currentId {
repositionAndResize(position, size)
previewView.updateWithResizedCopy(preview, size)
previewView.updateContents(preview, size)
}
}
+8 -8
View File
@@ -323,7 +323,7 @@ class ThumbnailView: FlippedView {
private func updateAppIcon(_ element: Window, _ title: String) {
let appIconSize = ThumbnailView.iconSize()
appIcon.updateWithResizedCopy(.cgImage(element.icon), appIconSize)
appIcon.updateContents(.cgImage(element.icon), appIconSize)
appIcon.setAccessibilityLabel(title)
}
@@ -342,11 +342,11 @@ class ThumbnailView: FlippedView {
if !thumbnail.isHidden {
if let screenshot = element.thumbnail {
let thumbnailSize = ThumbnailView.thumbnailSize(element.size, false)
thumbnail.updateWithResizedCopy(screenshot, thumbnailSize)
thumbnail.updateContents(screenshot, thumbnailSize)
} else {
// if no thumbnail, show appIcon instead
let thumbnailSize = ThumbnailView.thumbnailSize(element.icon?.size(), true)
thumbnail.updateWithResizedCopy(.cgImage(element.icon), thumbnailSize)
thumbnail.updateContents(.cgImage(element.icon), thumbnailSize)
}
// for Accessibility > "speak items under the pointer"
thumbnail.setAccessibilityLabel(element.title)
@@ -373,7 +373,7 @@ class ThumbnailView: FlippedView {
setAccessibilityHelp(getAccessibilityHelp(element.application.localizedName, element.dockLabel))
if !windowlessIcon.isHidden {
let windowlessIconSize = ThumbnailView.thumbnailSize(element.icon?.size(), true)
windowlessIcon.updateWithResizedCopy(.cgImage(element.icon), windowlessIconSize)
windowlessIcon.updateContents(.cgImage(element.icon), windowlessIconSize)
}
windowControlIcons.forEach { $0.window_ = element }
showOrHideWindowControls(isShowingWindowControls)
@@ -575,8 +575,8 @@ class ThumbnailView: FlippedView {
return maxWidth
}
static func minThumbnailWidth() -> CGFloat {
return ThumbnailsPanel.maxThumbnailsWidth() * Appearance.windowMinWidthInRow - Appearance.interCellPadding * 2
static func minThumbnailWidth(_ screen: NSScreen = NSScreen.preferred) -> CGFloat {
return ThumbnailsPanel.maxThumbnailsWidth(screen) * Appearance.windowMinWidthInRow - Appearance.interCellPadding * 2
}
/// The maximum height that a thumbnail can be drawn
@@ -622,9 +622,9 @@ class ThumbnailView: FlippedView {
return NSSize(width: width.rounded(), height: height.rounded())
}
static func iconSize() -> NSSize {
static func iconSize(_ screen: NSScreen = NSScreen.preferred) -> NSSize {
if Preferences.appearanceStyle == .appIcons {
let widthMin = ThumbnailView.minThumbnailWidth()
let widthMin = ThumbnailView.minThumbnailWidth(screen)
let contentWidth = Appearance.iconSize
let frameWidth = contentWidth + Appearance.edgeInsetsSize * 2
let width = max(frameWidth, widthMin).rounded()
+17 -1
View File
@@ -5,6 +5,7 @@ class ThumbnailsPanel: NSPanel {
override var canBecomeKey: Bool { true }
private var didDisplayOnce = false
static var maxPossibleThumbnailSize = NSSize.zero
static var maxPossibleAppIconSize = NSSize.zero
convenience init() {
self.init(contentRect: .zero, styleMask: .nonactivatingPanel, backing: .buffered, defer: false)
@@ -96,7 +97,22 @@ class ThumbnailsPanel: NSPanel {
(max(acc.0, ThumbnailView.maxThumbnailWidth(screen) * screen.backingScaleFactor),
max(acc.1, ThumbnailView.maxThumbnailHeight(screen) * screen.backingScaleFactor))
}
maxPossibleThumbnailSize = NSSize(width: w, height: h)
maxPossibleThumbnailSize = NSSize(width: w.rounded(), height: h.rounded())
}
static func updateMaxPossibleAppIconSize() {
let (w, h) = NSScreen.screens.reduce((CGFloat.zero, CGFloat.zero)) { acc, screen in
// in Thumbnails Appearance, AppIcons can be used for windowless apps, thus much bigger than the app icon near the title
if Preferences.appearanceStyle == .thumbnails {
return (max(acc.0, ThumbnailView.maxThumbnailWidth(screen) * screen.backingScaleFactor),
max(acc.1, ThumbnailView.maxThumbnailHeight(screen) * screen.backingScaleFactor))
} else {
let size = ThumbnailView.iconSize(screen)
return (max(acc.0, size.width * screen.backingScaleFactor),
max(acc.1, size.height * screen.backingScaleFactor))
}
}
maxPossibleAppIconSize = NSSize(width: w.rounded(), height: h.rounded())
}
}
+8 -1
View File
@@ -24,8 +24,15 @@ class ThumbnailsView {
// it would be nicer to remove this whole "reset" logic, and instead update each component to check Appearance properties before showing
// Maybe in some Appkit willDraw() function that triggers before drawing it
NSScreen.updatePreferred()
ThumbnailsPanel.updateMaxPossibleThumbnailSize()
Appearance.update()
// thumbnails are captured continuously. They will pick up the new size on the next cycle
ThumbnailsPanel.updateMaxPossibleThumbnailSize()
// app icons are captured once at launch; we need to manually update them if needed
let old = ThumbnailsPanel.maxPossibleAppIconSize.width
ThumbnailsPanel.updateMaxPossibleAppIconSize()
if old != ThumbnailsPanel.maxPossibleAppIconSize.width {
Applications.updateAppIcons()
}
updateBackgroundView()
App.app.thumbnailsPanel.contentView = contentView
for i in 0..<ThumbnailsView.recycledViews.count {
@@ -40,7 +40,7 @@ class PermissionsWindow: NSWindow {
private func setupView() {
let appIcon = LightImageView()
appIcon.updateWithResizedCopy(.cgImage(App.appIcon), NSSize(width: 80, height: 80))
appIcon.updateContents(.cgImage(App.appIcon), NSSize(width: 80, height: 80))
appIcon.fit(80, 80)
let appText = TitleLabel(NSLocalizedString("AltTab needs some permissions", comment: ""))
appText.preferredMaxLayoutWidth = 380
@@ -3,7 +3,7 @@ import Cocoa
class AboutTab {
static func initTab() -> NSView {
let appIcon = LightImageView()
appIcon.updateWithResizedCopy(.cgImage(App.appIcon), NSSize(width: 256, height: 256))
appIcon.updateContents(.cgImage(App.appIcon), NSSize(width: 256, height: 256))
appIcon.fit(256, 256)
let appText = StackView([
BoldLabel(App.name),