chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
import AppKit
|
||||
import CodexBarCore
|
||||
import SwiftUI
|
||||
|
||||
/// Sidebar destinations of the settings window: fixed app panes plus one entry per provider.
|
||||
enum SettingsPane: Hashable {
|
||||
case general
|
||||
case notifications
|
||||
case menuBar
|
||||
case menu
|
||||
case advanced
|
||||
case about
|
||||
case debug
|
||||
case provider(UsageProvider)
|
||||
|
||||
static let windowWidth: CGFloat = 880
|
||||
static let windowHeight: CGFloat = 620
|
||||
static let windowMinWidth: CGFloat = 800
|
||||
static let windowMinHeight: CGFloat = 540
|
||||
static let sidebarWidth: CGFloat = 260
|
||||
static let detailMaxWidth: CGFloat = 780
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .general: L("tab_general")
|
||||
case .notifications: L("tab_notifications")
|
||||
case .menuBar: L("tab_menu_bar")
|
||||
case .menu: L("tab_menu")
|
||||
case .advanced: L("tab_advanced")
|
||||
case .about: L("tab_about")
|
||||
case .debug: L("tab_debug")
|
||||
case let .provider(provider):
|
||||
ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
struct PreferencesView: View {
|
||||
@Bindable var settings: SettingsStore
|
||||
@Bindable var store: UsageStore
|
||||
let updater: UpdaterProviding
|
||||
@Bindable var selection: PreferencesSelection
|
||||
let managedCodexAccountCoordinator: ManagedCodexAccountCoordinator
|
||||
let codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator
|
||||
let runProviderLoginFlow: @MainActor (UsageProvider) async -> Void
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
init(
|
||||
settings: SettingsStore,
|
||||
store: UsageStore,
|
||||
updater: UpdaterProviding,
|
||||
selection: PreferencesSelection,
|
||||
managedCodexAccountCoordinator: ManagedCodexAccountCoordinator = ManagedCodexAccountCoordinator(),
|
||||
codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator? = nil,
|
||||
runProviderLoginFlow: @escaping @MainActor (UsageProvider) async -> Void = { _ in })
|
||||
{
|
||||
self.settings = settings
|
||||
self.store = store
|
||||
self.updater = updater
|
||||
self.selection = selection
|
||||
self.managedCodexAccountCoordinator = managedCodexAccountCoordinator
|
||||
self.codexAccountPromotionCoordinator = codexAccountPromotionCoordinator
|
||||
?? CodexAccountPromotionCoordinator(
|
||||
settingsStore: settings,
|
||||
usageStore: store,
|
||||
managedAccountCoordinator: managedCodexAccountCoordinator)
|
||||
self.runProviderLoginFlow = runProviderLoginFlow
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
// Golden Gate-style sidebar: edge-to-edge material with a hairline separator,
|
||||
// no floating card chrome. The material ignores the safe area so it runs up
|
||||
// behind the transparent titlebar.
|
||||
SettingsSidebarView(settings: self.settings, store: self.store, selection: self.$selection.pane)
|
||||
.frame(width: SettingsPane.sidebarWidth)
|
||||
.background {
|
||||
SettingsSidebarMaterial()
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
|
||||
Divider()
|
||||
.ignoresSafeArea()
|
||||
|
||||
self.detailView
|
||||
.frame(
|
||||
maxWidth: SettingsPane.detailMaxWidth,
|
||||
maxHeight: .infinity,
|
||||
alignment: .topLeading)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
}
|
||||
.frame(
|
||||
minWidth: SettingsPane.windowMinWidth,
|
||||
idealWidth: SettingsPane.windowWidth,
|
||||
maxWidth: .infinity,
|
||||
minHeight: SettingsPane.windowMinHeight,
|
||||
idealHeight: SettingsPane.windowHeight,
|
||||
maxHeight: .infinity)
|
||||
.id(self.settings.appLanguage)
|
||||
.background {
|
||||
SettingsWindowAppearanceBridge(colorScheme: self.colorScheme, windowTitle: self.selection.pane.title)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
.onAppear {
|
||||
self.ensureValidSelection()
|
||||
}
|
||||
.onChange(of: self.settings.debugMenuEnabled) { _, _ in
|
||||
self.ensureValidSelection()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var detailView: some View {
|
||||
switch self.selection.pane {
|
||||
case .general:
|
||||
GeneralPane(settings: self.settings)
|
||||
case .notifications:
|
||||
NotificationsPane(settings: self.settings)
|
||||
case .menuBar:
|
||||
MenuBarPane(settings: self.settings, store: self.store)
|
||||
case .menu:
|
||||
MenuPane(settings: self.settings, store: self.store)
|
||||
case .advanced:
|
||||
AdvancedPane(settings: self.settings, store: self.store)
|
||||
case .about:
|
||||
AboutPane(updater: self.updater)
|
||||
case .debug:
|
||||
DebugPane(settings: self.settings, store: self.store)
|
||||
case let .provider(provider):
|
||||
ProvidersPane(
|
||||
provider: provider,
|
||||
settings: self.settings,
|
||||
store: self.store,
|
||||
managedCodexAccountCoordinator: self.managedCodexAccountCoordinator,
|
||||
codexAccountPromotionCoordinator: self.codexAccountPromotionCoordinator,
|
||||
runProviderLoginFlow: self.runProviderLoginFlow)
|
||||
.id(provider)
|
||||
}
|
||||
}
|
||||
|
||||
private func ensureValidSelection() {
|
||||
if !self.settings.debugMenuEnabled, self.selection.pane == .debug {
|
||||
self.selection.pane = .general
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
enum SettingsWindowSizing {
|
||||
static func enforceMinimumSize(_ window: NSWindow) {
|
||||
let toolbarHeight = max(0, window.frame.height - window.contentLayoutRect.height)
|
||||
let minimumSize = NSSize(
|
||||
width: SettingsPane.windowMinWidth,
|
||||
height: SettingsPane.windowMinHeight + toolbarHeight)
|
||||
window.minSize = minimumSize
|
||||
|
||||
if window.frame.width < minimumSize.width || window.frame.height < minimumSize.height {
|
||||
var frame = window.frame
|
||||
let repairedSize = NSSize(
|
||||
width: max(frame.width, minimumSize.width),
|
||||
height: max(frame.height, minimumSize.height))
|
||||
frame.origin.y += frame.height - repairedSize.height
|
||||
frame.size = repairedSize
|
||||
window.setFrame(frame, display: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
enum SettingsWindowAppearance {
|
||||
typealias ResetAction = @MainActor @Sendable () -> Void
|
||||
typealias ResetScheduler = @MainActor @Sendable (@escaping ResetAction) -> Void
|
||||
|
||||
static func refresh(
|
||||
_ window: NSWindow,
|
||||
application: NSApplication = NSApp,
|
||||
scheduleReset: ResetScheduler = Self.scheduleReset)
|
||||
{
|
||||
SettingsWindowSizing.enforceMinimumSize(window)
|
||||
window.appearanceSource = application
|
||||
// Pulse the exact effective appearance so the native toolbar redraws without
|
||||
// dropping inherited accessibility attributes, then restore KVO inheritance.
|
||||
window.appearance = application.effectiveAppearance
|
||||
scheduleReset { [weak window] in
|
||||
if let window {
|
||||
SettingsWindowSizing.enforceMinimumSize(window)
|
||||
}
|
||||
window?.appearance = nil
|
||||
window?.viewsNeedDisplay = true
|
||||
}
|
||||
}
|
||||
|
||||
static func scheduleReset(_ action: @escaping ResetAction) {
|
||||
Task { @MainActor in
|
||||
await Task.yield()
|
||||
action()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
struct SettingsWindowAppearanceBridge: NSViewRepresentable {
|
||||
let colorScheme: ColorScheme
|
||||
let windowTitle: String
|
||||
|
||||
func makeNSView(context: Context) -> SettingsWindowAppearanceView {
|
||||
SettingsWindowAppearanceView()
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: SettingsWindowAppearanceView, context: Context) {
|
||||
nsView.refreshWindowAppearance(for: self.colorScheme, windowTitle: self.windowTitle)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class SettingsWindowAppearanceView: NSView {
|
||||
private let scheduleReset: SettingsWindowAppearance.ResetScheduler
|
||||
private var colorScheme: ColorScheme?
|
||||
private var windowTitle: String?
|
||||
|
||||
init(scheduleReset: @escaping SettingsWindowAppearance.ResetScheduler = SettingsWindowAppearance.scheduleReset) {
|
||||
self.scheduleReset = scheduleReset
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
override func viewDidMoveToWindow() {
|
||||
super.viewDidMoveToWindow()
|
||||
NotificationCenter.default.removeObserver(self, name: NSWindow.didUpdateNotification, object: nil)
|
||||
if let window {
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(self.windowDidUpdate(_:)),
|
||||
name: NSWindow.didUpdateNotification,
|
||||
object: window)
|
||||
}
|
||||
self.configureWindowStyle()
|
||||
self.refreshWindowAppearance()
|
||||
}
|
||||
|
||||
@objc private func windowDidUpdate(_ notification: Notification) {
|
||||
self.configureWindowStyle()
|
||||
}
|
||||
|
||||
func refreshWindowAppearance(for colorScheme: ColorScheme, windowTitle: String? = nil) {
|
||||
let colorSchemeChanged = self.colorScheme != colorScheme
|
||||
let windowTitleChanged = self.windowTitle != windowTitle
|
||||
guard colorSchemeChanged || windowTitleChanged else { return }
|
||||
self.colorScheme = colorScheme
|
||||
self.windowTitle = windowTitle
|
||||
|
||||
guard let window else { return }
|
||||
self.configureWindowStyle()
|
||||
if windowTitleChanged, let windowTitle {
|
||||
window.title = windowTitle
|
||||
}
|
||||
if colorSchemeChanged {
|
||||
SettingsWindowAppearance.refresh(window, scheduleReset: self.scheduleReset)
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshWindowAppearance() {
|
||||
guard let window else { return }
|
||||
self.configureWindowStyle()
|
||||
if let windowTitle {
|
||||
window.title = windowTitle
|
||||
}
|
||||
SettingsWindowAppearance.refresh(window, scheduleReset: self.scheduleReset)
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
self.configureWindowStyle()
|
||||
}
|
||||
|
||||
private func configureWindowStyle() {
|
||||
guard let window else { return }
|
||||
if !window.styleMask.contains(.resizable) {
|
||||
window.styleMask.insert(.resizable)
|
||||
}
|
||||
if !window.titlebarAppearsTransparent {
|
||||
window.titlebarAppearsTransparent = true
|
||||
}
|
||||
if window.titleVisibility != .visible {
|
||||
window.titleVisibility = .visible
|
||||
}
|
||||
if window.titlebarSeparatorStyle != .none {
|
||||
window.titlebarSeparatorStyle = .none
|
||||
}
|
||||
if window.toolbar != nil {
|
||||
window.toolbar = nil
|
||||
}
|
||||
// Full-size content lets the sidebar material extend behind the titlebar so the
|
||||
// edge-to-edge sidebar reaches the top of the window; content stays below the
|
||||
// titlebar via the safe area.
|
||||
if !window.styleMask.contains(.fullSizeContentView) {
|
||||
window.styleMask.insert(.fullSizeContentView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private struct SettingsSidebarMaterial: NSViewRepresentable {
|
||||
func makeNSView(context: Context) -> NSVisualEffectView {
|
||||
let view = NSVisualEffectView()
|
||||
self.configure(view)
|
||||
return view
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: NSVisualEffectView, context: Context) {
|
||||
self.configure(nsView)
|
||||
}
|
||||
|
||||
private func configure(_ view: NSVisualEffectView) {
|
||||
view.material = .sidebar
|
||||
view.blendingMode = .behindWindow
|
||||
view.state = .followsWindowActiveState
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user