主窗口改用液态玻璃(macOS 26),分段切换改为苹果标准样式,新增全局加载框

- 新增 Design/LiquidGlass.swift:dsGlass 修饰器(macOS 26 用 glassEffect,更早系统与离屏截图退回实色)、isInsideGlass 环境值避免玻璃叠玻璃、LoadingHUD 全局加载框
- 侧边栏是浮在窗口里的玻璃面板,选中项为滑动的淡蓝圆角底;顶栏“在菜单栏显示”一组收进玻璃胶囊
- DSButtonStyle:主按钮蓝色玻璃胶囊、次要按钮透明玻璃;IconButton / MiniIconButton 为圆形玻璃;ChipButton 为玻璃胶囊;DSToggle 在 macOS 26 用系统开关
- SegmentedControl 统一为苹果标准样式:灰色圆角底槽、等宽分段、选中段实心品牌蓝白字并滑动;PillSwitch 同样灰底
- 清理、卸载、导出诊断、修改 DNS 进行中显示全局加载框
- 没有用 safeAreaBar 让页面从顶栏下滚过:macOS 上交界处会画一条分隔线,柔和或隐藏滚动边缘都去不掉
This commit is contained in:
gentpan committed 2026-09-16 18:26:09 +05:00
1 parent f3c0f860cb
commit d68fbce42a
10 files changed
+421 -160

No files matched your search

