开源机械手硬件设计:7大模型完整指南与自适应抓取实践
【免费下载链接】Lunar
Intelligent adaptive brightness for your external monitors
Lunar 是 macOS 上为外接显示器提供智能亮度调节的开源应用,其菜单栏快捷操作面板(Quick Actions)是用户日常使用最频繁的入口之一。版本 5.7.8 是一次聚焦的修复型发布,官方发布说明(ReleaseNotes/5.7.8.md)仅包含两条修复项:修复 Options 菜单溢出屏幕、修复 Options 菜单强制使用浅色模式。本文以这两条修复为主线,结合仓库中 QuickActionsMenuView.swift、StatusItemButtonController.swift、OSDWindow.swift 等核心实现,深入解读菜单布局与外观体系的工作原理,帮助开发者理解这类菜单栏弹层类 bug 的根因与修复思路。
一、背景:Options 菜单是什么
在 Lunar 中,点击菜单栏图标的弹出面板(由 QuickActionsMenuView.swift 承载)是主菜单,而 Options 菜单则是主菜单右侧/左侧并排出现的一个辅助面板,用于承载 Layout、Advanced、HDR 三个标签页以及底部的「Reset 设置」按钮。它的显隐由一个布尔型配置项 showOptionsMenu 控制(默认关闭,见 DataStore.swift 同文件第 95 行附近对 showOptionsMenu、keepOptionsMenu 的预加载声明),用户通过主菜单顶部的齿轮按钮「Settings」切换:
SwiftUI.Button(
action: { showOptionsMenu.toggle() },
label: {
HStack(spacing: 2) {
Image(systemName: "gear.circle.fill")
Text("Settings")
}
}
)
对应的 Options 菜单本体(QuickActionsMenuView.swift)是一个固定宽度为 OPTIONS_MENU_WIDTH(390pt)的面板,内部根据 env.optionsTab 在三个子视图间切换。整个菜单窗口是一个自绘的无边框 NSPanel(PanelWindow,定义于 OSDWindow.swift),通过 AppDelegate.swift 中的 initMenuWindow() 用 NSHostingController(rootView: QuickActionsView()) 创建。
理解了菜单的结构,下面分别剖析 5.7.8 修复的两个问题。
二、修复一:Options 菜单溢出屏幕
问题表现与根因
Options 菜单展开后,主菜单与 Options 菜单是一段并排布局。主菜单宽度为 MENU_WIDTH(320pt),Options 菜单宽度为 OPTIONS_MENU_WIDTH(390pt),两者之间还需要水平内边距 MENU_HORIZONTAL_PADDING(24pt)。这些宽度常量定义在 QuickActionsViewController.swift:
let MENU_WIDTH: CGFloat = 320
let OPTIONS_MENU_WIDTH: CGFloat = 390
let FULL_OPTIONS_MENU_WIDTH: CGFloat = 412
let MENU_HORIZONTAL_PADDING: CGFloat = 24
let MENU_VERTICAL_PADDING: CGFloat = 40
let FULL_MENU_WIDTH: CGFloat = MENU_WIDTH + (MENU_HORIZONTAL_PADDING * 2)
也就是说,展开 Options 后整个面板总宽约 MENU_WIDTH + FULL_OPTIONS_MENU_WIDTH = 732pt(FULL_OPTIONS_MENU_WIDTH = OPTIONS_MENU_WIDTH + 两侧内边距)。如果菜单栏图标位于屏幕右缘附近(这是菜单栏图标最常见的默认位置),732pt 的宽度很容易超出屏幕可见区域(visibleFrame),导致菜单被切出屏幕、用户无法点击到部分内容——这就是 5.7.8 之前「Options 菜单溢出屏幕」的直接表现。
修复后的实现:动态溢出判断
从当前源码结构看,修复后的核心是新增了 isOptionsMenuOverflowing() 判断函数(QuickActionsMenuView.swift):
func isOptionsMenuOverflowing() -> Bool {
guard let screen = NSScreen.main else { return false }
return menuBarIcon.storedPosition.x + MENU_WIDTH + MENU_WIDTH / 2 + FULL_OPTIONS_MENU_WIDTH >= screen.visibleFrame.maxX
}
该函数把「菜单栏图标的横坐标 + 主菜单宽度 + 半宽余量 + Options 菜单总宽」与屏幕可见区域右边界 visibleFrame.maxX 比较,一旦即将越界即判定为溢出。在视图 body 中(QuickActionsMenuView.swift)据此做了布局方向翻转:
let optionsMenuOverflow = showOptionsMenu ? isOptionsMenuOverflowing() : false
HStack(alignment: .top, spacing: 1) {
if optionsMenuOverflow {
optionsMenu.padding(.leading, 20) // 溢出时:Options 放在主菜单左侧
.matchedGeometryEffect(id: "options-menu", in: namespace)
}
// …… 主菜单内容 ……
if showOptionsMenu, !optionsMenuOverflow {
optionsMenu.padding(.trailing, 20) // 正常时:Options 放在主菜单右侧
.matchedGeometryEffect(id: "options-menu", in: namespace)
}
}
也就是说,修复后的策略是:菜单栏图标靠左时 Options 从右侧展开;图标靠近屏幕右缘、判断为溢出时,Options 自动改从左侧展开,从而始终保证面板完整落在屏幕可见区域内。matchedGeometryEffect 让两种状态下菜单出现位置变化时带有平滑的几何过渡动画。
与窗口定位的联动
溢出判断不只用于 SwiftUI 层布局,还同步作用于菜单窗口的整体定位。在 StatusItemButtonController.swift 的 repositionWindow() 中,同样加入了基于 FULL_OPTIONS_MENU_WIDTH 的边缘感知:
if Defaults[.showOptionsMenu] {
if position.x + appd.env.menuWidth + appd.env.menuWidth / 2 + FULL_OPTIONS_MENU_WIDTH >= screen.visibleFrame.maxX {
menuWindow.show(at: position.applying(.init(translationX: -FULL_OPTIONS_MENU_WIDTH / 2, y: 1)), animate: animate)
} else {
menuWindow.show(at: position.applying(.init(translationX: FULL_OPTIONS_MENU_WIDTH / 2, y: 1)), animate: animate)
}
} else {
menuWindow.show(at: position, animate: animate)
}
可见这套修复是「布局层 + 窗口定位层」双管齐下:SwiftUI 层决定 Options 面板放在 HStack 的哪一侧,NSPanel 层(窗口 show(at:) 实现在 OSDWindow.swift)决定整个弹窗在屏幕坐标上的落点,二者共用同一套 screen.visibleFrame.maxX 边界判断,保证菜单无论在何种屏幕分辨率与菜单栏图标位置下都不越界。这也解释了为何 5.7.8 同时涉及 QuickActionsMenuView 与 StatusItemButtonController 两处改动。
三、修复二:Options 菜单强制浅色模式
问题表现
「强制浅色模式」指:无论系统当前处于深色还是浅色外观,Options 菜单面板始终以浅色配色渲染,在深色系统外观下显得突兀刺眼,与主菜单的毛玻璃深色背景不协调。
根源:硬编码的浅色配色
从当前(修复后)的 QuickActionsMenuView.swift 看,Options 菜单背景色已经改为跟随系统外观动态选择:
.background(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(colorScheme == .dark ? Color.peach : Color.sunYellow)
.shadow(color: Color.blackMauve.opacity(colorScheme == .dark ? 0.5 : 0.2), radius: 8, x: 0, y: 6)
)
.padding(.bottom, 20)
.foregroundColor(Color.blackMauve)
视图通过 @Environment(\.colorScheme)(QuickActionsMenuView.swift)感知系统深浅色,深色外观下使用 Color.peach、浅色外观下使用 Color.sunYellow。可以推断,5.7.8 之前的版本中该背景很可能是固定写死为 sunYellow 等浅色常量、且未接入 colorScheme 环境值,导致无论系统深浅色都渲染成浅色——这正是「强制浅色」问题的来源。
修复后的外观跟随体系
修复后,Options 菜单的颜色切换融入 Lunar 全局的外观机制。Lunar 在 DataStore.swift 中定义了外观配置项:
static let colorScheme = Key<ColorScheme>("colorScheme", default: .system)
支持 .system(跟随系统)、深色、浅色三种模式,并通过 colorSchemePublisher 发布变更(DataStore.swift)。AppDelegate 订阅该发布者(AppDelegate.swift),在 colorScheme 变化时以 0.5 秒去抖后重建设置窗口:
colorSchemePublisher
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.sink { [self] _ in
recreateWindow(page: Page.settings.rawValue)
}.store(in: &observers)
对于其他 NSPopover 弹层(Help、Settings、Colors、DDC、Reset),AppDelegate 的 initPopovers()(AppDelegate.swift)会按 darkMode 状态选择 NSAppearance(named: .vibrantDark) 或 .vibrantLight 作为外观。Options 菜单作为 SwiftUI 承载的 PanelWindow,则依赖 @Environment(\.colorScheme) 这一 SwiftUI 原生机制在 PanelWindow 根视图内自动传播外观状态,从而与主菜单(其背景 bg(optionsMenuOverflow:) 同样按 colorScheme 深浅分别取不同透明度的黑白叠加,见 QuickActionsMenuView.swift)保持视觉一致。
需要说明的是:darkMode 本身在 Lunar 中还与系统级外观读写相关联(GammaControl.swift 通过 SLSGetAppearanceThemeLegacy / SLSSetAppearanceThemeLegacy 实现),Night Shift 模块也定义了自己的 SystemAppearance(light / dark / autoLight / autoDark,见 GammaControl.swift),由此可见外观深浅在 Lunar 中是一个贯穿窗口、弹层与系统联动机制的完整体系,Options 菜单修复只是让 SwiftUI 菜单面板接入其中一环。
四、与修复相关的配置键与行为细节
两条修复背后还涉及几个需要开发者留意的配置行为:
showOptionsMenu(是否展开 Options 菜单):主菜单设置按钮的toggle目标;同时,当用户关闭菜单栏(menuBarClosed置为 true)时,若未开启keepOptionsMenu,Lunar 会自动收起 Options 菜单(StatusItemButtonController.swift):
Defaults[.menuBarClosed] = true
if Defaults[.showOptionsMenu], !Defaults[.keepOptionsMenu] {
Defaults[.showOptionsMenu] = false
}
-
keepOptionsMenu(保持 Options 展开状态):决定关闭菜单栏时是否记住并保持 Options 的展开状态。 -
FULL_OPTIONS_MENU_WIDTH = 412:这是溢出判定中使用的关键常量(Options 面板 390pt + 两侧内边距),后续若调整 Options 面板内部宽度(如新增标签页内容),需要同步核对这两处边缘判定逻辑(QuickActionsMenuView.isOptionsMenuOverflowing()与StatusItemButtonController.repositionWindow()),否则溢出修复可能失效。 -
布局联动:
QuickActionsMenuView.body中,showOptionsMenu变化时会调用setMenuWidth()(QuickActionsMenuView.swift)并触发appDelegate?.statusItemButtonController?.repositionWindow(),即展开/收起 Options 时窗口宽度与位置会同步重算,这也是溢出判断能实时生效的调用链:showOptionsMenu 变化 → setMenuWidth 更新 env.menuWidth → repositionWindow 按屏幕边缘重新定位。
五、升级与验证建议
- 升级路径:5.7.8 属于 5.7.x 系列内的修复发布,建议菜单栏图标位于屏幕右侧、或使用小尺寸屏幕(如 13 寸笔记本外接显示器场景)的用户优先升级,以消除 Options 菜单不可见的问题。
- 验证方法:将菜单栏图标拖到屏幕最右侧后展开 Options,确认面板完整可见;随后在系统设置中切换「深色/浅色外观」,确认 Options 菜单背景随外观联动,且与主菜单风格一致。
- 回归检查:调整「Keep options menu」与「Launch at login」等布局相关开关(对应
keepOptionsMenu、startAtLogin配置),确认 Options 展开/收起过程无窗口跳动或越界,相关逻辑可对照 QuickActionsMenuView.swift 与 StatusItemButtonController.swift 检查。
结语
Lunar 5.7.8 以两条修复覆盖了菜单栏弹层最常见的两类问题——布局越界与外观不跟随。从源码看,前者通过「isOptionsMenuOverflowing() 溢出判定 + 左右两侧切换摆放 + 窗口定位联动」解决,后者通过「@Environment(\.colorScheme) 动态取色 + 接入全局 colorScheme 发布订阅体系」解决。对于从事 macOS 菜单栏工具开发的技术人员,这两条修复的完整调用链(SwiftUI 布局层 ↔ NSPanel 窗口层 ↔ Defaults 配置层)是处理同类弹层兼容性问题的可复用范本。
【免费下载链接】Lunar
Intelligent adaptive brightness for your external monitors
更多推荐


所有评论(0)