// SPDX-FileCopyrightText: 2026 Kaede Akino // SPDX-License-Identifier: GPL-3.0-only import AppKit import SwiftUI import TermindPresentation /// The window-level presentation for Command-P. /// /// The dimming layer owns dismissal while the glass surface intercepts clicks /// inside its bounds. Keeping this out of `RootView` leaves the window shell in /// charge of *when* the palette is visible and this view in charge of how it /// behaves while it is visible. struct QuickOpenOverlay: View { let dismiss: () -> Void @Environment(\.accessibilityReduceMotion) private var reduceMotion @Environment(\.colorScheme) private var colorScheme var body: some View { ZStack { TermindColor.shadow.opacity(colorScheme == .dark ? 0.14 : 0.07) .ignoresSafeArea() .contentShape(.rect) .onTapGesture(perform: close) QuickOpenView(dismiss: close) .shadow( color: TermindColor.shadow.opacity(colorScheme == .dark ? 0.30 : 0.18), radius: 32, y: 14 ) .contentShape(.rect) .padding(Metrics.Space.xl) } .onExitCommand(perform: close) .accessibilityElement(children: .contain) .accessibilityLabel(Text(AppStrings.Navigation.quickOpen)) } private func close() { if reduceMotion { dismiss() } else { withAnimation(Motion.panel) { dismiss() } } } } struct QuickOpenView: View { let dismiss: () -> Void @Environment(AppModel.self) private var app @Environment(WindowModel.self) private var window @Environment(\.accessibilityReduceMotion) private var reduceMotion @State private var query = "" @State private var selectedID: AppCommandDescriptor.ID? @FocusState private var searchFocused: Bool @Namespace private var glassNamespace private var results: QuickOpenResults { QuickOpenResults( commands: app.commands.commands(app: app, window: window), query: query ) } var body: some View { glassIdentity( integratedContent .clipShape(.rect(cornerRadius: Metrics.QuickOpen.cornerRadius)) .termindGlassSurface( cornerRadius: Metrics.QuickOpen.cornerRadius, isInteractive: true ) ) .frame(width: Metrics.QuickOpen.width, height: Metrics.QuickOpen.height) .onAppear { selectedID = results.commands.first?.id } .onChange(of: window.isQuickOpenPresented) { if !window.isQuickOpenPresented { searchFocused = false } } .onChange(of: query) { selectedID = results.commands.first?.id } } private var integratedContent: some View { VStack(spacing: 0) { searchHeader Divider() .opacity(0.48) .padding(.horizontal, Metrics.Space.xl) resultContent Divider() .opacity(0.36) .padding(.horizontal, Metrics.Space.xl) shortcutFooter } } @ViewBuilder private func glassIdentity(_ content: Content) -> some View { if #available(macOS 26.0, *) { content .glassEffectID(QuickOpenGlassSurface.palette, in: glassNamespace) .glassEffectTransition(reduceMotion ? .identity : .materialize) } else { content } } private var searchHeader: some View { HStack(spacing: Metrics.Space.l) { Image(systemName: "magnifyingglass") .font(.system(size: 17, weight: .medium)) .foregroundStyle(.secondary) .accessibilityHidden(true) TextField( AppStrings.Navigation.searchPagesChatsWorkspacesHostsAndActions.swiftUIKey, text: $query.singleLineText, axis: .horizontal ) .singleLinePaste() .lineLimit(1) .textFieldStyle(.plain) .font(.system(size: 17, weight: .medium)) .focused($searchFocused) .background { QuickOpenFocusMount(isPresented: window.isQuickOpenPresented) { guard window.isQuickOpenPresented else { return } searchFocused = true } } .onSubmit(performSelection) .onKeyPress(.upArrow) { moveSelection(by: -1) } .onKeyPress(.downArrow) { moveSelection(by: 1) } .onExitCommand(perform: dismiss) .accessibilityLabel(Text(AppStrings.Navigation.searchQuickOpen)) if !query.isEmpty { Button { query = "" searchFocused = true } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(.tertiary) .contentShape(.circle) } .buttonStyle(.plain) .focusable(false) .help(Text(AppStrings.Common.clearSearch)) .accessibilityLabel(Text(AppStrings.Common.clearSearch)) } QuickOpenKeycap(keys: "esc") .accessibilityHidden(true) } .padding(.horizontal, Metrics.Space.xl) .frame(height: Metrics.QuickOpen.searchHeight) } @ViewBuilder private var resultContent: some View { if results.commands.isEmpty { VStack(spacing: Metrics.Space.m) { Image(systemName: "magnifyingglass") .font(.system(size: 25, weight: .medium)) .foregroundStyle(.tertiary) .accessibilityHidden(true) Text(AppStrings.Common.noResults) .font(.headline) Text(AppStrings.Common.noResultsMatch(query)) .font(.callout) .foregroundStyle(.secondary) .lineLimit(1) } .frame(maxWidth: .infinity, maxHeight: .infinity) .accessibilityElement(children: .combine) } else { ScrollViewReader { proxy in ScrollView { LazyVStack(spacing: 0) { ForEach(listItems) { item in Group { switch item { case .section(let section): quickOpenSection(section) case .command(let command, let isSelected): quickOpenCommand(command, isSelected: isSelected) } } .frame(height: item.height) .id(item.id) } } .padding(.vertical, Metrics.Space.xs) } .onAppear { scrollSelectionIntoView(using: proxy) } .onChange(of: selectedID) { _, _ in scrollSelectionIntoView(using: proxy) } } } } private var listItems: [QuickOpenListItem] { results.listItems(selectedID: selectedID) } private func scrollSelectionIntoView(using proxy: ScrollViewProxy) { guard let selectedID else { return } proxy.scrollTo(QuickOpenListItem.ID.command(selectedID)) } private func quickOpenSection(_ section: AppCommandSection) -> some View { Text(section.title) .font(.caption.weight(.semibold)) .foregroundStyle(.secondary) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading) .padding(.horizontal, Metrics.Space.xl + Metrics.Space.xs) .padding(.bottom, Metrics.Space.xs) .accessibilityAddTraits(.isHeader) } private func quickOpenCommand( _ command: AppCommandDescriptor, isSelected: Bool ) -> some View { Button { perform(command) } label: { HStack(spacing: Metrics.Space.l) { Image(systemName: command.symbol) .font(.system(size: 13, weight: .semibold)) .foregroundStyle( isSelected ? AnyShapeStyle(.tint) : AnyShapeStyle(.secondary) ) .frame(width: Metrics.QuickOpen.iconSize, height: Metrics.QuickOpen.iconSize) .background( isSelected ? AnyShapeStyle(Color.accentColor.opacity(0.12)) : AnyShapeStyle(Color.primary.opacity(0.055)), in: .rect(cornerRadius: Metrics.Radius.medium) ) .accessibilityHidden(true) VStack(alignment: .leading, spacing: Metrics.Space.xxs) { Text(command.title) .font(.callout.weight(isSelected ? .medium : .regular)) .lineLimit(1) if let subtitle = command.subtitle { Text(subtitle) .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) } } Spacer(minLength: 0) } .padding(.horizontal, Metrics.Space.l) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) .background( Highlight.fill(isSelected: isSelected), in: .rect(cornerRadius: Metrics.QuickOpen.selectionCornerRadius) ) .overlay { RoundedRectangle(cornerRadius: Metrics.QuickOpen.selectionCornerRadius) .strokeBorder( isSelected ? Color.accentColor.opacity(0.24) : .clear, lineWidth: 1 ) .allowsHitTesting(false) } .contentShape(.rect(cornerRadius: Metrics.QuickOpen.selectionCornerRadius)) } .buttonStyle(.plain) .padding(.horizontal, Metrics.Space.s) .onHover { hovering in if hovering { selectedID = command.id } } .accessibilityElement(children: .combine) .accessibilityLabel(commandAccessibilityLabel(command)) .accessibilityAddTraits(isSelected ? .isSelected : []) .animation(reduceMotion ? nil : Motion.workspaceSwitch, value: isSelected) } private var shortcutFooter: some View { HStack(spacing: Metrics.Space.xl) { QuickOpenShortcutHint(keys: "↑↓", label: String(localized: AppStrings.Navigation.navigate)) QuickOpenShortcutHint(keys: "↩", label: String(localized: AppStrings.Common.open)) QuickOpenShortcutHint(keys: "esc", label: String(localized: AppStrings.Common.close)) Spacer(minLength: 0) } .padding(.horizontal, Metrics.Space.xl) .frame(height: Metrics.QuickOpen.footerHeight) .accessibilityHidden(true) } private func commandAccessibilityLabel(_ command: AppCommandDescriptor) -> Text { if let subtitle = command.subtitle { Text("\(command.title), \(subtitle)") } else { Text(command.title) } } private func moveSelection(by offset: Int) -> KeyPress.Result { guard let next = results.selection(movingFrom: selectedID, by: offset) else { return .ignored } selectedID = next return .handled } private func performSelection() { let command = selectedID.flatMap { id in results.commands.first { $0.id == id } } ?? results.commands.first if let command { perform(command) } } private func perform(_ command: AppCommandDescriptor) { app.commands.perform(command.target, app: app, window: window) dismiss() } } private nonisolated enum QuickOpenGlassSurface: Hashable, Sendable { case palette } /// SwiftUI can run `onAppear` before the search field joins its window. Request /// focus after AppKit mounts it, and retire that request even during a closing transition. private struct QuickOpenFocusMount: NSViewRepresentable { let isPresented: Bool let focus: () -> Void func makeNSView(context: Context) -> MountView { let view = MountView() view.focus = focus view.isPresented = isPresented return view } func updateNSView(_ view: MountView, context: Context) { view.focus = focus view.isPresented = isPresented } static func dismantleNSView(_ view: MountView, coordinator: ()) { view.isPresented = false view.focus = nil } final class MountView: NSView { var focus: (() -> Void)? var isPresented = false { didSet { if isPresented != oldValue { scheduleFocus() } } } private var generation: UInt64 = 0 override func viewDidMoveToWindow() { super.viewDidMoveToWindow() scheduleFocus() } private func scheduleFocus() { generation &+= 1 guard isPresented else { return } let request = generation DispatchQueue.main.async { [weak self] in self?.deliverFocus(request: request, remainingAttempts: 20) } } private func deliverFocus(request: UInt64, remainingAttempts: Int) { guard isPresented, generation == request, let window, self.window === window else { return } guard window.isKeyWindow else { guard remainingAttempts > 0 else { return } DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in self?.deliverFocus( request: request, remainingAttempts: remainingAttempts - 1 ) } return } focus?() } } } private struct QuickOpenShortcutHint: View { let keys: String let label: String var body: some View { HStack(spacing: Metrics.Space.s) { QuickOpenKeycap(keys: keys) Text(label) .font(.caption) .foregroundStyle(.secondary) } } } private struct QuickOpenKeycap: View { let keys: String var body: some View { Text(verbatim: keys) .font(.system(size: 10, weight: .semibold, design: .rounded)) .foregroundStyle(.secondary) .padding(.horizontal, Metrics.Space.s) .frame(minWidth: 22, minHeight: 18) .background( Color.primary.opacity(0.055), in: .rect(cornerRadius: Metrics.Radius.small) ) .overlay { RoundedRectangle(cornerRadius: Metrics.Radius.small) .strokeBorder(Color.primary.opacity(0.10), lineWidth: 1) } } } nonisolated struct QuickOpenResults: Equatable, Sendable { let commands: [AppCommandDescriptor] init(commands: [AppCommandDescriptor], query: String) { let matches = commands.filter { $0.matches(query) } self.commands = AppCommandSection.allCases.flatMap { section in matches.filter { $0.section == section } } } var sections: [AppCommandSection] { AppCommandSection.allCases.filter { section in commands.contains { $0.section == section } } } func listItems(selectedID: AppCommandDescriptor.ID?) -> [QuickOpenListItem] { sections.flatMap { section in [.section(section)] + commands.compactMap { command in guard command.section == section else { return nil } return .command(command, isSelected: command.id == selectedID) } } } func selection( movingFrom selectedID: AppCommandDescriptor.ID?, by offset: Int ) -> AppCommandDescriptor.ID? { guard !commands.isEmpty else { return nil } let current = selectedID.flatMap { id in commands.firstIndex { $0.id == id } } ?? (offset > 0 ? -1 : commands.count) let next = min(max(current + offset, 0), commands.count - 1) return commands[next].id } } nonisolated enum QuickOpenListItem: Identifiable, Hashable, Sendable { nonisolated enum ID: Hashable, Sendable { case section(AppCommandSection) case command(AppCommandDescriptor.ID) } case section(AppCommandSection) case command(AppCommandDescriptor, isSelected: Bool) var id: ID { switch self { case .section(let section): .section(section) case .command(let command, _): .command(command.id) } } var height: CGFloat { switch self { case .section: Metrics.QuickOpen.sectionHeight case .command: Metrics.QuickOpen.rowHeight } } var isSelected: Bool { guard case .command(_, let isSelected) = self else { return false } return isSelected } }