feat(renderer): support rainmeter tool modules - #3
Open
PIKACHUIM wants to merge 12 commits into
Open
Conversation
PIKACHUIM
marked this pull request as draft
July 15, 2026 06:52
新增功能: - 完整的 .rmskin 格式桌面小组件导入/浏览/筛选/应用/管理 - RmskinRenderer 渲染引擎 (C++/ObjC, 纯 Apple 框架) - 多进程架构, 每个主题独立进程互不干扰 - 浮动桌面窗口, 跨所有 Space - 会话持久化, 自动恢复已启用小组件 修复: - 修复 SMAppService 'Operation not permitted' 错误 添加状态检查避免重复注册, 失败时回退 UI 状态 - 修复 'No symbol named " found' 警告 为 FRShowOnly 中音频响应/可自定义提供有效 SF Symbol 名称 - 修复 NSHostingView Update Constraints 无限循环 窗口初始尺寸与 SwiftUI minSize 对齐, 设置 contentMinSize
- 禁用所有窗口的 isRestorable,修复 restoreWindowWithIdentifier 错误 - 移除 windowDidBecomeKey 中的 withAnimation 包裹, 避免在 display cycle 期间触发大量约束重新计算 - 重构 ContentView body 结构:将 VStack 保持常驻, 仅切换内部子视图,避免 HSplitView 子视图整体插入/移除 - 移除 .animation(.spring()) 修饰符,改用 .animation(.default) spring 动画在约束更新期间会反复触发布局 pass - 将 default 分支从 fatalError() 改为 EmptyView() - NSHostingView 使用 translatesAutoresizingMaskIntoConstraints=false 让其自行管理与窗口的约束关系
根本原因: NSHostingView 直接作为 window.contentView 时, 其 intrinsicContentSize 变化会通过 Auto Layout 约束系统与 窗口的 display cycle 产生循环 (hitTest → graphDidChange → flushTransactions → setNeedsUpdateConstraints → 循环) 修复方案: - 引入中间容器 NSView, NSHostingView 作为子视图通过 autoresizingMask 填充, 不参与 Auto Layout 约束协商 - 移除 ContentView 的 .frame(minWidth:minHeight:idealHeight:) 窗口最小尺寸完全由 NSWindow.contentMinSize 控制, 避免 NSHostingView 通过 intrinsicContentSize 注入多余约束 - 在 applicationWillFinishLaunching 设置 NSQuitAlwaysKeepsWindows=false 禁用窗口状态恢复
根本原因: SwiftUI 的 NSHostingView 在 hitTest 期间会 flush graph transactions, 触发 setNeedsUpdateConstraints, 此时窗口正处于 display cycle 中, 再次请求约束更新导致无限循环。 容器隔离方案无法阻止 _informContainerThatSubviewsNeedUpdateConstraints 沿视图树向上传播到窗口。 修复: 创建 StableHostingView<Content> 子类: - 覆盖 updateConstraints() 添加重入保护 (isUpdatingConstraints flag) - 覆盖 needsUpdateConstraints setter, 在约束更新期间抑制新的请求 - 这样当 hitTest → graphDidChange → requestUpdate → setNeedsUpdate 再次触发时, 会被重入保护截断, 不再向上传播到窗口
根因分析: 这个问题在 80926f5 之前就潜在存在,原始代码中: - 窗口初始 480x300 但 SwiftUI 要求 minWidth:1000 minHeight:640 - windowDidBecomeKey 用 withAnimation 切换 isStaging - .animation(.spring()) 在布局 pass 中产生额外约束变更 80926f5 引入 RmskinViewModel 后问题被放大: - RmskinViewModel.init() 同步调用 refresh() - refresh() 异步加载后立即更新 @published 属性 - 在窗口首次 display cycle 期间产生额外 graph transactions - 增加了 hitTest 时 flushTransactions 触发约束循环的概率 修复方案 (三层防护): 1. StableHostingView: 强制 translatesAutoresizingMaskIntoConstraints=true 覆盖 intrinsicContentSize 返回 noIntrinsicMetric,彻底切断 NSHostingView 通过 Auto Layout 向窗口传播约束更新的路径 2. 容器隔离: 使用普通 NSView 作为 contentView,NSHostingView 作为子视图 通过 autoresizingMask 填充,即使 setNeedsUpdateConstraints 被调用 也不会到达窗口的约束系统 3. RmskinViewModel: 延迟首次 refresh() 到下一个 runloop 周期 避免在窗口初始化期间触发 @published 更新
堆栈分析 frame 14-16 明确显示: NSHostingView.updateConstraints() 内部通过 defer 闭包 调用子视图 updateConstraints → _prepareForTwoPassConstraints → setNeedsUpdateConstraints → 向上传播到窗口 → 循环 之前的方案(覆盖 needsUpdateConstraints/intrinsicContentSize) 无法阻止这一路径,因为循环发生在 super.updateConstraints() 内部。 新方案: 完全跳过 NSHostingView 的 updateConstraints 实现, 通过 ObjC runtime 直接调用 NSView 基类的版本。 NSHostingView 的渲染并不依赖 Auto Layout 约束, 它通过自身的 layout()/draw() 正确渲染 SwiftUI 内容。
关键发现: 堆栈 frame 15-16 是 SwiftUI 内部 NSHostingView 的 updateConstraints, frame 7 是 _NSConstraintBasedLayoutHostingView。这说明触发循环的 不是根 hosting view, 而是 SwiftUI 为 TextField/.background/Divider 等桥接 AppKit 的子视图内部创建的 NSHostingView。 因此子类化根视图 (之前的 StableHostingView 方案) 完全无效 —— 拦截不到 SwiftUI 内部动态创建的 hosting views。 正确方案: NSHostingView 默认根据 SwiftUI 内容的 intrinsicContentSize/minSize 在每次 graph change 时更新 Auto Layout 约束。macOS 13+ 可通过 sizingOptions=[] 完全禁用这一行为, 改由 autoresizingMask 驱动尺寸。 这从源头切断了 updateConstraints → setNeedsUpdateConstraints 循环。 - 移除无效的 StableHostingView 子类及 ObjC runtime hack - 主窗口和设置窗口的 NSHostingView 均设置 sizingOptions=[] + autoresizingMask 填充
回溯 commit 结论: - 80926f5 (pikachu 引入 rmskin) 之前: ContentView 观察 3 个 ViewModel, TopTabBar 3 个标签, 正常工作 - 80926f5 之后: ContentView 新增第 4 个 @ObservedObject rmskinViewModel, AppDelegate 立即创建 RmskinViewModel()。始终渲染的 ContentView SwiftUI 依赖图变复杂/更易抖动, 配合 HSplitView + NSHostingView 直接 作为 window.contentView 的已知缺陷, 在初始 display cycle 触发 updateConstraints <-> setNeedsUpdateConstraints 无限循环 (窗口高度 668<->669 1px 振荡为其典型特征) 修复: NSHostingView 直接作为 contentView 时, 其 setNeedsUpdateConstraints 会直接进入窗口 display-cycle 约束 pass 形成循环。改用 NSHostingController 作为 contentViewController, 由 AppKit 托管 hosting view 的约束集成, 走独立布局路径规避循环。 主窗口与设置窗口均改用此方式, sizingOptions=[] 禁止按内容尺寸 反向调整窗口。同时移除之前所有无效的 NSHostingView hack。
laobamac
self-requested a review
July 15, 2026 14:11
PIKACHUIM
marked this pull request as ready for review
July 19, 2026 05:11
Collaborator
Author
|
@copilot resolve the merge conflicts in this pull request |
# Conflicts: # .github/workflows/build-macos.yml # Mirage/Mirage Wallpaper/AppDelegate.swift # Mirage/Mirage Wallpaper/ContentView/Components/TopTabBar.swift # Mirage/Mirage Wallpaper/ContentView/ContentView.swift # Mirage/Mirage Wallpaper/ContentView/MainWindow.swift # Mirage/Mirage Wallpaper/Services/GlobalSettingsService.swift # Mirage/scripts/bundle_renderers.sh # README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR: 新增雨滴(Rainmeter)桌面小组件支持
概述
为 Mirage Wallpaper 新增对 Rainmeter
.rmskin格式桌面小组件的完整支持。用户可以导入、浏览、筛选、应用和管理 Rainmeter 皮肤包,将小组件渲染为 macOS 桌面上可拖动的浮动窗口。新增功能
用户面向
~/Library/Application Support/Mirage/Rmskins/)square.grid.2x2.fill渲染能力
.ini:Section、Measure、Meter、MeterStyle、变量、Bang 命令#VAR#变量替换、[MeasureName]动态值、嵌套括号、条件表达式Rainmeter.ini(Active、WindowX/WindowY、Anchor)、坐标百分比 / 像素#WORKAREAWIDTH#等无法解析的 Rainmeter 内置变量,自动使用右上角级联布局文件变更清单
新增文件
Swift 端(Mirage 主应用)
Services/RmskinTheme.swiftRMSKIN.ini,提取元数据和预览图Services/RmskinLibrary.swift.rmskin归档Services/RmskinController.swiftRmskinWallpaper子进程,通过 stdin JSON 控制ContentView/ViewModels/RmskinViewModel.swiftContentView/Components/Widget/RmskinItem.swiftContentView/Components/Widget/WidgetExplorer.swiftLazyVGrid+ 拖放导入)ContentView/Components/Widget/WidgetFilterSidebar.swiftContentView/Components/Widget/WidgetPreview.swiftC++/ObjC 端(RmskinRenderer 渲染引擎)
整个
RmskinRenderer/目录,使用纯 Apple 框架,零第三方依赖:RMSkin.h/mmRMLayout.h/mmRMSKIN.ini+Rainmeter.ini,标记未解析公式RMConfigParser.h/mm.ini解析,构建 Measure/Meter 对象树RMMeasure.h/mmRMMeter.h/mmRMMathParser.h/mm#VAR#、[Measure]、嵌套表达式)RMIniFile.h/mmRMFontManager.h/mmRMBangs.h/mmRMSkinView.h/mmisFlipped=YES),定时器驱动 tick + 重绘,拖拽/点击/滚轮转发RmskinWallpaper.mmControlChannel.h/mRmskinViewer.mmCMakeLists.txt(x3)scripts/build.sh修改文件
AppDelegate.swiftrmskinViewModel实例;applicationWillTerminate中调用stopAll()ContentView/ContentView.swiftcase 3标签页路由(Widget 视图)ContentView/Components/TopTabBar.swiftMirage/scripts/bundle_renderers.shRmskinWallpaper打包到Resources/Renderers/.github/workflows/build-macos.ymlscripts/build_all.shREADME.mdBug 修复(随本 PR 一同提交)
1. SMAppService "Operation not permitted"
文件:
Services/GlobalSettingsService.swift原因:
didAddToLoginItem无条件调用register()/unregister(),当已处于目标状态时触发权限错误。修复: 添加
appService.status检查,仅在状态不一致时操作;失败时回退 UI 设置。2. "No symbol named '' found in system symbol set"
文件:
ContentView/ViewModels/FilterResultsViewModel.swift原因:
FRShowOnly.allOptions中"音频响应"和"可自定义"的 SF Symbol 名称为空字符串。修复: 替换为有效符号
"waveform.path.ecg"和"slider.horizontal.3"。3. NSHostingView Update Constraints 无限循环
文件:
ContentView/MainWindow.swift原因: 窗口初始尺寸 (480×300) 小于 SwiftUI 声明的
minWidth: 1000, minHeight: 640,导致约束循环。修复:
1029×669contentMinSize = NSSize(width: 1000, height: 640)autoresizingMask替代 Auto Layout 约束架构设计
关键设计决策
kCGNormalWindowLevel-1,位于普通应用之下、桌面之上;canJoinAllSpaces+stationary跨所有 SpaceisFlipped=YES+ 坐标转换处理#WORKAREAWIDTH#等无法解析的公式,使用右上角级联布局降级appliedThemeIDs持久化到UserDefaults,启动时自动恢复,退出时不丢失RendererController模式一致小组件生命周期
构建与测试
构建 RmskinRenderer
调试单个主题
RmskinRenderer/build/release/Tools/RmskinViewer/RmskinViewer "Quanto_1.31 Release.rmskin"打包到 App Bundle
测试要点
.rmskin文件或目录 → 验证自动解压和卡片展示#WORKAREAWIDTH#公式的主题 → 验证右上角级联布局