@@ -680,6 +680,7 @@ npm 缓存 npm Cache
正在同步… Syncing…
正在安装,完成后 OpenStats 会自动重启。 Installing. OpenStats will relaunch when it's done.
正在导出… Exporting…
正在导出诊断信息… Exporting diagnostics…
正在扫描应用… Scanning apps…
正在收集走势… Collecting trend…
正在本机生成… Generating on this Mac…
@@ -331,8 +331,31 @@ private struct DSButtonBody: View {
let kind: DSButtonStyle.Kind
@State private var hovering = false
@Environment(\.isEnabled) private var isEnabled
@Environment(\.isInsideGlass) private var isInsideGlass
var body: some View {
if DS.Glass.isAvailable {
glassBody
} else {
solidBody
}
}
/// macOS 26:胶囊形玻璃按钮。主按钮是蓝色玻璃,次要按钮是透明玻璃,文字按钮不加玻璃
private var glassBody: some View {
configuration.label
.dsFont(.sm, weight: .medium)
.foregroundStyle(foreground)
.padding(.horizontal, DS.Space.s4)
.frame(height: DS.Size.controlHeight)
.contentShape(Capsule())
.modifier(GlassButtonSurface(kind: kind, active: active, isInsideGlass: isInsideGlass))
.opacity(isEnabled ? 1 : 0.5)
.onHover { hovering = $0 }
.animation(DS.Motion.quick, value: hovering)
}
private var solidBody: some View {
configuration.label
.dsFont(.sm, weight: .medium)
.foregroundStyle(foreground)
@@ -374,15 +397,18 @@ struct IconButton: View {
let help: String
let action: () -> Void
@State private var hovering = false
@Environment(\.isInsideGlass) private var isInsideGlass
var body: some View {
// macOS 26 上是圆形玻璃按钮(与系统设置的前进后退一致);已经在玻璃面板里时只有悬停底色
let glass = DS.Glass.isAvailable && !isInsideGlass
Button(action: action) {
Image(systemName: systemName)
.font(.system(size: DS.TextSize.sm.rawValue, weight: .medium))
.foregroundStyle(hovering ? DS.Palette.primary : DS.Palette.textSecondary)
.foregroundStyle(hovering ? DS.Palette.primary : glass ? DS.Palette.textPrimary : DS.Palette.textSecondary)
.frame(width: DS.Size.controlHeight, height: DS.Size.controlHeight)
.background(hovering ? DS.Palette.surfaceHover : .clear, in: RoundedRectangle(cornerRadius: DS.Radius.md))
.contentShape(Rectangle())
.modifier(IconButtonSurface(hovering: hovering, glass: glass))
.contentShape(Circle())
}
.buttonStyle(.plain)
.onHover { hovering = $0 }
@@ -391,6 +417,39 @@ struct IconButton: View {
}
}
/// 玻璃按钮的底:主按钮蓝色玻璃,次要按钮透明玻璃;文字按钮、或已在玻璃面板里的次要按钮只有悬停底色
private struct GlassButtonSurface: ViewModifier {
let kind: DSButtonStyle.Kind
let active: Bool
let isInsideGlass: Bool
func body(content: Content) -> some View {
switch kind {
case .primary:
content.dsGlass(in: Capsule(), tint: DS.Palette.primary, interactive: true)
case .secondary where !isInsideGlass:
content.dsGlass(in: Capsule(), interactive: true)
default:
content.background(active ? DS.Palette.surfaceHover : .clear, in: Capsule())
}
}
}
private struct IconButtonSurface: ViewModifier {
let hovering: Bool
let glass: Bool
func body(content: Content) -> some View {
if glass {
content.dsGlass(in: Circle(), interactive: true)
} else if DS.Glass.isAvailable {
content.background(hovering ? DS.Palette.surfaceHover : .clear, in: Circle())
} else {
content.background(hovering ? DS.Palette.surfaceHover : .clear, in: RoundedRectangle(cornerRadius: DS.Radius.md))
}
}
}
// MARK: - 侧边栏
/// 侧边栏的一项。选中态不填色:由容器上的 `sidebarGlider()` 画左侧滑动的蓝色光条与淡淡的高亮
@@ -442,15 +501,32 @@ struct SidebarRowsKey: PreferenceKey {
}
extension View {
/// 放在包含 SidebarButton 的容器上:左侧一条两端渐隐的细线,选中项位置有一段发光的蓝色光条,
/// 切换时带回弹地滑过去,光条右侧拖出一片渐淡的蓝色高亮
/// 放在包含 SidebarButton 的容器上,画出选中态。
/// macOS 26:选中项垫一块淡蓝色圆角底,切换时滑过去(侧边栏本身是玻璃面板,里面不再叠玻璃);
/// 更早的系统:左侧一条两端渐隐的细线,选中项位置有一段发光的蓝色光条,带回弹地滑过去
@ViewBuilder
func sidebarGlider() -> some View {
overlayPreferenceValue(SidebarRowsKey.self) { rows in
GeometryReader { proxy in
SidebarGlider(frames: rows.map { proxy[$0.bounds] },
selected: rows.first(where: \.isSelected).map { proxy[$0.bounds] })
if DS.Glass.isAvailable {
backgroundPreferenceValue(SidebarRowsKey.self) { rows in
GeometryReader { proxy in
if let selected = rows.first(where: \.isSelected).map({ proxy[$0.bounds] }) {
RoundedRectangle(cornerRadius: DS.Radius.md, style: .continuous)
.fill(DS.Palette.sidebarSelected)
.frame(width: selected.width, height: selected.height)
.offset(x: selected.minX, y: selected.minY)
.animation(.spring(response: 0.35, dampingFraction: 0.8), value: selected)
}
}
.allowsHitTesting(false)
}
} else {
overlayPreferenceValue(SidebarRowsKey.self) { rows in
GeometryReader { proxy in
SidebarGlider(frames: rows.map { proxy[$0.bounds] },
selected: rows.first(where: \.isSelected).map { proxy[$0.bounds] })
}
.allowsHitTesting(false)
}
.allowsHitTesting(false)
}
}
}
@@ -497,9 +573,14 @@ private struct SidebarGlider: View {
// MARK: - 分段控件
/// 分段切换(苹果标准样式,与系统设置一致):一条灰色圆角底槽,各段等宽铺满,
/// 选中的一段是实心品牌蓝、白字,切换时蓝块滑过去
struct SegmentedControl<Value: Hashable>: View {
@Binding var selection: Value
let options: [(value: Value, title: String)]
@Namespace private var namespace
private static var radius: CGFloat { DS.Radius.md - DS.Space.s1 / 2 }
var body: some View {
HStack(spacing: 0) {
@@ -509,16 +590,17 @@ struct SegmentedControl<Value: Hashable>: View {
selection = option.value
} label: {
Text(option.title)
.dsFont(.sm, weight: selected ? .semibold : .regular)
.foregroundStyle(selected ? DS.Palette.textPrimary : DS.Palette.textSecondary)
.dsFont(.sm)
.foregroundStyle(selected ? DS.Palette.onPrimary : DS.Palette.textPrimary)
.lineLimit(1)
.padding(.horizontal, DS.Space.s2)
.frame(maxWidth: .infinity)
.frame(height: DS.Size.segmentHeight)
.frame(height: DS.Size.segmentHeight + DS.Space.s1)
.background {
if selected {
RoundedRectangle(cornerRadius: DS.Radius.md - DS.Space.s1)
.fill(DS.Palette.elevated)
.dsShadow(DS.Shadow.level1)
RoundedRectangle(cornerRadius: Self.radius, style: .continuous)
.fill(DS.Palette.primary)
.matchedGeometryEffect(id: "selection", in: namespace)
}
}
.contentShape(Rectangle())
@@ -527,9 +609,8 @@ struct SegmentedControl<Value: Hashable>: View {
.accessibilityAddTraits(selected ? .isSelected : [])
}
}
.padding(DS.Space.s1)
.background(DS.Palette.track, in: RoundedRectangle(cornerRadius: DS.Radius.md))
.animation(DS.Motion.quick, value: selection)
.background(DS.Palette.track, in: RoundedRectangle(cornerRadius: Self.radius, style: .continuous))
.animation(.spring(response: 0.3, dampingFraction: 0.85), value: selection)
}
}
@@ -538,8 +619,22 @@ struct SegmentedControl<Value: Hashable>: View {
struct DSToggle: View {
@Binding var isOn: Bool
var label: String = ""
@Environment(\.isSnapshot) private var isSnapshot
var body: some View {
// 离屏截图时窗口不在前台,系统开关会画成灰色,截图里用自绘的蓝色开关
if DS.Glass.isAvailable, !isSnapshot {
// macOS 26 的系统开关自带液态玻璃(拖动时滑块变成玻璃),颜色沿用品牌蓝
Toggle(isOn: $isOn) { Text(label) }
.labelsHidden()
.toggleStyle(.switch)
.tint(DS.Palette.primary)
} else {
solidBody
}
}
private var solidBody: some View {
Button {
isOn.toggle()
} label: {
@@ -785,6 +880,27 @@ struct ChipButton: View {
@State private var hovering = false
var body: some View {
if DS.Glass.isAvailable {
// macOS 26:每个选项是一颗玻璃胶囊,选中的是蓝色玻璃
Button(action: action) {
Text(title)
.dsFont(.xs, weight: isSelected ? .semibold : .medium)
.foregroundStyle(isSelected ? DS.Palette.onPrimary : DS.Palette.textPrimary)
.lineLimit(1)
.frame(maxWidth: .infinity)
.frame(height: DS.Size.segmentHeight)
.contentShape(Capsule())
.dsGlass(in: Capsule(), tint: isSelected ? DS.Palette.primary : nil, interactive: true)
}
.buttonStyle(.plain)
.animation(DS.Motion.quick, value: isSelected)
.accessibilityAddTraits(isSelected ? .isSelected : [])
} else {
solidBody
}
}
private var solidBody: some View {
Button(action: action) {
Text(title)
.dsFont(.xs, weight: isSelected ? .semibold : .medium)
@@ -0,0 +1,111 @@
import Localization
import SwiftUI
// MARK: - 液态玻璃
/// macOS 26 起,侧边栏、顶栏控件、按钮与切换控件用系统的液态玻璃;更早的系统退回原来的实色样式。
/// 玻璃只给浮在内容之上的导航与控件用,卡片、设置分组这类内容本身保持实色(与系统设置一致)。
extension EnvironmentValues {
/// 已经在一块玻璃里面(比如侧边栏面板):里面的控件不再叠一层玻璃,改用普通的悬停底色
@Entry var isInsideGlass = false
}
extension DS {
enum Glass {
/// 当前系统是否支持液态玻璃
static var isAvailable: Bool {
if #available(macOS 26.0, *) { true } else { false }
}
}
}
extension View {
/// 给控件加一层液态玻璃。`tint` 为品牌色时是蓝色玻璃(主按钮、选中项);
/// `interactive` 为真时按下会有玻璃的形变反馈。不支持玻璃的系统上、以及离屏截图时(拍不出玻璃)画 `fallback` 实色
func dsGlass<S: InsettableShape>(in shape: S, tint: Color? = nil, interactive: Bool = false,
fallback: Color = DS.Palette.elevated) -> some View {
modifier(GlassSurface(shape: shape, tint: tint, interactive: interactive, fallback: fallback))
}
}
private struct GlassSurface<S: InsettableShape>: ViewModifier {
let shape: S
let tint: Color?
let interactive: Bool
let fallback: Color
@Environment(\.isSnapshot) private var isSnapshot
func body(content: Content) -> some View {
if #available(macOS 26.0, *), !isSnapshot {
content.glassEffect(DS.Glass.style(tint: tint, interactive: interactive), in: shape)
} else {
content.background(tint ?? fallback, in: shape)
}
}
}
@available(macOS 26.0, *)
extension DS.Glass {
static func style(tint: Color?, interactive: Bool) -> SwiftUI.Glass {
var glass = SwiftUI.Glass.regular
if let tint { glass = glass.tint(tint) }
if interactive { glass = glass.interactive() }
return glass
}
}
// MARK: - 全局加载
/// 需要等待、期间不宜继续操作的任务(清理、卸载、导出诊断、修改 DNS):
/// 整个窗口压暗,中间浮一块玻璃加载框,写明正在做什么;任务结束自动消失
struct LoadingHUD: View {
let message: String
var body: some View {
ZStack {
// 压暗并拦住点击,避免任务进行中又触发别的操作
Rectangle()
.fill(DS.Palette.scrim)
.contentShape(Rectangle())
.onTapGesture {}
HStack(spacing: DS.Space.s3) {
ProgressView().controlSize(.regular)
Text(verbatim: message)
.dsFont(.base, weight: .medium)
.foregroundStyle(DS.Palette.textPrimary)
.lineLimit(2)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.horizontal, DS.Space.s8)
.padding(.vertical, DS.Space.s6)
.frame(minWidth: DS.Size.sidebarWidth + DS.Space.s16)
.modifier(HUDBackground())
.accessibilityElement(children: .combine)
.accessibilityAddTraits(.updatesFrequently)
}
.transition(.opacity)
}
}
private struct HUDBackground: ViewModifier {
func body(content: Content) -> some View {
if #available(macOS 26.0, *) {
content.glassEffect(.regular, in: RoundedRectangle(cornerRadius: DS.Radius.xl, style: .continuous))
} else {
content
.background(DS.Palette.elevated, in: RoundedRectangle(cornerRadius: DS.Radius.xl, style: .continuous))
.dsShadow(DS.Shadow.level2)
}
}
}
extension View {
/// 有文字时在上方盖一层全局加载框
func loadingHUD(_ message: String?) -> some View {
overlay {
if let message { LoadingHUD(message: message) }
}
.animation(DS.Motion.quick, value: message)
}
}
@@ -38,6 +38,10 @@ extension DS {
static let neutral500 = Color.dynamic(light: 0x6B7280, dark: 0x4B5563)
/// 浅一档的品牌蓝,用来区分同一图表里的第二类数据(如性能核)
static let primarySoft = Color.dynamic(light: 0x93C5FD, dark: 0x60A5FA)
/// 侧边栏选中项的淡蓝底
static let sidebarSelected = Color.dynamic(light: 0x2563EB, lightAlpha: 0.12, dark: 0x3B82F6, darkAlpha: 0.22)
/// 全局加载时压暗整个窗口
static let scrim = Color.dynamic(light: 0x000000, lightAlpha: 0.12, dark: 0x000000, darkAlpha: 0.40)
/// 核心类型配色:最高性能档为品牌蓝,其次浅蓝,能效核灰色
static func cluster(_ id: Int) -> Color {
@@ -184,6 +188,7 @@ extension DS {
}
static let level1 = Level(color: .black.opacity(0.08), radius: 3, y: 1)
static let level2 = Level(color: .black.opacity(0.10), radius: 12, y: 4)
}
enum Motion {
@@ -3,8 +3,9 @@ import Localization
import Metrics
import SwiftUI
/// 主窗口:侧边栏、标题栏与页面在同一张底色上,不分栏着色,也不画分隔线。
/// 左侧切换页面,右侧是仪表盘、各指标详情与工具页;窗口宽高都可调整
/// 主窗口。左侧切换页面,右侧是仪表盘、各指标详情与工具页;窗口宽高都可调整。
/// macOS 26 起侧边栏是一块浮在窗口里的液态玻璃面板,顶栏控件是玻璃胶囊;
/// 更早的系统上侧边栏、标题栏与页面在同一张底色上,不分栏着色,也不画分隔线
public struct MainWindowView: View {
@Environment(AppModel.self) private var model
@Environment(\.isSnapshot) private var isSnapshot
@@ -19,55 +20,93 @@ public struct MainWindowView: View {
.background(alignment: .top) {
WindowDragArea().frame(height: DS.Size.windowHeader)
}
.modifier(SidebarPanel())
VStack(spacing: 0) {
// 页面的滚动视图会自动向上延伸到标题栏下面,与顶栏重叠;顶栏必须在它之上,否则开关收不到点击
PageHeader()
.zIndex(1)
Group {
switch model.settings.panelTab {
case .overview: OverviewPage()
case .system: SystemInfoPage()
case .history: HistoryPage()
case .cpu: DetailPage { CPUPopover() }
case .gpu: DetailPage { GPUPopover() }
case .memory: DetailPage { MemoryPopover() }
case .disk: DiskPage()
case .network:
DetailPage {
NetworkPopover()
NetworkSettings()
}
case .thermal: ThermalPage()
case .battery: DetailPage { BatteryPopover() }
case .processes: ProcessesPage()
case .keepAwake: KeepAwakePage()
case .cleaner: CleanerPage()
case .uninstaller: UninstallerPage()
case .startupItems: StartupItemsPage()
case .settingsGeneral: SettingsTabPage { GeneralSettings() }
case .settingsMenuBar: SettingsTabPage { MenuBarSettings() }
case .settingsNotifications: SettingsTabPage { NotificationSettings() }
case .settingsAccount: SettingsTabPage { AccountSettings() }
case .settingsHelper: SettingsTabPage { HelperSettings() }
case .settingsAbout: SettingsTabPage { AboutSettings() }
}
}
.frame(maxWidth: .infinity, maxHeight: isSnapshot ? nil : .infinity, alignment: .top)
// 截图时页面取自身高度:侧边栏更高时,多出的高度不能分给页面里可伸展的卡片
.fixedSize(horizontal: false, vertical: isSnapshot)
}
.frame(minWidth: DS.Size.panelWidth, maxWidth: .infinity)
.frame(maxHeight: isSnapshot ? nil : .infinity, alignment: .top)
content
.frame(minWidth: DS.Size.panelWidth, maxWidth: .infinity)
.frame(maxHeight: isSnapshot ? nil : .infinity, alignment: .top)
}
.frame(width: isSnapshot ? DS.Size.sidebarWidth + DS.Size.panelWidth : nil)
.fixedSize(horizontal: false, vertical: isSnapshot)
.background(DS.Palette.background)
// 需要等待的任务进行中,整个窗口压暗并显示加载框
.loadingHUD(isSnapshot ? nil : busyMessage)
// 内容延伸到透明标题栏下方,由顶栏高度留出红绿灯按钮的位置
.ignoresSafeArea()
// 文案在各视图计算时翻译好,切换语言后整棵视图重建
.id(model.settings.language)
}
/// 顶栏在上、页面在下。没有用 macOS 26 的 safeAreaBar 让页面从顶栏下面滚过:
/// 它在 macOS 上会在交界处画一条分隔线,滚动边缘样式设成柔和或隐藏都去不掉
private var content: some View {
VStack(spacing: 0) {
// 页面的滚动视图会自动向上延伸到标题栏下面,与顶栏重叠;顶栏必须在它之上,否则开关收不到点击
PageHeader()
.zIndex(1)
page
}
}
/// 需要等待、期间不宜继续操作的任务
private var busyMessage: String? {
if model.cleaner.phase == .cleaning { return tr("正在清理…") }
if model.uninstaller.isRemoving { return tr("正在移除…") }
if model.diagnostics.phase == .collecting { return tr("正在导出诊断信息…") }
if model.maintenance.isApplyingDNS { return tr("正在修改 DNS…") }
return nil
}
private var page: some View {
Group {
switch model.settings.panelTab {
case .overview: OverviewPage()
case .system: SystemInfoPage()
case .history: HistoryPage()
case .cpu: DetailPage { CPUPopover() }
case .gpu: DetailPage { GPUPopover() }
case .memory: DetailPage { MemoryPopover() }
case .disk: DiskPage()
case .network:
DetailPage {
NetworkPopover()
NetworkSettings()
}
case .thermal: ThermalPage()
case .battery: DetailPage { BatteryPopover() }
case .processes: ProcessesPage()
case .keepAwake: KeepAwakePage()
case .cleaner: CleanerPage()
case .uninstaller: UninstallerPage()
case .startupItems: StartupItemsPage()
case .settingsGeneral: SettingsTabPage { GeneralSettings() }
case .settingsMenuBar: SettingsTabPage { MenuBarSettings() }
case .settingsNotifications: SettingsTabPage { NotificationSettings() }
case .settingsAccount: SettingsTabPage { AccountSettings() }
case .settingsHelper: SettingsTabPage { HelperSettings() }
case .settingsAbout: SettingsTabPage { AboutSettings() }
}
}
.frame(maxWidth: .infinity, maxHeight: isSnapshot ? nil : .infinity, alignment: .top)
// 截图时页面取自身高度:侧边栏更高时,多出的高度不能分给页面里可伸展的卡片
.fixedSize(horizontal: false, vertical: isSnapshot)
}
}
/// macOS 26:侧边栏浮在窗口里,四周留一圈边距,整块是液态玻璃,红绿灯按钮落在面板里
private struct SidebarPanel: ViewModifier {
@Environment(\.isSnapshot) private var isSnapshot
func body(content: Content) -> some View {
if DS.Glass.isAvailable, !isSnapshot {
content
.environment(\.isInsideGlass, true)
.dsGlass(in: RoundedRectangle(cornerRadius: DS.Radius.xl, style: .continuous))
.padding(DS.Space.s2)
} else {
content
}
}
}
/// 指标详情页:复用菜单栏弹窗的内容,显示全部区块
@@ -104,11 +143,13 @@ private struct MainSidebar: View {
}
.padding(.leading, DS.Space.s1)
.padding(.trailing, DS.Space.s3)
// 顶部留出窗口红绿灯按钮的位置
.padding(.top, DS.Size.windowHeader + DS.Space.s2)
// 顶部留出窗口红绿灯按钮的位置;玻璃面板本身离窗口顶边已有一圈边距
.padding(.top, DS.Size.windowHeader + (usesGlassPanel ? 0 : DS.Space.s2))
.padding(.bottom, DS.Space.s3)
}
private var usesGlassPanel: Bool { DS.Glass.isAvailable && !isSnapshot }
private var navigation: some View {
VStack(alignment: .leading, spacing: DS.Space.s1) {
group(tr("监控"), PanelTab.monitors)
@@ -171,15 +212,19 @@ private struct PageHeader: View {
// 每个监控页都能在这里开关自己的菜单栏项目;温度与风扇页有两项,各带一个小标签
let items = tab.menuBarItems
if !items.isEmpty {
Text(tr("在菜单栏显示")).dsFont(.xs).foregroundStyle(DS.Palette.textSecondary)
ForEach(items) { item in
if items.count > 1 {
Text(item.popoverTitle).dsFont(.xs).foregroundStyle(DS.Palette.textTertiary)
// macOS 26 上这一组是一颗玻璃胶囊,与访达工具栏的分组一致
HStack(spacing: DS.Space.s3) {
Text(tr("在菜单栏显示")).dsFont(.xs).foregroundStyle(DS.Palette.textSecondary)
ForEach(items) { item in
if items.count > 1 {
Text(item.popoverTitle).dsFont(.xs).foregroundStyle(DS.Palette.textTertiary)
}
DSToggle(isOn: Binding(get: { settings.isEnabled(item) }, set: { settings.setEnabled(item, $0) }),
label: tr("在菜单栏显示\(item.title)"))
.help(tr("开启后图标出现在菜单栏;按住 ⌘ 键拖动图标可以调整位置"))
}
DSToggle(isOn: Binding(get: { settings.isEnabled(item) }, set: { settings.setEnabled(item, $0) }),
label: tr("在菜单栏显示\(item.title)"))
.help(tr("开启后图标出现在菜单栏;按住 ⌘ 键拖动图标可以调整位置"))
}
.modifier(HeaderControlGroup())
}
}
.padding(.horizontal, DS.Space.s3 + DS.Space.s1)
@@ -187,6 +232,21 @@ private struct PageHeader: View {
}
}
/// 顶栏右侧的一组控件:macOS 26 上收进一颗玻璃胶囊,更早的系统直接排开
private struct HeaderControlGroup: ViewModifier {
func body(content: Content) -> some View {
if DS.Glass.isAvailable {
content
.padding(.leading, DS.Space.s4)
.padding(.trailing, DS.Space.s2)
.frame(height: DS.Size.controlHeight)
.dsGlass(in: Capsule())
} else {
content
}
}
}
/// 在浅色与深色之间切换;当前跟随系统时,切到与系统相反的那一种
private struct ThemeToggle: View {
@Environment(AppModel.self) private var model
@@ -187,8 +187,8 @@ struct MiniIconButton: View {
.font(.system(size: DS.TextSize.xs.rawValue, weight: .semibold))
.foregroundStyle(hovering ? DS.Palette.textPrimary : DS.Palette.textSecondary)
.frame(width: DS.Size.segmentHeight, height: DS.Size.segmentHeight)
.background(hovering ? DS.Palette.surfaceHover : .clear, in: RoundedRectangle(cornerRadius: DS.Radius.sm))
.contentShape(Rectangle())
.modifier(MiniIconSurface(hovering: hovering))
.contentShape(Circle())
}
.buttonStyle(.plain)
.onHover { hovering = $0 }
@@ -197,6 +197,19 @@ struct MiniIconButton: View {
}
}
/// macOS 26 上是一颗小玻璃圆钮,更早的系统只有悬停底色
private struct MiniIconSurface: ViewModifier {
let hovering: Bool
func body(content: Content) -> some View {
if DS.Glass.isAvailable {
content.dsGlass(in: Circle(), interactive: true)
} else {
content.background(hovering ? DS.Palette.surfaceHover : .clear, in: RoundedRectangle(cornerRadius: DS.Radius.sm))
}
}
}
/// 区块标题栏里的刷新按钮:查询进行中换成系统的转圈,查完自动变回图标
struct RefreshButton: View {
var loading: Bool