perf: cache expensive layout values

This commit is contained in:
lwouis committed 2026-02-11 21:35:27 +01:00
1 parent 1f3a653faa
commit 904bd17c53
3 files changed
+36 -18

No files matched your search

+7 -8
View File
@@ -2,6 +2,7 @@ import Cocoa
class ThumbnailTitleView: NSTextField {
private var currentWidth: CGFloat = -1
private var widthConstraint: NSLayoutConstraint?
convenience init(font: NSFont) {
self.init(labelWithString: "")
@@ -21,15 +22,13 @@ class ThumbnailTitleView: NSTextField {
guard currentWidth != width else { return }
currentWidth = width
frame.size.width = width
// TODO: NSTextField does some internal magic, and ends up with constraints.
// we can't use addOrUpdateConstraint for some reason, otherwise it will only actually apply after the UI is shown twice
// i tried everything and ended up removing all constraints then adding a fresh one. This seems to work
let toRemove = constraints.filter {
($0.firstItem as? NSView) === self && $0.firstAttribute == .width ||
($0.secondItem as? NSView) === self && $0.secondAttribute == .width
if let widthConstraint {
widthConstraint.constant = width
} else {
let constraint = widthAnchor.constraint(equalToConstant: width)
constraint.isActive = true
widthConstraint = constraint
}
toRemove.forEach { removeConstraint($0) }
widthAnchor.constraint(equalToConstant: width).isActive = true
}
override func mouseMoved(with event: NSEvent) {
+9 -9
View File
@@ -4,6 +4,7 @@ class ThumbnailView: FlippedView {
static let noOpenWindowToolTip = NSLocalizedString("App is running but has no open window", comment: "")
// when calculating the width of a nstextfield, somehow we need to add this suffix to get the correct width
static let extraTextForPadding = "lmnopqrstuvw"
var window_: Window?
var thumbnail = LightImageView(withTransparencyChecks: true)
var appIcon = LightImageView()
@@ -310,7 +311,7 @@ class ThumbnailView: FlippedView {
}
}
let xPosition = -leftOffset
let height = label.fittingSize.height
let height = ThumbnailsView.layoutCache.labelHeight
let yPosition = hStackView.frame.origin.y + hStackView.frame.height + Appearance.intraCellPadding * 2
label.frame = NSRect(x: xPosition, y: yPosition, width: effectiveLabelWidth, height: height)
label.setWidth(effectiveLabelWidth)
@@ -379,7 +380,7 @@ class ThumbnailView: FlippedView {
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, label.fittingSize.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()
label.setWidth(labelWidth)
}
@@ -391,12 +392,12 @@ class ThumbnailView: FlippedView {
assignIfDifferent(&appIcon.frame.origin.x, App.shared.userInterfaceLayoutDirection == .leftToRight
? 0
: hStackView.frame.width - appIcon.frame.width)
let iconWidth = windowIndicatorIcons.first!.fittingSize.width
let iconWidth = ThumbnailsView.layoutCache.iconWidth
var indicatorSpace = CGFloat(0)
for icon in windowIndicatorIcons {
if !icon.isHidden {
indicatorSpace += iconWidth
icon.centerFrameInParent(y: true)
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)
@@ -406,7 +407,7 @@ class ThumbnailView: FlippedView {
assignIfDifferent(&label.frame.origin.x, App.shared.userInterfaceLayoutDirection == .leftToRight
? appIcon.frame.maxX + Appearance.appIconLabelSpacing
: hStackView.frame.width - appIcon.frame.width - Appearance.appIconLabelSpacing - labelWidth)
label.centerFrameInParent(y: true)
assignIfDifferent(&label.frame.origin.y, ((hStackView.frame.height - ThumbnailsView.layoutCache.labelHeight) / 2).rounded())
}
if Preferences.appearanceStyle == .thumbnails {
assignIfDifferent(&thumbnail.frame.origin, NSPoint(x: Appearance.edgeInsetsSize, y: hStackView.frame.maxY + Appearance.intraCellPadding))
@@ -438,8 +439,8 @@ class ThumbnailView: FlippedView {
// we set dockLabelIcon origin, without checking if .isHidden
// This is because its updated async. We need it positioned correctly always
let (offsetX, offsetY) = dockLabelOffset()
assignIfDifferent(&dockLabelIcon.frame.origin.x, appIcon.frame.maxX - (dockLabelIcon.fittingSize.width * offsetX).rounded())
assignIfDifferent(&dockLabelIcon.frame.origin.y, appIcon.frame.maxY - (dockLabelIcon.fittingSize.height * offsetY).rounded())
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())
}
/// positioning the dock label is messy because it's an NSTextField so it's visual size doesn't match what we can through APIs
@@ -469,8 +470,7 @@ class ThumbnailView: FlippedView {
}
private func indicatorsSpace() -> CGFloat {
let iconWidth = windowIndicatorIcons.first!.fittingSize.width
return CGFloat(windowIndicatorIcons.filter { !$0.isHidden }.count) * iconWidth
return CGFloat(windowIndicatorIcons.filter { !$0.isHidden }.count) * ThumbnailsView.layoutCache.iconWidth
}
private func getAppOrAndWindowTitle() -> String {
+20 -1
View File
@@ -8,11 +8,22 @@ class ThumbnailsView {
static var recycledViews = [ThumbnailView]()
static var thumbnailsWidth = CGFloat(0.0)
static var thumbnailsHeight = CGFloat(0.0)
static var layoutCache = LayoutCache()
init() {
updateBackgroundView()
// TODO: think about this optimization more
(1...20).forEach { _ in ThumbnailsView.recycledViews.append(ThumbnailView()) }
Self.updateCachedSizes()
}
static func updateCachedSizes() {
guard let firstView = ThumbnailsView.recycledViews.first else { return }
layoutCache.labelHeight = firstView.label.fittingSize.height
let iconFittingSize = firstView.windowIndicatorIcons.first!.fittingSize
layoutCache.iconWidth = iconFittingSize.width
layoutCache.iconHeight = iconFittingSize.height
layoutCache.dockLabelSize = firstView.dockLabelIcon.fittingSize
}
func updateBackgroundView() {
@@ -39,6 +50,7 @@ class ThumbnailsView {
for i in 0..<ThumbnailsView.recycledViews.count {
ThumbnailsView.recycledViews[i] = ThumbnailView()
}
Self.updateCachedSizes()
}
static func highlight(_ indexInRecycledViews: Int) {
@@ -116,7 +128,7 @@ class ThumbnailsView {
}
private func layoutThumbnailViews(_ widthMax: CGFloat) -> (CGFloat, CGFloat, CGFloat, [Int])? {
let labelHeight = ThumbnailsView.recycledViews.first!.label.fittingSize.height
let labelHeight = Self.layoutCache.labelHeight
let height = ThumbnailView.height(labelHeight)
let isLeftToRight = App.shared.userInterfaceLayoutDirection == .leftToRight
let startingX = isLeftToRight ? Appearance.interCellPadding : widthMax - Appearance.interCellPadding
@@ -247,6 +259,13 @@ class ThumbnailsView {
}
}
}
struct LayoutCache {
var labelHeight = CGFloat(0)
var iconWidth = CGFloat(0)
var iconHeight = CGFloat(0)
var dockLabelSize = NSSize.zero
}
}
class ScrollView: NSScrollView {