perf: have views under and over the thumbnailview

This commit is contained in:
lwouis committed 2026-02-11 21:35:34 +01:00
1 parent fe52511885
commit b9637e5802
5 files changed
+130 -130

No files matched your search

+8 -4
View File
@@ -210,7 +210,8 @@
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 */; };
D04BAFC1A1B2C3D4E5F60002 /* ThumbnailOverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAFC0A1B2C3D4E5F60001 /* ThumbnailOverView.swift */; };
D04BAFC3A1B2C3D4E5F60004 /* ThumbnailUnderLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAFC2A1B2C3D4E5F60003 /* ThumbnailUnderLayer.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 */; };
@@ -675,7 +676,8 @@
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>"; };
D04BAFC0A1B2C3D4E5F60001 /* ThumbnailOverView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailOverView.swift; sourceTree = "<group>"; };
D04BAFC2A1B2C3D4E5F60003 /* ThumbnailUnderLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailUnderLayer.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>"; };
@@ -1655,7 +1657,8 @@
D04BA1DF8CAB2FAB7FE9244B /* ThumbnailFontIconView.swift */,
D04BA258B56193958D60978A /* ThumbnailView.swift */,
D04BAA998119CAA8B70A2B67 /* ThumbnailsView.swift */,
D04BAFC0A1B2C3D4E5F60001 /* HighlightOverlayView.swift */,
D04BAFC0A1B2C3D4E5F60001 /* ThumbnailOverView.swift */,
D04BAFC2A1B2C3D4E5F60003 /* ThumbnailUnderLayer.swift */,
AA974BA829B7D84C0099A29E /* PreviewPanel.swift */,
);
path = "main-window";
@@ -2489,7 +2492,8 @@
D04BAD2A7F2E8BF64EE982E9 /* TextArea.swift in Sources */,
5F17452E2F1A908D00128EF8 /* WindowDiscriminator.swift in Sources */,
D04BA11E56383D082D7BE5A5 /* ThumbnailsView.swift in Sources */,
D04BAFC1A1B2C3D4E5F60002 /* HighlightOverlayView.swift in Sources */,
D04BAFC1A1B2C3D4E5F60002 /* ThumbnailOverView.swift in Sources */,
D04BAFC3A1B2C3D4E5F60004 /* ThumbnailUnderLayer.swift in Sources */,
5F21C9E42C6E94700091F72F /* CustomizeStyleSheet.swift in Sources */,
D04BA1621718FF26F8D5E75D /* CustomRecorderControl.swift in Sources */,
D04BA1637E125D38546C26E2 /* StackView.swift in Sources */,
@@ -1,13 +1,13 @@
import Cocoa
class HighlightOverlayView: FlippedView {
let focusedLayer = noAnimation { CALayer() }
let hoveredLayer = noAnimation { CALayer() }
class ThumbnailOverView: FlippedView {
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
weak var scrollView: ScrollView?
var previousTarget: ThumbnailView?
var windowControlButtons: [TrafficLightButton] { [quitButton, closeButton, minimizeButton, maximizeButton] }
@@ -15,14 +15,11 @@ class HighlightOverlayView: FlippedView {
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
}
addTrackingArea(NSTrackingArea(rect: .zero, options: [.mouseMoved, .mouseEnteredAndExited, .activeAlways, .inVisibleRect], owner: self, userInfo: nil))
}
override func hitTest(_ point: NSPoint) -> NSView? {
@@ -36,35 +33,54 @@ class HighlightOverlayView: FlippedView {
return nil
}
func updateHighlight(focusedView: ThumbnailView?, hoveredView: ThumbnailView?) {
updateLayer(focusedLayer, for: focusedView, isFocused: true)
updateLayer(hoveredLayer, for: hoveredView, isFocused: false)
// MARK: - Mouse hover management
override func mouseExited(with event: NSEvent) {
caTransaction {
previousTarget = nil
resetHoveredWindow()
}
}
private func updateLayer(_ highlightLayer: CALayer, for view: ThumbnailView?, isFocused: Bool) {
guard let view, view.frame != .zero else {
highlightLayer.isHidden = true
return
override func mouseMoved(with event: NSEvent) {
guard let scrollView, !scrollView.isCurrentlyScrolling && CursorEvents.isAllowedToMouseHover else { return }
let location = convert(App.app.thumbnailsPanel.mouseLocationOutsideOfEventStream, from: nil)
let newTarget = findTarget(location)
guard newTarget !== previousTarget else { return }
caTransaction {
if let newTarget {
hideWindowControls()
newTarget.mouseMoved()
showWindowControls(for: newTarget)
} else {
resetHoveredWindow()
}
previousTarget = newTarget
}
let hf = view.highlightFrame
let rect = CGRect(
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
highlightLayer.backgroundColor = (isFocused
? Appearance.highlightFocusedBackgroundColor
: Appearance.highlightHoveredBackgroundColor).cgColor
highlightLayer.borderColor = (isFocused
? Appearance.highlightFocusedBorderColor
: Appearance.highlightHoveredBorderColor).cgColor
highlightLayer.borderWidth = Appearance.highlightBorderWidth
highlightLayer.isHidden = false
}
private func findTarget(_ location: NSPoint) -> ThumbnailView? {
guard let documentView = superview else { return nil }
for case let view as ThumbnailView in documentView.subviews {
let frame = view.frame
let expandedFrame = CGRect(x: frame.minX - (App.shared.userInterfaceLayoutDirection == .leftToRight ? 0 : 1), y: frame.minY, width: frame.width + 1, height: frame.height + 1)
if expandedFrame.contains(location) {
return view
}
}
return nil
}
private func resetHoveredWindow() {
if let oldIndex = Windows.hoveredWindowIndex {
Windows.hoveredWindowIndex = nil
ThumbnailsView.highlight(oldIndex)
}
hideWindowControls()
}
// MARK: - Window controls
func showWindowControls(for view: ThumbnailView) {
guard Preferences.appearanceStyle == .thumbnails else { hideWindowControls(); return }
let shouldShow = !Preferences.hideColoredCircles && !Appearance.hideThumbnails
@@ -0,0 +1,46 @@
import Cocoa
class ThumbnailUnderLayer: CALayer {
let focusedLayer = noAnimation { CALayer() }
let hoveredLayer = noAnimation { CALayer() }
override init() {
super.init()
delegate = NoAnimationDelegate.shared
for highlightLayer in [focusedLayer, hoveredLayer] {
highlightLayer.isHidden = true
addSublayer(highlightLayer)
}
}
required init?(coder: NSCoder) { fatalError() }
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 hf = view.highlightFrame
let rect = CGRect(
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
highlightLayer.backgroundColor = (isFocused
? Appearance.highlightFocusedBackgroundColor
: Appearance.highlightHoveredBackgroundColor).cgColor
highlightLayer.borderColor = (isFocused
? Appearance.highlightFocusedBorderColor
: Appearance.highlightHoveredBorderColor).cgColor
highlightLayer.borderWidth = Appearance.highlightBorderWidth
highlightLayer.isHidden = false
}
}
+7 -11
View File
@@ -114,7 +114,7 @@ class ThumbnailView: FlippedView {
observeDragAndDrop()
}
/// The frame used by HighlightOverlayView to position the highlight rectangle.
/// The frame used by ThumbnailUnderLayer 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 {
@@ -360,7 +360,7 @@ class ThumbnailView: FlippedView {
// This is because its updated async. We need it positioned correctly always
let (offsetX, offsetY) = dockLabelOffset()
assignIfDifferent(&dockLabelIcon.frame.origin.x, appIcon.frame.maxX - (ThumbnailsView.layoutCache.dockLabelSize.width * offsetX).rounded())
assignIfDifferent(&dockLabelIcon.frame.origin.y, appIcon.frame.maxY - (ThumbnailsView.layoutCache.dockLabelSize.height * offsetY).rounded())
assignIfDifferent(&dockLabelIcon.frame.origin.y, (ThumbnailsView.layoutCache.dockLabelSize.height * offsetY).rounded())
}
/// positioning the dock label is messy because it's an NSTextField so it's visual size doesn't match what we can through APIs
@@ -376,15 +376,11 @@ class ThumbnailView: FlippedView {
offsetX = 0.92
}
}
var offsetY = offsetX
if Preferences.appearanceStyle == .appIcons {
if Preferences.appearanceSize == .small {
offsetY = 0.88
} else if Preferences.appearanceSize == .medium {
offsetY = 0.90
} else { // .large
offsetY = 0.92
}
var offsetY = 0.0
if Preferences.appearanceStyle == .thumbnails {
offsetY = 0.5
} else if Preferences.appearanceStyle == .titles {
offsetY = 0.1
}
return (offsetX, offsetY)
}
+22 -84
View File
@@ -9,7 +9,8 @@ class ThumbnailsView {
static var thumbnailsWidth = CGFloat(0.0)
static var thumbnailsHeight = CGFloat(0.0)
static var layoutCache = LayoutCache()
var highlightOverlay = HighlightOverlayView()
var thumbnailUnderLayer = ThumbnailUnderLayer()
var thumbnailOverView = ThumbnailOverView()
init() {
updateBackgroundView()
@@ -52,7 +53,10 @@ class ThumbnailsView {
for i in 0..<ThumbnailsView.recycledViews.count {
ThumbnailsView.recycledViews[i] = ThumbnailView()
}
highlightOverlay = HighlightOverlayView()
thumbnailUnderLayer = ThumbnailUnderLayer()
thumbnailOverView = ThumbnailOverView()
thumbnailOverView.scrollView = scrollView
lastRowSignature.removeAll()
Self.updateCachedSizes()
}
@@ -61,10 +65,10 @@ class ThumbnailsView {
view.indexInRecycledViews = indexInRecycledViews
guard view.frame != .zero else { return }
view.drawHighlight()
let overlay = App.app.thumbnailsPanel.thumbnailsView.highlightOverlay
let underLayer = App.app.thumbnailsPanel.thumbnailsView.thumbnailUnderLayer
let focusedView = recycledViews[Windows.selectedWindowIndex]
let hoveredView = Windows.hoveredWindowIndex.map { recycledViews[$0] }
overlay.updateHighlight(
underLayer.updateHighlight(
focusedView: focusedView.frame != .zero ? focusedView : nil,
hoveredView: hoveredView != focusedView && hoveredView?.frame != .zero ? hoveredView : nil
)
@@ -183,7 +187,12 @@ class ThumbnailsView {
}
}
scrollView.documentView!.subviews = newViews
scrollView.documentView!.addSubview(highlightOverlay)
scrollView.documentView!.addSubview(thumbnailOverView)
thumbnailOverView.scrollView = scrollView
let docLayer = scrollView.documentView!.layer!
if thumbnailUnderLayer.superlayer !== docLayer {
docLayer.insertSublayer(thumbnailUnderLayer, at: 0)
}
return (maxX, maxY, labelHeight, rowSignature)
}
@@ -227,7 +236,9 @@ 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)
let docSize = scrollView.documentView!.frame.size
thumbnailOverView.frame = CGRect(origin: .zero, size: docSize)
thumbnailUnderLayer.frame = CGRect(origin: .zero, size: docSize)
}
func centerRows(_ maxX: CGFloat) {
@@ -254,8 +265,8 @@ class ThumbnailsView {
ThumbnailsView.highlight(Windows.selectedWindowIndex)
if let hoveredWindowIndex = Windows.hoveredWindowIndex {
ThumbnailsView.highlight(hoveredWindowIndex)
if highlightOverlay.isShowingWindowControls {
highlightOverlay.showWindowControls(for: ThumbnailsView.recycledViews[hoveredWindowIndex])
if thumbnailOverView.isShowingWindowControls {
thumbnailOverView.showWindowControls(for: ThumbnailsView.recycledViews[hoveredWindowIndex])
}
}
}
@@ -292,11 +303,11 @@ class ScrollView: NSScrollView {
override class var isCompatibleWithResponsiveScrolling: Bool { true }
var isCurrentlyScrolling = false
var previousTarget: ThumbnailView?
convenience init() {
self.init(frame: .zero)
documentView = FlippedView(frame: .zero)
documentView!.wantsLayer = true
drawsBackground = false
hasVerticalScroller = true
verticalScrollElasticity = .none
@@ -304,7 +315,6 @@ class ScrollView: NSScrollView {
scrollerKnobStyle = .light
horizontalScrollElasticity = .none
usesPredominantAxisScrolling = true
addTrackingArea(NSTrackingArea(rect: .zero, options: [.mouseMoved, .mouseEnteredAndExited, .activeAlways, .inVisibleRect], owner: self, userInfo: nil))
observeScrollingEvents()
}
@@ -313,80 +323,8 @@ class ScrollView: NSScrollView {
NotificationCenter.default.addObserver(self, selector: #selector(scrollingEnded), name: NSScrollView.didEndLiveScrollNotification, object: nil)
}
@objc private func scrollingStarted() {
isCurrentlyScrolling = true
}
@objc private func scrollingEnded() {
isCurrentlyScrolling = false
}
private func resetHoveredWindow() {
if let oldIndex = Windows.hoveredWindowIndex {
Windows.hoveredWindowIndex = nil
ThumbnailsView.highlight(oldIndex)
}
App.app.thumbnailsPanel.thumbnailsView.highlightOverlay.hideWindowControls()
}
override func mouseExited(with event: NSEvent) {
caTransaction {
previousTarget = nil
resetHoveredWindow()
}
}
override func mouseMoved(with event: NSEvent) {
guard let documentView, !isCurrentlyScrolling && CursorEvents.isAllowedToMouseHover else { return }
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 {
overlay.hideWindowControls()
newTarget.mouseMoved()
overlay.showWindowControls(for: newTarget)
} else {
resetHoveredWindow()
}
previousTarget = newTarget
}
}
private func findTarget(_ location: NSPoint) -> ThumbnailView? {
for case let view as ThumbnailView in documentView!.subviews {
let frame = view.frame
let expandedFrame = CGRect(x: frame.minX - (App.shared.userInterfaceLayoutDirection == .leftToRight ? 0 : 1), y: frame.minY, width: frame.width + 1, height: frame.height + 1)
if expandedFrame.contains(location) {
return view
}
}
return nil
}
/// Checks whether the mouse pointer is within the padding area around a thumbnail.
///
/// This is used to avoid gaps between thumbnail views where the mouse pointer might not be detected.
///
/// @return `true` if the mouse pointer is within the padding area around a thumbnail; `false` otherwise.
private func checkIfWithinInterPadding() -> Bool {
if Preferences.appearanceStyle == .appIcons {
let mouseLocation = App.app.thumbnailsPanel.mouseLocationOutsideOfEventStream
let mouseRect = NSRect(x: mouseLocation.x - Appearance.interCellPadding,
y: mouseLocation.y - Appearance.interCellPadding,
width: 2 * Appearance.interCellPadding,
height: 2 * Appearance.interCellPadding)
if let hoveredWindowIndex = Windows.hoveredWindowIndex {
let thumbnail = ThumbnailsView.recycledViews[hoveredWindowIndex]
let mouseRectInView = thumbnail.convert(mouseRect, from: nil)
if thumbnail.bounds.intersects(mouseRectInView) {
return true
}
}
}
return false
}
@objc private func scrollingStarted() { isCurrentlyScrolling = true }
@objc private func scrollingEnded() { isCurrentlyScrolling = false }
/// holding shift and using the scrolling wheel will generate a horizontal movement
/// shift can be part of shortcuts so we force shift scrolls to be vertical