| | |
| | | @MainActor |
| | | final class QuickSwitchRootView: NSView { |
| | | private let backgroundLayer = CAGradientLayer() |
| | | private let safeAreaFillLayer = CALayer() |
| | | private let glassLayer = CALayer() |
| | | private let laneLayer = CALayer() |
| | | private let spaceLaneContentLayer = CALayer() |
| | |
| | | static let normalGap: CGFloat = 16 |
| | | static let minGap: CGFloat = 8 |
| | | static let maxRows = 2 |
| | | } |
| | | |
| | | private enum RootLayoutMetrics { |
| | | static let horizontalMargin: CGFloat = 24 |
| | | static let topContentMargin: CGFloat = 24 |
| | | static let shelfGap: CGFloat = 16 |
| | | static let waterfallTopGap: CGFloat = 14 |
| | | static let bottomMargin: CGFloat = 34 |
| | | } |
| | | |
| | | private enum WaterfallMetrics { |
| | |
| | | "viewClass": String(describing: Self.self), |
| | | "isLayerBacked": wantsLayer, |
| | | "bounds": dictionary(from: bounds), |
| | | "safeAreaTopInset": Double(topSafeAreaInset), |
| | | "safeAreaFillFrame": dictionary(from: safeAreaFillLayer.frame), |
| | | "glassFrame": dictionary(from: glassLayer.frame), |
| | | "glassCornerRadius": Double(glassLayer.cornerRadius), |
| | | "glassBorderWidth": Double(glassLayer.borderWidth), |
| | | "glassShadowOpacity": Double(glassLayer.shadowOpacity), |
| | | "spaceLaneFrame": dictionary(from: laneLayer.frame), |
| | | "spaceLaneContentWidth": Double(spaceLaneContentWidth), |
| | | "spaceLaneVisibleWidth": Double(laneLayer.bounds.width), |
| | |
| | | backgroundLayer.endPoint = CGPoint(x: 0.5, y: 0.0) |
| | | rootLayer.addSublayer(backgroundLayer) |
| | | |
| | | glassLayer.cornerRadius = 24 |
| | | glassLayer.backgroundColor = NSColor.windowBackgroundColor.withAlphaComponent(0.72).cgColor |
| | | glassLayer.borderColor = NSColor.separatorColor.withAlphaComponent(0.35).cgColor |
| | | glassLayer.borderWidth = 1 |
| | | glassLayer.shadowColor = NSColor.black.cgColor |
| | | glassLayer.shadowOpacity = 0.16 |
| | | glassLayer.shadowRadius = 24 |
| | | glassLayer.shadowOffset = CGSize(width: 0, height: -8) |
| | | safeAreaFillLayer.backgroundColor = NSColor.black.cgColor |
| | | rootLayer.addSublayer(safeAreaFillLayer) |
| | | |
| | | glassLayer.cornerRadius = 0 |
| | | glassLayer.backgroundColor = NSColor.clear.cgColor |
| | | glassLayer.borderColor = NSColor.clear.cgColor |
| | | glassLayer.borderWidth = 0 |
| | | glassLayer.shadowOpacity = 0 |
| | | rootLayer.addSublayer(glassLayer) |
| | | |
| | | laneLayer.backgroundColor = NSColor.controlBackgroundColor.withAlphaComponent(0.50).cgColor |
| | |
| | | |
| | | backgroundLayer.frame = bounds |
| | | |
| | | let horizontalInset = max(36, bounds.width * 0.045) |
| | | let topInset = max(36, bounds.height * 0.055) |
| | | glassLayer.frame = bounds.insetBy(dx: horizontalInset, dy: topInset) |
| | | let safeAreaTopInset = topSafeAreaInset |
| | | safeAreaFillLayer.frame = CGRect( |
| | | x: 0, |
| | | y: max(0, bounds.height - safeAreaTopInset), |
| | | width: bounds.width, |
| | | height: safeAreaTopInset |
| | | ) |
| | | safeAreaFillLayer.isHidden = safeAreaTopInset <= 0.5 |
| | | glassLayer.frame = bounds |
| | | |
| | | let contentBounds = glassLayer.bounds |
| | | let horizontalMargin: CGFloat = 22 |
| | | let horizontalMargin = RootLayoutMetrics.horizontalMargin |
| | | let topMargin = safeAreaTopInset + RootLayoutMetrics.topContentMargin |
| | | let laneHeight = min(104, max(82, contentBounds.height * 0.12)) |
| | | let availableContentWidth = contentBounds.width - horizontalMargin * 2 |
| | | appShelfLayout = appShelfLayout(for: availableContentWidth) |
| | | let shelfHeight = appShelfHeight(forRows: appShelfLayout.rows) |
| | | laneLayer.frame = CGRect( |
| | | x: horizontalMargin, |
| | | y: contentBounds.height - horizontalMargin - laneHeight, |
| | | y: contentBounds.height - topMargin - laneHeight, |
| | | width: availableContentWidth, |
| | | height: laneHeight |
| | | ) |
| | | shelfLayer.frame = CGRect( |
| | | x: horizontalMargin, |
| | | y: laneLayer.frame.minY - 16 - shelfHeight, |
| | | y: laneLayer.frame.minY - RootLayoutMetrics.shelfGap - shelfHeight, |
| | | width: laneLayer.frame.width, |
| | | height: shelfHeight |
| | | ) |
| | | waterfallLayer.frame = CGRect( |
| | | x: shelfLayer.frame.minX, |
| | | y: 34, |
| | | y: RootLayoutMetrics.bottomMargin, |
| | | width: shelfLayer.frame.width, |
| | | height: shelfLayer.frame.minY - 48 |
| | | height: max(1, shelfLayer.frame.minY - RootLayoutMetrics.bottomMargin - RootLayoutMetrics.waterfallTopGap) |
| | | ) |
| | | |
| | | layoutSpaceLaneSegments() |
| | |
| | | CATransaction.commit() |
| | | } |
| | | |
| | | private var topSafeAreaInset: CGFloat { |
| | | max(0, window?.screen?.safeAreaInsets.top ?? safeAreaInsets.top) |
| | | } |
| | | |
| | | private func appShelfLayout(for availableWidth: CGFloat) -> AppShelfLayout { |
| | | let itemCount = appShelfItems.count |
| | | guard itemCount > 0 else { |