| | |
| | | |
| | | @MainActor |
| | | final class QuickSwitchRootView: NSView { |
| | | private let backdropBlurView = NSVisualEffectView() |
| | | private let layerHostView = NSView() |
| | | private let backgroundLayer = CAGradientLayer() |
| | | private let safeAreaFillLayer = CALayer() |
| | | private let glassLayer = CALayer() |
| | |
| | | |
| | | override init(frame frameRect: NSRect) { |
| | | super.init(frame: frameRect) |
| | | setupBackdropBlurView() |
| | | setupLayerHostView() |
| | | wantsLayer = true |
| | | setupLayers() |
| | | } |
| | | |
| | | required init?(coder: NSCoder) { |
| | | super.init(coder: coder) |
| | | setupBackdropBlurView() |
| | | setupLayerHostView() |
| | | wantsLayer = true |
| | | setupLayers() |
| | | } |
| | |
| | | |
| | | override func layout() { |
| | | super.layout() |
| | | backdropBlurView.frame = bounds |
| | | layerHostView.frame = bounds |
| | | layoutLayers() |
| | | } |
| | | |
| | |
| | | mouseResponseLatencyMilliseconds.append(elapsed) |
| | | if isHoverCommand(command) { |
| | | hoverResponseLatencyMilliseconds.append(elapsed) |
| | | } |
| | | if lastCommitSource == .mouse { |
| | | break |
| | | } |
| | | } |
| | | } |
| | |
| | | "spaceID": segment.space.id, |
| | | "label": segment.space.label |
| | | ]) |
| | | focusSpace(segment.space.id, persistent: true) |
| | | lastSpaceLaneClickSpaceID = segment.space.id |
| | | |
| | | guard let firstCard = firstWaterfallCard(inSpaceID: segment.space.id) else { |
| | | DevelopmentDiagnostics.log("quickSwitch.view.clickSpaceLane.emptySpaceUnsupported", [ |
| | | "spaceID": segment.space.id, |
| | | "label": segment.space.label |
| | | ]) |
| | | return |
| | | } |
| | | |
| | | commitWindowCard(firstCard, trigger: "spaceLane") |
| | | } |
| | | |
| | | private func focusSpace(_ spaceID: UInt64, persistent: Bool) { |
| | |
| | | columnSelectionHistory[nextSelection.appGroupIndex] = nextSelection.windowIndex |
| | | lastAppShelfClickChangedSelection = previousSelection != nextSelection |
| | | ensureWaterfallSelectionVisible(nextSelection, horizontalIntent: .alignWithAppShelf(animated: true)) |
| | | updateHoverState(appGroupIndex: appGroupIndex, windowID: nil, spaceID: nil) |
| | | commitWindowCard(firstCard, trigger: "appShelf") |
| | | } else { |
| | | lastAppShelfClickChangedSelection = false |
| | | ensureWaterfallColumnVisible(column) |
| | | updateHoverState(appGroupIndex: appGroupIndex, windowID: nil, spaceID: nil) |
| | | needsLayout = true |
| | | } |
| | | updateHoverState(appGroupIndex: appGroupIndex, windowID: nil, spaceID: nil) |
| | | needsLayout = true |
| | | } |
| | | |
| | | private func scrollAppShelf(to targetOffset: CGFloat) { |
| | |
| | | |
| | | private func clickWindowCard(_ card: WaterfallCardLayers) { |
| | | hoverWindowCard(card) |
| | | commitWindowCard(card, trigger: "windowCard") |
| | | } |
| | | |
| | | private func commitWindowCard(_ card: WaterfallCardLayers, trigger: String) { |
| | | let selection = QuickSwitchSelection( |
| | | appGroupIndex: card.item.appGroupIndex, |
| | | windowIndex: card.item.windowIndex, |
| | | windowID: card.item.window.id |
| | | ) |
| | | DevelopmentDiagnostics.log("quickSwitch.view.clickWindowCard", [ |
| | | DevelopmentDiagnostics.log("quickSwitch.view.commitMouseWindow", [ |
| | | "trigger": trigger, |
| | | "appGroupIndex": selection.appGroupIndex, |
| | | "windowIndex": selection.windowIndex, |
| | | "windowID": selection.windowID, |
| | |
| | | value ?? NSNull() |
| | | } |
| | | |
| | | private func setupBackdropBlurView() { |
| | | backdropBlurView.material = .fullScreenUI |
| | | backdropBlurView.blendingMode = .behindWindow |
| | | backdropBlurView.state = .active |
| | | backdropBlurView.isEmphasized = false |
| | | backdropBlurView.frame = bounds |
| | | backdropBlurView.autoresizingMask = [.width, .height] |
| | | addSubview(backdropBlurView, positioned: .below, relativeTo: nil) |
| | | } |
| | | |
| | | private func setupLayerHostView() { |
| | | layerHostView.wantsLayer = true |
| | | layerHostView.frame = bounds |
| | | layerHostView.autoresizingMask = [.width, .height] |
| | | layerHostView.layer?.masksToBounds = true |
| | | layerHostView.layer?.backgroundColor = NSColor.clear.cgColor |
| | | addSubview(layerHostView, positioned: .above, relativeTo: backdropBlurView) |
| | | } |
| | | |
| | | private func setupLayers() { |
| | | guard let rootLayer = layer else { return } |
| | | guard let rootLayer = layerHostView.layer else { return } |
| | | |
| | | rootLayer.masksToBounds = true |
| | | backgroundLayer.colors = [ |
| | | NSColor.windowBackgroundColor.withAlphaComponent(0.72).cgColor, |
| | | NSColor.controlBackgroundColor.withAlphaComponent(0.62).cgColor |
| | | NSColor.windowBackgroundColor.withAlphaComponent(0.58).cgColor, |
| | | NSColor.controlBackgroundColor.withAlphaComponent(0.48).cgColor |
| | | ] |
| | | backgroundLayer.startPoint = CGPoint(x: 0.5, y: 1.0) |
| | | backgroundLayer.endPoint = CGPoint(x: 0.5, y: 0.0) |
| | |
| | | glassLayer.shadowOpacity = 0 |
| | | rootLayer.addSublayer(glassLayer) |
| | | |
| | | laneLayer.backgroundColor = NSColor.controlBackgroundColor.withAlphaComponent(0.50).cgColor |
| | | laneLayer.backgroundColor = NSColor.controlBackgroundColor.withAlphaComponent(0.68).cgColor |
| | | laneLayer.cornerRadius = 16 |
| | | laneLayer.masksToBounds = true |
| | | glassLayer.addSublayer(laneLayer) |
| | | laneLayer.addSublayer(spaceLaneContentLayer) |
| | | |
| | | shelfLayer.backgroundColor = NSColor.controlBackgroundColor.withAlphaComponent(0.42).cgColor |
| | | shelfLayer.backgroundColor = NSColor.clear.cgColor |
| | | shelfLayer.cornerRadius = 16 |
| | | shelfLayer.masksToBounds = true |
| | | glassLayer.addSublayer(shelfLayer) |
| | |
| | | container.name = column.app.name |
| | | container.cornerRadius = 14 |
| | | container.masksToBounds = true |
| | | container.backgroundColor = NSColor.controlBackgroundColor.withAlphaComponent(0.34).cgColor |
| | | container.borderColor = NSColor.separatorColor.withAlphaComponent(0.28).cgColor |
| | | container.backgroundColor = NSColor.controlBackgroundColor.withAlphaComponent(0.56).cgColor |
| | | container.borderColor = NSColor.separatorColor.withAlphaComponent(0.34).cgColor |
| | | container.borderWidth = 1 |
| | | |
| | | let header = CALayer() |
| | | header.backgroundColor = NSColor.controlBackgroundColor.withAlphaComponent(0.58).cgColor |
| | | header.backgroundColor = NSColor.controlBackgroundColor.withAlphaComponent(0.68).cgColor |
| | | |
| | | let appName = makeTextLayer( |
| | | string: column.app.name, |
| | |
| | | .sorted() |
| | | } |
| | | |
| | | private func firstWaterfallCard(inSpaceID spaceID: UInt64) -> WaterfallCardLayers? { |
| | | for column in waterfallColumns.sorted(by: { $0.column.appGroupIndex < $1.column.appGroupIndex }) { |
| | | if let card = column.cards |
| | | .sorted(by: { $0.item.windowIndex < $1.item.windowIndex }) |
| | | .first(where: { $0.item.primarySpaceID == spaceID }) { |
| | | return card |
| | | } |
| | | } |
| | | |
| | | return nil |
| | | } |
| | | |
| | | private func spaceFocusWindowCount(for appGroupIndex: Int) -> Int { |
| | | guard let activeSpaceFocusID else { return 0 } |
| | | |