perf: simplify and combine views for better perf

This commit is contained in:
lwouis committed 2026-02-11 21:35:30 +01:00
1 parent ec5802a487
commit 3a8c0f3bee
3 files changed
+166 -70

No files matched your search

@@ -46,12 +46,12 @@ class HighlightOverlayView: FlippedView {
highlightLayer.isHidden = true
return
}
let vStackFrame = view.vStackView.frame
let hf = view.highlightFrame
let rect = CGRect(
x: view.frame.origin.x + vStackFrame.origin.x,
y: view.frame.origin.y + vStackFrame.origin.y,
width: vStackFrame.width,
height: vStackFrame.height
x: view.frame.origin.x + hf.origin.x,
y: view.frame.origin.y + hf.origin.y,
width: hf.width,
height: hf.height
)
highlightLayer.frame = rect
highlightLayer.cornerRadius = Appearance.cellCornerRadius
+160 -64
View File
@@ -9,17 +9,12 @@ class ThumbnailView: FlippedView {
var thumbnail = LightImageView(withTransparencyChecks: true)
var appIcon = LightImageView()
var label = ThumbnailTitleView(font: Appearance.font)
var fullscreenIcon = ThumbnailFontIconView(symbol: .circledPlusSign, tooltip: NSLocalizedString("Window is fullscreen", comment: ""))
var minimizedIcon = ThumbnailFontIconView(symbol: .circledMinusSign, tooltip: NSLocalizedString("Window is minimized", comment: ""))
var hiddenIcon = ThumbnailFontIconView(symbol: .circledSlashSign, tooltip: NSLocalizedString("App is hidden", comment: ""))
var spaceIcon = ThumbnailFontIconView(symbol: .circledNumber0)
var statusIcons = StatusIconsView()
var dockLabelIcon = ThumbnailFilledFontIconView(
ThumbnailFontIconView(symbol: .filledCircledNumber0, size: dockLabelLabelSize(), color: NSColor(srgbRed: 1, green: 0.30, blue: 0.25, alpha: 1)),
backgroundColor: NSColor.white, size: dockLabelLabelSize())
var windowlessAppIndicator = WindowlessAppIndicator(tooltip: ThumbnailView.noOpenWindowToolTip)
let hStackView = FlippedView()
let vStackView = FlippedView()
var mouseUpCallback: (() -> Void)!
var mouseMovedCallback: (() -> Void)!
var dragAndDropTimer: Timer?
@@ -29,9 +24,6 @@ class ThumbnailView: FlippedView {
var isLastInRow = false
var indexInRow = 0
var numberOfViewsInRow = 0
private var lastLabelWidth: CGFloat = -1
var windowIndicatorIcons: [ThumbnailFontIconView] { [hiddenIcon, fullscreenIcon, minimizedIcon, spaceIcon] }
var receivedMouseDown = false
@@ -122,6 +114,16 @@ class ThumbnailView: FlippedView {
observeDragAndDrop()
}
/// The frame used by HighlightOverlayView to position the highlight rectangle.
/// In appIcons style, it covers appIcon + edge insets. Otherwise, it covers the full cell.
var highlightFrame: CGRect {
if Preferences.appearanceStyle == .appIcons {
return CGRect(x: 0, y: 0,
width: frame.width, height: appIcon.frame.height + Appearance.edgeInsetsSize * 2)
}
return CGRect(origin: .zero, size: frame.size)
}
func updateRecycledCellWithNewContent(_ element: Window, _ index: Int, _ newHeight: CGFloat) {
window_ = element
label.toolTip = nil
@@ -139,7 +141,6 @@ class ThumbnailView: FlippedView {
}
}
func updateDockLabelIcon(_ dockLabel: String?) {
assignIfDifferent(&dockLabelIcon.isHidden, dockLabel == nil || Preferences.hideAppBadges || Appearance.iconSize == 0)
if !dockLabelIcon.isHidden, let dockLabel {
@@ -170,25 +171,21 @@ class ThumbnailView: FlippedView {
dockLabelIcon.shadow = shadow
appIcon.setSubviewAbove(dockLabelIcon)
label.fixHeight()
vStackView.wantsLayer = true
setSubviews([vStackView])
vStackView.setSubviews([hStackView])
hStackView.setSubviews([appIcon])
setSubviews([appIcon])
}
private func setupStyleSpecificSubviews() {
if Preferences.appearanceStyle == .appIcons {
addSubviews([label])
hStackView.setSubviewAbove(windowlessAppIndicator)
setSubviewAbove(windowlessAppIndicator)
label.alignment = .center
label.isHidden = true
} else if Preferences.appearanceStyle == .thumbnails {
vStackView.addSubviews([thumbnail])
addSubviews([thumbnail, label, statusIcons])
thumbnail.setSubviewAbove(windowlessAppIndicator)
hStackView.addSubviews([label] + windowIndicatorIcons)
} else {
hStackView.setSubviewAbove(windowlessAppIndicator)
hStackView.addSubviews([label] + windowIndicatorIcons)
setSubviewAbove(windowlessAppIndicator)
addSubviews([label, statusIcons])
}
}
@@ -254,7 +251,7 @@ class ThumbnailView: FlippedView {
}
let xPosition = -leftOffset
let height = ThumbnailsView.layoutCache.labelHeight
let yPosition = hStackView.frame.origin.y + hStackView.frame.height + Appearance.intraCellPadding * 2
let yPosition = appIcon.frame.maxY + Appearance.intraCellPadding * 2
label.frame = NSRect(x: xPosition, y: yPosition, width: effectiveLabelWidth, height: height)
label.setWidth(effectiveLabelWidth)
label.toolTip = label.cell!.cellSize.width >= label.frame.size.width ? label.stringValue : nil
@@ -268,14 +265,16 @@ class ThumbnailView: FlippedView {
private func updateValues(_ element: Window, _ index: Int, _ newHeight: CGFloat) {
assignIfDifferent(&windowlessAppIndicator.isHidden, !element.isWindowlessApp)
assignIfDifferent(&hiddenIcon.isHidden, !element.isHidden || Preferences.hideStatusIcons)
assignIfDifferent(&fullscreenIcon.isHidden, !element.isFullscreen || Preferences.hideStatusIcons)
assignIfDifferent(&minimizedIcon.isHidden, !element.isMinimized || Preferences.hideStatusIcons)
assignIfDifferent(&spaceIcon.isHidden, element.isWindowlessApp || Spaces.isSingleSpace() || Preferences.hideSpaceNumberLabels || (
Preferences.spacesToShow[App.app.shortcutIndex] == .visible && (
NSScreen.screens.count < 2 || Preferences.screensToShow[App.app.shortcutIndex] == .showingAltTab
)
))
statusIcons.update(
isHidden: element.isHidden && !Preferences.hideStatusIcons,
isFullscreen: element.isFullscreen && !Preferences.hideStatusIcons,
isMinimized: element.isMinimized && !Preferences.hideStatusIcons,
showSpace: !(element.isWindowlessApp || Spaces.isSingleSpace() || Preferences.hideSpaceNumberLabels || (
Preferences.spacesToShow[App.app.shortcutIndex] == .visible && (
NSScreen.screens.count < 2 || Preferences.screensToShow[App.app.shortcutIndex] == .showingAltTab
)
))
)
thumbnail.toolTip = element.isWindowlessApp ? ThumbnailView.noOpenWindowToolTip : nil
if !thumbnail.isHidden {
if let screenshot = element.thumbnail {
@@ -296,14 +295,12 @@ class ThumbnailView: FlippedView {
setAccessibilityLabel(title)
}
label.updateTruncationModeIfNeeded()
if !spaceIcon.isHidden {
if statusIcons.spaceVisible {
let spaceIndex = element.spaceIndexes.first
if element.isOnAllSpaces || (spaceIndex != nil && spaceIndex! > 30) {
spaceIcon.setStar()
spaceIcon.toolTip = NSLocalizedString("Window is on every Space", comment: "")
statusIcons.setSpaceStar()
} else if let spaceIndex {
spaceIcon.setNumber(spaceIndex, false)
spaceIcon.toolTip = String(format: NSLocalizedString("Window is on Space %d", comment: ""), spaceIndex)
statusIcons.setSpaceNumber(spaceIndex)
}
}
updateAppIcon(element, title)
@@ -315,42 +312,36 @@ class ThumbnailView: FlippedView {
private func updateSizes(_ newHeight: CGFloat) {
setFrameWidthHeight(newHeight)
if Preferences.appearanceStyle == .appIcons {
assignIfDifferent(&vStackView.frame.size, NSSize(width: frame.width, height: appIcon.frame.height + Appearance.edgeInsetsSize * 2))
assignIfDifferent(&hStackView.frame.size, NSSize(width: appIcon.frame.width, height: appIcon.frame.height))
} else {
assignIfDifferent(&vStackView.frame.size, NSSize(width: frame.width, height: frame.height))
assignIfDifferent(&hStackView.frame.size, NSSize(width: frame.width - Appearance.edgeInsetsSize * 2, height: max(appIcon.frame.height, ThumbnailsView.layoutCache.labelHeight)))
let labelWidth = hStackView.frame.width - appIcon.frame.width - Appearance.appIconLabelSpacing - indicatorsSpace()
if Preferences.appearanceStyle != .appIcons {
let hWidth = frame.width - Appearance.edgeInsetsSize * 2
let labelWidth = hWidth - appIcon.frame.width - Appearance.appIconLabelSpacing - statusIcons.totalWidth
label.setWidth(labelWidth)
}
}
private func updatePositions(_ newHeight: CGFloat) {
assignIfDifferent(&hStackView.frame.origin, NSPoint(x: Appearance.edgeInsetsSize, y: Appearance.edgeInsetsSize))
let edgeInsets = Appearance.edgeInsetsSize
assignIfDifferent(&appIcon.frame.origin, NSPoint(x: edgeInsets, y: edgeInsets))
if Preferences.appearanceStyle != .appIcons {
assignIfDifferent(&appIcon.frame.origin.x, App.shared.userInterfaceLayoutDirection == .leftToRight
? 0
: hStackView.frame.width - appIcon.frame.width)
let iconWidth = ThumbnailsView.layoutCache.iconWidth
var indicatorSpace = CGFloat(0)
for icon in windowIndicatorIcons {
if !icon.isHidden {
indicatorSpace += iconWidth
assignIfDifferent(&icon.frame.origin.y, ((hStackView.frame.height - ThumbnailsView.layoutCache.iconHeight) / 2).rounded())
assignIfDifferent(&icon.frame.origin.x, App.shared.userInterfaceLayoutDirection == .leftToRight
? hStackView.frame.width - indicatorSpace
: indicatorSpace - iconWidth)
}
let hWidth = frame.width - edgeInsets * 2
let hHeight = max(appIcon.frame.height, ThumbnailsView.layoutCache.labelHeight)
if App.shared.userInterfaceLayoutDirection == .rightToLeft {
assignIfDifferent(&appIcon.frame.origin.x, edgeInsets + hWidth - appIcon.frame.width)
}
let labelWidth = hStackView.frame.width - appIcon.frame.width - Appearance.appIconLabelSpacing - indicatorSpace
assignIfDifferent(&label.frame.origin.x, App.shared.userInterfaceLayoutDirection == .leftToRight
? appIcon.frame.maxX + Appearance.appIconLabelSpacing
: hStackView.frame.width - appIcon.frame.width - Appearance.appIconLabelSpacing - labelWidth)
assignIfDifferent(&label.frame.origin.y, ((hStackView.frame.height - ThumbnailsView.layoutCache.labelHeight) / 2).rounded())
statusIcons.layoutIcons(hWidth: hWidth, hHeight: hHeight, edgeInsets: edgeInsets)
let labelWidth = hWidth - appIcon.frame.width - Appearance.appIconLabelSpacing - statusIcons.totalWidth
let labelX: CGFloat
if App.shared.userInterfaceLayoutDirection == .leftToRight {
labelX = appIcon.frame.maxX + Appearance.appIconLabelSpacing
} else {
labelX = edgeInsets + hWidth - appIcon.frame.width - Appearance.appIconLabelSpacing - labelWidth
}
assignIfDifferent(&label.frame.origin.x, labelX)
assignIfDifferent(&label.frame.origin.y, edgeInsets + ((hHeight - ThumbnailsView.layoutCache.labelHeight) / 2).rounded())
}
if Preferences.appearanceStyle == .thumbnails {
assignIfDifferent(&thumbnail.frame.origin, NSPoint(x: Appearance.edgeInsetsSize, y: hStackView.frame.maxY + Appearance.intraCellPadding))
let hHeight = max(appIcon.frame.height, ThumbnailsView.layoutCache.labelHeight)
assignIfDifferent(&thumbnail.frame.origin, NSPoint(x: edgeInsets, y: edgeInsets + hHeight + Appearance.intraCellPadding))
thumbnail.centerFrameInParent(x: true)
}
if !windowlessAppIndicator.isHidden {
@@ -399,10 +390,6 @@ class ThumbnailView: FlippedView {
return (offsetX, offsetY)
}
private func indicatorsSpace() -> CGFloat {
return CGFloat(windowIndicatorIcons.filter { !$0.isHidden }.count) * ThumbnailsView.layoutCache.iconWidth
}
private func getAppOrAndWindowTitle() -> String {
let appName = window_?.application.localizedName
let windowTitle = window_?.title
@@ -564,3 +551,112 @@ class ThumbnailView: FlippedView {
return ThumbnailView.maxThumbnailHeight()
}
}
class StatusIconsView: FlippedView {
struct Icon {
var symbol: String
var tooltip: String?
var visible = false
}
static let hiddenIdx = 0
static let fullscreenIdx = 1
static let minimizedIdx = 2
static let spaceIdx = 3
private static let defaultSymbols: [(Symbols, String?)] = [
(.circledSlashSign, NSLocalizedString("App is hidden", comment: "")),
(.circledPlusSign, NSLocalizedString("Window is fullscreen", comment: "")),
(.circledMinusSign, NSLocalizedString("Window is minimized", comment: "")),
(.circledNumber0, nil),
]
var icons: [Icon]
private let attrs: [NSAttributedString.Key: Any]
private var visibleCount = 0
/// Single-character cell size, cached at init for the layout cache
let iconCellSize: NSSize
convenience init() {
self.init(frame: .zero)
}
override init(frame: NSRect) {
let font = NSFont(name: "SF Pro Text", size: (Appearance.fontHeight * 0.85).rounded())!
let paragraphStyle = ThumbnailFontIconView.paragraphStyle
attrs = [.font: font, .foregroundColor: Appearance.fontColor, .paragraphStyle: paragraphStyle]
icons = Self.defaultSymbols.map { Icon(symbol: $0.0.rawValue, tooltip: $0.1) }
let measure = NSAttributedString(string: Symbols.circledNumber0.rawValue, attributes: attrs)
iconCellSize = measure.size()
super.init(frame: frame)
}
required init?(coder: NSCoder) { fatalError() }
var totalWidth: CGFloat { CGFloat(visibleCount) * ThumbnailsView.layoutCache.iconWidth }
func update(isHidden: Bool, isFullscreen: Bool, isMinimized: Bool, showSpace: Bool) {
icons[Self.hiddenIdx].visible = isHidden
icons[Self.fullscreenIdx].visible = isFullscreen
icons[Self.minimizedIdx].visible = isMinimized
icons[Self.spaceIdx].visible = showSpace
visibleCount = icons.count(where: { $0.visible })
}
func setSpaceStar() {
icons[Self.spaceIdx].symbol = Symbols.circledStar.rawValue
icons[Self.spaceIdx].tooltip = NSLocalizedString("Window is on every Space", comment: "")
}
func setSpaceNumber(_ number: Int) {
let (base, offset) = number <= 9
? (Symbols.circledNumber0.rawValue, number * 2)
: (Symbols.circledNumber10.rawValue, number - 10)
icons[Self.spaceIdx].symbol = String(UnicodeScalar(Int(base.unicodeScalars.first!.value) + offset)!)
icons[Self.spaceIdx].tooltip = String(format: NSLocalizedString("Window is on Space %d", comment: ""), number)
}
var spaceVisible: Bool { icons[Self.spaceIdx].visible }
func layoutIcons(hWidth: CGFloat, hHeight: CGFloat, edgeInsets: CGFloat) {
let indicatorSpace = totalWidth
assignIfDifferent(&frame.size.width, indicatorSpace)
assignIfDifferent(&frame.size.height, hHeight)
let isLTR = App.shared.userInterfaceLayoutDirection == .leftToRight
assignIfDifferent(&frame.origin.x, isLTR ? edgeInsets + hWidth - indicatorSpace : edgeInsets)
assignIfDifferent(&frame.origin.y, edgeInsets)
removeAllToolTips()
let iconWidth = ThumbnailsView.layoutCache.iconWidth
let iconHeight = ThumbnailsView.layoutCache.iconHeight
let yOffset = ((hHeight - iconHeight) / 2).rounded()
var offset = CGFloat(0)
for icon in icons {
guard icon.visible else { continue }
offset += iconWidth
let x = isLTR ? indicatorSpace - offset : offset - iconWidth
if let tooltip = icon.tooltip {
_ = addToolTip(NSRect(x: x, y: yOffset, width: iconWidth, height: iconHeight), owner: tooltip as NSString, userData: nil)
}
}
needsDisplay = true
}
override func draw(_ dirtyRect: NSRect) {
guard visibleCount > 0 else { return }
let iconWidth = ThumbnailsView.layoutCache.iconWidth
let iconHeight = ThumbnailsView.layoutCache.iconHeight
let isLTR = App.shared.userInterfaceLayoutDirection == .leftToRight
let yOffset = ((frame.height - iconHeight) / 2).rounded()
var offset = CGFloat(0)
for icon in icons {
guard icon.visible else { continue }
offset += iconWidth
let x = isLTR ? frame.width - offset : offset - iconWidth
icon.symbol.draw(at: NSPoint(x: x, y: yOffset), withAttributes: attrs)
}
}
override func mouseMoved(with event: NSEvent) {
// no-op prevents tooltips from disappearing on mouseMoved
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ class ThumbnailsView {
static func updateCachedSizes() {
guard let firstView = ThumbnailsView.recycledViews.first else { return }
layoutCache.labelHeight = firstView.label.cell!.cellSize.height
let iconCellSize = firstView.windowIndicatorIcons.first!.cell!.cellSize
let iconCellSize = firstView.statusIcons.iconCellSize
layoutCache.iconWidth = iconCellSize.width
layoutCache.iconHeight = iconCellSize.height
layoutCache.dockLabelSize = firstView.dockLabelIcon.frame.size