From 904bd17c53993de3faeb7adf43e9188dba60bd93 Mon Sep 17 00:00:00 2001 From: lwouis Date: Sun, 8 Feb 2026 11:55:14 +0100 Subject: [PATCH] perf: cache expensive layout values --- src/ui/main-window/ThumbnailTitleView.swift | 15 +++++++-------- src/ui/main-window/ThumbnailView.swift | 18 +++++++++--------- src/ui/main-window/ThumbnailsView.swift | 21 ++++++++++++++++++++- 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/src/ui/main-window/ThumbnailTitleView.swift b/src/ui/main-window/ThumbnailTitleView.swift index 31c2e744..ab092d73 100644 --- a/src/ui/main-window/ThumbnailTitleView.swift +++ b/src/ui/main-window/ThumbnailTitleView.swift @@ -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) { diff --git a/src/ui/main-window/ThumbnailView.swift b/src/ui/main-window/ThumbnailView.swift index 8fd1ac7e..36a20306 100644 --- a/src/ui/main-window/ThumbnailView.swift +++ b/src/ui/main-window/ThumbnailView.swift @@ -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 { diff --git a/src/ui/main-window/ThumbnailsView.swift b/src/ui/main-window/ThumbnailsView.swift index d7b6f8ec..316c8ee9 100644 --- a/src/ui/main-window/ThumbnailsView.swift +++ b/src/ui/main-window/ThumbnailsView.swift @@ -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.. (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 {