| | |
| | | private var windowIndexKeyPendingClearTask: Task<Void, Never>? |
| | | private var lastWindowIndexKeyCommand: String? |
| | | private var lastWindowIndexKeyCommitCode: String? |
| | | private var windowShortcutFilterPhase: WindowShortcutFilterPhase = .none |
| | | private var lastWindowShortcutPreCommitFilterSnapshot: WindowShortcutPreCommitFilterSnapshot? |
| | | private var lastBoundaryBounceAxis: String? |
| | | private var lastBoundaryBounceDirection: String? |
| | | private var boundaryBounceCount = 0 |
| | |
| | | let appSymbol: String |
| | | let appGroupIndex: Int |
| | | let startedAt: CFTimeInterval |
| | | } |
| | | |
| | | private enum WindowShortcutFilterPhase: String { |
| | | case none |
| | | case appPending |
| | | case targetPreCommit |
| | | case invalid |
| | | case expired |
| | | } |
| | | |
| | | private enum WindowShortcutFilterState: String { |
| | | case none |
| | | case matched |
| | | case dimmed |
| | | case target |
| | | } |
| | | |
| | | private struct WindowShortcutPreCommitFilterSnapshot { |
| | | let targetCode: String |
| | | let targetWindowID: UInt32 |
| | | let matchedWindowIDs: [UInt32] |
| | | let dimmedWindowIDs: [UInt32] |
| | | |
| | | var reportDictionary: [String: Any] { |
| | | [ |
| | | "phase": WindowShortcutFilterPhase.targetPreCommit.rawValue, |
| | | "targetCode": targetCode, |
| | | "targetWindowID": Int(targetWindowID), |
| | | "matchedWindowIDs": matchedWindowIDs.map(Int.init), |
| | | "dimmedWindowIDs": dimmedWindowIDs.map(Int.init) |
| | | ] |
| | | } |
| | | } |
| | | |
| | | private struct WindowShortcutFilterReport { |
| | | let active: Bool |
| | | let phase: WindowShortcutFilterPhase |
| | | let pendingAppGroupIndex: Int? |
| | | let prefix: String? |
| | | let matchedWindowIDs: [UInt32] |
| | | let dimmedWindowIDs: [UInt32] |
| | | } |
| | | |
| | | private struct ProjectionLayerSnapshot { |
| | |
| | | clearWindowIndexKeyPending() |
| | | lastWindowIndexKeyCommand = nil |
| | | lastWindowIndexKeyCommitCode = nil |
| | | windowShortcutFilterPhase = .none |
| | | lastWindowShortcutPreCommitFilterSnapshot = nil |
| | | lastBoundaryBounceAxis = nil |
| | | lastBoundaryBounceDirection = nil |
| | | boundaryBounceCount = 0 |
| | |
| | | let firstFrameGatePassed = firstFrameP95.map { $0 < 150 } ?? false |
| | | let keyboardGatePassed = keyboardP95.map { $0 < 50 } ?? false |
| | | let hoverGatePassed = hoverP95.map { $0 <= 32 } ?? false |
| | | let shortcutFilterReport = makeWindowShortcutFilterReport() |
| | | |
| | | return [ |
| | | "viewClass": String(describing: Self.self), |
| | |
| | | "windowIndexKeyPendingAppSymbol": windowIndexKeyPending?.appSymbol ?? NSNull(), |
| | | "lastWindowIndexKeyCommand": lastWindowIndexKeyCommand ?? NSNull(), |
| | | "lastWindowIndexKeyCommitCode": lastWindowIndexKeyCommitCode ?? NSNull(), |
| | | "windowShortcutFilterActive": shortcutFilterReport.active, |
| | | "windowShortcutFilterPhase": shortcutFilterReport.phase.rawValue, |
| | | "windowShortcutFilterPendingAppGroupIndex": shortcutFilterReport.pendingAppGroupIndex ?? NSNull(), |
| | | "windowShortcutFilterPrefix": shortcutFilterReport.prefix ?? NSNull(), |
| | | "windowShortcutFilterMatchedCount": shortcutFilterReport.matchedWindowIDs.count, |
| | | "windowShortcutFilterDimmedCount": shortcutFilterReport.dimmedWindowIDs.count, |
| | | "windowShortcutFilterMatchedWindowIDs": shortcutFilterReport.matchedWindowIDs.map(Int.init), |
| | | "windowShortcutFilterDimmedWindowIDs": shortcutFilterReport.dimmedWindowIDs.map(Int.init), |
| | | "lastWindowShortcutPreCommitFilterSnapshot": lastWindowShortcutPreCommitFilterSnapshot?.reportDictionary ?? NSNull(), |
| | | "lastBoundaryBounceAxis": lastBoundaryBounceAxis ?? NSNull(), |
| | | "lastBoundaryBounceDirection": lastBoundaryBounceDirection ?? NSNull(), |
| | | "boundaryBounceCount": boundaryBounceCount, |
| | |
| | | recordKeyboardCommand("window:\(code)") |
| | | lastWindowIndexKeyCommand = "window:\(code)" |
| | | lastWindowIndexKeyCommitCode = code |
| | | clearWindowIndexKeyPending() |
| | | lastWindowShortcutPreCommitFilterSnapshot = makeWindowShortcutPreCommitFilterSnapshot( |
| | | target: card, |
| | | code: code |
| | | ) |
| | | windowShortcutFilterPhase = .targetPreCommit |
| | | clearWindowIndexKeyPending(resetShortcutFilter: false) |
| | | commitKeyboardWindowCard(card, trigger: "windowIndexKey") |
| | | return true |
| | | } |
| | |
| | | recordKeyboardCommand("windowInvalid:\(code)") |
| | | lastWindowIndexKeyCommand = "windowInvalid:\(code)" |
| | | lastWindowIndexKeyCommitCode = nil |
| | | windowShortcutFilterPhase = .invalid |
| | | lastWindowShortcutPreCommitFilterSnapshot = nil |
| | | selectionChangedByLastCommand = false |
| | | clearWindowIndexKeyPending() |
| | | clearWindowIndexKeyPending(resetShortcutFilter: false) |
| | | return true |
| | | } |
| | | |
| | |
| | | if focusAppShelfItemForKeyboard(indexSymbol: symbol, commandLabel: recordedCommand), |
| | | let ordinal = Self.appShelfIndexSymbols.firstIndex(of: symbol), |
| | | ordinal < appShelfItems.count { |
| | | setWindowIndexKeyPending( |
| | | appSymbol: symbol, |
| | | appGroupIndex: appShelfItems[ordinal].item.appGroupIndex, |
| | | startedAt: now |
| | | ) |
| | | let appGroupIndex = appShelfItems[ordinal].item.appGroupIndex |
| | | if let card = singleVisibleWaterfallCard(appGroupIndex: appGroupIndex) { |
| | | recordKeyboardCommand("windowSingle:\(symbol)") |
| | | lastWindowIndexKeyCommand = "windowSingle:\(symbol)" |
| | | lastWindowIndexKeyCommitCode = symbol |
| | | lastWindowShortcutPreCommitFilterSnapshot = nil |
| | | clearWindowIndexKeyPending() |
| | | commitKeyboardWindowCard(card, trigger: "windowIndexSingleApp") |
| | | } else { |
| | | setWindowIndexKeyPending( |
| | | appSymbol: symbol, |
| | | appGroupIndex: appGroupIndex, |
| | | startedAt: now |
| | | ) |
| | | } |
| | | } else { |
| | | selectionChangedByLastCommand = false |
| | | clearWindowIndexKeyPending() |
| | |
| | | appGroupIndex: appGroupIndex, |
| | | startedAt: startedAt |
| | | ) |
| | | windowShortcutFilterPhase = .appPending |
| | | lastWindowShortcutPreCommitFilterSnapshot = nil |
| | | needsLayout = true |
| | | windowIndexKeyPendingClearTask = Task { @MainActor [weak self] in |
| | | do { |
| | | try await Task.sleep(nanoseconds: UInt64(Self.windowIndexKeyPendingInterval * 1_000_000_000)) |
| | |
| | | } |
| | | self.windowIndexKeyPending = nil |
| | | self.windowIndexKeyPendingClearTask = nil |
| | | self.windowShortcutFilterPhase = .expired |
| | | self.lastWindowShortcutPreCommitFilterSnapshot = nil |
| | | self.needsLayout = true |
| | | } |
| | | } |
| | | |
| | | private func clearWindowIndexKeyPending() { |
| | | private func clearWindowIndexKeyPending(resetShortcutFilter: Bool = true) { |
| | | let hadFilterState = windowIndexKeyPending != nil |
| | | || windowShortcutFilterPhase != .none |
| | | || lastWindowShortcutPreCommitFilterSnapshot != nil |
| | | windowIndexKeyPendingClearTask?.cancel() |
| | | windowIndexKeyPendingClearTask = nil |
| | | windowIndexKeyPending = nil |
| | | if resetShortcutFilter { |
| | | windowShortcutFilterPhase = .none |
| | | lastWindowShortcutPreCommitFilterSnapshot = nil |
| | | } |
| | | if hadFilterState { |
| | | needsLayout = true |
| | | } |
| | | } |
| | | |
| | | @discardableResult |
| | |
| | | ) |
| | | : nil |
| | | |
| | | applySpaceLaneContentVisualState(segment, isLocked: isLocked) |
| | | applySpaceLaneContentVisualState( |
| | | segment, |
| | | isLocked: isLocked, |
| | | usesLightActiveFill: resolvedTheme == .dark && hasWindows && !isLocked && (isActive || isAssociated) |
| | | ) |
| | | } |
| | | |
| | | private func applySpaceLaneContentVisualState(_ segment: SpaceLaneSegmentLayers, isLocked: Bool) { |
| | | let primaryTextColor: NSColor = isLocked ? .white : themePalette.spaceLanePrimaryText |
| | | private func applySpaceLaneContentVisualState( |
| | | _ segment: SpaceLaneSegmentLayers, |
| | | isLocked: Bool, |
| | | usesLightActiveFill: Bool |
| | | ) { |
| | | let primaryTextColor: NSColor = isLocked |
| | | ? .white |
| | | : usesLightActiveFill |
| | | ? NSColor.black.withAlphaComponent(0.82) |
| | | : themePalette.spaceLanePrimaryText |
| | | let secondaryTextColor: NSColor = isLocked |
| | | ? .white |
| | | : themePalette.spaceLaneSecondaryText |
| | | : usesLightActiveFill |
| | | ? NSColor.black.withAlphaComponent(0.58) |
| | | : themePalette.spaceLaneSecondaryText |
| | | let markerTextColor: NSColor = isLocked |
| | | ? .white |
| | | : usesLightActiveFill |
| | | ? NSColor.black.withAlphaComponent(0.62) |
| | | : themePalette.spaceLaneSecondaryText.withAlphaComponent(segment.space.isCurrent ? 0.72 : 0.54) |
| | | let blockColor: NSColor = isLocked |
| | | ? .white |
| | | : usesLightActiveFill |
| | | ? NSColor.black.withAlphaComponent(0.20) |
| | | : spaceLaneOccupiedColor.withAlphaComponent(segment.space.isCurrent ? 0.72 : 0.58) |
| | | |
| | | segment.labelLayer.foregroundColor = primaryTextColor.cgColor |
| | | segment.countLayer.foregroundColor = (isLocked ? NSColor.white : themePalette.spaceLaneSecondaryText).cgColor |
| | | segment.countLayer.foregroundColor = secondaryTextColor.cgColor |
| | | segment.appLayer.foregroundColor = (segment.space.type == .fullscreen |
| | | ? primaryTextColor |
| | | : secondaryTextColor |
| | |
| | | } |
| | | |
| | | for block in segment.windowBlocks { |
| | | block.backgroundColor = (isLocked |
| | | ? NSColor.white |
| | | : spaceLaneOccupiedColor.withAlphaComponent(segment.space.isCurrent ? 0.72 : 0.58) |
| | | ).cgColor |
| | | block.backgroundColor = blockColor.cgColor |
| | | } |
| | | |
| | | (segment.fullscreenMarkerLayer as? CAShapeLayer)?.strokeColor = (isLocked |
| | | ? NSColor.white |
| | | : themePalette.spaceLaneSecondaryText.withAlphaComponent(segment.space.isCurrent ? 0.72 : 0.54) |
| | | ).cgColor |
| | | (segment.fullscreenMarkerLayer as? CAShapeLayer)?.strokeColor = markerTextColor.cgColor |
| | | } |
| | | |
| | | private func shouldScrollSpaceLane(for event: NSEvent) -> Bool { |
| | |
| | | let isKeyboardFocused = card.item.window.id == keyboardFocusedWindowID |
| | | let isHorizontalAppDefaultFocused = isHorizontalAppDefaultCard(card) |
| | | let isAppLinked = isWaterfallCardAppLinked(card) |
| | | let shortcutFilterState = windowShortcutFilterState(for: card) |
| | | applyWaterfallCardVisualState( |
| | | card, |
| | | selected: shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered) |
| | | || isKeyboardFocused |
| | | || isHorizontalAppDefaultFocused, |
| | | hovered: isHovered, |
| | | appLinked: isAppLinked |
| | | appLinked: isAppLinked, |
| | | shortcutFilterState: shortcutFilterState |
| | | ) |
| | | |
| | | let outerInset: CGFloat = 0 |
| | |
| | | windowIndex: card.item.windowIndex, |
| | | windowID: card.item.window.id |
| | | ), |
| | | visible: isHovered, |
| | | visible: isHovered && shortcutFilterState != .dimmed, |
| | | anchorTopRight: CGPoint(x: bounds.maxX - 12, y: bounds.maxY - 12) |
| | | ) |
| | | card.shineLayer.frame = CGRect( |
| | |
| | | _ card: WaterfallCardLayers, |
| | | selected: Bool, |
| | | hovered: Bool, |
| | | appLinked: Bool |
| | | appLinked: Bool, |
| | | shortcutFilterState: WindowShortcutFilterState |
| | | ) { |
| | | let isSpaceFocused = isWaterfallCardSpaceFocused(card) |
| | | let isShortcutDimmed = shortcutFilterState == .dimmed |
| | | card.containerLayer.backgroundColor = (selected |
| | | ? themePalette.cardSelectedBackground |
| | | : hovered |
| | |
| | | ? accentTintedWindowBackground(fraction: 0.10, alpha: 0.94) |
| | | : themePalette.cardBackground |
| | | ).cgColor |
| | | card.containerLayer.opacity = isShortcutDimmed ? windowShortcutFilterDimOpacity : 1 |
| | | card.containerLayer.borderColor = (selected |
| | | ? NSColor.controlAccentColor.withAlphaComponent(0.52) |
| | | : hovered |
| | |
| | | card.containerLayer.borderWidth = (selected || hovered || appLinked || isSpaceFocused) ? 1.5 : 1 |
| | | card.containerLayer.shadowColor = NSColor.black.cgColor |
| | | let darkShadowBoost: Float = resolvedTheme == .dark ? 0.08 : 0 |
| | | card.containerLayer.shadowOpacity = selected |
| | | let baseShadowOpacity: Float = selected |
| | | ? 0.24 + darkShadowBoost |
| | | : hovered |
| | | ? 0.14 + darkShadowBoost |
| | |
| | | : isSpaceFocused |
| | | ? 0.20 + darkShadowBoost |
| | | : 0 |
| | | card.containerLayer.shadowOpacity = isShortcutDimmed ? min(baseShadowOpacity, 0.03) : baseShadowOpacity |
| | | card.containerLayer.shadowRadius = selected ? 22 : hovered ? 17 : appLinked ? 18 : isSpaceFocused ? 24 : 0 |
| | | card.containerLayer.shadowOffset = selected |
| | | ? CGSize(width: 0, height: -6) |
| | |
| | | : .zero |
| | | card.containerLayer.zPosition = selected ? 20 : hovered ? 12 : appLinked ? 10 : isSpaceFocused ? 8 : 0 |
| | | let focusedScale: CGFloat = selected ? WaterfallMetrics.selectedCardScale : isSpaceFocused ? 1.012 : 1.008 |
| | | card.containerLayer.transform = (selected || hovered || isSpaceFocused) |
| | | card.containerLayer.transform = (!isShortcutDimmed && (selected || hovered || isSpaceFocused)) |
| | | ? CATransform3DMakeScale(focusedScale, focusedScale, 1) |
| | | : CATransform3DIdentity |
| | | card.containerLayer.shadowPath = (selected || hovered || appLinked || isSpaceFocused) |
| | | card.containerLayer.shadowPath = (!isShortcutDimmed && (selected || hovered || appLinked || isSpaceFocused)) |
| | | ? CGPath( |
| | | roundedRect: card.containerLayer.bounds, |
| | | cornerWidth: card.containerLayer.cornerRadius, |
| | |
| | | ).cgColor |
| | | card.titleBarLayer.borderColor = NSColor.clear.cgColor |
| | | card.titleBarLayer.borderWidth = 0 |
| | | card.shineLayer.opacity = selected ? 1 : 0 |
| | | card.shineLayer.opacity = (!isShortcutDimmed && selected) ? 1 : 0 |
| | | } |
| | | |
| | | private func layoutCloseButton( |
| | |
| | | return Self.appShelfIndexSymbols[ordinal] |
| | | } |
| | | |
| | | private var visibleWaterfallCards: [WaterfallCardLayers] { |
| | | waterfallColumns.flatMap(\.cards) |
| | | } |
| | | |
| | | private func singleVisibleWaterfallCard(appGroupIndex: Int) -> WaterfallCardLayers? { |
| | | let cards = visibleWaterfallCards.filter { $0.item.appGroupIndex == appGroupIndex } |
| | | return cards.count == 1 ? cards[0] : nil |
| | | } |
| | | |
| | | private var windowShortcutFilterDimOpacity: Float { |
| | | resolvedTheme == .dark ? 0.44 : 0.38 |
| | | } |
| | | |
| | | private func windowShortcutFilterState(for card: WaterfallCardLayers) -> WindowShortcutFilterState { |
| | | switch windowShortcutFilterPhase { |
| | | case .appPending: |
| | | guard let pending = windowIndexKeyPending else { return .none } |
| | | return card.item.appGroupIndex == pending.appGroupIndex ? .matched : .dimmed |
| | | case .targetPreCommit: |
| | | guard let snapshot = lastWindowShortcutPreCommitFilterSnapshot else { return .none } |
| | | if card.item.window.id == snapshot.targetWindowID { |
| | | return .target |
| | | } |
| | | if snapshot.dimmedWindowIDs.contains(card.item.window.id) { |
| | | return .dimmed |
| | | } |
| | | if snapshot.matchedWindowIDs.contains(card.item.window.id) { |
| | | return .matched |
| | | } |
| | | return .none |
| | | case .none, .invalid, .expired: |
| | | return .none |
| | | } |
| | | } |
| | | |
| | | private func makeWindowShortcutPreCommitFilterSnapshot( |
| | | target card: WaterfallCardLayers, |
| | | code: String |
| | | ) -> WindowShortcutPreCommitFilterSnapshot { |
| | | let targetWindowID = card.item.window.id |
| | | let allWindowIDs = visibleWaterfallCards.map { $0.item.window.id } |
| | | return WindowShortcutPreCommitFilterSnapshot( |
| | | targetCode: code, |
| | | targetWindowID: targetWindowID, |
| | | matchedWindowIDs: [targetWindowID], |
| | | dimmedWindowIDs: allWindowIDs.filter { $0 != targetWindowID }.sorted() |
| | | ) |
| | | } |
| | | |
| | | private func makeWindowShortcutFilterReport() -> WindowShortcutFilterReport { |
| | | switch windowShortcutFilterPhase { |
| | | case .appPending: |
| | | guard let pending = windowIndexKeyPending else { |
| | | return emptyWindowShortcutFilterReport(phase: .none) |
| | | } |
| | | let cards = visibleWaterfallCards |
| | | let matched = cards |
| | | .filter { $0.item.appGroupIndex == pending.appGroupIndex } |
| | | .map { $0.item.window.id } |
| | | .sorted() |
| | | let dimmed = cards |
| | | .filter { $0.item.appGroupIndex != pending.appGroupIndex } |
| | | .map { $0.item.window.id } |
| | | .sorted() |
| | | return WindowShortcutFilterReport( |
| | | active: true, |
| | | phase: .appPending, |
| | | pendingAppGroupIndex: pending.appGroupIndex, |
| | | prefix: pending.appSymbol, |
| | | matchedWindowIDs: matched, |
| | | dimmedWindowIDs: dimmed |
| | | ) |
| | | case .targetPreCommit: |
| | | guard let snapshot = lastWindowShortcutPreCommitFilterSnapshot else { |
| | | return emptyWindowShortcutFilterReport(phase: .none) |
| | | } |
| | | return WindowShortcutFilterReport( |
| | | active: true, |
| | | phase: .targetPreCommit, |
| | | pendingAppGroupIndex: nil, |
| | | prefix: snapshot.targetCode, |
| | | matchedWindowIDs: snapshot.matchedWindowIDs, |
| | | dimmedWindowIDs: snapshot.dimmedWindowIDs |
| | | ) |
| | | case .invalid, .expired: |
| | | return emptyWindowShortcutFilterReport(phase: windowShortcutFilterPhase) |
| | | case .none: |
| | | return emptyWindowShortcutFilterReport(phase: .none) |
| | | } |
| | | } |
| | | |
| | | private func emptyWindowShortcutFilterReport( |
| | | phase: WindowShortcutFilterPhase |
| | | ) -> WindowShortcutFilterReport { |
| | | WindowShortcutFilterReport( |
| | | active: false, |
| | | phase: phase, |
| | | pendingAppGroupIndex: nil, |
| | | prefix: nil, |
| | | matchedWindowIDs: [], |
| | | dimmedWindowIDs: [] |
| | | ) |
| | | } |
| | | |
| | | private func waterfallStateText(for card: QuickSwitchWindowCardViewModel) -> String { |
| | | var states: [String] = [] |
| | | if card.window.isMinimized { |
| | |
| | | dy: column.containerLayer.frame.minY + column.cardsClipLayer.frame.minY |
| | | ) |
| | | let screenshotSource = screenshotSourcesByWindowID[card.item.window.id] |
| | | let shortcutFilterState = windowShortcutFilterState(for: card) |
| | | let shortcutFilterOpacity = shortcutFilterState == .dimmed |
| | | ? windowShortcutFilterDimOpacity |
| | | : Float(1) |
| | | |
| | | var report = [ |
| | | "appGroupIndex": card.item.appGroupIndex, |
| | |
| | | "isSelected": card.item.window.id == effectiveSelection?.windowID, |
| | | "isKeyboardFocused": card.item.window.id == keyboardFocusedWindowID, |
| | | "isHorizontalAppDefault": isHorizontalAppDefaultCard(card), |
| | | "windowShortcutFilterState": shortcutFilterState.rawValue, |
| | | "windowShortcutFilterOpacity": Double(shortcutFilterOpacity), |
| | | "visualStates": waterfallCardVisualStates(for: card), |
| | | "thumbnailStrategy": thumbnailStrategyString(for: card.item.window), |
| | | "screenshotSource": screenshotSourceString(screenshotSource, for: card.item.window), |
| | |
| | | if isWaterfallCardSpaceFocused(card) { |
| | | states.append("spaceFocused") |
| | | } |
| | | switch windowShortcutFilterState(for: card) { |
| | | case .matched: |
| | | states.append("shortcutMatched") |
| | | case .dimmed: |
| | | states.append("shortcutDimmed") |
| | | case .target: |
| | | states.append("shortcutTarget") |
| | | case .none: |
| | | break |
| | | } |
| | | if card.item.window.isMinimized { |
| | | states.append("minimized") |
| | | } |
| | |
| | | } |
| | | let isAppAssociated = appHoverAssociatedSpaceIDs.contains(space.id) |
| | | let isWindowAssociated = space.id == windowHoverAssociatedSpaceID |
| | | let isLocked = space.id == currentViewModel?.lockedSpaceID |
| | | let isActive = space.id == activeSpaceFocusID || isLocked |
| | | let isAssociated = (isAppAssociated || isWindowAssociated) && !isActive |
| | | let usesLightActiveFill = resolvedTheme == .dark && space.windowCount > 0 && !isLocked && (isActive || isAssociated) |
| | | let group = spaceLaneDisplayGroup(containingSpaceID: space.id) |
| | | let visibleFrame = segment.containerLayer.frame.offsetBy( |
| | | dx: (group?.containerLayer.frame.minX ?? 0) + spaceLaneContentLayer.frame.minX, |
| | |
| | | "label": space.label, |
| | | "type": spaceTypeName(space.type), |
| | | "visualStates": visualStates(for: space), |
| | | "usesLightActiveFill": usesLightActiveFill, |
| | | "foregroundContrastStyle": usesLightActiveFill ? "darkOnLight" : "themeDefault", |
| | | "fullscreenMarkerVisible": segment.fullscreenMarkerLayer != nil, |
| | | "fullscreenMarkerKind": fullscreenMarkerKind( |
| | | markerLayer: segment.fullscreenMarkerLayer, |