| | |
| | | if let card = waterfallCard(appGroupIndex: appGroupIndex, windowIndex: windowIndex) { |
| | | clickWindowCard(card) |
| | | } |
| | | case "click-card-id" where parts.count == 2: |
| | | guard let windowID = UInt32(parts[1]) else { |
| | | recordMouseCommand("invalid:\(command)") |
| | | return |
| | | } |
| | | recordMouseCommand("click-card-id:\(windowID)") |
| | | if let card = waterfallCard(windowID: windowID) { |
| | | clickWindowCard(card) |
| | | } |
| | | case "hover-card-close" where parts.count == 3: |
| | | guard let appGroupIndex = Int(parts[1]), let windowIndex = Int(parts[2]) else { |
| | | recordMouseCommand("invalid:\(command)") |
| | |
| | | .first(where: { $0.item.windowIndex == windowIndex }) |
| | | } |
| | | |
| | | private func waterfallCard(windowID: UInt32) -> WaterfallCardLayers? { |
| | | waterfallColumns |
| | | .lazy |
| | | .flatMap(\.cards) |
| | | .first(where: { $0.item.window.id == windowID }) |
| | | } |
| | | |
| | | private func layoutSpaceLaneSegmentContents(_ segment: SpaceLaneSegmentLayers) { |
| | | let bounds = segment.containerLayer.bounds |
| | | let splitAppNames = splitViewAppNames(for: segment.space) |
| | | let isSplitView = splitAppNames.count >= 2 |
| | | segment.labelLayer.frame = CGRect(x: 10, y: bounds.height - 29, width: max(1, bounds.width - 48), height: 20) |
| | | segment.countLayer.frame = CGRect(x: bounds.width - 34, y: bounds.height - 29, width: 24, height: 20) |
| | | segment.countLayer.string = isSplitView ? "" : "\(segment.space.windowCount)" |
| | | segment.countLayer.isHidden = isSplitView |
| | | if segment.space.type == .fullscreen { |
| | | let markerFrame = fullscreenMarkerFrame(in: bounds) |
| | | let splitAppNames = splitViewAppNames(for: segment.space) |
| | | segment.appLayer.isHidden = splitAppNames.count >= 2 |
| | | if splitAppNames.count >= 2 { |
| | | segment.appLayer.isHidden = isSplitView |
| | | if isSplitView { |
| | | let labelY = markerFrame.midY - 8.5 |
| | | let halfWidth = markerFrame.width / 2 |
| | | for (index, splitAppLayer) in segment.splitAppLayers.enumerated() { |
| | |
| | | layoutFullscreenMarkerLayer( |
| | | fullscreenMarkerLayer, |
| | | in: bounds, |
| | | isSplitView: splitViewAppNames(for: segment.space).count >= 2 |
| | | isSplitView: isSplitView |
| | | ) |
| | | } |
| | | |
| | |
| | | .cards |
| | | .compactMap(\.item.primarySpaceID) ?? [] |
| | | ) |
| | | } |
| | | |
| | | private var appIconHoverLinkedAppGroupIndex: Int? { |
| | | if let debugHoveredAppGroupIndex { |
| | | return debugHoveredAppGroupIndex |
| | | } |
| | | guard case .app(let appGroupIndex) = hoverTarget else { return nil } |
| | | return appGroupIndex |
| | | } |
| | | |
| | | private var appHoverAssociatedSpaceLabels: [String] { |
| | |
| | | let bounds = card.containerLayer.bounds |
| | | let isSelected = card.item.window.id == effectiveSelection?.windowID |
| | | let isHovered = card.item.window.id == hoveredWindowID |
| | | let isAppLinked = isHorizontalMasonryMode |
| | | && card.item.appGroupIndex == appIconHoverLinkedAppGroupIndex |
| | | applyWaterfallCardVisualState( |
| | | card, |
| | | selected: shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered), |
| | | hovered: isHovered |
| | | hovered: isHovered, |
| | | appLinked: isAppLinked |
| | | ) |
| | | |
| | | let outerInset: CGFloat = 0 |
| | |
| | | ) |
| | | } |
| | | |
| | | private func applyWaterfallCardVisualState(_ card: WaterfallCardLayers, selected: Bool, hovered: Bool) { |
| | | private func applyWaterfallCardVisualState( |
| | | _ card: WaterfallCardLayers, |
| | | selected: Bool, |
| | | hovered: Bool, |
| | | appLinked: Bool |
| | | ) { |
| | | let isSpaceFocused = isWaterfallCardSpaceFocused(card) |
| | | card.containerLayer.backgroundColor = (selected |
| | | ? NSColor.windowBackgroundColor.withAlphaComponent(0.94) |
| | | : hovered |
| | | ? accentTintedWindowBackground(fraction: 0.09, alpha: 0.95) |
| | | : appLinked |
| | | ? accentTintedWindowBackground(fraction: 0.08, alpha: 0.93) |
| | | : isSpaceFocused |
| | | ? accentTintedWindowBackground(fraction: 0.10, alpha: 0.94) |
| | | : NSColor.windowBackgroundColor.withAlphaComponent(0.82) |
| | |
| | | ? NSColor.controlAccentColor.withAlphaComponent(0.52) |
| | | : hovered |
| | | ? NSColor.controlAccentColor.withAlphaComponent(0.44) |
| | | : appLinked |
| | | ? NSColor.controlAccentColor.withAlphaComponent(0.38) |
| | | : isSpaceFocused |
| | | ? NSColor.controlAccentColor.withAlphaComponent(0.42) |
| | | : NSColor.separatorColor.withAlphaComponent(0.28) |
| | | ).cgColor |
| | | card.containerLayer.borderWidth = (selected || hovered || isSpaceFocused) ? 1.5 : 1 |
| | | card.containerLayer.borderWidth = (selected || hovered || appLinked || isSpaceFocused) ? 1.5 : 1 |
| | | card.containerLayer.shadowColor = NSColor.black.cgColor |
| | | card.containerLayer.shadowOpacity = selected ? 0.24 : hovered ? 0.14 : isSpaceFocused ? 0.20 : 0 |
| | | card.containerLayer.shadowRadius = selected ? 22 : hovered ? 17 : isSpaceFocused ? 24 : 0 |
| | | card.containerLayer.shadowOpacity = selected ? 0.24 : hovered ? 0.14 : appLinked ? 0.16 : isSpaceFocused ? 0.20 : 0 |
| | | card.containerLayer.shadowRadius = selected ? 22 : hovered ? 17 : appLinked ? 18 : isSpaceFocused ? 24 : 0 |
| | | card.containerLayer.shadowOffset = selected |
| | | ? CGSize(width: 0, height: -6) |
| | | : hovered |
| | | ? CGSize(width: 0, height: -2.5) |
| | | : appLinked |
| | | ? CGSize(width: 0, height: -3) |
| | | : isSpaceFocused |
| | | ? CGSize(width: 0, height: -5) |
| | | : .zero |
| | | card.containerLayer.zPosition = selected ? 20 : hovered ? 10 : isSpaceFocused ? 8 : 0 |
| | | card.containerLayer.zPosition = selected ? 20 : hovered ? 12 : appLinked ? 10 : isSpaceFocused ? 8 : 0 |
| | | let focusedScale: CGFloat = selected ? 1.015 : isSpaceFocused ? 1.012 : 1.008 |
| | | card.containerLayer.transform = (selected || hovered || isSpaceFocused) |
| | | ? CATransform3DMakeScale(focusedScale, focusedScale, 1) |
| | | : CATransform3DIdentity |
| | | card.containerLayer.shadowPath = (selected || hovered || isSpaceFocused) |
| | | card.containerLayer.shadowPath = (selected || hovered || appLinked || isSpaceFocused) |
| | | ? CGPath( |
| | | roundedRect: card.containerLayer.bounds, |
| | | cornerWidth: card.containerLayer.cornerRadius, |
| | |
| | | transform: nil |
| | | ) |
| | | : nil |
| | | card.titleBarLayer.backgroundColor = NSColor.clear.cgColor |
| | | card.titleBarLayer.backgroundColor = appLinked |
| | | ? spaceLaneOccupiedColor.withAlphaComponent(0.18).cgColor |
| | | : NSColor.clear.cgColor |
| | | card.titleBarLayer.borderColor = NSColor.clear.cgColor |
| | | card.titleBarLayer.borderWidth = 0 |
| | | card.shineLayer.opacity = selected ? 1 : 0 |
| | |
| | | |
| | | let bounds = shapeLayer.bounds |
| | | if isSplitView { |
| | | let insetBounds = bounds.insetBy(dx: 0.6, dy: 0.6) |
| | | let radius = min(7, max(5, insetBounds.height * 0.22)) |
| | | let path = CGMutablePath() |
| | | path.addRoundedRect( |
| | | in: insetBounds, |
| | | cornerWidth: radius, |
| | | cornerHeight: radius |
| | | ) |
| | | path.move(to: CGPoint(x: bounds.midX, y: insetBounds.minY + 1.5)) |
| | | path.addLine(to: CGPoint(x: bounds.midX, y: insetBounds.maxY - 1.5)) |
| | | path.move(to: CGPoint(x: bounds.midX, y: bounds.minY)) |
| | | path.addLine(to: CGPoint(x: bounds.midX, y: bounds.maxY)) |
| | | shapeLayer.path = path |
| | | shapeLayer.lineCap = .butt |
| | | return |
| | | } |
| | | |
| | |
| | | path.addLine(to: CGPoint(x: 0, y: bounds.maxY - cornerLength)) |
| | | |
| | | shapeLayer.path = path |
| | | shapeLayer.lineCap = .round |
| | | } |
| | | |
| | | private func fullscreenMarkerFrame(in segmentBounds: CGRect) -> CGRect { |
| | |
| | | "shadowRadius": Double(card.containerLayer.shadowRadius), |
| | | "thumbnailFrame": dictionary(from: card.thumbnailLayer.frame), |
| | | "titleBarFrame": dictionary(from: card.titleBarLayer.frame), |
| | | "titleBarBackgroundColor": colorDictionary(from: card.titleBarLayer.backgroundColor), |
| | | "titleBarBorderWidth": Double(card.titleBarLayer.borderWidth), |
| | | "titleFrame": dictionary(from: card.titleLayer.frame), |
| | | "metaFrame": dictionary(from: card.metaLayer.frame), |
| | |
| | | var states: [String] = [] |
| | | let isSelected = card.item.window.id == effectiveSelection?.windowID |
| | | let isHovered = card.item.window.id == hoveredWindowID |
| | | let isAppLinked = isHorizontalMasonryMode |
| | | && card.item.appGroupIndex == appIconHoverLinkedAppGroupIndex |
| | | if shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered) { |
| | | states.append("selected") |
| | | } else if isSelected { |
| | |
| | | } |
| | | if isHovered { |
| | | states.append("hover") |
| | | } |
| | | if isAppLinked { |
| | | states.append("appLinked") |
| | | } |
| | | if isWaterfallCardSpaceFocused(card) { |
| | | states.append("spaceFocused") |
| | |
| | | markerLayer: segment.fullscreenMarkerLayer, |
| | | splitAppNames: splitAppNames |
| | | ), |
| | | "fullscreenMarkerPathStyle": fullscreenMarkerPathStyle( |
| | | markerLayer: segment.fullscreenMarkerLayer, |
| | | splitAppNames: splitAppNames |
| | | ), |
| | | "fullscreenMarkerFrame": fullscreenMarkerFrame.map { dictionary(from: $0) } ?? NSNull(), |
| | | "fullscreenMarkerCenterRatio": fullscreenMarkerCenterRatio ?? NSNull(), |
| | | "splitViewAppNames": splitAppNames, |
| | |
| | | "windowBlockColors": segment.windowBlocks.map { colorDictionary(from: $0.backgroundColor) }, |
| | | "windowBlockFrames": segment.windowBlocks.map { dictionary(from: $0.frame) }, |
| | | "labelColor": colorDictionary(from: segment.labelLayer.foregroundColor), |
| | | "countText": string(from: segment.countLayer.string), |
| | | "countVisible": !segment.countLayer.isHidden && segment.countLayer.opacity > 0, |
| | | "countColor": colorDictionary(from: segment.countLayer.foregroundColor), |
| | | "appColor": colorDictionary(from: segment.appLayer.foregroundColor), |
| | | "fullscreenMarkerStrokeColor": colorDictionary( |
| | |
| | | private func fullscreenMarkerKind(markerLayer: CALayer?, splitAppNames: [String]) -> String { |
| | | guard markerLayer != nil else { return "none" } |
| | | return splitAppNames.count >= 2 ? "splitViewPair" : "cornerBracket" |
| | | } |
| | | |
| | | private func fullscreenMarkerPathStyle(markerLayer: CALayer?, splitAppNames: [String]) -> String { |
| | | guard markerLayer != nil else { return "none" } |
| | | return splitAppNames.count >= 2 ? "centerDividerOnly" : "cornerBracket" |
| | | } |
| | | |
| | | private func visualStates(for space: QuickSwitchSpaceViewModel) -> [String] { |
| | |
| | | ] |
| | | } |
| | | |
| | | private func string(from textLayerValue: Any?) -> String { |
| | | if let string = textLayerValue as? String { |
| | | return string |
| | | } |
| | | if let attributedString = textLayerValue as? NSAttributedString { |
| | | return attributedString.string |
| | | } |
| | | return "" |
| | | } |
| | | |
| | | private func colorDictionary(from color: CGColor?) -> [String: Double] { |
| | | guard let color, |
| | | let nsColor = NSColor(cgColor: color)?.usingColorSpace(.deviceRGB) |