perf: create overlay view to optimize view count

This commit is contained in:
lwouis committed 2026-02-11 21:35:29 +01:00
1 parent fefc491ce5
commit ec5802a487
5 files changed
+133 -82

No files matched your search

+4
View File
@@ -209,6 +209,7 @@
D04BA0DD6819010987B578D1 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BA07BBF2237CC6C72D264 /* Localizable.strings */; };
D04BA100BD0F47828EB649FF /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BAAEC2847830A3991F8D1 /* InfoPlist.strings */; };
D04BA11E56383D082D7BE5A5 /* ThumbnailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAA998119CAA8B70A2B67 /* ThumbnailsView.swift */; };
D04BAFC1A1B2C3D4E5F60002 /* HighlightOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAFC0A1B2C3D4E5F60001 /* HighlightOverlayView.swift */; };
D04BA14D93726795A6937832 /* LabelAndControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA2526DC6726E0F7ACF7C /* LabelAndControl.swift */; };
D04BA15A1B0C4871EA7CB899 /* GeneralTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BACE22DC907F03D193075 /* GeneralTab.swift */; };
D04BA1621718FF26F8D5E75D /* CustomRecorderControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAE333F7170E87C5AC0EF /* CustomRecorderControl.swift */; };
@@ -672,6 +673,7 @@
D04BAA4442D0CE3A4972937F /* nl */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = nl; path = InfoPlist.strings; sourceTree = "<group>"; };
D04BAA6E43F1E6EB81F953AF /* ru */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = ru; path = InfoPlist.strings; sourceTree = "<group>"; };
D04BAA998119CAA8B70A2B67 /* ThumbnailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailsView.swift; sourceTree = "<group>"; };
D04BAFC0A1B2C3D4E5F60001 /* HighlightOverlayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HighlightOverlayView.swift; sourceTree = "<group>"; };
D04BAA9E0539EE620D08F63F /* fr */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = fr; path = InfoPlist.strings; sourceTree = "<group>"; };
D04BAAA1835480F9B3CCF170 /* blacklists@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "blacklists@2x.png"; sourceTree = "<group>"; };
D04BAAB92261FC04854FDDE9 /* App.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
@@ -1650,6 +1652,7 @@
D04BA1DF8CAB2FAB7FE9244B /* ThumbnailFontIconView.swift */,
D04BA258B56193958D60978A /* ThumbnailView.swift */,
D04BAA998119CAA8B70A2B67 /* ThumbnailsView.swift */,
D04BAFC0A1B2C3D4E5F60001 /* HighlightOverlayView.swift */,
AA974BA829B7D84C0099A29E /* PreviewPanel.swift */,
);
path = "main-window";
@@ -2483,6 +2486,7 @@
D04BAD2A7F2E8BF64EE982E9 /* TextArea.swift in Sources */,
5F17452E2F1A908D00128EF8 /* WindowDiscriminator.swift in Sources */,
D04BA11E56383D082D7BE5A5 /* ThumbnailsView.swift in Sources */,
D04BAFC1A1B2C3D4E5F60002 /* HighlightOverlayView.swift in Sources */,
5F21C9E42C6E94700091F72F /* CustomizeStyleSheet.swift in Sources */,
D04BA1621718FF26F8D5E75D /* CustomRecorderControl.swift in Sources */,
D04BA1637E125D38546C26E2 /* StackView.swift in Sources */,
@@ -13,7 +13,7 @@ class TrafficLightButton: NSButton {
self.type = type
target = self
action = #selector(onClick)
addTrackingArea(NSTrackingArea(rect: bounds, options: [.mouseEnteredAndExited, .activeInKeyWindow], owner: self, userInfo: nil))
addTrackingArea(NSTrackingArea(rect: .zero, options: [.mouseEnteredAndExited, .activeInKeyWindow, .inVisibleRect], owner: self, userInfo: nil))
toolTip = tooltip
appearance = NSAppearance(named: .aqua)
}
@@ -0,0 +1,106 @@
import Cocoa
class HighlightOverlayView: FlippedView {
let focusedLayer = noAnimation { CALayer() }
let hoveredLayer = noAnimation { CALayer() }
var quitButton = TrafficLightButton(.quit, NSLocalizedString("Quit app", comment: ""))
var closeButton = TrafficLightButton(.close, NSLocalizedString("Close window", comment: ""))
var minimizeButton = TrafficLightButton(.miniaturize, NSLocalizedString("Minimize/Deminimize window", comment: ""))
var maximizeButton = TrafficLightButton(.fullscreen, NSLocalizedString("Fullscreen/Defullscreen window", comment: ""))
var isShowingWindowControls = false
var windowControlButtons: [TrafficLightButton] { [quitButton, closeButton, minimizeButton, maximizeButton] }
convenience init() {
self.init(frame: .zero)
wantsLayer = true
layer!.masksToBounds = false
for highlightLayer in [focusedLayer, hoveredLayer] {
highlightLayer.isHidden = true
layer!.addSublayer(highlightLayer)
}
for button in windowControlButtons {
addSubview(button)
button.isHidden = true
}
}
override func hitTest(_ point: NSPoint) -> NSView? {
for button in windowControlButtons {
guard !button.isHidden else { continue }
let buttonPoint = button.convert(point, from: self)
if button.bounds.contains(buttonPoint) {
return button.hitTest(button.convert(point, from: self))
}
}
return nil
}
func updateHighlight(focusedView: ThumbnailView?, hoveredView: ThumbnailView?) {
updateLayer(focusedLayer, for: focusedView, isFocused: true)
updateLayer(hoveredLayer, for: hoveredView, isFocused: false)
}
private func updateLayer(_ highlightLayer: CALayer, for view: ThumbnailView?, isFocused: Bool) {
guard let view, view.frame != .zero else {
highlightLayer.isHidden = true
return
}
let vStackFrame = view.vStackView.frame
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
)
highlightLayer.frame = rect
highlightLayer.cornerRadius = Appearance.cellCornerRadius
highlightLayer.backgroundColor = (isFocused
? Appearance.highlightFocusedBackgroundColor
: Appearance.highlightHoveredBackgroundColor).cgColor
highlightLayer.borderColor = (isFocused
? Appearance.highlightFocusedBorderColor
: Appearance.highlightHoveredBorderColor).cgColor
highlightLayer.borderWidth = Appearance.highlightBorderWidth
highlightLayer.isHidden = false
}
func showWindowControls(for view: ThumbnailView) {
guard Preferences.appearanceStyle == .thumbnails else { hideWindowControls(); return }
let shouldShow = !Preferences.hideColoredCircles && !Appearance.hideThumbnails
guard shouldShow, let window = view.window_ else { hideWindowControls(); return }
isShowingWindowControls = true
for button in windowControlButtons { button.window_ = window }
let thumbnailOrigin = NSPoint(
x: view.frame.origin.x + view.thumbnail.frame.origin.x,
y: view.frame.origin.y + view.thumbnail.frame.origin.y
)
let thumbnailWidth = view.thumbnail.frame.width
var xOffset = CGFloat(3)
var yOffset = CGFloat(2)
for button in windowControlButtons {
let shouldHide =
(button.type == .quit && !window.application.canBeQuit()) ||
(button.type == .close && !window.canBeClosed()) ||
((button.type == .miniaturize || button.type == .fullscreen) && !window.canBeMinDeminOrFullscreened())
if button.isHidden != shouldHide {
button.isHidden = shouldHide
button.needsDisplay = true
}
assignIfDifferent(&button.frame.origin, NSPoint(x: thumbnailOrigin.x + xOffset, y: thumbnailOrigin.y + yOffset))
xOffset += TrafficLightButton.size + TrafficLightButton.spacing
if xOffset + TrafficLightButton.size > thumbnailWidth {
xOffset = 3
yOffset += TrafficLightButton.size + TrafficLightButton.spacing
}
}
}
func hideWindowControls() {
guard isShowingWindowControls else { return }
isShowingWindowControls = false
for button in windowControlButtons {
button.isHidden = true
}
}
}
+2 -76
View File
@@ -16,10 +16,6 @@ class ThumbnailView: FlippedView {
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 quitIcon = TrafficLightButton(.quit, NSLocalizedString("Quit app", comment: ""))
var closeIcon = TrafficLightButton(.close, NSLocalizedString("Close window", comment: ""))
var minimizeIcon = TrafficLightButton(.miniaturize, NSLocalizedString("Minimize/Deminimize window", comment: ""))
var maximizeIcon = TrafficLightButton(.fullscreen, NSLocalizedString("Fullscreen/Defullscreen window", comment: ""))
var windowlessAppIndicator = WindowlessAppIndicator(tooltip: ThumbnailView.noOpenWindowToolTip)
let hStackView = FlippedView()
@@ -28,7 +24,6 @@ class ThumbnailView: FlippedView {
var mouseMovedCallback: (() -> Void)!
var dragAndDropTimer: Timer?
var indexInRecycledViews: Int!
var isShowingWindowControls = false
var isFirstInRow = false
var isLastInRow = false
@@ -36,7 +31,6 @@ class ThumbnailView: FlippedView {
var numberOfViewsInRow = 0
private var lastLabelWidth: CGFloat = -1
var windowControlIcons: [TrafficLightButton] { [quitIcon, closeIcon, minimizeIcon, maximizeIcon] }
var windowIndicatorIcons: [ThumbnailFontIconView] { [hiddenIcon, fullscreenIcon, minimizedIcon, spaceIcon] }
var receivedMouseDown = false
@@ -113,7 +107,6 @@ class ThumbnailView: FlippedView {
}
func mouseMoved() {
showOrHideWindowControls(true)
updateLabelTooltipIfNeeded()
mouseMovedCallback()
}
@@ -138,34 +131,14 @@ class ThumbnailView: FlippedView {
}
func drawHighlight() {
let isFocused = indexInRecycledViews == Windows.selectedWindowIndex
let isHovered = indexInRecycledViews == Windows.hoveredWindowIndex
setBackground(isFocused: isFocused, isHovered: isHovered)
setBorder(isFocused: isFocused, isHovered: isHovered)
if Preferences.appearanceStyle == .appIcons {
let isFocused = indexInRecycledViews == Windows.selectedWindowIndex
let isHovered = indexInRecycledViews == Windows.hoveredWindowIndex
label.isHidden = !(isFocused || isHovered)
updateAppIconsLabel(isFocused: isFocused, isHovered: isHovered)
}
}
func showOrHideWindowControls(_ shouldShowWindowControls: Bool) {
guard Preferences.appearanceStyle == .thumbnails else { return }
let shouldShow = shouldShowWindowControls && !Preferences.hideColoredCircles && !Appearance.hideThumbnails
guard isShowingWindowControls != shouldShow else { return }
isShowingWindowControls = shouldShow
for icon in windowControlIcons {
let shouldHide = !shouldShow
|| (icon.type == .quit && !(window_?.application.canBeQuit() ?? true))
|| (icon.type == .close && !(window_?.canBeClosed() ?? true))
|| ((icon.type == .miniaturize || icon.type == .fullscreen) && !(window_?.canBeMinDeminOrFullscreened() ?? true))
if icon.isHidden != shouldHide {
icon.isHidden = shouldHide
icon.needsDisplay = true
}
}
}
func updateDockLabelIcon(_ dockLabel: String?) {
assignIfDifferent(&dockLabelIcon.isHidden, dockLabel == nil || Preferences.hideAppBadges || Appearance.iconSize == 0)
@@ -198,10 +171,6 @@ class ThumbnailView: FlippedView {
appIcon.setSubviewAbove(dockLabelIcon)
label.fixHeight()
vStackView.wantsLayer = true
vStackView.layer!.backgroundColor = .clear
vStackView.layer!.borderColor = .clear
vStackView.layer!.cornerRadius = Appearance.cellCornerRadius
vStackView.layer!.borderWidth = CGFloat(1)
setSubviews([vStackView])
vStackView.setSubviews([hStackView])
hStackView.setSubviews([appIcon])
@@ -216,10 +185,6 @@ class ThumbnailView: FlippedView {
} else if Preferences.appearanceStyle == .thumbnails {
vStackView.addSubviews([thumbnail])
thumbnail.setSubviewAbove(windowlessAppIndicator)
for icon in windowControlIcons {
thumbnail.setSubviewAbove(icon)
icon.isHidden = true
}
hStackView.addSubviews([label] + windowIndicatorIcons)
} else {
hStackView.setSubviewAbove(windowlessAppIndicator)
@@ -227,33 +192,6 @@ class ThumbnailView: FlippedView {
}
}
private func getBackgroundColor(isFocused: Bool, isHovered: Bool) -> NSColor {
if isFocused {
return Appearance.highlightFocusedBackgroundColor
}
if isHovered {
return Appearance.highlightHoveredBackgroundColor
}
return NSColor.clear
}
private func setBackground(isFocused: Bool, isHovered: Bool) {
vStackView.layer!.backgroundColor = getBackgroundColor(isFocused: isFocused, isHovered: isHovered).cgColor
}
private func setBorder(isFocused: Bool, isHovered: Bool) {
if isFocused {
vStackView.layer!.borderColor = Appearance.highlightFocusedBorderColor.cgColor
vStackView.layer!.borderWidth = Appearance.highlightBorderWidth
} else if isHovered {
vStackView.layer!.borderColor = Appearance.highlightHoveredBorderColor.cgColor
vStackView.layer!.borderWidth = Appearance.highlightBorderWidth
} else {
vStackView.layer!.borderColor = NSColor.clear.cgColor
vStackView.layer!.borderWidth = 0
}
}
private func updateAppIconsLabel(isFocused: Bool, isHovered: Bool) {
let focusedView = ThumbnailsView.recycledViews[Windows.selectedWindowIndex]
var hoveredView: ThumbnailView? = nil
@@ -371,8 +309,6 @@ class ThumbnailView: FlippedView {
updateAppIcon(element, title)
updateDockLabelIcon(element.dockLabel)
setAccessibilityHelp(getAccessibilityHelp(element.application.localizedName, element.dockLabel))
windowControlIcons.forEach { $0.window_ = element }
showOrHideWindowControls(isShowingWindowControls)
mouseUpCallback = { () -> Void in App.app.focusSelectedWindow(element) }
mouseMovedCallback = { () -> Void in Windows.updateSelectedAndHoveredWindowIndex(index, true) }
}
@@ -416,16 +352,6 @@ class ThumbnailView: FlippedView {
if Preferences.appearanceStyle == .thumbnails {
assignIfDifferent(&thumbnail.frame.origin, NSPoint(x: Appearance.edgeInsetsSize, y: hStackView.frame.maxY + Appearance.intraCellPadding))
thumbnail.centerFrameInParent(x: true)
var xOffset = CGFloat(3)
var yOffset = thumbnail.frame.height - CGFloat(2)
for icon in windowControlIcons {
assignIfDifferent(&icon.frame.origin, NSPoint(x: xOffset, y: yOffset - TrafficLightButton.size))
xOffset += TrafficLightButton.size + TrafficLightButton.spacing
if xOffset + TrafficLightButton.size > thumbnail.frame.width {
xOffset = 3
yOffset -= TrafficLightButton.size + TrafficLightButton.spacing
}
}
}
if !windowlessAppIndicator.isHidden {
if Preferences.appearanceStyle != .titles {
+20 -5
View File
@@ -9,6 +9,7 @@ class ThumbnailsView {
static var thumbnailsWidth = CGFloat(0.0)
static var thumbnailsHeight = CGFloat(0.0)
static var layoutCache = LayoutCache()
var highlightOverlay = HighlightOverlayView()
init() {
updateBackgroundView()
@@ -51,15 +52,22 @@ class ThumbnailsView {
for i in 0..<ThumbnailsView.recycledViews.count {
ThumbnailsView.recycledViews[i] = ThumbnailView()
}
highlightOverlay = HighlightOverlayView()
Self.updateCachedSizes()
}
static func highlight(_ indexInRecycledViews: Int) {
let view = recycledViews[indexInRecycledViews]
view.indexInRecycledViews = indexInRecycledViews
if view.frame != NSRect.zero {
view.drawHighlight()
}
guard view.frame != .zero else { return }
view.drawHighlight()
let overlay = App.app.thumbnailsPanel.thumbnailsView.highlightOverlay
let focusedView = recycledViews[Windows.selectedWindowIndex]
let hoveredView = Windows.hoveredWindowIndex.map { recycledViews[$0] }
overlay.updateHighlight(
focusedView: focusedView.frame != .zero ? focusedView : nil,
hoveredView: hoveredView != focusedView && hoveredView?.frame != .zero ? hoveredView : nil
)
}
func nextRow(_ direction: Direction, allowWrap: Bool = true) -> [ThumbnailView]? {
@@ -175,6 +183,7 @@ class ThumbnailsView {
}
}
scrollView.documentView!.subviews = newViews
scrollView.documentView!.addSubview(highlightOverlay)
return (maxX, maxY, labelHeight, rowSignature)
}
@@ -218,6 +227,7 @@ class ThumbnailsView {
scrollView.documentView!.subviews.forEach { $0.frame.origin.x -= croppedWidth }
}
scrollView.documentView!.frame.size = NSSize(width: maxX, height: maxY)
highlightOverlay.frame = CGRect(origin: .zero, size: scrollView.documentView!.frame.size)
if let existingTrackingArea = scrollView.trackingAreas.first {
scrollView.removeTrackingArea(existingTrackingArea)
}
@@ -249,6 +259,9 @@ class ThumbnailsView {
ThumbnailsView.highlight(Windows.selectedWindowIndex)
if let hoveredWindowIndex = Windows.hoveredWindowIndex {
ThumbnailsView.highlight(hoveredWindowIndex)
if highlightOverlay.isShowingWindowControls {
highlightOverlay.showWindowControls(for: ThumbnailsView.recycledViews[hoveredWindowIndex])
}
}
}
@@ -316,8 +329,8 @@ class ScrollView: NSScrollView {
if let oldIndex = Windows.hoveredWindowIndex {
Windows.hoveredWindowIndex = nil
ThumbnailsView.highlight(oldIndex)
ThumbnailsView.recycledViews[oldIndex].showOrHideWindowControls(false)
}
App.app.thumbnailsPanel.thumbnailsView.highlightOverlay.hideWindowControls()
}
override func mouseExited(with event: NSEvent) {
@@ -332,10 +345,12 @@ class ScrollView: NSScrollView {
let location = documentView.convert(App.app.thumbnailsPanel.mouseLocationOutsideOfEventStream, from: nil)
let newTarget = findTarget(location)
guard newTarget !== previousTarget else { return }
let overlay = App.app.thumbnailsPanel.thumbnailsView.highlightOverlay
caTransaction {
if let newTarget {
previousTarget?.showOrHideWindowControls(false)
overlay.hideWindowControls()
newTarget.mouseMoved()
overlay.showWindowControls(for: newTarget)
} else {
resetHoveredWindow()
}