import AppKit
|
import Foundation
|
import QuartzCore
|
import AlignerCore
|
|
@MainActor
|
final class Round1PerformancePoCController {
|
static let overlayTitle = "Aligner Round01 Performance PoC"
|
|
private let view = Round1PerformancePoCView()
|
private lazy var coordinator = WindowCoordinator(
|
contentView: view,
|
title: Self.overlayTitle
|
)
|
private var autoCycleTimer: Timer?
|
private var lifecycleTargetCycles = 0
|
private var lifecycleCompletedCycles = 0
|
private var lifecycleMaximumOverlayWindows = 0
|
private var lifecycleMaximumVisibleOverlayWindows = 0
|
private var lifecycleResidualOverlayWindows = 0
|
private var lifecycleResidualVisibleOverlayWindows = 0
|
|
func show(autoCycle: Bool) {
|
view.onEscape = { [weak self] in
|
self?.hide(reason: .escape)
|
}
|
view.beginFirstFrameMeasurement()
|
coordinator.openQuickSwitch()
|
coordinator.promoteForTextInput()
|
view.window?.makeFirstResponder(view)
|
view.layoutSubtreeIfNeeded()
|
DispatchQueue.main.async { [weak view] in
|
view?.recordFirstFrameIfNeeded()
|
}
|
view.startSimulatedScreenshotRefill()
|
updateLifecycleWindowHighWaterMark()
|
|
if autoCycle {
|
startAutoCycle()
|
}
|
}
|
|
func hide() {
|
hide(reason: .userClosed)
|
}
|
|
func reportDictionary() -> [String: Any] {
|
var report = view.reportDictionary()
|
report["overlayVisible"] = coordinator.isQuickSwitchVisible
|
report["overlayWindowCount"] = overlayWindowCount()
|
report["visibleOverlayWindowCount"] = visibleOverlayWindowCount()
|
report["lifecycleTargetCycles"] = lifecycleTargetCycles
|
report["lifecycleCompletedCycles"] = lifecycleCompletedCycles
|
report["lifecycleMaximumOverlayWindows"] = lifecycleMaximumOverlayWindows
|
report["lifecycleMaximumVisibleOverlayWindows"] = lifecycleMaximumVisibleOverlayWindows
|
report["lifecycleResidualOverlayWindows"] = lifecycleResidualOverlayWindows
|
report["lifecycleResidualVisibleOverlayWindows"] = lifecycleResidualVisibleOverlayWindows
|
report["lifecycleGatePassed"] = lifecycleTargetCycles == 0
|
|| (
|
lifecycleCompletedCycles == lifecycleTargetCycles
|
&& lifecycleMaximumOverlayWindows <= 1
|
&& lifecycleMaximumVisibleOverlayWindows <= 1
|
&& lifecycleResidualOverlayWindows <= 1
|
&& lifecycleResidualVisibleOverlayWindows == 0
|
)
|
return report
|
}
|
|
func prepareLifecycleRun(targetCycles: Int) {
|
lifecycleTargetCycles = max(0, targetCycles)
|
lifecycleCompletedCycles = 0
|
lifecycleMaximumOverlayWindows = 0
|
lifecycleMaximumVisibleOverlayWindows = 0
|
lifecycleResidualOverlayWindows = 0
|
lifecycleResidualVisibleOverlayWindows = 0
|
view.resetPerformanceMeasurements()
|
}
|
|
func markLifecycleCycleCompleted() {
|
lifecycleCompletedCycles += 1
|
updateLifecycleWindowHighWaterMark()
|
}
|
|
func finalizeLifecycleRun() {
|
lifecycleResidualOverlayWindows = overlayWindowCount()
|
lifecycleResidualVisibleOverlayWindows = visibleOverlayWindowCount()
|
updateLifecycleWindowHighWaterMark()
|
}
|
|
private func hide(reason: DismissReason) {
|
stopAutoCycle()
|
view.stopSimulatedScreenshotRefill()
|
coordinator.closeQuickSwitch(reason: reason)
|
lifecycleResidualOverlayWindows = overlayWindowCount()
|
lifecycleResidualVisibleOverlayWindows = visibleOverlayWindowCount()
|
}
|
|
private func startAutoCycle() {
|
stopAutoCycle()
|
autoCycleTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { [weak self] _ in
|
Task { @MainActor in
|
guard let self else { return }
|
self.view.moveSelection(delta: 1)
|
self.view.simulateHoverStep(delta: 7)
|
}
|
}
|
}
|
|
private func stopAutoCycle() {
|
autoCycleTimer?.invalidate()
|
autoCycleTimer = nil
|
}
|
|
private func updateLifecycleWindowHighWaterMark() {
|
lifecycleMaximumOverlayWindows = max(
|
lifecycleMaximumOverlayWindows,
|
overlayWindowCount()
|
)
|
lifecycleMaximumVisibleOverlayWindows = max(
|
lifecycleMaximumVisibleOverlayWindows,
|
visibleOverlayWindowCount()
|
)
|
}
|
|
private func overlayWindowCount() -> Int {
|
NSApp.windows.filter { window in
|
window.title == Self.overlayTitle
|
}.count
|
}
|
|
private func visibleOverlayWindowCount() -> Int {
|
NSApp.windows.filter { window in
|
window.title == Self.overlayTitle && window.isVisible
|
}.count
|
}
|
}
|
|
struct Round1PerformancePoCOptions {
|
let showPerformancePoC: Bool
|
let autoCycle: Bool
|
let autoHideAfter: TimeInterval?
|
let quitAfter: TimeInterval?
|
let reportPath: String?
|
let lifecycleCycles: Int?
|
let lifecycleInterval: TimeInterval
|
let lifecycleVisibleDuration: TimeInterval
|
|
static func parse(arguments: [String]) -> Round1PerformancePoCOptions {
|
Round1PerformancePoCOptions(
|
showPerformancePoC: arguments.contains("--round01-performance-poc")
|
|| arguments.contains("--round1-performance-poc"),
|
autoCycle: arguments.contains("--round01-poc-auto-cycle")
|
|| arguments.contains("--round1-poc-auto-cycle"),
|
autoHideAfter: timeInterval(for: "--round01-poc-auto-hide-after", in: arguments)
|
?? timeInterval(for: "--round1-poc-auto-hide-after", in: arguments),
|
quitAfter: timeInterval(for: "--round01-poc-quit-after", in: arguments)
|
?? timeInterval(for: "--round1-poc-quit-after", in: arguments),
|
reportPath: stringValue(for: "--round01-poc-report", in: arguments)
|
?? stringValue(for: "--round1-poc-report", in: arguments),
|
lifecycleCycles: intValue(for: "--round01-poc-lifecycle-cycles", in: arguments)
|
?? intValue(for: "--round1-poc-lifecycle-cycles", in: arguments),
|
lifecycleInterval: timeInterval(for: "--round01-poc-lifecycle-interval", in: arguments)
|
?? timeInterval(for: "--round1-poc-lifecycle-interval", in: arguments)
|
?? 0.03,
|
lifecycleVisibleDuration: timeInterval(for: "--round01-poc-lifecycle-visible-duration", in: arguments)
|
?? timeInterval(for: "--round1-poc-lifecycle-visible-duration", in: arguments)
|
?? 0.24
|
)
|
}
|
|
private static func timeInterval(for key: String, in arguments: [String]) -> TimeInterval? {
|
let prefix = "\(key)="
|
guard let argument = arguments.first(where: { $0.hasPrefix(prefix) }) else {
|
return nil
|
}
|
|
return TimeInterval(argument.dropFirst(prefix.count))
|
}
|
|
private static func stringValue(for key: String, in arguments: [String]) -> String? {
|
let prefix = "\(key)="
|
guard let argument = arguments.first(where: { $0.hasPrefix(prefix) }) else {
|
return nil
|
}
|
|
return String(argument.dropFirst(prefix.count))
|
}
|
|
private static func intValue(for key: String, in arguments: [String]) -> Int? {
|
let prefix = "\(key)="
|
guard let argument = arguments.first(where: { $0.hasPrefix(prefix) }) else {
|
return nil
|
}
|
|
return Int(argument.dropFirst(prefix.count))
|
}
|
}
|
|
@MainActor
|
private final class Round1PerformancePoCView: NSView {
|
private static let cardCount = 200
|
private static let appCount = 20
|
|
private let backgroundLayer = CAGradientLayer()
|
private let appShelfLayer = CALayer()
|
private let waterfallLayer = CALayer()
|
private let spotlightLayer = CAGradientLayer()
|
private var cardLayers: [CALayer] = []
|
private var thumbnailLayers: [CALayer] = []
|
private var appLayers: [CALayer] = []
|
private var trackingArea: NSTrackingArea?
|
private var selectedIndex = 0
|
private var hoveredIndex: Int?
|
private var scrollOffset: CGFloat = 0
|
private var scrollAdjustmentCount = 0
|
private var simulatedScreenshotRefillCount = 0
|
private var firstFrameMeasurementStart: CFTimeInterval?
|
private var firstFrameLatencyMilliseconds: [Double] = []
|
private var keyboardResponseLatencyMilliseconds: [Double] = []
|
private var hoverResponseLatencyMilliseconds: [Double] = []
|
private var screenshotRefillTask: Task<Void, Never>?
|
var onEscape: (() -> Void)?
|
|
override init(frame frameRect: NSRect) {
|
super.init(frame: frameRect)
|
wantsLayer = true
|
setupLayers()
|
}
|
|
required init?(coder: NSCoder) {
|
super.init(coder: coder)
|
wantsLayer = true
|
setupLayers()
|
}
|
|
override var acceptsFirstResponder: Bool {
|
true
|
}
|
|
override func updateTrackingAreas() {
|
super.updateTrackingAreas()
|
|
if let trackingArea {
|
removeTrackingArea(trackingArea)
|
}
|
|
let area = NSTrackingArea(
|
rect: bounds,
|
options: [.mouseMoved, .activeAlways, .inVisibleRect],
|
owner: self
|
)
|
addTrackingArea(area)
|
trackingArea = area
|
}
|
|
override func layout() {
|
super.layout()
|
layoutLayers()
|
}
|
|
override func mouseMoved(with event: NSEvent) {
|
let point = convert(event.locationInWindow, from: nil)
|
guard let index = cardLayers.firstIndex(where: { $0.frame.offsetBy(dx: 0, dy: scrollOffset).contains(point) }) else { return }
|
hoverCard(at: index, animated: true)
|
selectCard(at: index)
|
}
|
|
override func keyDown(with event: NSEvent) {
|
switch event.keyCode {
|
case 48, 124, 125:
|
moveSelection(delta: 1)
|
case 123, 126:
|
moveSelection(delta: -1)
|
case 53:
|
onEscape?()
|
default:
|
super.keyDown(with: event)
|
}
|
}
|
|
override func scrollWheel(with event: NSEvent) {
|
setScrollOffset(scrollOffset - event.scrollingDeltaY, animated: true)
|
}
|
|
func moveSelection(delta: Int) {
|
guard !cardLayers.isEmpty else { return }
|
let measurementStart = CACurrentMediaTime()
|
let nextIndex = min(max(selectedIndex + delta, 0), cardLayers.count - 1)
|
selectCard(at: nextIndex)
|
CATransaction.flush()
|
keyboardResponseLatencyMilliseconds.append(milliseconds(since: measurementStart))
|
}
|
|
func simulateHoverStep(delta: Int) {
|
guard !cardLayers.isEmpty else { return }
|
let baseIndex = hoveredIndex ?? selectedIndex
|
let nextIndex = (baseIndex + delta) % cardLayers.count
|
let measurementStart = CACurrentMediaTime()
|
hoverCard(at: nextIndex, animated: true)
|
selectCard(at: nextIndex)
|
CATransaction.flush()
|
hoverResponseLatencyMilliseconds.append(milliseconds(since: measurementStart))
|
}
|
|
func startSimulatedScreenshotRefill() {
|
stopSimulatedScreenshotRefill()
|
simulatedScreenshotRefillCount = 0
|
screenshotRefillTask = Task { @MainActor [weak self] in
|
guard let self else { return }
|
let batchSize = 20
|
for startIndex in stride(from: 0, to: self.thumbnailLayers.count, by: batchSize) {
|
if Task.isCancelled { return }
|
let endIndex = min(startIndex + batchSize, self.thumbnailLayers.count)
|
self.refillSimulatedScreenshots(in: startIndex..<endIndex)
|
try? await Task.sleep(nanoseconds: 20_000_000)
|
}
|
}
|
}
|
|
func stopSimulatedScreenshotRefill() {
|
screenshotRefillTask?.cancel()
|
screenshotRefillTask = nil
|
}
|
|
func beginFirstFrameMeasurement() {
|
firstFrameMeasurementStart = CACurrentMediaTime()
|
}
|
|
func recordFirstFrameIfNeeded() {
|
guard let firstFrameMeasurementStart else { return }
|
|
CATransaction.flush()
|
firstFrameLatencyMilliseconds.append(milliseconds(since: firstFrameMeasurementStart))
|
self.firstFrameMeasurementStart = nil
|
}
|
|
func resetPerformanceMeasurements() {
|
firstFrameMeasurementStart = nil
|
firstFrameLatencyMilliseconds = []
|
keyboardResponseLatencyMilliseconds = []
|
hoverResponseLatencyMilliseconds = []
|
}
|
|
func reportDictionary() -> [String: Any] {
|
let firstFrameP95 = percentile95(firstFrameLatencyMilliseconds)
|
let keyboardP95 = percentile95(keyboardResponseLatencyMilliseconds)
|
let hoverP95 = percentile95(hoverResponseLatencyMilliseconds)
|
let firstFrameGatePassed = firstFrameP95.map { $0 < 150 } ?? false
|
let keyboardGatePassed = keyboardP95.map { $0 < 50 } ?? false
|
let hoverGatePassed = hoverP95.map { $0 <= 32 } ?? false
|
let interactionPerformanceGatePassed = firstFrameGatePassed && keyboardGatePassed && hoverGatePassed
|
|
return [
|
"cardCount": Self.cardCount,
|
"appCount": Self.appCount,
|
"cardLayerCount": cardLayers.count,
|
"appLayerCount": appLayers.count,
|
"thumbnailLayerCount": thumbnailLayers.count,
|
"simulatedScreenshotRefillCount": simulatedScreenshotRefillCount,
|
"selectedIndex": selectedIndex,
|
"scrollOffset": Double(scrollOffset),
|
"maximumScrollOffset": Double(maximumScrollOffset()),
|
"scrollAdjustmentCount": scrollAdjustmentCount,
|
"waterfallScrollable": maximumScrollOffset() > 0,
|
"spotlightVisible": spotlightLayer.opacity > 0,
|
"spotlightAlignedWithSelectedCard": spotlightLayer.frame.equalTo(
|
cardLayers[selectedIndex].frame.insetBy(dx: -10, dy: -8)
|
),
|
"firstFrameLatencySampleCount": firstFrameLatencyMilliseconds.count,
|
"firstFrameLatencyP95Milliseconds": jsonNumber(firstFrameP95),
|
"firstFrameLatencyMaxMilliseconds": jsonNumber(firstFrameLatencyMilliseconds.max()),
|
"keyboardResponseSampleCount": keyboardResponseLatencyMilliseconds.count,
|
"keyboardResponseP95Milliseconds": jsonNumber(keyboardP95),
|
"keyboardResponseMaxMilliseconds": jsonNumber(keyboardResponseLatencyMilliseconds.max()),
|
"hoverResponseSampleCount": hoverResponseLatencyMilliseconds.count,
|
"hoverResponseP95Milliseconds": jsonNumber(hoverP95),
|
"hoverResponseMaxMilliseconds": jsonNumber(hoverResponseLatencyMilliseconds.max()),
|
"performanceThresholds": [
|
"firstFrameMilliseconds": 150,
|
"keyboardP95Milliseconds": 50,
|
"hoverP95Milliseconds": 32
|
],
|
"interactionPerformanceGatePassed": interactionPerformanceGatePassed,
|
"performanceGatePassed": interactionPerformanceGatePassed
|
]
|
}
|
|
private func setupLayers() {
|
guard let rootLayer = layer else { return }
|
|
rootLayer.masksToBounds = true
|
backgroundLayer.colors = [
|
NSColor.windowBackgroundColor.withAlphaComponent(0.92).cgColor,
|
NSColor.controlBackgroundColor.withAlphaComponent(0.84).cgColor
|
]
|
backgroundLayer.startPoint = CGPoint(x: 0.5, y: 1.0)
|
backgroundLayer.endPoint = CGPoint(x: 0.5, y: 0.0)
|
rootLayer.addSublayer(backgroundLayer)
|
|
rootLayer.addSublayer(appShelfLayer)
|
rootLayer.addSublayer(waterfallLayer)
|
|
appLayers = (0..<Self.appCount).map { index in
|
let appLayer = CALayer()
|
appLayer.cornerRadius = 12
|
appLayer.backgroundColor = appColor(index: index).cgColor
|
appLayer.shadowColor = NSColor.black.cgColor
|
appLayer.shadowOpacity = 0.18
|
appLayer.shadowRadius = 8
|
appLayer.shadowOffset = CGSize(width: 0, height: -1)
|
appShelfLayer.addSublayer(appLayer)
|
return appLayer
|
}
|
|
let cardsAndThumbnails: [(CALayer, CALayer)] = (0..<Self.cardCount).map { index in
|
let cardLayer = CALayer()
|
cardLayer.cornerRadius = 14
|
cardLayer.backgroundColor = cardColor(index: index).cgColor
|
cardLayer.borderColor = NSColor.separatorColor.withAlphaComponent(0.3).cgColor
|
cardLayer.borderWidth = 1
|
cardLayer.shadowColor = NSColor.black.cgColor
|
cardLayer.shadowOpacity = 0.0
|
cardLayer.shadowRadius = 0
|
cardLayer.shadowOffset = CGSize(width: 0, height: -2)
|
|
let thumbnailLayer = CALayer()
|
thumbnailLayer.cornerRadius = 9
|
thumbnailLayer.backgroundColor = NSColor.quaternaryLabelColor.withAlphaComponent(0.28).cgColor
|
thumbnailLayer.borderColor = NSColor.separatorColor.withAlphaComponent(0.2).cgColor
|
thumbnailLayer.borderWidth = 1
|
cardLayer.addSublayer(thumbnailLayer)
|
|
waterfallLayer.addSublayer(cardLayer)
|
return (cardLayer, thumbnailLayer)
|
}
|
cardLayers = cardsAndThumbnails.map(\.0)
|
thumbnailLayers = cardsAndThumbnails.map(\.1)
|
|
spotlightLayer.colors = [
|
NSColor.clear.cgColor,
|
NSColor.white.withAlphaComponent(0.28).cgColor,
|
NSColor.clear.cgColor
|
]
|
spotlightLayer.locations = [0, 0.5, 1]
|
spotlightLayer.startPoint = CGPoint(x: 0, y: 0.5)
|
spotlightLayer.endPoint = CGPoint(x: 1, y: 0.5)
|
spotlightLayer.opacity = 0
|
waterfallLayer.addSublayer(spotlightLayer)
|
selectCard(at: 0, animated: false)
|
}
|
|
private func layoutLayers() {
|
CATransaction.begin()
|
CATransaction.setDisableActions(true)
|
|
backgroundLayer.frame = bounds
|
appShelfLayer.frame = CGRect(x: 0, y: bounds.height - 108, width: bounds.width, height: 92)
|
waterfallLayer.frame = bounds
|
|
layoutAppShelf()
|
layoutCards()
|
applyScrollOffset(animated: false)
|
positionSpotlight(animated: false)
|
|
CATransaction.commit()
|
}
|
|
private func layoutAppShelf() {
|
let iconSize: CGFloat = 48
|
let gap: CGFloat = 14
|
let totalWidth = CGFloat(appLayers.count) * iconSize + CGFloat(max(0, appLayers.count - 1)) * gap
|
let startX = max(24, (bounds.width - totalWidth) / 2)
|
|
for (index, layer) in appLayers.enumerated() {
|
layer.frame = CGRect(
|
x: startX + CGFloat(index) * (iconSize + gap),
|
y: 20,
|
width: iconSize,
|
height: iconSize
|
)
|
}
|
}
|
|
private func layoutCards() {
|
let columns = max(1, min(10, Int(bounds.width / 180)))
|
let gap: CGFloat = 16
|
let sideInset: CGFloat = max(32, (bounds.width - CGFloat(columns) * 156 - CGFloat(columns - 1) * gap) / 2)
|
let topY = bounds.height - 204
|
|
for (index, layer) in cardLayers.enumerated() {
|
let column = index % columns
|
let row = index / columns
|
layer.frame = CGRect(
|
x: sideInset + CGFloat(column) * (156 + gap),
|
y: topY - CGFloat(row) * 104,
|
width: 156,
|
height: 86
|
)
|
thumbnailLayers[index].frame = CGRect(x: 12, y: 16, width: 132, height: 52)
|
}
|
setScrollOffset(scrollOffset, animated: false)
|
updateCardVisual(at: selectedIndex, animated: false)
|
if let hoveredIndex {
|
updateCardVisual(at: hoveredIndex, animated: false)
|
}
|
}
|
|
private func selectCard(at index: Int, animated: Bool = true) {
|
guard cardLayers.indices.contains(index) else { return }
|
let oldIndex = selectedIndex
|
selectedIndex = index
|
|
updateCardVisual(at: oldIndex, animated: animated)
|
updateCardVisual(at: selectedIndex, animated: animated)
|
scrollSelectedCardIntoView(animated: animated)
|
positionSpotlight(animated: animated)
|
}
|
|
private func hoverCard(at index: Int, animated: Bool) {
|
guard cardLayers.indices.contains(index) else { return }
|
|
let oldIndex = hoveredIndex
|
hoveredIndex = index
|
|
if let oldIndex {
|
updateCardVisual(at: oldIndex, animated: animated)
|
}
|
updateCardVisual(at: index, animated: animated)
|
}
|
|
private func updateCardVisual(at index: Int, animated: Bool) {
|
guard cardLayers.indices.contains(index) else { return }
|
let layer = cardLayers[index]
|
let isSelected = index == selectedIndex
|
let isHovered = index == hoveredIndex
|
let scale: CGFloat
|
let shadowOpacity: Float
|
let shadowRadius: CGFloat
|
let borderColor: CGColor
|
|
if isSelected {
|
scale = 1.045
|
shadowOpacity = 0.32
|
shadowRadius = 18
|
borderColor = NSColor.keyboardFocusIndicatorColor.withAlphaComponent(0.72).cgColor
|
} else if isHovered {
|
scale = 1.025
|
shadowOpacity = 0.22
|
shadowRadius = 12
|
borderColor = NSColor.controlAccentColor.withAlphaComponent(0.48).cgColor
|
} else {
|
scale = 1
|
shadowOpacity = 0
|
shadowRadius = 0
|
borderColor = NSColor.separatorColor.withAlphaComponent(0.3).cgColor
|
}
|
|
CATransaction.begin()
|
CATransaction.setAnimationDuration(animated ? 0.16 : 0)
|
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: .easeOut))
|
layer.transform = scale == 1 ? CATransform3DIdentity : CATransform3DMakeScale(scale, scale, 1)
|
layer.shadowOpacity = shadowOpacity
|
layer.shadowRadius = shadowRadius
|
layer.borderColor = borderColor
|
CATransaction.commit()
|
}
|
|
private func positionSpotlight(animated: Bool) {
|
guard cardLayers.indices.contains(selectedIndex) else { return }
|
let selectedFrame = cardLayers[selectedIndex].frame.insetBy(dx: -10, dy: -8)
|
|
CATransaction.begin()
|
CATransaction.setAnimationDuration(animated ? 0.16 : 0)
|
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: .easeOut))
|
spotlightLayer.frame = selectedFrame
|
spotlightLayer.cornerRadius = 16
|
spotlightLayer.opacity = 1
|
CATransaction.commit()
|
|
guard animated else { return }
|
let animation = CABasicAnimation(keyPath: "locations")
|
animation.fromValue = [-0.6, -0.2, 0.2]
|
animation.toValue = [0.8, 1.2, 1.6]
|
animation.duration = 0.8
|
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
spotlightLayer.add(animation, forKey: "aligner.round1.spotlight")
|
}
|
|
private func refillSimulatedScreenshots(in range: Range<Int>) {
|
CATransaction.begin()
|
CATransaction.setAnimationDuration(0.12)
|
for index in range where thumbnailLayers.indices.contains(index) {
|
thumbnailLayers[index].backgroundColor = simulatedScreenshotColor(index: index).cgColor
|
thumbnailLayers[index].borderColor = NSColor.white.withAlphaComponent(0.36).cgColor
|
simulatedScreenshotRefillCount += 1
|
}
|
CATransaction.commit()
|
}
|
|
private func scrollSelectedCardIntoView(animated: Bool) {
|
guard cardLayers.indices.contains(selectedIndex) else { return }
|
|
let visibleMinY: CGFloat = 24
|
let visibleMaxY = max(visibleMinY, bounds.height - 128)
|
let selectedFrame = cardLayers[selectedIndex].frame.offsetBy(dx: 0, dy: scrollOffset)
|
var nextOffset = scrollOffset
|
|
if selectedFrame.minY < visibleMinY {
|
nextOffset += visibleMinY - selectedFrame.minY
|
} else if selectedFrame.maxY > visibleMaxY {
|
nextOffset -= selectedFrame.maxY - visibleMaxY
|
}
|
|
let clampedOffset = min(max(nextOffset, 0), maximumScrollOffset())
|
if clampedOffset != scrollOffset {
|
scrollAdjustmentCount += 1
|
setScrollOffset(clampedOffset, animated: animated)
|
}
|
}
|
|
private func setScrollOffset(_ offset: CGFloat, animated: Bool) {
|
let clampedOffset = min(max(offset, 0), maximumScrollOffset())
|
guard clampedOffset != scrollOffset || waterfallLayer.sublayerTransform.m42 != clampedOffset else { return }
|
scrollOffset = clampedOffset
|
applyScrollOffset(animated: animated)
|
positionSpotlight(animated: animated)
|
}
|
|
private func applyScrollOffset(animated: Bool) {
|
CATransaction.begin()
|
CATransaction.setAnimationDuration(animated ? 0.12 : 0)
|
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: .easeOut))
|
waterfallLayer.sublayerTransform = CATransform3DMakeTranslation(0, scrollOffset, 0)
|
CATransaction.commit()
|
}
|
|
private func maximumScrollOffset() -> CGFloat {
|
guard let minY = cardLayers.map(\.frame.minY).min() else { return 0 }
|
return max(0, 24 - minY)
|
}
|
|
private func appColor(index: Int) -> NSColor {
|
let hue = CGFloat(index) / CGFloat(Self.appCount)
|
return NSColor(calibratedHue: hue, saturation: 0.56, brightness: 0.88, alpha: 1)
|
}
|
|
private func cardColor(index: Int) -> NSColor {
|
let hue = CGFloat(index % Self.appCount) / CGFloat(Self.appCount)
|
return NSColor(calibratedHue: hue, saturation: 0.18, brightness: 0.96, alpha: 0.92)
|
}
|
|
private func simulatedScreenshotColor(index: Int) -> NSColor {
|
let hue = CGFloat((index * 7) % Self.cardCount) / CGFloat(Self.cardCount)
|
return NSColor(calibratedHue: hue, saturation: 0.36, brightness: 0.78, alpha: 0.9)
|
}
|
|
private func milliseconds(since start: CFTimeInterval) -> Double {
|
(CACurrentMediaTime() - start) * 1_000
|
}
|
|
private func percentile95(_ values: [Double]) -> Double? {
|
guard !values.isEmpty else { return nil }
|
|
let sortedValues = values.sorted()
|
let index = min(
|
sortedValues.count - 1,
|
Int(ceil(Double(sortedValues.count) * 0.95)) - 1
|
)
|
return sortedValues[index]
|
}
|
|
private func jsonNumber(_ value: Double?) -> Any {
|
value ?? NSNull()
|
}
|
}
|