Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 119 additions & 12 deletions Sources/Lithe/LitheApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,6 @@ struct LitheApp: App {
.environmentObject(settings)
.environmentObject(updateChecker)
.environment(\.locale, settings.language.locale)
.preferredColorScheme(settings.themePreference.preferredColorScheme)
}
.defaultSize(width: 1040, height: 720)
.windowResizability(.contentMinSize)
Expand Down Expand Up @@ -439,18 +438,31 @@ private struct SettingsWindow: View {
@ObservedObject var model: AppModel
@ObservedObject var settings: AppSettings
@StateObject private var windowReference = SettingsWindowReference()
@StateObject private var viewState: SettingsViewState

init(model: AppModel, settings: AppSettings) {
self.model = model
self.settings = settings
_viewState = StateObject(wrappedValue: SettingsViewState(
initialCategory: model.requestedSettingsCategory
))
}

var body: some View {
SettingsView(
settings: settings,
initialCategory: model.requestedSettingsCategory,
onDismiss: close
)
.environmentObject(model)
SettingsAppearanceContainer(themePreference: settings.themePreference) {
SettingsView(
settings: settings,
viewState: viewState,
initialCategory: model.requestedSettingsCategory,
onDismiss: close
)
.environmentObject(model)
}
.background(
SettingsWindowAccessor(
reference: windowReference,
title: settingsWindowTitle(for: settings.language)
title: settingsWindowTitle(for: settings.language),
themePreference: settings.themePreference
)
)
.onDisappear {
Expand All @@ -464,37 +476,124 @@ private struct SettingsWindow: View {
}
}

struct SettingsAppearanceContainer<Content: View>: View {
let themePreference: AppThemePreference
let content: Content

init(
themePreference: AppThemePreference,
@ViewBuilder content: () -> Content
) {
self.themePreference = themePreference
self.content = content()
}

var body: some View {
content.preferredColorScheme(themePreference.preferredColorScheme)
}
}

@MainActor
private final class SettingsWindowReference: ObservableObject {
weak var window: NSWindow?
}

private final class SettingsTitlebarBackgroundView: NSView {
override func hitTest(_ point: NSPoint) -> NSView? { nil }
}

private final class SettingsWindowProbe: NSView {
var onEffectiveAppearanceChange: (() -> Void)?

override func viewDidChangeEffectiveAppearance() {
super.viewDidChangeEffectiveAppearance()
onEffectiveAppearanceChange?()
}
}

private struct SettingsWindowAccessor: NSViewRepresentable {
let reference: SettingsWindowReference
let title: String
let themePreference: AppThemePreference

func makeNSView(context: Context) -> NSView {
let view = NSView(frame: .zero)
func makeNSView(context: Context) -> SettingsWindowProbe {
let view = SettingsWindowProbe(frame: .zero)
bindAppearanceUpdates(to: view)
configureWindow(for: view)
return view
}

func updateNSView(_ view: NSView, context: Context) {
func updateNSView(_ view: SettingsWindowProbe, context: Context) {
bindAppearanceUpdates(to: view)
configureWindow(for: view)
}

private func bindAppearanceUpdates(to view: SettingsWindowProbe) {
view.onEffectiveAppearanceChange = { [weak view] in
guard let view else { return }
configureWindow(for: view)
}
}

private func configureWindow(for view: NSView) {
DispatchQueue.main.async {
guard let window = view.window else { return }
reference.window = window
window.title = title
let windowAppearance = themePreference.windowAppearance
if window.appearance?.name != windowAppearance?.name {
window.appearance = windowAppearance
}
if window.contentView?.appearance?.name != windowAppearance?.name {
window.contentView?.appearance = windowAppearance
}
window.styleMask.insert(.fullSizeContentView)
window.titlebarAppearsTransparent = true
window.titleVisibility = .visible
window.backgroundColor = NSColor(LitheTheme.settingsSurface)
window.titlebarSeparatorStyle = .none
window.isOpaque = true
let settingsSurface = LitheTheme.settingsSurfaceNSColor(
for: window.effectiveAppearance
)
window.backgroundColor = settingsSurface
applySettingsSurface(toTitlebarOf: window, color: settingsSurface)
window.standardWindowButton(.miniaturizeButton)?.isEnabled = false
window.standardWindowButton(.zoomButton)?.isEnabled = true
}
}

private func applySettingsSurface(toTitlebarOf window: NSWindow, color: NSColor) {
// AppKit places the titlebar in multiple nested views. Styling only
// the close-button's immediate superview leaves the opaque theme
// frame above it untouched, which is the extra strip seen in the
// settings window. Apply the same surface to each titlebar ancestor.
var view = window.standardWindowButton(.closeButton)?.superview
var titlebarHost: NSView?
while let current = view, current !== window.contentView {
current.wantsLayer = true
current.layer?.backgroundColor = color.cgColor
if current.bounds.width >= window.frame.width * 0.8,
current.bounds.height <= 100 {
titlebarHost = current
}
view = current.superview
}

guard let titlebarHost else { return }
let backgroundView: SettingsTitlebarBackgroundView
if let existing = titlebarHost.subviews.first(where: {
$0 is SettingsTitlebarBackgroundView
}) as? SettingsTitlebarBackgroundView {
backgroundView = existing
} else {
backgroundView = SettingsTitlebarBackgroundView(frame: titlebarHost.bounds)
titlebarHost.addSubview(backgroundView, positioned: .below, relativeTo: nil)
}
backgroundView.frame = titlebarHost.bounds
backgroundView.autoresizingMask = [.width, .height]
backgroundView.wantsLayer = true
backgroundView.layer?.backgroundColor = color.cgColor
}
}

private func settingsWindowTitle(for language: AppLanguage) -> String {
Expand All @@ -506,6 +605,14 @@ private func settingsWindowTitle(for language: AppLanguage) -> String {
}

private extension AppThemePreference {
var windowAppearance: NSAppearance? {
switch self {
case .system: nil
case .light: NSAppearance(named: .aqua)
case .dark: NSAppearance(named: .darkAqua)
}
}

var preferredColorScheme: ColorScheme? {
switch self {
case .system: nil
Expand Down
34 changes: 32 additions & 2 deletions Sources/Lithe/Theme/LitheTheme.swift
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,35 @@ enum LitheTheme {
// MARK: - 背景层次
static var window: Color { adaptive(\.window) }
static var titlebar: Color { adaptive(\.titlebar) }
static var settingsSurface: Color { editor }
static var settingsSurface: Color {
Color(nsColor: NSColor(name: nil) { appearance in
settingsSurfaceNSColor(for: appearance)
})
}
static func settingsSurfaceNSColor(for appearance: NSAppearance) -> NSColor {
let isDark = appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua
return isDark
? NSColor(srgbRed: 0.157, green: 0.161, blue: 0.173, alpha: 1)
: NSColor(srgbRed: 0.925, green: 0.933, blue: 0.945, alpha: 1)
}
static let settingsPrimaryAction = Color(
red: 56.0 / 255.0,
green: 113.0 / 255.0,
blue: 225.0 / 255.0
)
static let settingsSelection = Color(
red: 43.0 / 255.0,
green: 66.0 / 255.0,
blue: 113.0 / 255.0
)
static var settingsControlBackground: Color {
Color(nsColor: NSColor(name: nil) { appearance in
let isDark = appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua
return isDark
? NSColor(srgbRed: 43.0 / 255.0, green: 45.0 / 255.0, blue: 48.0 / 255.0, alpha: 1)
: NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
})
}
static var toolHeader: Color { adaptive(\.toolHeader) }
static var toolHeaderInactive: Color { adaptive(\.toolHeaderInactive) }
static var sidebar: Color { adaptive(\.sidebar) }
Expand Down Expand Up @@ -459,6 +487,8 @@ private struct LitheRowHoverModifier: ViewModifier {
// MARK: - 按钮样式

struct LithePrimaryButtonStyle: ButtonStyle {
var backgroundColor = LitheTheme.accent
var restingOpacity = 0.92
@State private var isHovering = false

func makeBody(configuration: Configuration) -> some View {
Expand All @@ -469,7 +499,7 @@ struct LithePrimaryButtonStyle: ButtonStyle {
.frame(height: 30)
.background(
RoundedRectangle(cornerRadius: LitheTheme.Metrics.controlCornerRadius)
.fill(LitheTheme.accent.opacity(configuration.isPressed ? 0.78 : (isHovering ? 1 : 0.92)))
.fill(backgroundColor.opacity(configuration.isPressed ? 0.78 : (isHovering ? 1 : restingOpacity)))
)
.contentShape(Rectangle())
.onHover { isHovering = $0 }
Expand Down
11 changes: 5 additions & 6 deletions Sources/Lithe/Views/App/KeyboardShortcutSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ struct KeyboardShortcutSettingsView: View {
.padding(.horizontal, 24)
.padding(.bottom, 24)
}
.litheScrollViewChrome(alwaysShowVertical: true, usesCompactScrollers: true)
}
.background(LitheTheme.window)
.background(LitheTheme.settingsSurface)
}

private var header: some View {
Expand All @@ -65,12 +66,10 @@ struct KeyboardShortcutSettingsView: View {
.lithePointer()
}

TextField("Search actions or shortcuts", text: $query)
.textFieldStyle(.roundedBorder)
.onChange(of: query) { _ in
LitheSettingsSearchField("Search actions or shortcuts", text: $query) { _ in
editingTarget = nil
validationIssue = nil
}
}
}
.foregroundStyle(LitheTheme.primaryText)
.padding(24)
Expand Down Expand Up @@ -203,7 +202,7 @@ struct KeyboardShortcutSettingsView: View {
}
.padding(.horizontal, 7)
.frame(height: 24)
.background(LitheTheme.inputBackground)
.background(LitheTheme.settingsControlBackground)
.clipShape(RoundedRectangle(cornerRadius: 4))
.overlay {
RoundedRectangle(cornerRadius: 4)
Expand Down
Loading
Loading