diff --git a/docs/spec/settings.md b/docs/spec/settings.md index b29f3bc6..f000711a 100644 --- a/docs/spec/settings.md +++ b/docs/spec/settings.md @@ -79,15 +79,28 @@ Defines: - Settings must include a Permissions section. - Screen Recording permission is required for the current native macOS capture host. +- Settings must present Screen Recording as the only permission needed by the current native macOS + capture host. - When Screen Recording is missing at launch or at capture start, Rsnap must open the macOS Screen Recording privacy page and present a small Rsnap-owned floating drag guide near System Settings. -- Accessibility and Input Monitoring may be displayed as diagnostic permissions, but they must not - be presented as required when the current native host does not need them. +- Accessibility and Input Monitoring must not be displayed in Settings while the current native host + does not need them. - Permission recovery should provide a visible drag-the-app affordance for adding Rsnap to System Settings where macOS allows that workflow, including a directional guide from the floating window toward System Settings and an Open System Settings fallback. - Permission status refresh must be available without restarting Rsnap. +## About Settings + +- Settings must include an About section. +- The About section must identify Yvette Cipher as the creator and describe Rsnap as an + open-source macOS capture tool. +- The About section must include external links to `https://github.com/hack-ink/rsnap` and + `https://x.com/YvetteCipher`. +- The creator link may encourage following for ongoing Rsnap updates and may state that follows + help support future work through X creator rewards. +- The About section must not expose capture defaults or a Restore Defaults action. + ## Default-Size Usability - At the default Settings window size, primary setting labels, selected option labels, and current diff --git a/native/macos-host/Sources/RsnapNativeHostKit/LiveChromeWindows.swift b/native/macos-host/Sources/RsnapNativeHostKit/LiveChromeWindows.swift index 782c944f..1227cbe6 100644 --- a/native/macos-host/Sources/RsnapNativeHostKit/LiveChromeWindows.swift +++ b/native/macos-host/Sources/RsnapNativeHostKit/LiveChromeWindows.swift @@ -340,8 +340,8 @@ final class LiveChromeLiquidGlassView: NSView { } return Color( hue: settings.hudTintHue.clamped(to: 0...1), - saturation: 0.48, - brightness: 1.0, + saturation: settings.hudTintSaturation.clamped(to: 0...1), + brightness: settings.hudTintBrightness.clamped(to: 0...1), opacity: strength * 0.12 ) } diff --git a/native/macos-host/Sources/RsnapNativeHostKit/NativeHostApp.swift b/native/macos-host/Sources/RsnapNativeHostKit/NativeHostApp.swift index b163c317..d00c9385 100644 --- a/native/macos-host/Sources/RsnapNativeHostKit/NativeHostApp.swift +++ b/native/macos-host/Sources/RsnapNativeHostKit/NativeHostApp.swift @@ -6364,13 +6364,25 @@ final class CaptureHostView: NSView { } private func updateLiveChromeBackdrops(hudFrame: CGRect?, loupeFrame: CGRect?) { - controller?.updateLiveChromeBackdrops(nil) hideClassicGlassMaterialViews() + guard scene.mode == .live, settings.usesClassicHudGlass else { + controller?.updateLiveChromeBackdrops(nil) + return + } + controller?.updateLiveChromeBackdrops( + LiveChromeBackdropSnapshot( + sourceWindowNumber: window?.windowNumber, + hudFrame: hudFrame.flatMap(globalRect(from:)), + loupeFrame: loupeFrame.flatMap(globalRect(from:)), + theme: chromeTheme(), + settings: settings + ) + ) } private func moveLiveChromeLayers() { let frames = currentLiveChromeLayerFrames() - hideClassicGlassMaterialViews() + updateLiveChromeBackdrops(hudFrame: frames.hud, loupeFrame: frames.loupe) moveExistingLiveLiquidGlassViews(hudFrame: frames.hud, loupeFrame: frames.loupe) liveRenderer.moveLiveChrome(hudFrame: frames.hud, loupeFrame: frames.loupe) } @@ -8754,10 +8766,12 @@ enum CaptureChrome { for theme: CaptureChromeTheme, settings: NativeHostSettings ) -> NSColor { let hue = CGFloat(settings.hudTintHue.clamped(to: 0...1)) + let saturation = CGFloat(settings.hudTintSaturation.clamped(to: 0...1)) + let brightness = CGFloat(settings.hudTintBrightness.clamped(to: 0...1)) return NSColor( calibratedHue: hue, - saturation: theme == .dark ? 0.48 : 0.34, - brightness: theme == .dark ? 0.62 : 0.94, + saturation: saturation, + brightness: brightness, alpha: 1 ) } diff --git a/native/macos-host/Sources/RsnapNativeHostKit/NativeHostPanels.swift b/native/macos-host/Sources/RsnapNativeHostKit/NativeHostPanels.swift index 11185b6b..6abffb06 100644 --- a/native/macos-host/Sources/RsnapNativeHostKit/NativeHostPanels.swift +++ b/native/macos-host/Sources/RsnapNativeHostKit/NativeHostPanels.swift @@ -110,6 +110,7 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate { showWindow(nil) NSRunningApplication.current.activate(options: [.activateAllWindows]) window?.makeKeyAndOrderFront(nil) + window?.invalidateShadow() NSApp.activate(ignoringOtherApps: true) } diff --git a/native/macos-host/Sources/RsnapNativeHostKit/NativeHostSettings.swift b/native/macos-host/Sources/RsnapNativeHostKit/NativeHostSettings.swift index 72e3812a..f2470892 100644 --- a/native/macos-host/Sources/RsnapNativeHostKit/NativeHostSettings.swift +++ b/native/macos-host/Sources/RsnapNativeHostKit/NativeHostSettings.swift @@ -20,6 +20,8 @@ final class NativeHostSettingsStore { static let hudBlur = "hudBlur" static let hudTint = "hudTint" static let hudTintHue = "hudTintHue" + static let hudTintSaturation = "hudTintSaturation" + static let hudTintBrightness = "hudTintBrightness" static let liquidGlassStyle = "liquidGlassStyle" static let loupeSampleSize = "loupeSampleSize" } @@ -62,6 +64,10 @@ final class NativeHostSettingsStore { ?? baseSettings.hudTint, hudTintHue: defaults.object(forKey: DefaultsKey.hudTintHue) as? Double ?? baseSettings.hudTintHue, + hudTintSaturation: defaults.object(forKey: DefaultsKey.hudTintSaturation) as? Double + ?? baseSettings.hudTintSaturation, + hudTintBrightness: defaults.object(forKey: DefaultsKey.hudTintBrightness) as? Double + ?? baseSettings.hudTintBrightness, liquidGlassStyle: LiquidGlassStylePreference( rawValue: defaults.string(forKey: DefaultsKey.liquidGlassStyle) ?? "") ?? baseSettings.liquidGlassStyle, @@ -105,6 +111,8 @@ final class NativeHostSettingsStore { defaults.set(settings.hudBlur, forKey: DefaultsKey.hudBlur) defaults.set(settings.hudTint, forKey: DefaultsKey.hudTint) defaults.set(settings.hudTintHue, forKey: DefaultsKey.hudTintHue) + defaults.set(settings.hudTintSaturation, forKey: DefaultsKey.hudTintSaturation) + defaults.set(settings.hudTintBrightness, forKey: DefaultsKey.hudTintBrightness) defaults.set(settings.liquidGlassStyle.rawValue, forKey: DefaultsKey.liquidGlassStyle) defaults.set(settings.loupeSampleSize.rawValue, forKey: DefaultsKey.loupeSampleSize) } @@ -124,6 +132,8 @@ struct NativeHostSettings: Equatable { var hudBlur: Double var hudTint: Double var hudTintHue: Double + var hudTintSaturation: Double + var hudTintBrightness: Double var liquidGlassStyle: LiquidGlassStylePreference var loupeSampleSize: LoupeSampleSizePreference @@ -143,6 +153,8 @@ struct NativeHostSettings: Equatable { hudBlur: 0.5032628676470589, hudTint: 0.4990234375, hudTintHue: 0.6074879184861536, + hudTintSaturation: 0.72, + hudTintBrightness: 0.95, liquidGlassStyle: .clear, loupeSampleSize: .small ) @@ -159,6 +171,8 @@ struct NativeHostSettings: Equatable { copy.hudBlur = copy.hudBlur.clamped(to: 0...1) copy.hudTint = copy.hudTint.clamped(to: 0...1) copy.hudTintHue = copy.hudTintHue.clamped(to: 0...1) + copy.hudTintSaturation = copy.hudTintSaturation.clamped(to: 0...1) + copy.hudTintBrightness = copy.hudTintBrightness.clamped(to: 0...1) return copy } diff --git a/native/macos-host/Sources/RsnapNativeHostKit/NativeHostSettingsView.swift b/native/macos-host/Sources/RsnapNativeHostKit/NativeHostSettingsView.swift index f9b469f1..71f03df6 100644 --- a/native/macos-host/Sources/RsnapNativeHostKit/NativeHostSettingsView.swift +++ b/native/macos-host/Sources/RsnapNativeHostKit/NativeHostSettingsView.swift @@ -4,8 +4,9 @@ import SwiftUI enum NativeHostSettingsWindowMetrics { static let width: CGFloat = 620 - static let minHeight: CGFloat = 320 - static let idealHeight: CGFloat = 336 + static let minHeight: CGFloat = 304 + static let idealHeight: CGFloat = 304 + static let cornerRadius: CGFloat = 18 } @MainActor @@ -85,6 +86,7 @@ private enum NativeHostSettingsSection: String, CaseIterable, Identifiable { case capture case output case permissions + case about var id: Self { self } @@ -98,6 +100,8 @@ private enum NativeHostSettingsSection: String, CaseIterable, Identifiable { return "Output" case .permissions: return "Permissions" + case .about: + return "About" } } @@ -111,6 +115,8 @@ private enum NativeHostSettingsSection: String, CaseIterable, Identifiable { return "Files" case .permissions: return "Access" + case .about: + return "Project" } } @@ -124,8 +130,24 @@ private enum NativeHostSettingsSection: String, CaseIterable, Identifiable { return "folder" case .permissions: return "lock.shield" + case .about: + return "info.circle" } } + + var allowsRestoreDefaults: Bool { + switch self { + case .appearance, .capture, .output: + return true + case .permissions, .about: + return false + } + } +} + +private enum NativeHostAboutLinks { + static let source = "https://github.com/hack-ink/rsnap" + static let creator = "https://x.com/YvetteCipher" } private enum SettingsControlLayout { @@ -143,13 +165,9 @@ private struct SettingsRail: View { VStack(alignment: .leading, spacing: 14) { HStack(spacing: 8) { SettingsBrandIcon() - VStack(alignment: .leading, spacing: 2) { - Text(NativeHostBrand.displayName) - .font(.system(size: 17, weight: .semibold, design: .rounded)) - Text("Settings") - .font(.system(size: 10.5, weight: .medium)) - .foregroundStyle(.secondary) - } + Text(NativeHostBrand.displayName) + .font(.system(size: 17, weight: .semibold, design: .rounded)) + .lineLimit(1) } .padding(.horizontal, 2) @@ -303,6 +321,8 @@ private struct SettingsDashboard: View { OutputSettingsPanel(model: model) case .permissions: PermissionsSettingsPanel() + case .about: + AboutSettingsPanel() } } } @@ -322,7 +342,7 @@ private struct SettingsContentHeader: View { } .frame(maxWidth: .infinity, alignment: .leading) - if section != .permissions { + if section.allowsRestoreDefaults { Button(action: restoreDefaults) { Label("Restore Defaults", systemImage: "arrow.counterclockwise") .labelStyle(.titleAndIcon) @@ -350,6 +370,8 @@ private struct SettingsSectionInspector: View { OutputInspector(settings: model.settings) case .permissions: PermissionsInspector() + case .about: + AboutInspector() } } .padding(14) @@ -400,8 +422,8 @@ private struct AppearanceInspector: View { private var tintHex: String { let color = NSColor( hue: CGFloat(settings.hudTintHue), - saturation: 0.72, - brightness: 0.95, + saturation: CGFloat(settings.hudTintSaturation), + brightness: CGFloat(settings.hudTintBrightness), alpha: 1 ) let converted = color.usingColorSpace(.deviceRGB) ?? color @@ -463,21 +485,14 @@ private struct OutputInspector: View { } private struct PermissionsInspector: View { - private let requiredKinds = [ - PermissionKind.screenRecording, - .accessibility, - .inputMonitoring, - ] - var body: some View { - let required = requiredKinds.filter { NativePermissions.requiredForCurrentNativeHost($0) } - let granted = required.filter { NativePermissions.status(for: $0) }.count + let granted = NativePermissions.status(for: .screenRecording) ? 1 : 0 VStack(alignment: .leading, spacing: 12) { - PermissionProgressBadge(granted: granted, total: required.count) + PermissionProgressBadge(granted: granted, total: 1) InspectorMetric( title: "Required", - value: "\(required.count)", + value: "1", symbolName: "lock.shield" ) InspectorMetric( @@ -489,6 +504,20 @@ private struct PermissionsInspector: View { } } +private struct AboutInspector: View { + var body: some View { + VStack(alignment: .leading, spacing: 12) { + SettingsBrandIcon() + .frame(width: 42, height: 42) + InspectorMetric( + title: "Source", + value: "Open", + symbolName: "curlybraces.square" + ) + } + } +} + private struct MiniHudPreview: View { let settings: NativeHostSettings @Environment(\.colorScheme) private var colorScheme @@ -532,7 +561,11 @@ private struct MiniHudPreview: View { } private var tintColor: Color { - Color(hue: settings.hudTintHue, saturation: 0.72, brightness: 0.95) + Color( + hue: settings.hudTintHue, + saturation: settings.hudTintSaturation, + brightness: settings.hudTintBrightness + ) } private var previewFill: Color { @@ -919,6 +952,7 @@ private struct AppearanceSettingsPanel: View { } .disabled(!model.settings.hudGlassEnabled) } + .transition(.opacity) } } @@ -942,7 +976,7 @@ private struct AppearanceSettingsPanel: View { isEnabled: model.settings.hudGlassEnabled ) } - .transition(.opacity.combined(with: .move(edge: .top))) + .transition(.opacity) } VStack(spacing: 0) { @@ -989,14 +1023,22 @@ private struct AppearanceSettingsPanel: View { get: { Color( hue: model.settings.hudTintHue, - saturation: 0.72, - brightness: 0.95 + saturation: model.settings.hudTintSaturation, + brightness: model.settings.hudTintBrightness ) }, set: { color in let nsColor = NSColor(color) let converted = nsColor.usingColorSpace(.deviceRGB) ?? nsColor - model.update { $0.hudTintHue = Double(converted.hueComponent) } + var hue: CGFloat = 0 + var saturation: CGFloat = 0 + var brightness: CGFloat = 0 + converted.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil) + model.update { + $0.hudTintHue = Double(hue) + $0.hudTintSaturation = Double(saturation) + $0.hudTintBrightness = Double(brightness) + } } ) } @@ -1178,21 +1220,24 @@ private struct FlatColorSwatch: View { @State private var isHovered = false var body: some View { - SettingsColorWell(selection: $selection, isEnabled: isEnabled) - .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous)) - .overlay { - RoundedRectangle(cornerRadius: 6, style: .continuous) - .stroke(borderColor, lineWidth: 1) - .allowsHitTesting(false) - } - .frame(width: 30, height: 18) - .opacity(isEnabled ? 1 : 0.45) - .scaleEffect(isHovered && isEnabled ? 1.04 : 1) - .animation(.easeOut(duration: 0.12), value: isHovered) - .onHover { hovering in - isHovered = hovering - } - .disabled(!isEnabled) + ZStack { + RoundedRectangle(cornerRadius: 6, style: .continuous) + .fill(selection) + SettingsColorWell(selection: $selection, isEnabled: isEnabled) + } + .overlay { + RoundedRectangle(cornerRadius: 6, style: .continuous) + .stroke(borderColor, lineWidth: 1) + .allowsHitTesting(false) + } + .frame(width: 30, height: 18) + .opacity(isEnabled ? 1 : 0.45) + .scaleEffect(isHovered && isEnabled ? 1.04 : 1) + .animation(.easeOut(duration: 0.12), value: isHovered) + .onHover { hovering in + isHovered = hovering + } + .allowsHitTesting(isEnabled) } private var borderColor: Color { @@ -1204,21 +1249,19 @@ private struct SettingsColorWell: NSViewRepresentable { @Binding var selection: Color let isEnabled: Bool - func makeNSView(context: Context) -> NSColorWell { - let colorWell = NSColorWell(frame: .zero) - colorWell.isBordered = false - colorWell.supportsAlpha = false - colorWell.target = context.coordinator - colorWell.action = #selector(Coordinator.colorChanged(_:)) - return colorWell + func makeNSView(context: Context) -> ColorPanelTriggerView { + let view = ColorPanelTriggerView() + view.coordinator = context.coordinator + return view } - func updateNSView(_ colorWell: NSColorWell, context: Context) { + func updateNSView(_ view: ColorPanelTriggerView, context: Context) { context.coordinator.selection = $selection - colorWell.isEnabled = isEnabled - let nextColor = NSColor(selection).usingColorSpace(.deviceRGB) ?? NSColor(selection) - if !Self.matches(colorWell.color, nextColor) { - colorWell.color = nextColor + view.allowsColorPanel = isEnabled + view.color = NSColor(selection).usingColorSpace(.deviceRGB) ?? NSColor(selection) + view.coordinator = context.coordinator + if NSColorPanel.sharedColorPanelExists, NSColorPanel.shared.isVisible { + NSColorPanel.shared.color = view.color } } @@ -1226,13 +1269,28 @@ private struct SettingsColorWell: NSViewRepresentable { Coordinator(selection: $selection) } - private static func matches(_ lhs: NSColor, _ rhs: NSColor) -> Bool { - let left = lhs.usingColorSpace(.deviceRGB) ?? lhs - let right = rhs.usingColorSpace(.deviceRGB) ?? rhs - return abs(left.redComponent - right.redComponent) < 0.001 - && abs(left.greenComponent - right.greenComponent) < 0.001 - && abs(left.blueComponent - right.blueComponent) < 0.001 - && abs(left.alphaComponent - right.alphaComponent) < 0.001 + final class ColorPanelTriggerView: NSView { + var allowsColorPanel = true + var color = NSColor.systemBlue + weak var coordinator: Coordinator? + + override var acceptsFirstResponder: Bool { + false + } + + override func mouseDown(with event: NSEvent) { + guard allowsColorPanel else { + return + } + NSApp.activate(ignoringOtherApps: true) + let panel = NSColorPanel.shared + panel.showsAlpha = false + panel.isContinuous = true + panel.color = color + panel.setTarget(coordinator) + panel.setAction(#selector(Coordinator.colorChanged(_:))) + panel.orderFront(self) + } } final class Coordinator: NSObject { @@ -1244,7 +1302,7 @@ private struct SettingsColorWell: NSViewRepresentable { @MainActor @objc - func colorChanged(_ sender: NSColorWell) { + func colorChanged(_ sender: NSColorPanel) { NSColorPanel.shared.showsAlpha = false selection.wrappedValue = Color(nsColor: sender.color) } @@ -1378,7 +1436,7 @@ private struct PermissionsSettingsPanel: View { private let primaryKind = PermissionKind.screenRecording var body: some View { - VStack(spacing: 6) { + VStack(spacing: 0) { PermissionGrantCard( kind: primaryKind, refreshID: refreshID, @@ -1391,19 +1449,6 @@ private struct PermissionsSettingsPanel: View { refreshID += 1 } ) - - VStack(spacing: 0) { - ForEach(Self.rows) { row in - PermissionStatusTile( - row: row, - refreshID: refreshID, - openSettings: { kind in - NativePermissions.openSystemSettings(for: kind) - refreshID += 1 - } - ) - } - } } } @@ -1414,27 +1459,6 @@ private struct PermissionsSettingsPanel: View { private static var appIcon: NSImage { NSWorkspace.shared.icon(forFile: appBundleURL.path) } - - private static let rows: [PermissionSettingsRow] = [ - PermissionSettingsRow( - kind: .accessibility, - title: "Accessibility", - symbolName: "accessibility" - ), - PermissionSettingsRow( - kind: .inputMonitoring, - title: "Input Monitoring", - symbolName: "keyboard" - ), - ] -} - -private struct PermissionSettingsRow: Identifiable { - let kind: PermissionKind - let title: String - let symbolName: String - - var id: PermissionKind { kind } } private struct PermissionGrantCard: View { @@ -1550,86 +1574,10 @@ private struct PermissionGrantCard: View { } -private struct PermissionStatusTile: View { - let row: PermissionSettingsRow - let refreshID: Int - let openSettings: (PermissionKind) -> Void - - var body: some View { - HStack(alignment: .center, spacing: 10) { - SettingsTileIcon(symbolName: row.symbolName, size: 19) - VStack(alignment: .leading, spacing: 2) { - Text(row.title) - .font(.system(size: 13, weight: .semibold)) - .lineLimit(1) - .minimumScaleFactor(0.9) - Text(subtitle) - .font(.system(size: 10.5, weight: .medium)) - .foregroundStyle(.secondary) - .lineLimit(2) - .fixedSize(horizontal: false, vertical: true) - } - .layoutPriority(1) - Spacer(minLength: 8) - HStack(spacing: 7) { - PermissionStateBadge(title: badgeTitle, style: badgeStyle) - if canOpen { - Button { - openSettings(row.kind) - } label: { - Image(systemName: "arrow.up.forward.app") - .frame(width: 13, height: 13) - } - .rsnapGlassButton(prominent: false) - .controlSize(.small) - .help("Open \(row.title)") - } - } - } - .padding(.vertical, 4) - .frame(maxWidth: .infinity, minHeight: 44, alignment: .leading) - } - - private var isGranted: Bool { - _ = refreshID - return NativePermissions.status(for: row.kind) - } - - private var isRequired: Bool { - NativePermissions.requiredForCurrentNativeHost(row.kind) - } - - private var subtitle: String { - if isGranted { - return "Granted." - } - return isRequired ? "Required for native capture." : "Not used by current host." - } - - private var badgeTitle: String { - if isGranted { - return "Granted" - } - return isRequired ? "Required" : "Not Used" - } - - private var badgeStyle: PermissionStateBadge.Style { - if isGranted { - return .granted - } - return isRequired ? .required : .muted - } - - private var canOpen: Bool { - isRequired && !isGranted - } -} - private struct PermissionStateBadge: View { enum Style { case granted case required - case muted } let title: String @@ -1657,8 +1605,6 @@ private struct PermissionStateBadge: View { return Color.green case .required: return Color.accentColor - case .muted: - return Color.secondary } } @@ -1668,8 +1614,6 @@ private struct PermissionStateBadge: View { return Color.green.opacity(colorScheme == .light ? 0.10 : 0.16) case .required: return Color.accentColor.opacity(colorScheme == .light ? 0.10 : 0.18) - case .muted: - return Color.secondary.opacity(colorScheme == .light ? 0.08 : 0.12) } } @@ -1679,12 +1623,135 @@ private struct PermissionStateBadge: View { return Color.green.opacity(colorScheme == .light ? 0.20 : 0.26) case .required: return Color.accentColor.opacity(colorScheme == .light ? 0.20 : 0.28) - case .muted: - return Color.secondary.opacity(colorScheme == .light ? 0.14 : 0.20) } } } +private struct AboutSettingsPanel: View { + var body: some View { + VStack(alignment: .leading, spacing: 8) { + AboutIntroBlock() + + VStack(spacing: 0) { + AboutLinkTile( + symbolName: "curlybraces.square", + title: "Open Source", + buttonTitle: "GitHub", + urlString: NativeHostAboutLinks.source + ) + } + } + } +} + +private struct AboutIntroBlock: View { + var body: some View { + HStack(alignment: .top, spacing: 10) { + SettingsTileIcon(symbolName: "sparkles", size: 20) + VStack(alignment: .leading, spacing: 5) { + HStack(alignment: .firstTextBaseline, spacing: 8) { + Text("Built by Yvette Cipher") + .font(.system(size: 13, weight: .semibold)) + Spacer(minLength: 8) + Button(action: openCreator) { + Label("Follow on X", systemImage: "arrow.up.forward") + .labelStyle(.titleAndIcon) + } + .rsnapGlassButton(prominent: false) + .controlSize(.small) + .help(NativeHostAboutLinks.creator) + } + Text( + "Rsnap is an open-source macOS capture tool. I keep sharing progress, design notes, and release updates on X; following helps support future work through X creator rewards." + ) + .font(.system(size: 10.8, weight: .medium)) + .foregroundStyle(.secondary) + .lineLimit(4) + .fixedSize(horizontal: false, vertical: true) + } + .layoutPriority(1) + } + .padding(.vertical, 6) + .frame(maxWidth: .infinity, alignment: .leading) + } + + private func openCreator() { + guard let url = URL(string: NativeHostAboutLinks.creator) else { + return + } + NSWorkspace.shared.open(url) + } +} + +private struct AboutLinkTile: View { + let symbolName: String + let title: String + let subtitle: String? + let buttonTitle: String + let urlString: String + + init( + symbolName: String, + title: String, + subtitle: String? = nil, + buttonTitle: String, + urlString: String + ) { + self.symbolName = symbolName + self.title = title + self.subtitle = subtitle + self.buttonTitle = buttonTitle + self.urlString = urlString + } + + var body: some View { + HStack(spacing: 10) { + SettingsTileIcon(symbolName: symbolName, size: 19) + VStack(alignment: .leading, spacing: hasSubtitle ? 2 : 0) { + Text(title) + .font(.system(size: 13, weight: .semibold)) + .lineLimit(1) + if let subtitle, !subtitle.isEmpty { + Text(subtitle) + .font(.system(size: 10.5, weight: .medium)) + .foregroundStyle(.secondary) + .lineLimit(2) + .fixedSize(horizontal: false, vertical: true) + } + } + .layoutPriority(1) + Spacer(minLength: 10) + HStack { + Spacer(minLength: 0) + Button(action: openURL) { + Label(buttonTitle, systemImage: "arrow.up.forward") + .labelStyle(.titleAndIcon) + } + .rsnapGlassButton(prominent: false) + .controlSize(.small) + .help(urlString) + } + .frame(width: SettingsControlLayout.controlColumnWidth, alignment: .trailing) + } + .padding(.vertical, 5) + .frame(maxWidth: .infinity, minHeight: 44, alignment: .leading) + } + + private func openURL() { + guard let url = URL(string: urlString) else { + return + } + NSWorkspace.shared.open(url) + } + + private var hasSubtitle: Bool { + guard let subtitle else { + return false + } + return !subtitle.isEmpty + } +} + private struct OutputSettingsPanel: View { @ObservedObject var model: NativeHostSettingsViewModel @@ -1988,12 +2055,29 @@ private struct SettingsAtmosphere: View { Color.black.opacity(0.14) } } + .clipShape(windowShape) + .overlay { + windowShape + .stroke(windowBorderColor, lineWidth: 1) + .allowsHitTesting(false) + } .ignoresSafeArea() } + private var windowShape: RoundedRectangle { + RoundedRectangle( + cornerRadius: NativeHostSettingsWindowMetrics.cornerRadius, + style: .continuous + ) + } + private var tintColor: Color { Color(hue: tintHue, saturation: 0.58, brightness: 0.94) } + + private var windowBorderColor: Color { + colorScheme == .light ? Color.white.opacity(0.58) : Color.white.opacity(0.10) + } } extension View { @@ -2148,7 +2232,6 @@ private struct SettingsGlassSurfaceModifier: ViewModifier { .stroke(panelBorderColor, lineWidth: 1) .allowsHitTesting(false) } - .shadow(color: panelShadowColor, radius: 12, y: 4) } private var shape: RoundedRectangle { @@ -2164,8 +2247,4 @@ private struct SettingsGlassSurfaceModifier: ViewModifier { private var panelBorderColor: Color { colorScheme == .light ? Color.white.opacity(0.62) : Color.white.opacity(0.090) } - - private var panelShadowColor: Color { - colorScheme == .light ? Color.black.opacity(0.075) : Color.black.opacity(0.28) - } }