From 0bd934cf4d43b0687da5dd47f4a18a867cf559d7 Mon Sep 17 00:00:00 2001 From: wangzhizhou <824219521@qq.com> Date: Sat, 7 Nov 2026 22:19:38 +0800 Subject: [PATCH 1/2] docs: record the 1.2.0 practice candidate install and its machine checks The package carries the practice window and the quality close-out. Post install measurements show a steady state p95 of 5.8 milliseconds across the three segments, cold first key at 28.9 and 33.4, no connections, and private memory of 28.5 to 28.8 mebibytes under a limit of 30. The report lists the manual walkthrough items: the practice window's two exercise shapes, reopening without leftovers, the learning area default state, and the progress file appearing, growing and resetting on deletion. Signed-off-by: wangzhizhou <824219521@qq.com> --- HANDOFF.md | 3 +- docs/reviews/259-1.2.0-b4b5-candidate.md | 46 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 docs/reviews/259-1.2.0-b4b5-candidate.md diff --git a/HANDOFF.md b/HANDOFF.md index c150bf8..7803c22 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -11,7 +11,7 @@ | 设备 | 已装包与执行 SHA | 数据 / 输入源 | 恢复点 | |---|---|---|---| -| **air**(arm64 / macOS 27.2 / Xcode 27,用户 `joker`;pi 跑在 Background 会话,驱动不了 GUI 点击) | **1.2.0 / build 39**(候选,2026-11-07 装机),宿主执行 `b152a001…`;输入源 `Lampyris.default` 在用;回滚点 `updates/install-backup-20261107T063934Z/` | config `5d4271fb…`(`full_width=half` 已是 ADR-305 枚举形态)、词库 1139 行(用户在用,行数会变) | `uninstalled/`、`updates/` 可回滚到 1.0.0…1.0.2 | +| **air**(arm64 / macOS 27.2 / Xcode 27,用户 `joker`;pi 跑在 Background 会话,驱动不了 GUI 点击) | **1.2.0 / build 39(B4+B5 候选,2026-11-07 装机)**,宿主执行 `28e24d26…`;输入源 `Lampyris.default` 在用;`show_wubi_learning=false` | config `5d4271fb…`(`full_width=half` 已是 ADR-305 枚举形态)、词库 1139 行(用户在用,行数会变) | `uninstalled/`、`updates/` 可回滚到 1.0.0…1.0.2 | | **pro**(Intel / macOS 13.7.8,用户 `bot`,SSH 别名 `pro`) | **1.1.0 / build 38**,宿主执行 `d3c5a2ef…`(2026-11-06 经更新程序升级,旧版备份在 `updates/upgrade-2026-11-06T14-27-15Z/backup/`) | config 与词库保持 1.0.0 基线(`show_wubi_learning=true` 是用户有意设置) | 升级前有 `uninstalled/bundle.*` | ## 2. 本轮完成 @@ -124,6 +124,7 @@ make macos-offline-tests ## 9. 会话日志(最近) +- 2026-11-06 / pi(air):**1.2.0(B4+B5)候选装机(报告 259)**。PR #232 → CI 包(`exec 28e24d26…`)装 air;稳态 p95 **5.80 ms**(三段 5.12–5.97)、冷首键 28.86/33.39、0 外联、memory-gate 私有 **28.5–28.8 MiB**(限 30)PASS。待用户走查:⌥K 练习窗口(A/C 两种题型)、⌥L 学习区默认关、practice.tsv 生成与重置。 - 2026-11-06 / pi(air):**1.2.0/build 39 装机 + 首轮体验数据(报告 254)**。PR #227 合并 → CI 验收包 → 装 air:版本/执行 SHA `b152a001…`/公证全部核对,config 与 1225 行词库未变。评测 ALL 命中 98.2%/首选 86.4%(jianma2 100%/100%);三段延迟稳态 p95 6.7–8.3 ms;**冷首键 27–40 ms,比 1.1.0 的 16–26 ms 高**(待查是否学习区布局所致);soak 3 分钟 17 轮 0 外联、footprint 38.5 MiB(比 1.1.0 高约 5 MiB,待单独归因)。 - 2026-11-06 / pi(air):**B3 学习区实现(报告 253)**。`WubiLearningView` 改两行布局(完整码+简码 / 字根格+识别码两行),候选窗零改动;新增 `LearningHints`(读 `B0-runtime.tsv`,缺失退回 D14);词组按输入码位置映射到字(KWWL → 中K 华W 人W 国L)。`wubi-learning.swift` 扩到可注入资源并生成亮/暗实拍;离线回归全绿。 - 2026-11-06 / pi(air):**B2 数据地基(报告 252 / ADR-311)**。B0 扩到 3,000 字并预渲染成 `B0-runtime.tsv`(287,310 B ≈ 96 B/字,预算 512 KiB 反而有余量;B0.json 同规模是 1.62 MiB),登记 derived `b0-runtime`,新增 `test-b0-runtime.py` 守体积/格式/无 PUA/消费契约。ADR-311 定装载方式(bundle 内只读文件、宿主查表、Rust 不参与)。 diff --git a/docs/reviews/259-1.2.0-b4b5-candidate.md b/docs/reviews/259-1.2.0-b4b5-candidate.md new file mode 100644 index 0000000..b667f9c --- /dev/null +++ b/docs/reviews/259-1.2.0-b4b5-candidate.md @@ -0,0 +1,46 @@ +# 259 — 1.2.0(B4+B5)候选装机与机器验收数据 + +- 日期:2026-11-06 | 授权:用户「装」 | 环境:air(arm64 / macOS 27.2) +- 候选:main `99e9b15…`(PR #232)→ CI 验收包 + +## 1. 包身份 + +| 项 | 值 | +|---|---| +| ZIP SHA | `4039cdfcabd03e55…` | +| 宿主执行 SHA | `28e24d26519f83a4…`(装前包内 = 装后已装) | +| 版本 / 构建 | 1.2.0 / 39 | +| 签名 | Notarized Developer ID | +| 回滚点 | `updates/install-backup-*`(上一候选) | +| config | 未变(`show_wubi_learning=false` 默认;`hotkey.open_practice` 尚无显式行 → 用 Schema 默认 `alt+k`) | + +## 2. 机器验收(装上后实测) + +| 项 | 结果 | +|---|---| +| 稳态 p95(三段) | **5.80 ms**(basic 5.97 / long-code 5.26 / paging 5.12)PASS | +| 冷首键 | p50 28.86 / p95 33.39(与 B5 结论一致) | +| 外联 | **0** 连接 | +| 内存(memory-gate 3 轮) | 私有已提交 **28.5 / 28.6 / 28.7–28.8** MiB,限 30 → **PASS** | +| footprint(perf 单点) | 32.2 MiB(参考值) | + +## 3. 待用户人工走查(本轮重点) + +1. **⌥K 打开练习窗口**: + - 题型 A(看字打码):随便打几个,确认对错反馈与"显示答案"; + - 题型 C(识别码判断):选末笔 + 结构,确认判分与解释; + - 关窗后再按 ⌥K,确认能重新打开且不残留多个窗口。 +2. **⌥L 学习区**:默认关闭;在 TextEdit 里打开,确认两行式(完整码+简码 / 字根格+识别码)。 +3. **练习进度**:`~/Library/Application Support/Lampyris/practice.tsv` 应出现并随答题增长; + 「打开进度文件」按钮应定位到该文件;删掉文件即重置。 +4. **热键不冲突**:⌥K 在 TextEdit / Terminal 里都不应插入字符(Terminal 的 ⌥L 冲突是终端自身行为,⌥K 未见于其快捷键表)。 + +## 4. 已知限制(本轮不修) + +- 覆盖安装会**让 macOS 把输入源移出启用列表**(报告 255 §6),本次装后已确认 `current` 仍为萤火虫; + 若菜单里消失,按 `docs/故障恢复.md` §4.1 重新添加。 +- 练习窗口尚未做深色模式/无障碍专项走查(题面用系统颜色,未自定义)。 + +## 5. 关联 + +ADR-313/314、报告 257/258、`docs/design/l3-hint.md`。 From 69ba943bcc2f96ba10d701675f6ed5c2e747b680 Mon Sep 17 00:00:00 2001 From: wangzhizhou <824219521@qq.com> Date: Sat, 7 Nov 2026 22:29:42 +0800 Subject: [PATCH 2/2] fix: lay the practice window out from the top so nothing overlaps or clips The old layout computed positions upwards from the window bottom, so the question overlapped the segmented control and the progress line fell outside the window. The content view is now flipped and every row is placed from the top with a fixed 460 by 336 window, shared by both exercise shapes; the recognition rows gained their own captions that update with the selection. A geometry self-check asserts for both kinds that every visible control stays inside the content bounds and clear of the segmented control, so this cannot regress silently. Signed-off-by: wangzhizhou <824219521@qq.com> --- HANDOFF.md | 1 + docs/design/practice-mock.png | Bin 0 -> 60616 bytes docs/reviews/260-practice-ui-layout.md | 34 ++++ platforms/macos/Sources/PracticeWindow.swift | 193 ++++++++++++++----- platforms/macos/tests/wubi-learning.swift | 5 + 5 files changed, 183 insertions(+), 50 deletions(-) create mode 100644 docs/design/practice-mock.png create mode 100644 docs/reviews/260-practice-ui-layout.md diff --git a/HANDOFF.md b/HANDOFF.md index 7803c22..6a51925 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -61,6 +61,7 @@ - **B3 学习区已实现(报告 253)**:⌥L 学习区两行式(完整码+简码 / 字根格+识别码),候选窗零改动;词组按输入码对应到字;`LearningHints` 读 `B0-runtime.tsv`,缺失退回 D14。离屏实拍 `docs/design/learning-real-*.png`。**装机后的性能/内存实测仍待候选包**。 - **B3 草图(已成历史)**(`docs/design/l3-hint.md` + `learning-area-mock.png`):按用户裁定改为**候选窗零改动**,学习信息只放 ⌥L 学习区,学习区自己两行封顶(单字:字+码+简码 / 字根+识别码依据;词组:前 3 字 + 「逐字 ▸」)。`partial` 未知字根显示 `?`、标注「拆解待核验」;数据源优先 B0-runtime,未覆盖的字走 D14 兜底。**实现未开始,等评审三个细节**。 - **B5 已收口(ADR-314 / 报告 258)**:冷首键 ~28 ms(学习区关着也一样,用户不可感知)→ 不优化;A1/A2/A3/A5 全部关闭并写触发条件;内存按 ADR-312 双阈值(默认 30 / 学习区 34)。 +- **练习窗口版式已修正(报告 260)**:翻转坐标从顶部排、固定 460×336、题干居中、进度行不再被裁、A/C 共用纵向基准,并加了版式自检(控件不越界、不与题型控件重叠)。**待装包看真机**。 - **B4 练习已实现(ADR-313 / 报告 257)**:只看 `full` 出题,题型 A(看字打码)+ C(识别码判断);进度写 `~/Library/Application Support/Lampyris/practice.tsv`(有界 3,000 行 / 256 KiB、原子写、删除即重置);快捷键 `hotkey.open_practice` 默认 **⌥K**,入口在设置页快捷键列表;**尚未装包实测**。 - **B2 数据地基已出产物(报告 252 / ADR-311)**:`data/generated/B0-runtime.tsv`(3,000 字、287,310 B、96 B/字,预算 512 KiB 有余量),已登记 derived `b0-runtime` 并有体积/契约测试;**宿主装载与 L3 提示属 B3,尚未做**。 diff --git a/docs/design/practice-mock.png b/docs/design/practice-mock.png new file mode 100644 index 0000000000000000000000000000000000000000..f7e26f11a790bfab8779e91979a912df8f16e033 GIT binary patch literal 60616 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 literal 0 HcmV?d00001 diff --git a/docs/reviews/260-practice-ui-layout.md b/docs/reviews/260-practice-ui-layout.md new file mode 100644 index 0000000..25a8bbb --- /dev/null +++ b/docs/reviews/260-practice-ui-layout.md @@ -0,0 +1,34 @@ +# 260 — 练习窗口 UI 修正:不再压字、不越界、两类题型共用一套版式 + +- 日期:2026-11-06 | 来源:用户截图反馈「练习的 UI 界面需要优化调整」 +- 现象(截图):题干汉字**压在题型切换控件上**;中部大片空白;底部「已练 N 字」被窗口边缘裁掉。 + +## 1. 根因 + +旧版用手写 frame 且**从窗口底部往上算**(非翻转坐标),高度估算不准就会: +题干与顶部控件重叠、进度行落到窗口之外。题型 C 的两行按钮还额外占用纵向空间,更容易冲突。 + +## 2. 改法 + +- 内容容器改为**翻转坐标**(`isFlipped = true`),一律**从顶部往下**排: + 题型切换 → 题干 → 提示 → 作答区 → 反馈 → 按钮 → 进度; +- 固定窗口尺寸 **460 × 336**,各行显式坐标(A 与 C 共用同一套纵向基准): + 20 / 56 / 120 / 144–210 / 218 / 264 / 300; +- 题干 40pt **居中**;提示与反馈居中;输入框整宽; +- 题型 C 增加「末笔」「结构」两个小标题,选中后标题就地更新为「末笔 撇」; +- 底部按钮等距(120×28,间距 10),进度行固定在最下方、不再越界; +- 新增**版式自检**(`layoutProblemsForTesting`):A/C 两种题型下,所有可见控件必须落在内容区内, + 且不得与顶部题型控件重叠——离线回归里断言为空。 + +## 3. 视觉 + +`docs/design/practice-mock.png`(按新坐标绘制,A 题型示例)。 + +## 4. 验证 + +- `make verify` PASS;离线回归 **62 文件 / 328 例 PASS**(含新增版式自检)。 +- 真机确认待装包(下一次候选)。 + +## 5. 关联 + +ADR-313、报告 257/259。 diff --git a/platforms/macos/Sources/PracticeWindow.swift b/platforms/macos/Sources/PracticeWindow.swift index 0e6057f..be74de1 100644 --- a/platforms/macos/Sources/PracticeWindow.swift +++ b/platforms/macos/Sources/PracticeWindow.swift @@ -1,32 +1,50 @@ import AppKit /// 练习窗口(ADR-313):独立、非模态、可随时关闭;不碰候选窗,不常驻焦点。 +/// +/// 布局用**翻转坐标**从顶部往下排,避免手动从底部算高度导致文字出界(报告 260): +/// 题型切换 → 题干 → 提示 → 作答区 → 反馈 → 按钮 → 进度。 final class PracticeWindow: NSObject, NSWindowDelegate { static let shared = PracticeWindow() + static let contentSize = NSSize(width: 460, height: 336) private var window: NSWindow? + private var content: FlippedView? private let questionLabel = NSTextField(labelWithString: "") - private let detailLabel = NSTextField(labelWithString: "") - private let feedbackLabel = NSTextField(labelWithString: "") + private let promptLabel = NSTextField(labelWithString: "") + private let feedbackLabel = NSTextField(wrappingLabelWithString: "") private let progressLabel = NSTextField(labelWithString: "") private let answerField = NSTextField(string: "") private let kindControl = NSSegmentedControl(labels: PracticeKind.allCases.map(\.title), trackingMode: .selectOne, target: nil, action: nil) + private let strokeTitle = NSTextField(labelWithString: "末笔") + private let structureTitle = NSTextField(labelWithString: "结构") private let strokeButtons = Practice.strokes.map { NSButton(title: $0, target: nil, action: nil) } private let structureButtons = Practice.structures.map { NSButton(title: $0, target: nil, action: nil) } - private let progress = PracticeProgress(url: PracticeProgress.defaultURL()) + private var practiceProgress: PracticeProgress? private var bank: [PracticeQuestion] = [] private var current: PracticeQuestion? private var pendingStroke: String? private var pendingStructure: String? + private var lastAnswer: String? + + private var progress: PracticeProgress { + if let practiceProgress { return practiceProgress } + let created = PracticeProgress(url: PracticeProgress.defaultURL()) + practiceProgress = created + return created + } private var kind: PracticeKind { PracticeKind.allCases[max(0, min(PracticeKind.allCases.count - 1, kindControl.selectedSegment))] } + // MARK: 展示 + func show() { if window == nil { build() } kindControl.selectedSegment = 0 + applyKindVisibility() reload() window?.makeKeyAndOrderFront(nil) NSApp.activate(ignoringOtherApps: true) @@ -34,87 +52,101 @@ final class PracticeWindow: NSObject, NSWindowDelegate { } private func build() { - let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 420, height: 300), + let window = NSWindow(contentRect: NSRect(origin: .zero, size: Self.contentSize), styleMask: [.titled, .closable], backing: .buffered, defer: false) window.title = "萤火虫 · 练习" window.isReleasedWhenClosed = false window.delegate = self + window.contentView = buildContent() window.center() + self.window = window + } - let container = NSView() - var y: CGFloat = 250 - func place(_ view: NSView, height: CGFloat, width: CGFloat = 360) { - view.frame = NSRect(x: 30, y: y, width: width, height: height) - container.addSubview(view) - y -= height + 10 - } + private func buildContent() -> NSView { + let content = FlippedView(frame: NSRect(origin: .zero, size: Self.contentSize)) + let width = Self.contentSize.width + let margin: CGFloat = 24 + kindControl.frame = NSRect(x: margin, y: 20, width: width - margin * 2, height: 26) kindControl.target = self kindControl.action = #selector(kindChanged) - place(kindControl, height: 26) + content.addSubview(kindControl) - questionLabel.font = .systemFont(ofSize: 34, weight: .medium) - place(questionLabel, height: 44) + questionLabel.alignment = .center + questionLabel.font = .systemFont(ofSize: 40, weight: .medium) + questionLabel.frame = NSRect(x: margin, y: 56, width: width - margin * 2, height: 60) + content.addSubview(questionLabel) - detailLabel.font = .systemFont(ofSize: 12) - detailLabel.textColor = .secondaryLabelColor - place(detailLabel, height: 18) + promptLabel.alignment = .center + promptLabel.font = .systemFont(ofSize: 12) + promptLabel.textColor = .secondaryLabelColor + promptLabel.frame = NSRect(x: margin, y: 120, width: width - margin * 2, height: 18) + content.addSubview(promptLabel) + answerField.frame = NSRect(x: margin, y: 148, width: width - margin * 2, height: 26) answerField.placeholderString = "输入完整五笔码后回车" answerField.target = self answerField.action = #selector(submitCode) - place(answerField, height: 24) + answerField.font = .monospacedSystemFont(ofSize: 14, weight: .regular) + content.addSubview(answerField) + // 识别码:两行带标题的按钮组 + strokeTitle.font = .systemFont(ofSize: 11) + strokeTitle.textColor = .secondaryLabelColor + strokeTitle.frame = NSRect(x: margin, y: 148, width: 32, height: 24) + content.addSubview(strokeTitle) for (index, button) in strokeButtons.enumerated() { button.target = self button.action = #selector(pickStroke(_:)) button.tag = index button.bezelStyle = .rounded - button.frame = NSRect(x: 30 + CGFloat(index) * 70, y: y, width: 64, height: 26) - container.addSubview(button) + button.frame = NSRect(x: margin + 36 + CGFloat(index) * 74, y: 144, width: 68, height: 28) + content.addSubview(button) } - y -= 36 + structureTitle.font = .systemFont(ofSize: 11) + structureTitle.textColor = .secondaryLabelColor + structureTitle.frame = NSRect(x: margin, y: 186, width: 32, height: 24) + content.addSubview(structureTitle) for (index, button) in structureButtons.enumerated() { button.target = self button.action = #selector(pickStructure(_:)) button.tag = index button.bezelStyle = .rounded - button.frame = NSRect(x: 30 + CGFloat(index) * 100, y: y, width: 94, height: 26) - container.addSubview(button) + button.frame = NSRect(x: margin + 36 + CGFloat(index) * 108, y: 182, width: 100, height: 28) + content.addSubview(button) } - y -= 40 + feedbackLabel.alignment = .center feedbackLabel.font = .systemFont(ofSize: 12) - place(feedbackLabel, height: 18) - - progressLabel.font = .systemFont(ofSize: 11) - progressLabel.textColor = .secondaryLabelColor - place(progressLabel, height: 16) + feedbackLabel.frame = NSRect(x: margin, y: 218, width: width - margin * 2, height: 40) + content.addSubview(feedbackLabel) let next = NSButton(title: "下一题", target: self, action: #selector(nextQuestion)) - next.bezelStyle = .rounded - next.frame = NSRect(x: 30, y: 12, width: 90, height: 26) - container.addSubview(next) let reveal = NSButton(title: "显示答案", target: self, action: #selector(revealAnswer)) - reveal.bezelStyle = .rounded - reveal.frame = NSRect(x: 130, y: 12, width: 90, height: 26) - container.addSubview(reveal) let export = NSButton(title: "打开进度文件", target: self, action: #selector(openProgress)) - export.bezelStyle = .rounded - export.frame = NSRect(x: 230, y: 12, width: 110, height: 26) - container.addSubview(export) + for (index, button) in [next, reveal, export].enumerated() { + button.bezelStyle = .rounded + button.frame = NSRect(x: margin + CGFloat(index) * 130, y: 264, width: 120, height: 28) + content.addSubview(button) + } - window.contentView = container - self.window = window - applyKindVisibility() + progressLabel.font = .systemFont(ofSize: 11) + progressLabel.textColor = .secondaryLabelColor + progressLabel.frame = NSRect(x: margin, y: 300, width: width - margin * 2, height: 16) + content.addSubview(progressLabel) + return content } private func applyKindVisibility() { let isCode = kind == .code answerField.isHidden = !isCode + strokeTitle.isHidden = isCode + structureTitle.isHidden = isCode for button in strokeButtons + structureButtons { button.isHidden = isCode } } + // MARK: 交互 + @objc private func kindChanged() { applyKindVisibility() reload() @@ -129,49 +161,58 @@ final class PracticeWindow: NSObject, NSWindowDelegate { @objc private func nextQuestion() { guard !bank.isEmpty else { questionLabel.stringValue = "题库为空" - detailLabel.stringValue = "当前没有可出题的完整条目(需要 quality=full)" + promptLabel.stringValue = "当前没有可出题的完整条目(需要 quality=full)" return } current = bank.randomElement() + lastAnswer = nil answerField.stringValue = "" pendingStroke = nil pendingStructure = nil feedbackLabel.stringValue = "" guard let question = current else { return } questionLabel.stringValue = question.char - if kind == .code { - detailLabel.stringValue = "打出它的完整五笔码(\(question.code.count) 位)" - } else { - detailLabel.stringValue = "拆解:\(question.rootHint) · 选末笔与结构" - } + promptLabel.stringValue = kind == .code + ? "打出它的完整五笔码(\(question.code.count) 位)" + : "拆解:\(question.rootHint)" progressLabel.stringValue = progress.summary if kind == .code { window?.makeFirstResponder(answerField) } } @objc private func submitCode() { guard let question = current else { return } - let correct = Practice.judgeCode(answerField.stringValue, question: question) + let answer = answerField.stringValue + let correct = Practice.judgeCode(answer, question: question) + lastAnswer = answer finish(correct: correct, explanation: "完整码 \(question.code.uppercased()) · 字根 \(question.rootHint)") } @objc private func pickStroke(_ sender: NSButton) { pendingStroke = Practice.strokes[sender.tag] + updateSelectionTitle() evaluateRecognition() } @objc private func pickStructure(_ sender: NSButton) { pendingStructure = Practice.structures[sender.tag] + updateSelectionTitle() evaluateRecognition() } + private func updateSelectionTitle() { + if let stroke = pendingStroke { strokeTitle.stringValue = "末笔 \(stroke)" } + if let structure = pendingStructure { structureTitle.stringValue = "结构 \(structure)" } + } + private func evaluateRecognition() { guard let question = current, let stroke = pendingStroke, let structure = pendingStructure else { - feedbackLabel.stringValue = "选末笔与结构各一个" + feedbackLabel.stringValue = "选一个末笔和一个结构" + feedbackLabel.textColor = .secondaryLabelColor return } let correct = Practice.judgeRecognition(stroke: stroke, structure: structure, question: question) let letter = question.recognition?.letter ?? "—" - finish(correct: correct, explanation: "识别码 \(letter)(\(stroke)·\(structure))· 完整码 \(question.code.uppercased())") + finish(correct: correct, explanation: "识别码 \(letter)(\(stroke)·\(structure))") } private func finish(correct: Bool, explanation: String) { @@ -192,4 +233,56 @@ final class PracticeWindow: NSObject, NSWindowDelegate { progress.save() NSWorkspace.shared.activateFileViewerSelecting([progress.url]) } + + // MARK: 测试 + + /// 离屏渲染(测试用):建一个不显示的窗口承载视图(AppKit 控件需要 window 才能绘制)。 + func renderForTesting(kind: PracticeKind, progressURL: URL, light: Bool) -> NSBitmapImageRep? { + practiceProgress = PracticeProgress(url: progressURL) + let view = buildContent() + kindControl.selectedSegment = PracticeKind.allCases.firstIndex(of: kind) ?? 0 + applyKindVisibility() + bank = Practice.bank(kind: kind) + current = bank.first + if let question = current { + questionLabel.stringValue = question.char + promptLabel.stringValue = kind == .code + ? "打出它的完整五笔码(\(question.code.count) 位)" + : "拆解:\(question.rootHint)" + feedbackLabel.stringValue = kind == .code ? "✓ 正确 · 完整码 \(question.code.uppercased())" + : "选一个末笔和一个结构" + } + progressLabel.stringValue = progress.summary + let host = NSWindow(contentRect: NSRect(origin: .zero, size: Self.contentSize), + styleMask: [.titled], backing: .buffered, defer: false) + host.contentView = view + if let appearance = NSAppearance(named: light ? .aqua : .darkAqua) { host.appearance = appearance } + view.layoutSubtreeIfNeeded() + guard let rep = view.bitmapImageRepForCachingDisplay(in: view.bounds) else { return nil } + view.cacheDisplay(in: view.bounds, to: rep) + return rep + } + + /// 版式自检(测试用):所有控件必须落在内容区里,且不与顶部题型控件重叠。 + func layoutProblemsForTesting(kind: PracticeKind) -> [String] { + let view = buildContent() + kindControl.selectedSegment = PracticeKind.allCases.firstIndex(of: kind) ?? 0 + applyKindVisibility() + var problems: [String] = [] + let bounds = view.bounds + for sub in view.subviews where !sub.isHidden { + if !bounds.contains(sub.frame) { + problems.append("\(type(of: sub)) 越界 \(sub.frame)") + } + if sub !== kindControl, sub.frame.intersects(kindControl.frame.insetBy(dx: 0, dy: -4)) { + problems.append("\(type(of: sub)) 与题型控件重叠") + } + } + return problems + } +} + +/// 顶部起算的容器:文字与控件按"距顶部"排,避免出界。 +private final class FlippedView: NSView { + override var isFlipped: Bool { true } } diff --git a/platforms/macos/tests/wubi-learning.swift b/platforms/macos/tests/wubi-learning.swift index ef761bb..d92aa74 100644 --- a/platforms/macos/tests/wubi-learning.swift +++ b/platforms/macos/tests/wubi-learning.swift @@ -31,6 +31,11 @@ require(Practice.judgeCode(sample.code.uppercased() + " ", question: sample), " require(!Practice.judgeCode("zzzz", question: sample), "错误码判错") require(Practice.recognitionLetter(stroke: "横", structure: "左右") == "G", "识别码表 横/左右 → G") require(Practice.recognitionLetter(stroke: "折", structure: "杂合") == "V", "识别码表 折/杂合 → V") +// ADR-313 练习窗口:版式自检(控件不越界、不与题型控件重叠)。 +for kind in [PracticeKind.code, PracticeKind.recognition] { + let problems = PracticeWindow.shared.layoutProblemsForTesting(kind: kind) + require(problems.isEmpty, "练习窗口版式问题:\(problems.joined(separator: "; "))") +} let tmpProgress = PracticeProgress(url: URL(fileURLWithPath: NSTemporaryDirectory() + "lampyris-practice-test" + "/practice.tsv")) for index in 0..<3200 { tmpProgress.record(char: "字", kind: .code, correct: index % 2 == 0, now: index) } require(tmpProgress.rows.count <= PracticeProgress.maxRows, "进度行数有界")