Commit Graph
304 Commits
Author SHA1 Message Date
AskaEth 92a3c1bbc8 refactor(app-iced): 「禁用不提交」的规则收进 Field::submit_message
原来 text_field 里内联着 `if state.is_disabled() { None } else { field.submit_message() }`,
那条分支在 Element 上量不到(没有内省接口)⇒ 等于没验。现在把状态作为参数交给
Field::submit_message(state),规则本身可单测:submit_is_opt_in 多了 Disabled 那一档断言
(接了回调 + 禁用 ⇒ None),text_field 相应少 3 行。
2026-11-11 19:55:21 +08:00
AskaEth ebce41b2c5 test(app-iced)+docs(appearance): 强调色「已提交」那一档的出图开关
D30 要拍「回车之后」的样子,但出图通道没有键盘(渲染第 3~4 帧就落盘)。所以把强调色
的两档取证开关(ULL_APPEARANCE_ACCENT / …_COMMIT)收进 accent 自己的模块:第二档在灌值
之后补一次 Accent::commit(),它做的正是回车那条消息(Change::AccentCommit)在状态层做的事
(State::apply 开头的 commit)。拍的因此是同一个状态,不是另一条渲染分支。

顺带两处口径订正:
- state.rs 少了 3 行(它贴 400 行硬线),强调色的 env 开关不再散在 state 里;
- rows.rs 的模块文档把「名字规则」指到 core 的
  game/saves/write/boundary::check_name(原来写的是 presenter)。
2026-11-11 19:55:21 +08:00
AskaEth f7cfc8ded6 feat(app-iced): 单行输入框透出 on_submit,回车接上存档改名与强调色提交
债务台账 P18(M250 + M247 的共同缺口):iced 0.14 的 text_input 有 on_submit、
没有 on_unfocus(builder 逐条核过:on_input/on_input_maybe/on_submit/on_submit_maybe/
on_paste/on_paste_maybe,on_unfocus 零命中),而本仓的 text_field 只透出
on_input ⇒ 行内重命名只能点按钮、强调色拿不到「用户确认」这一下。

- field.rs:Field 变 `Field<'a, M>`(M 只被新加的 on_submit 用到,调用点的类型由
  text_field 的 on_input 一并推出,一行都不用改),回车消息唯一从 submit_message()
  取,`on_submit_maybe(None)` 是默认档 ⇒ 既有调用点逐位不变;禁用态与 on_input 同一
  条规矩被摘掉。
- 存档改名:回车与「改名」按钮共用 commit_rename() 一个消息产地,空名/原名不变/
  非法字符那几道校验因此对回车同样成立。
- 强调色:回车发 Change::AccentCommit(不是又一次 Accent(原文))⇒ 立即 commit(),
  把「打了一半就放着」的 #12 亮成红框 + 错行;逐键不报短错的规矩不变,落盘仍取
  上一次合法值。
- 判据:submit_is_opt_in(未接就是 None)、text_field_forwards_the_submit_callback、
  submit_is_wired_only_at_the_two_call_sites(白名单扫源码)、
  enter_takes_the_same_message_as_the_rename_button、the_rename_field_wires_enter、
  the_accent_field_wires_enter_to_commit、enter_commits_a_half_typed_value_and_
  persists_only_legal_ones。真键盘回车量不到(Element 无内省、造不出 iced::Renderer
  的测试替身),报告里单列。
2026-11-11 19:55:21 +08:00
AskaEth 779a2ae254 feat(app-iced)+test(presenter): 句柄按 (目录,mtime,大小) 复用;缓存可清空;端到端出图
复审 r1 的三条:

1. 端到端图:DevDocs/artifacts/m251/saves-real-decode-light.png —— 真 host
   (ULL_SCREENSHOT_REAL_HOST=1 + --data-dir 一个临时数据根)→ 后台列 screenshots/ →
   palette_extract::thumbnail 真解码 → DTO → Handle::from_rgba → 上屏。三格是三张真 PNG
   的真像素,副文案的 2.3 KB / 2.2 KB / 24.1 KB 也是真扫描值。造根脚本与复现命令进
   artifacts/m251/{README.md,make-real-root.py},并写明「哪张是排版、哪张是端到端」。
2. 每帧重建 Handle 的成本:改成**缓存**(不是实测)。model.rs 新增 process-level
   memo,键 = (截图目录, 文件, mtime, 字节数)(与 presenter 的缓存键同源,目录单列一格),
   上限 120 条(≈12MB)。判据:the_same_screenshot_reuses_one_handle 断言两次
   from_state 拿到**同一个 Handle id**;变异(换回 Handle::from_rgba)即红。
   a_changed_screenshot_gets_a_new_handle 钉住「文件变了必须换句柄」。
3. 进程级 memo 的副作用:两层各加 #[cfg(test)] clear_cache()/clear_handles(),用例从
   已知状态起跑;两条上限从 240 收到 120(≈12MB/层,算术写进常量文档)。

顺带:句柄键改用 push 拼(format! 模板串会被页面字面量守门当成英文文案抓走);
preview 的页面模型用例拆到 tests/saves/preview.rs(saves.rs 破 400)。
2026-11-11 17:09:17 +08:00
AskaEth dd9962e610 test(presenter)+fix(i18n): 淘汰策略与共享缓存解耦;注释里的 ASCII 引号
- prune() 抽成独立函数:全局缓存被所有用例共享,谁往里面灌 240+ 条都会顺手清掉
  别的用例刚缓存的键(实测:并行跑时 the_second_identical_refresh_... 假红)。淘汰
  策略改在本地 Cache 上测,两条判据各自确定;
- clippy -D warnings:from_ref 代替 &[x.clone()];
- model.rs 注释里的 "原因 + OS 原文" 用 ASCII 引号 → 页面字面量白名单守门报红,改
  中文引号;core/events 的三处文档失真登记为 TowerFinding(scope 外);
- 出处文案口径:TooLarge 的 detail 只留数字(presenter 不产人读文案)。
2026-11-11 16:52:47 +08:00
AskaEth 0b6b8f0a62 fix(app-iced): 缩略图墙一行两格 + 副文案只留元数据(出图实测)
- 3 格装不下:详情列宽 = FrameMetrics::column ≤ 424(1180 窗宽),卡片内宽约 370,
  3×160+2×12=504 会把第 3 格挤成一条竖条(实测图上『未生成预览』竖着排)。改成 2 格
  = 332px,与同列元信息卡的两列栅格同宽;
- 原因只进占位框(短句),副文案仍画『大小 · 时间』:160px 里塞「原因 + OS 原文」会摊成
  三行,把墙变得比图还吵;原文留在 ScreenshotThumbFailure::detail 供排障;
- 出图:DevDocs/artifacts/m251/saves-light.png / saves-dark.png(1180×1400,
  --software-renderer --screenshot,ULL_SAVES_FIXTURE=1)。
2026-11-11 16:40:41 +08:00
AskaEth 843f6d08f9 feat(saves): 截图缩略图墙(M251 · D54 全套:解码/缓存/上限/可见失败)
按 D54 落地,解码在 palette-extract、缓存在 presenter、像素随快照走、壳层只画。

palette-extract(上一提交)→ presenter:
- dto/saves/preview.rs:ScreenshotPreview{NotGenerated,Ready,Failed} + ThumbnailView
  (带 is_consistent:长度 ≠ w×h×4 的像素不许进渲染器)+ ScreenshotThumbKind;
  ScreenshotView 新增 preview 字段(像素不落盘,随 IPC 面走);
- host/saves/thumbs.rs:attach() 在后台线程(spawn_list 同一条线程)按 mtime 倒序解前
  60 张、单张 >8 MiB 先查大小不进解码器、失败也逐条带原因;进程级 memo 键 =
  (路径, mtime, 大小),覆盖/替换都会重解;超过 240 条只留本拍碰到的键;
- presenter 新增依赖 unlucky-palette-extract(塔已批,方向 presenter → palette-extract)。

app-iced:
- saves.rs 拆成 model / rows / wall(原文件 385 行,加完墙必破 400):model 里建一次
  iced Handle(渲染回调零解码、零重复上传),wall 3 列排格子,没像素的格子给
  160×90 占位框 + 逐条原因;text.rs 拆出 text/saves.rs 承载存档/截图一族文案;
- i18n 加 4 条「这一格为什么没有图」(未生成/太大/读不出/解不出)+ 金表同步;
- 出图夹具(saves_shot)合成两种场景的像素,并摆出四种格子(真图×2/未生成/超限失败)。

用例:presenter 9 条(含缓存命中不重解、mtime 变了必重解、第 61 张不解、
>8MiB 不进解码器、缓存有界)+ app-iced 3 条(三种格子的副文案、坏像素降级)。
2026-11-11 16:33:56 +08:00
AskaEth c14622f828 test(app-iced): 钉住错行里不可见字符的 U+XXXX 形态(M250 r1 小事 2) 2026-11-11 15:54:59 +08:00
AskaEth 14282f3325 fix(app-iced): 复审 r1 的四条 p2 + 三条小事(M250)
- p2-1 删死状态:`Accent::scheme` 字段 / `scheme()` 取值口 / `set_flavor` 重派生全仓零读者,
  且注释谎称「Ui 的唯一入口」。色值 `color` 与深浅无关,`Ui` 由 `rebuild_ui` 重算,
  所以整条删掉,`new/set/reset` 也不再收 `flavor`
- p2-2 规则只定义一次:新增 `theme::tokens(flavor, accent, scheme, families) -> (Ui, Colors)`,
  `App::rebuild_ui` 与出图 harness 都调它(harness 传 scheme=None,如实说明不覆盖 D38 分支);
  README 改成「同一条规则、不是同一个调用点」
- p2-3 逐键不闪红:`Accent` 区分「显示中的错」与 `pending`。只有**不可能再变合法**(非 hex / 超 6 位)
  才立刻亮;位数不足先挂 pending,等 `commit()`(点到别的外观控件)或配置读回才亮
- p2-4 订正 `app/state.rs:151` 那句反话(强调色现在是 UI 写入字段)
- 小事:hint 文案改「中性表面与错误色不随强调色变」;`Digit{ch}` 里的不可见字符显示成 U+XXXX;
  8 颗色块补 tooltip(hex 原文,Overlay 底)

判据:新用例 `accent_errors_only_appear_when_they_are_actionable` 覆盖「输入中不亮 / 提交后亮 /
配置读回立刻亮」三态;gallery 出图逐字节与重构前相同(规则搬迁没改像素)。
2026-11-11 15:46:10 +08:00
AskaEth c609827c49 chore(app-iced): 撤回越界的注释改动(app/state.rs 不在 M250 scope) 2026-11-11 15:12:15 +08:00
AskaEth e345b55c09 fix(app-iced): 预设色板改成两行排版,注释与用例去掉色值字面量(M250)
- 预设色板原来挤在一行,出图实测被折成「7 + 1」(最后一颗掉到下一行)——改成
  「标签 + 出厂」一行、8 颗色块一行;这是有意排版,不是被挤出来的
- src/** 的色值字面量门禁把注释与测试一起扫(那条门禁是对的):文档里的 `#RRGGBB`
  实例改成不带 `#` 的描述,超长串用例改用 `TEAL.to_hex()` 拼出来
2026-11-11 15:10:03 +08:00
AskaEth e6b2200360 feat(app-iced): 强调色接线落地(M250 ACCENTWIRE)
appearance.accent_color 此前是死线:配置键、presenter 往返、预设色板都在,但造 tokens 的
唯一入口恒走 DesignTokens::brand(board),用户填了色界面不变。

- theme.rs:Ui::with_accent + accent_scheme(唯一调用 DesignTokens::with_accent 的地方)
- app/state/theme.rs:rebuild_ui 三选一(显式强调色 > D38 取色方案 > 出厂板),diag 一行说清是谁生效
- pages/appearance:新增 accent 状态(原文/解析结果/方案/结构化错误)+ 设置页输入行与预设色块;
  非法值保持上一次合法值并给出 Short/Long/Digit 三档原因,不静默回退出厂色
- presenter::prefs:AccentChoice::parse 成为唯一解析点,set_accent_checked 交回结构化原因
- app/appearance.rs:persist_appearance 带上 accent_color(此前写不进去),只写上一次合法值
- pages/harness/app.rs:gallery 也能吃 ULL_APPEARANCE_ACCENT,供出图矩阵
- core keys.rs:订正「空 = 出厂 #0078D4;accent 令牌全域生效」这句半真半假的说明
- i18n:强调色 7 条文案(链尾新域,金表只追加)

判据:同一组件(进度条 Primary)在空配置与 #006A5C 下色值不同(App 级 + 令牌级两条);
非法值拿到结构化错误、生效色不回退、不写盘、tokens 仍可构造。
2026-11-11 14:56:26 +08:00
AskaEth 31cb109445 fix(app-iced): 探针补回宽度判据,并订正覆盖面与 12.4 的口径(M248 r1 P2-1/P2-2)
P2-1:`TRACK_MIN_RUN = 200` 把「轨道被宿主裁短」这类几何回归从红变成绿——复审把 after 图里
0% 那条轨道右半截涂回画布色(连续段 160px)之后,那一行直接不成带,check_linear 靠另一条带
照样绿。现在拆成两条判据:成带阈值降到 **100**(实测:确定态行 318~320、不定态行 306~308、
文字行 22,两头都留得住),宽度**单独**判 ≥300(`TRACK_MIN_WIDTH`),不再藏在圆角检查的
find 里。反例复跑:两套主题都红「实测最宽段 160px @ 行 163」。

新增 `feedback/tests/bands.rs`(合成图,不依赖真实截图)两条用例,把两个阈值的取值区间钉住:
窄轨道必须仍成带且过不了宽度判据、22px 的文字段永远不成带。变异自证:调用点换回
`bands(.., 200)` → 两条用例都红(`left: [(12,15)] / right: [(5,8),(12,15)]`);
`TRACK_MIN_WIDTH=150` → 窄轨道用例红。

P2-2:订正两处写反的账——实测 `轨道带 2 条(高 [4, 5])`,**两条带都在**厚度/宽度判据里
(5 行那条含 1 行抗锯齿,near 容 ±1),不是「只剩一条」;12.4 只量主色两段的左右端点,
**不量**轨道厚度。根因(按单条 320 轨道心算 ~88px vs bands_by_run 按整行取 306px)已写进
`TRACK_MIN_RUN` 的文档。

顺带:12.1 的判据从 probe.rs 搬进 `probe/track.rs`——加完这几行 probe.rs 正好 401 行,
撞 400 红线;顺着 circular / panels 的先例一族几何一个文件。
2026-11-11 14:32:27 +08:00
AskaEth 8f5dac2ed6 fix(app-iced): 三处 Pixel 偏离按角色语义收敛(M248)
P2:Elevated 的容器改由高程令牌推导(Elevation::Level1.surface_role),解析值仍是
SurfaceContainerLow,图上逐像素不变;「比 Surface 高一档」从此只有 elevation.rs 一处定义,
组件里不再手抄角色名。

P3:Outlined 的 1dp 描边 OutlineVariant → Outline。design/color/mod.rs 的对比度纪律里
Outline 才是「非文字元素(目标 3:1)」,OutlineVariant 是纯装饰分隔线;图标按钮那一式本来
就是 Outline,文字按钮此前与它不一致(全站两套描边)。实测 Outline/Surface:Ink 5.08、
Silver 4.24(OutlineVariant 1.97 / 1.62)。

P4:进度轨道(线性 + 环形)SecondaryContainer → SurfaceContainerHighest——轨道是「还剩多少」
的背景尺,强调色只属于指示条。与 A1(LinearProgressIndicatorTokens.TrackColor)的差异在
progress.rs 模块文档与 geometry.rs 的 12.2 行登记,gallery 图注同步改成角色名。

判据:新增 buttons/tests/roles.rs 与进度轨道用例,断言的是「取到的角色」与 Ink / Silver /
Scheme::from_seed 三套方案下解析出的颜色,不钉任何 hex;三处各做一次变异自证(改回旧值即红)。

另修 feedback probe 的定位判据:轨道换成中性容器面后,「画布→轨道」的混色轴穿过中性文字,
按「计数」会把图注行数成轨道,改按「连续段」判(bands_by_run);顺带修掉 indeterminate_row
把标题行当主色带的既有问题(用改前的图 + 改前的探针复现过,与本次换色无关)。
2026-11-11 14:32:26 +08:00
AskaEth 32384b952e chore(presenter,app-iced): 复审 r2 三项(文档对齐 / 拆 dialog.rs / 收窄关闭路径)
1. 文档自相矛盾(复审 r2 第 1 项):`SavesState::apply` 的文档还写着「无论成败都清『在飞』与
   『确认中』」——改完之后这是假的,而且与模块文档「安全判据第 3 条」互相打脸。现在逐支写清:
   `Ok` 清 draft/pending、`confirm` 只在被确认的世界已不在快照里时清并登记 `closed_confirm`;
   `Err` 只清 pending。生命周期表里的 `dismissed` 改成真字段名 `closed_confirm`(消费点是
   `app/state/refresh.rs::settle_saves_confirm`)。

2. 拆 `app/update/dialog.rs`(复审裁决):397 行里 262 行是内联 `mod tests`,而它装的是
   主按钮 / Esc / 次按钮三条不可逆分支。照仓内先例(`app/dialog.rs` + `app/dialog/tests.rs`)
   把测试体搬到 `app/update/dialog/tests.rs`,**纯搬移**:`dialog.rs` 397 → 138,
   `dialog/tests.rs` 262;从 `6c87e2a` 的 `dialog.rs` 里抽出 `mod tests` 去一层缩进后与
   新文件**逐行比对 0 处差异**(262 = 262)。

3. 收窄关闭路径(复审 r2 加固):`SavesState::close_confirm(current)` 只在 `current` 等于登记的
   那个框时才清;`clear_confirm()` 保持无条件(换实例/复位用)。`escape()` 与 `Secondary|Dismiss`
   改成传当前框 id。新用例 `closing_another_dialog_leaves_our_target_alone`(别人关框不清我们的
   目标;自己关框才清)。
2026-11-11 14:25:26 +08:00
AskaEth 6c87e2ad8a fix(presenter,app-iced): 删除确认绑定到对话框身份 + 快照不再吃掉目标(M247 r2 复审)
复审 r1 P1-1(安全):`SavesState::confirm` 的「关闭即清」在 Esc 这条路径上没有落实,
而主按钮分支无条件先取目标 —— 「删存档 → Esc → 崩溃卡片 → 导出诊断包」会静默删掉那个世界。

两层修,各自有独立用例钉住:

1. **认框不认人**:`confirm` 改成 `Option<(DialogId, String)>`,`take_confirm(current)`
   只在 id 相等时交付;`DialogMessage::Primary` 传 `dialogs.current().id`。即使别的路径漏清,
   也核销不了别人那个框。用例 `a_leaked_target_cannot_be_redeemed_by_another_dialog`
   (去掉 id 校验即红:实测「漏清的目标被别的框兑现了:世界被静默删除」)。
2. **关闭路径补全**:`update/dialog.rs::escape()` 补 `clear_confirm()`(次按钮/遮罩早就清了,
   Esc 漏了)。用例 `escaping_the_delete_dialog_disarms_it_for_the_next_one` 复现整条事故链,
   断言没有下发 `DeleteWorld`、框走自己的路关掉(去掉这行即红)。

P2-1:`changed()` 不再无条件清 `confirm`(一拍无关快照会让主按钮变成静默空操作)——
只在「被确认的世界已经从快照里消失」时清,并登记 `closed_confirm` 让 app 层**收起那个框**
(`refresh_view → settle_saves_confirm`),避免留一个点了没反应的按钮。`failed()` 同理不动 confirm。
用例:`an_unrelated_snapshot_keeps_the_confirmation_redeemable`、`a_vanished_world_closes_its_confirmation`、
`an_unrelated_snapshot_no_longer_drops_the_confirmation`、`the_confirmation_is_dropped_and_reported_when_the_world_vanishes`。

P2-2:`catalog/instances.rs` 那段「这一版只读 / InstancesSavesReadOnly」的陈旧说明改成现状
(写操作已接上,边界由 core 的 `write::boundary` 守)。

拆分(400 行):`presenter/src/state/saves.rs` 的交互态方法 → `state/saves/interaction.rs`、
判据 → `state/saves/tests.rs`;`app/update/dialog.rs` 的接线用例内联在该文件(5 条)。
2026-11-11 13:51:07 +08:00
AskaEth 8c67e4eca6 fix(presenter,app-iced): 「游戏在跑」挡在落 core 之前(M247 r2)
塔裁决:三个写操作在实例启动/运行时必须拒,判据不能只靠界面灰按钮 ——
Windows 上是 os error 32 兜底,**Linux 上删一个正在跑的世界会成功**,那是在
用户玩到一半时把存档删掉。

- host/dispatch.rs:`AppCommand::Saves` 与 `AppCommand::Mods` 同款,先查
  `CoreHost::in_flight(&id)` 再把 `running` 传下去
- host/saves.rs::dispatch 增 `running`:为真时三个写操作**不 spawn、不碰盘**,
  直接回一条 `SavesFailureKind::GameRunning` 的结构化失败;读路径与「打开文件夹」不受影响
- dto:新增 `GameRunning` 类别(不折进 WriteFailed —— 那不是"写失败",是"我们主动没写")
- i18n:「游戏正在运行(先退出再操作)」中英成对 + 金表同步
- 用例:在飞时三条写命令盘上指纹不变/世界还在/备份落点没建,且回 GameRunning;
  反向用例钉住闸门没有把写操作一并关死;UI 侧断言三种动词的主句都不出现「写盘失败」且不画重试
- 拆分(400 行):host/saves.rs → host/saves/write.rs;两处测试各拆出 write 子模块
- SavesState 模块文档补「生命周期」表:draft / pending / confirm 谁写谁清、何时必须清,
  并把 confirm「取走即清」(连点两下不删两个)写成安全不变量
- 出图:ULL_SAVES_FIXTURE=running 一张(失败那句话的样子)
2026-11-11 13:29:56 +08:00
AskaEth c921b9c185 feat(core,presenter,app-iced): 存档三个写操作(重命名 / 删除 / 备份)+ 行操作区(M247 SAVESOPS)
core::game::saves 新增 write 模块,三个入口的第一句都是 boundary::guard:

- rename_world:saves/<old> → saves/<new>,撞名/同名/非法名字各自具名拒绝
- delete_world:递归删(不跟随符号链接、层级上限 64),先过边界校验
- backup_world:流式 zip(限额预检 → 临时文件 → 原子改名),
  <世界名>-YYYYMMDD-HHMMSS.zip,撞名顺延,绝不覆盖上一份

guard 三道判据(每次写前重做,不信任调用方):名字必须是单段裸名字;
saves/ 必须是运行目录的真实直属子目录;世界目录必须是 saves/ 下的真目录
(解析后比父级,不比字符串前缀)。落点目录要求绝对路径(相对路径会落到进程 CWD,
实测过一次)。

presenter:SavesCommand 增 RenameWorld/DeleteWorld/BackupWorld,host 三个后台入口
写成功后重新列一遍目录并把「回执」挂在快照上(SavesOped:改名两端 / 被删的名字 /
备份落点+字节数+跳过的链接数)。SavesFailureView 增 file 与 op,写失败八类一一对应。
SavesState 增 draft / pending / confirm 三样交互态(草稿是行内输入缓冲,
pending 与 ModsState::pending 同一条先例,confirm 是删除确认框在等谁)。

app-iced:世界行右侧重命名/备份/删除;行内输入框改名(现成模态队列没有「输入变了」
这一条消息,用输入模态等于重写一套);删除走现成 modal 的 Confirm,主按钮落点接在
app/update/dialog.rs。回执行画在卡片里(备份落点看不见就是静默失败)。

i18n:新增 25 条中英成对 key + 金表同步;删掉 M235 的「这一版只读」那条(写操作接上
之后它是假的)。出图夹具 ULL_SAVES_FIXTURE=rename|confirm|backed-up 三种形态。
2026-11-11 13:29:56 +08:00
AskaEth df0944894f fix(app-iced): 面板预算按内容推导 + 版本菜单不画 L2 阴影(M238)
两条都在「视口不终止内容」这一族里,修法各一半:

① 三栏档面板预算从死的 `PANEL_BUDGET = 300` 改成 `panel_budget(menu_items)`
   (内边距 24 + 首屏 76 + 间距 8 + 钉死区 88 + 菜单高 + 列表↔面板 21)。
   旧常数不含列表↔面板那 21、也不随版本菜单行数变,于是 760/900 档面板实得
   286/294px,滚动视口被挤到 54/62px——六枚 loader chip 只露出 10px(736 与 1180
   同样严重,纵向预算与窗宽无关)。修后 760 档视口 98px(chips 31px 整枚在屏)、
   900 档 106px;列表按整行让出:760 档 4→3 整行、900 档 7→6 整行;只有一条
   加载器版本时菜单只占 64px,列表仍是 4 整行。

② 版本菜单改用 `Menu::flat()`:不是「把阴影裁掉」,而是**不在滚动容器里画阴影**。
   `iced_tiny_skia` 0.14 画 quad 阴影走 `draw_pixmap(.., mask: None)`
   (engine.rs:136-143),绕过裁切掩码;`iced_wgpu` 那边是整层 scissor
   (quad.rs:96-101)⇒ 同一棵树两个后端画出来不一样。修前菜单底下 54px 一条被压暗
   (浅色 249→231,−7.2%,含 8px 缝与实例名框内上半部),修后逐像素等于画布色。
   菜单的 L2 令牌不动,真实浮层照旧带阴影。

判据(两条,抓得住的/抓不住的都写进文档注释):
- 结构:`rows/tail.rs` 断言三栏档 760/900 的滚动视口 ≥ 首屏 76px(对生产常量做算术);
- 像素:`options_shot/compact.rs` 断言 chip 描边上下两行都在、且实例名框上方那条缝
  与框内都是纯画布色。
变异自证:预算退回 300 ⇒ 结构断言红("滚动视口只有 54px < 首屏 76px");阴影无视
`flat()` ⇒ 像素断言红("y=660 x=304 是 [231,231,232] 而不是画布色")。

没验:滚动中/底部三态拍不出来(harness 无 `scroll_to` 派发通道,已另开 finding)。
2026-11-11 08:28:06 +08:00
AskaEth ba441351dd fix(app-iced): 复审 r2 三条收口(M237)
- P1-1 判据会让 CI 一亮红:`java_shot.rs` 原来写「没喂图时 `assert!(CI 未设置)`」,
  而 `.github/workflows/ci.yml` 的 test 作业在 ubuntu + windows 上都跑
  `cargo test --workspace --all-targets`,出图步骤只给 `options_shot` 备了 `ULL_DL_*_SHOT`
  (本判据的图不在出图清单里)⇒ `CI=true` 实测 exit 101,一合即红。改成:没喂图**一律响亮跳过**
  (`not_fed()` + CI 时多一句「本仓 CI 不出这张图,跳过是预期」),模块文档写清「要让它守门需要
  跨 scope 加出图步骤 + 注入 `ULL_M237_JAVA_SHOT`,以及那时该怎么改回硬失败」。
  自证:`CI=true` 不喂图 → exit 0(改前 101);无 CI 不喂图 → exit 0;喂图(浅/深)→ 真跑,exit 0。
- P2-1 同类错漏一处:实例列表卡画的是 `MaterialLayer::Card`(`instances.rs:263`),
  但 `list_row` 的 `ListItem` 没传底 ⇒ 行 hover/按下按 `Base::CANVAS` 合成。
  改成 `item.base(frame::BASE)`(同一层,不另立常量),并加进接线守卫。
  像素证据(`--force-state=hover`,1180×760 列表卡矩形 `(112,160)-(448,735)` 内 before/after 差):
  浅色 57923 px `(232,232,233)→(226,226,227)`,深色 57923 px `(32,38,31)→(35,40,35)`
  —— 即 hover 8% 从画布底换到卡底。
- 守卫自身升硬:`base_wiring.rs` 的计数原来按整段文本数,**把接线注释掉仍算数**(r2 实测绿)。
  改成先剔掉整行 `//` 注释再数;变异自证:注释掉 `item.base(frame::BASE)` ⇒ 红
  (`instances.rs: .base(..) 出现 0 次,期望 1 次`,退出码 101),还原后绿。
- 门禁(CARGO_TARGET_DIR=C:/Temp/ull-target/wt-237):fmt 0 / check --all-targets 0 /
  clippy --workspace --all-targets -D warnings 0 / test -p unlucky-app-iced --lib 907 通过 0 失败 /
  test --workspace 3344 通过 0 失败。
2026-11-11 07:56:11 +08:00
AskaEth 3dcd5fd117 feat(app-iced): 页面层把卡片底接进禁用/交互控件(M237)
M234 把 `interaction::Base` 与组件侧的 `.base(..)` 接好了,但页面层仍是默认的画布底:
卡片(`Role::SurfaceContainerLow`)里的禁用按钮 / chip / 开关把 10% / 12% / 38% 叠在画布色上。
M223 记的「卡片里禁用控件 CR 1.05、形丢失」因此只在 gallery 里修好,产品页没修。

- 导出容器底:`pages::frame::BASE = Base::of(MaterialLayer::Card.surface_role())`
  —— 取的是 `card()` **真正画出来的那一层**(不是「反正是 surface」);`logs::panel::BASE`
  是同一个常量的别名(两张卡画的是同一个材质层)。
- 接线(改前一律 `Base::CANVAS`,改后 `frame::BASE`):设置·Java(扫描 / 取消 / 设为默认 chip /
  三个输入框 / 添加 / 保存 / 预置项 chip / 下载徽标);设置·通用(语言三档、主题三档、日志行
  两个手写按钮 → 状态层叠 `BASE.role()`);实例详情(MODS 开关、SAVES 的重试与打开文件夹、
  启动参数区的两个输入框 + 后端 chip + 保存 / 恢复);日志页(诊断卡页脚 `Copy summary`)。
  详情头的启动 / 停止 / 动作行**不在卡里**,保持画布底并加注释。
- 判据:`pages/frame/tests.rs` 逐值断言「`BASE` == 卡片画出来的颜色」+ 源码结构盯 `card()`
  不许改画别的材质层;`pages/settings/tests/java_shot.rs` 是**页面级像素判据**(读 1180×760
  真截图、逐位匹配:卡区里「叠卡底」3136 px vs「叠画布」21 px,深色 3128 / 1);
  `pages/tests/base_wiring.rs` 数接线点,守像素够不到的页面(实例详情 MODS / SAVES)。
- 变异自证:撤掉「立即扫描」的 `.base(BASE)` ⇒ 像素判据红(73 px vs 3083 px,退出码 101);
  撤掉诊断卡页脚的 `.base(..)` ⇒ 接线判据红(0 vs 1,退出码 101)。均已还原。
- 门禁:fmt / check -p unlucky-app-iced --all-targets / test --workspace(3344 通过,0 失败)/
  clippy --workspace --all-targets -D warnings / 文件 ≤400(最大 389),全部本地过。
- 已知未尽:面板 α<1 的半透明卡(`Base` 只装一个 `Role`)、GPU 档、实例详情与日志页在
  1180×760 出图里没有可量测的禁用控件(夹具缺 running,已报 finding);app_bar / adopt pick /
  设置页分组列仍未导出 `BASE`;设置·下载 / 更新 / 助手、账户页、内容库、下载页仍未接线。
2026-11-11 07:56:11 +08:00
AskaEth 9bb3c33b8c fix(app-iced): 复审 r1 四条 P2 收口(M234)
- P2-1 判据恒绿:`gallery/probe/bases.rs` 的「反向检查」把字段档计数(`:172` 遮蔽后的
  `hits`)拿去比按钮档 stray ⇒ 恒绿。改成 `button_hits` / `field_hits` 两个计数,各比各的
  stray,失败信息的数值与单位对齐量测对象。变异自证:卡列里留一颗忘了 `.base(..)` 的按钮
  ⇒ 该断言单独红(2137 px vs 3178 px,退出码 101)。
- P2-2 FAB 的底错了:禁用态背景仍是满档 `PrimaryContainer`(`Disabled.layer() == None`),
  38% 内容却合在画布上。改成 `Base::of(PrimaryContainer)` + `on_container = false`
  (那一层 10% 在 FAB 身上不存在),两处调用收进一个 `content_color()`;新增值判据
  `disabled_fab_ink_composites_over_its_own_container`(修前 #8CA0AA / 修后 #548FBC 量级)。
- P2-3 漏改:`selection::paint::state_layer()`(hover/focus/pressed)与 `list_item` 的
  状态层叠底仍写死 `Role::Surface`,改成跟 `base`;同类两处(`assemble::transparent_state_color`
  的 Outlined/Text hover、drawer 项药丸)一并跟上。
- P2-4 文档失真:`feedback/{action,mod}.rs` 删掉「本地 α 合成 / 规则只认两档」的旧描述
  与失效链接;`list_item` 的旧签名;三处 `probe/base.rs` → `probe/bases.rs`;
  新增 `surfaces::elevated::tests::the_card_base_constant_is_the_role_the_card_paints`
  真断言(原来只在文档里声称那条测试守 `BASE`),并把这句描述改准。
- nit:`disabledbase.rs` 字符串里塌成一行的续行空格、probe 汇总行把列左缘横坐标说成
  「像素计数」的措辞。

门禁(CARGO_TARGET_DIR=C:/Temp/ull-target/wt-234):fmt / check --all-targets /
test -p unlucky-app-iced(896 + 6 通过,0 失败)/ clippy --workspace --all-targets -D warnings /
文件 ≤400 全绿;probe 浅色与深色都过。
2026-11-11 06:34:24 +08:00
AskaEth 35dbefb08d feat(app-iced): 禁用态合成底改成容器给的 Base(M234)
`interaction::disabled` 的四个入口一直收一个「底」,但 8 个调用点全传死
`Role::Surface`:卡片(SurfaceContainer)/ 面板(SurfaceContainerLow)里的禁用控件
把 10%(按钮族)/ 12%(面状控件族)叠在画布色上 —— M223「CR 1.05、形丢失」的根因。

- 新增 `interaction::Base`(画布 / 容器底的一等参数,`From<Role>` 兼容历史调用点);
  四个入口改成 `(ui, base, …)`,内部不再出现任何写死的 `Role::Surface`。
- 容器导出自己的 `BASE`:`surfaces::elevated::BASE`(卡)、`inputs::menu::PANEL_BASE`
  (菜单面板,项的 38% 与状态层都改叠它)、`navigation::drawer::BASE`。
- 控件收 `.base(..)`:Button / icon_on / Field / SearchBar / Slider / Checkbox / Radio /
  Switch / Chip / ListItem / Tabs;Fab 与 icon() 是画布默认值。
- `feedback/action.rs` 的本地 α 合成删掉,改走 `disabled_content_over_role` 并给真实容器底
  (Snackbar `InverseSurface` / Dialog `SurfaceContainerHigh`)。
- 判据:`interaction::disabled::tests`(卡片/面板/画布三档逐值)、
  `buttons::tests::base`(Button::base → StyleSpec → style_closure 接线)、
  `gallery` 新增 `disabledbase` 段 + `gallery::probe::bases` 精确像素断言
  (±10 容差区分不了 1/255 的差,所以自己数逐位相等的像素)。
- 变异自证:assemble 的调用点改回画布底后,上面两条都红(见 artifacts/m234/README.md)。

已知未尽:`crates/app-iced/src/pages/**` 不在 scope 内,页面侧「把控件放进卡片/面板」的
调用点尚未接 `.base(..)`(行为与本 mission 之前一致);`pages/…/java/tests/marker.rs`
有一行被迫更新(入口多了一个实参,语义不变 = 画布底)。
2026-11-11 06:34:23 +08:00
AskaEth f217d83680 feat(saves): 实例存档 / 截图的只读列表与打开文件夹(M235 STOREMVP)
用户诉求是 PCL 差距四项里的「存档截图 / 导入」,M230 审计确认三层全缺。塔裁决 U3
把这一版钉成「只读列表 + 打开文件夹」,不做缩略图墙(缩略图要引图片解码依赖,而
AGENTS §2 只允许 palette-extract 引 image 类依赖;截图/导入产物形态也没定型)。

core
- `game::saves`(新):只读列 `<run_dir>/saves`(世界目录)与 `<run_dir>/screenshots`
  (图片文件)。只调 symlink_metadata / read_dir / metadata,不建目录、不写文件、不读
  文件内容(`icon.png` 只判存在)。目录不存在 ⇒ 空列表不是错误;存在但不可用(符号
  链接 / 不是目录 / 读不了)⇒ 具名错误。条目级异常逐条登记原因,不静默丢。
- `platform::opener`(新):`open_dir` 是**目录**打开器的唯一收敛点(explorer / open /
  xdg-open),内部复用 `child::quiet`,cfg 分支只此一处。塔明确要求按 AGENTS §6 放
  platform 而不是业务模块,并要一条可执行判据防退化
  (`the_launcher_literals_live_here_only` 扫 `crates/core/src`:除本文件外不许出现
  `"explorer"` / `"xdg-open"` 字面量)。

presenter
- `dto::saves`:SavesCommand(List / OpenFolder)、SavesListView、SavesFailureView
  (结构 + OS 原文,不拼中文)、`SavesListView` 的计数助手。
- `host::saves`:走 `job::spawn`,UI 只收快照;运行目录仍是
  `isolation::derive_game_dir` 的同一个答案(与启动 / mod 列表同源,**不写盘**)。
  「打开文件夹」打开哪个目录由 host 定:`saves/` → `screenshots/` → 运行目录。
  失败分三类(InstanceUnavailable / DirUnusable / OpenFailed),可重试。
- `state::saves`:快照整体替换、迟到结果丢弃、失败保留绑定(重试才有意义)。
- 接线:CoreEvent::Saves / AppCommand::Saves / HostTask::Saves。

app-iced
- `pages::instances::saves`:详情页 SAVES 卡片(MODS 之后、启动参数之前;**不套第二层
  滚动条**,整列那一条是唯一入口)。世界行带 UTC 时间与「有图标」,截图行带大小与时间;
  第一行固定画「这一版只读」(U3 边界让用户看得见)。读盘失败画一条红字 + 「重试」;
  打开文件夹失败**不画**重试(那个动作的重试就是再点一次按钮)。
- 接线:Message::{ReloadSaves, OpenSavesFolder}、`sync_saves`(与 `sync_mods` 同一判据)、
  Model::saves、`saves_shot` 出图夹具(ULL_SAVES_FIXTURE=1)。

i18n
- `InstancesSaves*` 20 条中英成对,catalog 与金表在**同一条锚点**后追加、逐字节一致。

证据
- core 只读判据:`game::saves::tests` 在真盘建世界/截图,调用前后各拍一次
  (相对路径, 是否目录, 字节数, mtime)指纹并逐条比对;另有用例钉「目录缺失不建目录」。
- presenter:`host::saves::tests` 真盘端到端(custom 档位派生运行目录 → 快照)。
- GUI:`--software-renderer` 1180×760 浅/深各一张 + 1180×1280 两张「整块卡片可见」
  (1180×760 下 SAVES 卡片位于折叠线附近,只能看到卡头;第二对用 ULL_WINDOW_SIZE
  把窗口拉高,口径与 M98 的矩阵一致),落 `DevDocs/artifacts/M235/`。

没做的(U3 边界内,见 handoff):缩略图墙、导入、删除/重命名、`level.dat` 的
LevelName 解析(世界名暂时就是目录名)。
2026-11-11 06:00:27 +08:00
AskaEth 36bac74822 docs(app-iced): probe 模块文档的复现命令补 SECTION(M229 复审 r3 P2-1)
r5 报告说「模块文档复现命令已补 `ULL_GALLERY_SECTION`」,但 diff 只碰了 `shot_path()` 与
测试体——模块文档 9-15 行一个字没改,照抄那条命令仍 exit 101(panic 报只支持 buttons/divider)。

这轮补齐:
- 模块文档复现命令改成两组(`ULL_GALLERY_SECTION=buttons` / `=divider`),产物路径用
  `${CARGO_TARGET_DIR:-target}`(本仓一树一 target,旧命令的 `./target/debug` 在 worktree 里根本
  没有 exe),并写明 `all` 不支持及其理由;
- `shot_path()` 的文档改真:它只读 `ULL_GALLERY_SHOT`,「这张图属于哪一段」由测试体开头那条
  assert 管(assert 早已搬走,且现在也认 divider)——原文还写着「此处不满足就失败」;
- 收掉 assert 文案里的九连空格接缝;
- 矩阵带过滤的注释从「`all` 默认会画全段」改成「buttons 段没有后续段,过滤是防御性的」。

自证:按文档两组命令实跑,四条命令全部 exit 0;`grep -n ULL_GALLERY_SECTION probe.rs` 命中
8/15/17/21/23(模块文档)与 144/150/357(实现)。
2026-11-11 01:49:16 +08:00
AskaEth f5a2610e4d docs(app-iced): 值字级注释改真 + probe 只认 buttons/divider + 截图主题钉死回归测试(M229 复审 r2)
1) P1-2 的注释值与量测对齐(四处):`elevated.rs` 的 `PAD`/`Texts::title`/`texts()` 与
   `pages/instances/detail.rs` 还写着「值 14/400」,而实现是 12/400;`PAD` 文档更把 12px 的
   118px 量测挂在「14px 下」。全部改成真值,并把 14px=138px 与 12px=118px 两个量测并列写清。

2) probe 门禁只解决一半:`SECTION=buttons` 绿、`SECTION=all` 仍红(sizes 段的实心块撞「按钮
   40 高」判据),断言文案却写「all 或 buttons」。现在:
   - 只支持 `buttons`(按钮矩阵,实心块判据**只量矩阵区**,后续段不再参与)与 `divider`
     (分隔线两条判据,独立出口);
   - `all` / 其它 / 未给 → 明确失败并给理由(整页比 1180×760 高,分隔线段与按钮矩阵不可能同屏;
     整页图的 chip/卡片会被当按钮量);
   - 模块文档的复现命令补上 `ULL_GALLERY_SECTION`(以前照着跑会 panic);
   - `gallery/mod.rs` 的「五段」→ 七段;`gallery/buttons.rs` 的「8 行 + icon·standard」→ 9 行 + 四式。

3) 截图通道的主题档钉死此前没有回归测试(复审 r2 P2-a)。新增
   `the_screenshot_channel_pins_the_theme_mode_instead_of_reading_the_file`:两份不同的机器配置 +
   同一组截图参数 ⇒ 同一个档位且等于参数;反证非截图通道下两份配置确实分出两档。
   变异自证:把分支改回 `bridge.load().1` → 该测试红(left: Dark, right: Light)。

门禁:fmt --check 0 / check -p unlucky-app-iced --all-targets 0 / test -p unlucky-app-iced 0
(887 passed + 6 integration)/ clippy -p unlucky-app-iced --all-targets -D warnings 0 / 400 行扫描干净。
probe 亲跑:buttons 浅+深两张 sheet OK,divider 一张 OK,`all` 按预期被拒。
2026-11-11 01:49:16 +08:00
AskaEth 8958b7f67d fix(app-iced): IconButton Outlined 真的画环 + ELEVATED CARD 值串单行(M229 复审 P1)
P1-1:`icon()` 把 `StyleSpec.stroke` 写死 `0.0`,`IconVariant::Outlined` 的描边角色只到
`roles()` 为止、没画出来 —— Outlined 与 Plain 逐像素相同。改成「有描边角色就是 1px」,
并把「角色 → 填充/内容/描边」抽成 `icon_style_spec()`(`icon()` 直接吃它),单测因此能断言
**接线**而不只是断言 `icon_stroke()` 这个纯函数(第一版正是这样漏掉调用点的)。

回归门两条:纯函数/接线(`buttons/tests/icon.rs`,变异回 0.0 时红)+ 渲染层
(`gallery/probe/rows.rs`:图标按钮左缘 4px 带里 Outlined 有环墨、Plain 必须 0 px;
变异后重出图再跑会红)。环在软渲染里是 Outline 叠画布的抗锯齿混色(实测 110/124/110,
本体 123/138/123),所以判据是「这一带有墨/没墨」而不是颜色匹配。

P1-2:ELEVATED CARD 内距 24 + 值 16px 让「2026-10-29 19:22 UTC」折行,同排卡底不齐;
旧测试还断言 20/14(不变量 118+2×padding ≤ 160 变成 166 > 160,假绿)。
按塔裁决「收内距/字号、不破卡片宽度契约」:`PAD` 24 → 20(= card_padding)、值字级 → BodySmall 12。
量测:时戳 14px 墨水宽 138px(160 外框内宽 120 放不下)、12px 为 118px(放得下,余 2px)。
测试改成四条绑一起(字级 12 / 内距 20 / 160−2×PAD ≥ 118 / 2 列实际宽度也 ≥ 118),
并反证「14px 确实放不下」防登记被当噪音删掉;变异回 14px 时红。

P2(顺带):
- 截图通道的主题档由 `--screenshot-theme` 钉死(此前读机器 launcher.json,设置页主题行会漂)
- `gallery::probe` 只认 gallery sheet(必须给 `ULL_GALLERY_SECTION`,否则整页图的 chip 会被当按钮量)、
  行表 8 → 9 行(四式图标按钮)、分隔线判据只在渲染了 divider 段时执行、逐行阈值 40 → 30 并加
  「满档色像素必须少于 38% 像素」的反向判据、probe 拆出 `probe/rows.rs` 守 400 行
- FAB extended 的重复分支合成一处,右内距提取 `EXTENDED_PAD_END`;iced 0.14 无 `Length::Min`,
  「最小宽随标签增长」退化为固定宽已登记
- 五处注释谎言改真(theme/tests、general/theme、elevated、launch_args、app/tests/theme)

`buttons/tests.rs` 加用例后 402 行 ⇒ 图标按钮那组拆到 `buttons/tests/icon.rs`。
2026-11-11 01:49:15 +08:00
AskaEth 6ed26626db docs(app-iced): 登记禁用滑块激活轨 38% 为 Pixel 级偏离 #9(M229 复审 r3)
复审核对出 artifacts README 的归因错误:`page-settings-g1-dark.png` 里禁用滑块轨是
`on-surface@38% over surface`(轨带 6030 px),不是面状控件族的 12%;上一版引用的 7 px
12% 小簇在 Image path 字段标签附近,与滑轨无关。

按塔裁决**不改实现**:规格卡只定义了面状控件族(chip 描边 / switch 轨 / 字段底 / 滑轨未激活段)的
12%,而禁用**激活轨**沿用 M3 `SliderTokens.DisabledActiveTrack` 的 38% —— 两者是两个族。
登记写在本文件参考值表下方(`DevDocs/` 被 gitignore,README 里的同一段无法入库,故以代码为准)。
2026-11-11 01:49:15 +08:00
AskaEth 4ce24d6fbd fix(app-iced): 禁用态与遮罩改从品牌板合成(M229 复审 F1)+ 选择类图注回填(F2)
F1(核心):三处 `Scheme::baseline(ui.flavor())` 自己重算了一份方案,绕开 `Ui` 里注入的品牌板,
于是 Ink 板上的禁用控件与对话框遮罩仍按 tone 派生的天蓝中性合成(复审量到滑块轨道 #58646C、
填充框 #222E36、遮罩 #060E13,全是蓝系)。修法:`Ui::scheme()` 公开已注入的那份,
`interaction/disabled.rs`(两族合成 + 内容色)、`feedback/dialog.rs`(scrim)、
`feedback/action.rs`(禁用动作文字)改为从它取。

修完 Ink 下实测:禁用容器(12%)#292F28、按钮容器(10%)#252B24、遮罩 #0A0E0A(绿系中性,
R = B);Silver 档遮罩 #A9A9AA。旧三个蓝系值在 7 张深色图里出现次数均为 0。

回归门:`disabled_colors_follow_the_brand_board_not_the_tone_baseline`(按板合成 + 与 baseline
不等 + 深色档 G ≥ B)、`scrim_is_scrimalpha_precomposited_onto_the_canvas` 增加「不许退回 baseline」。

F2:`selection/{matrix,mod,checkbox,radio,geometry}.rs` 里「20×20 / rx2 / 外圈 20」回填成实际渲染的
「18×18 / rx4 / 外圈 18」(勾形比例不变 ⇒ 10.8dp),并按塔裁决把搜索药丸容器从
`SurfaceContainerHigh` 抬到 **`SurfaceContainerHighest`**(贴规格卡,与 filled 字段同底);
`search.rs` / `probe/search.rs` / gallery `inputs` 图注同步。展开的搜索面板与菜单是浮层,
仍留 High 档(已在文档里写明不是漏改)。

`interaction/disabled.rs` 加测试后 405 行 ⇒ 单测纯搬移到 `interaction/disabled/tests.rs`。
2026-11-11 01:49:14 +08:00
AskaEth f9877a89d8 fix(app-iced): 下载页页头「内容库」选中态反转(M229 复审 F3)
`pages/content.rs::section_switcher` 把 `on_content` 写死成 `true`,而下载页也调它,
于是版本管理页的页头把「内容库」画成选中、「版本管理」画成未选中。

改成由调用方给位置:下载页传 false、内容库页自己传 true;并加一条源码层面的调用点断言
(iced 的 Element 不可内省,判据只有一行,写错必红)。

不是 M229 引入的缺陷(M109 就有),按复审要求单独提交,不与收官换肤混在一起。
2026-11-11 01:49:14 +08:00
AskaEth db541c820e feat(app-iced): Ink Cat / Silver Cat 换肤 —— 双色板默认 + 规格卡组件几何(M229)
产品出厂外观从 tone 派生的 `Scheme::baseline` 切到品牌色板:深色 = Ink(淡绿)、
浅色 = Silver(钢蓝),D38 取色链(背景图 / 壁纸)仍然优先,取色失败回落到色板。

主题三档接进设置页「通用 → 主题」:Ink / Silver / 跟随系统(落盘仍是
`appearance.theme_mode` 的 dark / light / auto,不新增配置键)。

组件几何按规格卡对齐(M228 的 `inkcat` 登记项逐条落地):
- 卡片圆角 12 → 16;实例元信息卡改用新的 ELEVATED CARD(无描边、SurfaceContainer
  底、内距 24、上标注 11/600)
- Snackbar 48/4 → 52/16(双行 72)
- Checkbox 20/rx2 → 18/rx4;Radio 20 → 18 Ø(内点仍 10)
- 按钮加 XS~XL 五档(产品仍只用 S=40),内距改走 ControlSize::pad_x
- 新增 FAB 四态(40/56/80/160×56)与图标按钮第四式 outlined
- 语言与主题两行 chips 统一成规格卡 chip(32 高 / rx8)

`theme.rs` 原本正好 400 行,先把单测纯搬移到 `theme/tests.rs` 再加色板字段;
`app/tests.rs`、`settings/general.rs` 同样按纯搬移拆分守 400 行硬线。

验证:cargo fmt --all --check、cargo check -p unlucky-app-iced --all-targets、
cargo test --workspace(除 clippy 前的一轮 flake,见报告)、cargo clippy --workspace
--all-targets -- -D warnings、全仓 400 行扫描。视觉矩阵 42 张在
DevDocs/artifacts/m229/(gitignored,不进提交)。
2026-11-11 01:49:13 +08:00
AskaEth 4f5a76f238 docs(download): r2 复审返工——改名后的旧注释三处、视口高度口径如实分档、fade 单测改成对生产值断言(M227)
P2-1(三处改名后旧注释 + 一处口径,纯文档/注释,无像素变化,未重出图):
- `install.rs` 段标题「两个 Shared 分支」→「两支「面板先保预算」的分支(Shared / Narrow)」,
  两条子项分别标注 `(Shared)` / `(Narrow)`(改名后只剩一个 Shared,另一支已是 Narrow)。
- `options/view.rs` 的 `Shared` 条目删掉「(chips 档还可能有错误行)」——chips 档被 arm 1 接走
  给 `Narrow`,`Shared` 走不到错误行;`Narrow` 条目本来就写对。
- `tests/options_shot/chips.rs` 断言③ 的注释:实测这条断言在窗口 518–610 里命中的是
  y=550、宽 94px 的**实心「安装」药丸**,版本菜单容器上沿在 636–639(窗口外)——删掉
  「菜单容器的满宽底边」这个错误替代项,并把实测值写进注释。
- 滚动区高度口径:八审原文恢复为「1180×760 ≈96–104px」(署名真实性),另起一句写 M227 在
  tip 上的复量:1180×760 = 101px、1180×900 = 109px、窄档 = 120px,并注明 r2 量的 116px
  与 109px 只差「钉死区从哪一行算起」。`DevDocs/artifacts/m227/README.md` 同句一并改。

P2-2(fade 单测空转,r2 实测变异 STEPS=6 仍绿):
- 步数与 α 曲线提成 `pub(super) const VIEWPORT_FADE_STEPS` + `pub(super) fn step_alpha(i)`,
  `viewport_fade` 用它们;单测改成对**生产值**断言四条:步数 = 高度像素数(1px/条)、
  α 严格递增且除末条外 <1、相邻增量非递减(曲线要凸)、末条 = 1.0。

变异验证(自己跑,红过再还原):
- `VIEWPORT_FADE_STEPS` 12 → 6:`panicked … 步数必须等于高度像素数:12px 要 1px 一条,
  现在 6 条 ⇒ 2px/条`,MUTATION_EXIT=101;还原 12 后 GREEN_EXIT=0。
- `t.powf(1.6)` → `t.powf(1.0)`:`panicked … 第 2 条比前一条浓得更少…曲线要凸(t^1.6)`,
  MUTATION2_EXIT=101;还原后绿。

门禁(真实退出码,CARGO_TARGET_DIR=C:/Temp/ull-target/wt-227,-j 4):
fmt 0 / check -p unlucky-app-iced --all-targets 0 / test --workspace 0(67 个 ok,
app-iced lib 875 passed,四条像素守门 + fade 单测全绿)/ clippy --workspace --all-targets
-D warnings 0 / .rs 扫描 >400 行 0 个、<2 字节 0 个。
2026-11-10 23:07:20 +08:00
AskaEth 122accdd02 fix(download): 窄档滚动视口底部加 12px 淡出遮罩,去掉被硬裁的「空壳蓝条」(M227 返工)
D30 视觉审 hold 的必改项:滚动视口在 y=660 硬裁,把版本菜单的容器上沿切成 639–659 = 21px、
**零字形墨迹**的一条色块(深色下 (22,33,41) vs 画布 (9,21,28),CR 1.99),读起来像渲染错
而不是「下面还有内容可滚」。

改法(reviewer 推荐方案①,保留发现性):
- 新增 `options/fade.rs::viewport_fade` + `VIEWPORT_FADE_PX = 12`:12 条 1px、α 单调上升的
  **画布色**实色块拼成的斜坡(iced 0.14 没有渐变原语;1px 步进的差别在 1/255 量级);
- `options::section` 的窄档分支把滚动区包一层 `stack![滚动区, 底部对齐的遮罩]`,遮罩贴着
  视口内沿(≤660),压不到钉死区的实例名(≥668);
- 颜色取 `ui.layer(MaterialLayer::Canvas)`:浅色淡出到纸面、**深色淡出到墨底**(不是白雾),
  与它下方那块背景同色,所以两档都不新增颜色;
- 三栏档不铺这条遮罩:那里滚动区内容恒 76px < 视口 ≈118px,不会出现半截内容,铺了只是
  「画布盖画布」,反而会动 M222 已验的 1180 像素;
- `options/view.rs` 已 422 行、撞 400 行硬线,遮罩拆成 `options/fade.rs`(含一条几何自检单测)。

实测(720×760):
- 浅色 x=600 竖列 648 (211,229,245) → 659 (246,250,255) 单调升到画布色;深色 648 (55,72,85)
  → 659 (9,21,28),无白雾;中文与浅色逐值同串;
- 可见带 21px → 墨迹口径 18px + 底 12px 连续渐变(硬裁边消失,右侧滚动条仍在);
- 逐像素零回退:`77d675b`(遮罩前)vs 本轮 720 浅/深/中 + 700 守门,差异 bbox 只有
  (112,648)–(696,660) 这 12 行,其余像素一个都没动;1180×760 浅/深/中 与 1180×900 对
  M222 基线仍 **IDENTICAL**。

门禁:fmt ✓ / check ✓ / test --workspace(喂四张守门图)exit 0(app-iced lib 860 passed、
四条像素守门 + 400 行门全绿)/ clippy --workspace --all-targets -D warnings exit 0 /
.rs 全仓扫描 >400 行 0 个、<2 字节 0 个。

已知残留(本轮不动,按债务台账口径登记):menu 的 L2 阴影不随滚动视口裁切(压在实例名输入框上
约 6.9%,三栏档 fabric 同样存在)——根因在 `components::inputs::menu`,不在 M227 scope,
待专门 mission;finding `.tower/comms/findings/20261110-worker-m227-bug-l2.md`。
2026-11-10 23:07:20 +08:00
AskaEth a70d5f8010 fix(download): 窄档面板加专属预算,首屏露出 head+六枚 chip+说明、实例名回到 56px(M227)
720×760(chips 档)原先走 `TailShare::Shared`:面板与列表各吃 Fill 的一半,面板钉死区
(说明 + 菜单 + 实例名 ≈180px)把它吃穿 ⇒ 滚动区视口 0,head(版本 id + 安装按钮)、
六枚加载器 chip、说明一个像素都不在屏,实例名被挤成 35px 且降部被自身边框裁掉
(M222 D30 五审 `artifacts/m222/visual-verdict-r5-hold.md`)。

改法(把 M222 在 1180 档验证过的「面板先保预算、列表吃整行」落到窄档):

- `TailShare` 拆出第三支 `Narrow`(chips 档 + 面板占高度),`of()` 的三条 arm 与
  `Shared` 的机制注释按 r8 finding 重写(旧文档只写 M125 那一条条件);
- 新增 `CHIPS_STACK` 121 / `CHIPS_LIST_CHROME` 119 / `CHIPS_PANEL_BUDGET` 192 与
  `narrow_rows()`:720/760 下面板先拿预算,列表 = **132px = 3 整行**(M125 像素守门的下限);
- `options::section` 三分支:窄档钉死区**只留实例名**,head / 六枚 chip / 说明 / 版本菜单
  进滚动区(滚动区 120px ≥ 首屏 104,菜单露容器上沿 + 滚动条可见);
- `TailShare::list_height()` 把「列表吃多少」收到一处,两个装配点共用(三栏档行为不变)。

取舍:窄档列表 4 整行 → 3 整行;菜单从钉死改为可滚。理由(459px 里「钉住菜单」+「3 整行
列表」互斥)写在 `rows/tail.rs::CHIPS_PANEL_BUDGET`。

实测(tiny-skia 出图 + 自写 measure.py,DevDocs/artifacts/m227/):
720×760 分隔线 559→514、滚动区 0→120、head 539–578、chips 583–614(32px)、说明 623–634
(8.98:1 浅 / 10.83:1 深)、实例名 668–723 = 56px(inset 12/9/17);
700×760(options-fabric)实例名从「完全不在屏」回到 56px;四条像素守门全绿;
1180×760 浅/深/中 与 1180×900 **逐像素零变化**。
2026-11-10 23:07:19 +08:00
tower-worker a0f61d8bb5 feat(mods): 本地 Mod 列表与启停(M224 MODTOGGLE)
三层一次做完:core 读写目录 → presenter DTO/host → 实例详情页 MODS 区。
r1 复审后 rebase 到 main 71144df(M223 已合入),并修掉四条文档疵。

core(crates/core/src/addon/mods/)
- suffix:`.disabled` / `.old` 的**唯一判据**(四态枚举 + 展示名 + 双向改名目标名),
  收敛原先两份同构实现——`export/scan.rs::is_disabled_path` 与 `import/plan/paths.rs::suffix_variant`
  改为委派/再导出,签名与行为不变。
- list:单根语义(入口是 derive_game_dir 派生出的运行目录)列 `<run>/mods/`,不按扩展名过滤,
  符号链接/目录/怪名字逐条登记原因;`mods/` 本身是符号链接时拒(不跟随)。
- toggle:启停 = 在 `mods/` 里改名。五条守卫(裸文件名 / 目录可用 / 普通文件 / 不覆盖 / 不做
  copy+delete 兜底)+ 幂等;`.disabled` 只插在 `.old` 之前。

presenter
- dto/mods:ModsCommand / ModListView / ModFailureView(不拼文案,只给结构 + OS 原文)。
- host/mods:后台线程列目录与启停,运行目录走与启动同一份 D20 派生、不写任何设置;
  游戏在跑时 host 侧直接挡。
- state/mods:快照整体替换、迟到结果丢弃、在飞标记;`AppState::mods`。

app-iced(实例详情页)
- MODS 区(`pages/instances/mods.rs`):名字 + 大小 + `.old` 徽标 + Switch;运行中/在飞只读。
- 进入详情页即后台列一次(不走 3 s 观察窗,避免打红 M192 的「空闲不烧帧」守卫)。
- MODS 卡排在启动参数**之前**(详情列是滚动区,排在后面 1180×1000 下永远看不见)。
- i18n:19 条中英文案(`catalog/instances.rs` + 金表)。

rebase 冲突解法(三个文件,全是"两侧同段新增",都保留):
- `i18n/catalog/instances.rs` + `i18n/catalog/golden.rs`:M223 的 Adopt* 段在前、M224 的
  InstancesMods* 段在后(= `Key::ALL` 顺序 = 金表顺序)。
- `presenter/src/state.rs`:`adopt` 与 `mods` 字段/初始化/apply_core 分支都留;`tiles()` 归
  `state/tiles.rs`(本分支搬的),`apply_agent`/`agent_rail_visible` 归 `state/agent.rs`
  (M223 搬的)——两处搬移不重叠,state.rs 里都不再出现。

r1 四条文档疵:
1. `pages/instances/mods.rs` 模块文档:挂载位置写反("启动参数卡之后"→实际之前),并补上为什么;
2. `state/mods.rs`:引用不存在的 `CoreEvent::ModsChanged/ModsFailed` → 改成 `CoreEvent::Mods`
   + 文字说明 Ok 落快照 / Err 落具名失败;
3. `app/state/mods_shot.rs`:文档说"目录串取真实派生结果"但代码留空 → 改成"故意留空";
4. `app/state/refresh.rs::note_mods_failure`:文档说"连点两次看见两次红字"与去重机制不符 →
   改成按 `(实例, 文件, 大类)` 三元组去重(防刷屏),换文件/换实例/换大类仍会再弹。

顺带(r1 已确认的 main 红):`pages/download/options/view.rs:88-89` 两条
`clippy::doc_lazy_continuation` 只缩进修掉,文字未动。
2026-11-10 21:09:37 +08:00
AskaEth 44c00b4873 fix(app-iced): 测试助手改用 matches!,消掉 deny 级 clippy(复审 r2 P1-1)
`dispatched_adopt` 内层 `match (&**command, want) { … _ => false }` 被判
`match expression looks like matches! macro`(deny 级 ⟹ CI 的 `--workspace --all-targets
-D warnings` 必红;558fbf2 原树 `cargo clippy -p unlucky-app-iced --all-targets` 实测 exit 101)。

改成 `let AppCommand::Adopt(..) = command else { return false }` + 一条 `matches!`,
判据与原来逐字等价(四种命令种类 × 对应 slug)。同一处的注释补一句「为什么不能再写 match」,
免得下一个人改回去。

教训记账:r2 自验只跑了 fmt/check/test,漏了 clippy(T23「五步少一步等于那步不存在」);
本轮五步齐跑,退出码见完成报告。
2026-11-10 19:37:45 +08:00
AskaEth fa3fab9086 fix(app-iced): 复审 r1 三条(在飞守卫补 Back/Target、预检按版本过期、shell 文档如实)
P2-1(行为):`Busy::Importing` 的早退只盖了 Close。「返回」会派 Scan、「换目标档」会派
零写盘 Preview,两条都把 `adopt_epoch` +1,于是在飞的 Confirm 交卷时 Done 被代次丢掉——
实例写进去了,界面什么都没说(正是 Close 守卫要防的形状)。两处补同款早退,且守卫写在
改本地状态**之前**:挡下来的换档不该把界面改成 B 档而盘上写的是 A 档。

P2-2(显示陈旧):`Form::fresh_plan` 原来只按目标档过期。先选 1.20.1 + 版本目录档,再改选
1.19.4,目标档没变,旧预检的 notes(说的是 `versions/1.20.1`)会继续当新版本的后果原文画。
现在连 `plan.version_id == selected` 一起判;没选中版本时也返回 None。同处那句
「版本一致性由 Self::candidate 那一路保证(选中即重扫)」与代码不符(Select 只改本地字段、
不发命令),改成如实说明过期判定只能在这里做。

P2-3(文档):`app/adopt/shell.rs` 的「纯搬移:函数体一行未改」不成立——同一提交里卡壳几何、
页脚去重、预览摘要都改过。改成逐条列出改了什么与为什么。

用例(两条都是先写红再修,红时 panic 信息就是错在哪):
- back_and_target_are_ignored_while_the_registration_is_in_flight(记账后端断言「不许派
  Scan / 不许派 Preview」);红:`登记在飞时 Back 不许派 Scan(会把 Done 的代次顶掉)`;
- fresh_plan_expires_when_the_selected_version_changes;红:`换了版本之后旧预检必须过期
  (否则画的是 1.20.1 的后果原文)`。

顺带补图:`ULL_ADOPT_STATE` 加 `pick-selected` 一档(选中行底色 + 预检启用态),
出图 `DevDocs/artifacts/m223/adopt-pick-selected-light.png`(r1 复审说这格只有代码没截图)。
2026-11-10 19:37:45 +08:00
AskaEth 6ffbd51f60 feat(app-iced): 向导的出图种子 + 卡壳几何修正(M223 视觉自检)
D30 的截图通道在本机可用(`--software-renderer` 走 tiny-skia,1180×760 落盘),所以不再把视觉
证据留空:新增 `ULL_ADOPT_STATE`(与 `ULL_JAVA_STATE` / `ULL_AI_STATE` 同一族,**只在
`--screenshot` 下生效**),可把向导摆到七种状态之一(pick / empty / preview / shared / failed /
done + 深色),于是出图不靠「赌异步时序」。

顺带修掉三处只有出图才看得见的问题:

1. **卡壳把页脚裁掉了**:内容高于 `max_height` 时 iced 直接裁,第一版 PICK 段的「实例名」与
   按钮整排不可见。现在列表段(Pick/Failed)定高 + 中间区可滚、页脚固定;短内容段
   (Preview/Done)按内容收高,不留一大片空白;
2. **两枚一模一样的「关闭」**:Failed/Done 的主按钮就是关闭,文字关闭不该再出现;
3. Preview 段少了实例名一行,且标题下的摘要重复了「共用目录 · 共用游戏目录」。

夹具只造候选/问题/运行目录这些**版面数据**,后果原文仍由 core 的 `target_notes()` 现算,
图证明版面与文案、不证明 core 判据(见 `DevDocs/artifacts/m223/README.md` 的真/假表)。

为守 400 行(`components::gate` 实测 412 行红),卡壳/标题/页脚纯搬移到 `app/adopt/shell.rs`。
2026-11-10 19:37:44 +08:00
AskaEth a36e17a5d3 fix(app-iced): 点选共享档立刻显示后果原文,登记在飞时不吃关闭(M223 自检)
两处自己复审抓到的问题:

1. 契约 §5.2 要求 Shared/VersionDir 被**点选**就展示 target_notes() 原文,而上一版要等
   用户再点一次「预检」才出现。现在点选这两档会顺手发一次**零写盘**的预检,结果落在
   Pick 段当提示读(不把用户甩到确认页:确认仍由显式的「预检」按钮触发,Form::checked)。
   改档之后旧 plan 因目标档不一致而不再显示(Form::fresh_plan),避免画上一档的后果原文。
2. 「关闭」在登记在飞时被忽略:core 那条线程已经开跑,关面板会让 Done 事件被代次丢掉,
   用户看到的就是「实例写进去了,界面什么都没说」。

用例:a_foreign_target_shows_its_notes_without_jumping_to_confirm /
choosing_a_foreign_target_previews_but_waits_for_the_user /
closing_is_ignored_while_the_registration_is_in_flight。
2026-11-10 19:37:43 +08:00
AskaEth 41caae3571 feat(app-iced): 实例页「导入已有版本」向导(M223)
页头「导入」按钮(以前是占位动作,点了只打日志)现在打开四段向导:

| 段 | 画什么 |
|---|---|
| Pick | 候选版本(可导入的才可点)+ 目标三档 chip + 实例名(缺省 InstanceDir) |
| Preview | 生效运行目录(回程给的)+ 隔离档 + target_notes() 原文逐条 |
| Failed | 稳定码 + problems 逐条(不折叠成一句「导入失败」) |
| Done | 实例名 + 运行目录 + 提示原文 |

- 向导是叠在页面之上的一层(遮罩 + 居中卡),不搅乱 list-detail 的版式判据;
- 表单态在 App(app/adopt.rs 的 Form),事件真相在 presenter 的 AdoptState;
- 在等回程时主按钮禁用(连点「确认」不会写两条实例);
- i18n 中英目录 + 金表补 27 条向导文案(实例域末尾追加)。
2026-11-10 19:37:42 +08:00
AskaEth a16aadd467 docs(app-iced): 按 TailShare::of 的真实分支清扫下载页面板注释(r7 三条 + 全量扫)
(i) 上轮新引入的错误声明 rescope:错误行「恒为 None / 滚动区只有 head+chips」只在**三栏档 +
    带版本列表**这一支成立(has_loader_versions() 要求 problem.is_none())。**chips 档**(可用宽
    < 600,TailShare::of 对 Options/Install 无条件 Shared)里 problem 可以是 Some(静态不兼容 /
    列表取不到 / 空表三种),错误行会进滚动区且可能被滚走;show_versions() 为假时钉死区只剩
    实例名。install.rs 与 options/view.rs 都按这两支分别写清,并注明该行为域由 M227 重设计。
(ii) install.rs 的「显式给对半分档标志」「对半分:面板吃满自己那一半」改成预算优先口径
    (与同文件十行上方「不是早期的对半分」不再矛盾)。
(iii) rows.rs 三处:三栏档「照旧钉死」限定为「**不带版本列表**的选项区」;`content` 的 share
     说明改成引用 TailShare::of 的真实两支,不再复述「三栏钉死 / chips 各吃一半」;
     「只有对半分那一档才按整行取高」改成「只有 Shared(面板先保预算)那一档」。

全量扫附带修正:tail.rs 的 ROW_PITCH / PANEL_BUDGET 账(760 例改成 176/437、900 改成 569;
预算拆成滚动区 76 + 钉死区 153);options/model.rs 的 has_loader_versions 文档改成预算口径。

零代码零测试变更。
2026-11-10 19:34:15 +08:00
AskaEth 409c742aa8 docs(app-iced): 扫掉 r6 的三处文档残留(B 结构合入前最后一改)
(a) install.rs 的 panel/panel_with 文档按当前结构重写:`shared` 不再是「对半分」(现为
    面板先保预算 + 列表按整行,见 rows/tail.rs::whole_rows);滚动区实际只有 head + 六枚 chips
    (菜单 9358668 进钉死区、说明 a6f7974 进钉死区),钉死区 = 说明或标签 + 菜单 + 实例名;
    并写明错误行在 Shared 档恒为 None(静态不兼容时不画版本菜单、走不滚动那条路)。
(b) options/view.rs 里「标签与说明都搬到钉死区」自相矛盾:改成「**说明**搬到钉死区;
    『安装选项』标签在 Shared 档**删掉**(八审裁决),Pinned 档保留」。
(c) 同文件 section() 文档里「滚动区溢出时被滚走的是错误行」描述的是不可达情形:改成
    「滚动区只有 head + chips,用户可见内容不会被滚走(错误行在 Shared 档恒为 None)」。

零代码零测试变更。
2026-11-10 19:34:15 +08:00
AskaEth 33d05689e8 fix(app-iced): 说明只画一份(删滚动区重复)+ 口径三处改真
八审唯一阻断:中性说明画了两份(滚动区 546–557 与钉死区 576–587 像素级相同、重影)。
删掉滚动区那份,钉死区保留;01/02/05 与 900 的说明墨迹带现在只有一条(576–587 / 716–727)。

口径三处:
1. `section` 文档的滚动区内容改成实际(head + 六枚 chips + 错误行);
2. 旧的「≈150px / 无需滚动」改成八审实测(可见 96–104px、内容 76px、head 墨迹 462–501、
   chips 506–537 四角闭合);
3. 钉死区注释「标签与说明都搬到钉死区」改成事实:Shared 档的「安装选项」标签是**删掉**的
   (设计决定,八审裁决),只有说明是搬下去的;Pinned 档仍保留标签。
2026-11-10 19:34:15 +08:00
AskaEth e2e3c39f61 docs(app-iced): 扫掉 r5 的五处文档漂移(返工链留下的旧描述)
1. install.rs 模块文档:删掉「钉页脚本轮没做」(6f87c8b 已做:钉死区现在是菜单 + 实例名),
   三层滚动的第 3 层改引 `version_menu_height`(VERSION_ROWS 已改),并写明「选项区自己滚」
   这层是 M222 之后才有的、`Options` 态不再套外层 scrollable(只有 `Install` 态用)。
2. rows.rs:五个 loader chip → 六枚;「实例名那一格滚一下就到」→ 已钉死;分法描述改成
   「面板先保预算 + 列表按整行」(PANEL_BUDGET / whole_rows)。
3. rows/tail.rs 的 TailShare 表:两半各 Fill → 面板先吃预算;补上「带加载器版本列表」这一例外行;
   判据注释改成「像素由 window_height 算」(view_sized 已接)。
4. rows/tail.rs 的 M222 分支注释:删「仍欠一项 S1b」(107d619 已修、26ec3f4 已换预算公式),
   写清现在 1180×760 = 176px/4 行、900 = 308px/7 行。
5. options/view.rs 控件树示意:改成当前真实顺序(滚动区 = head + chips;钉死区 = 说明/标签 +
   菜单 + 实例名),并补上钉死档另走一条路的说明。
2026-11-10 19:34:15 +08:00
AskaEth 7ea3dbf8f0 fix(app-iced): 方案 B——说明与「安装选项」标签撤出滚动区,滚动区只装控件
六审算术:滚动视口 103px < 内容 130px,说明 0 像素 + chip 底边框被裁 2 行。方案 C(标签并排)
不可行——六枚 chip 已占 404px、正文列 424px,并排必横向溢出裁掉第六枚,等于用一个已完成项
换另一个。按塔授权走 B:

- **滚动区**(仅 Shared 档):head + 六枚 chips,间距 X2→X1;
- **钉死区**:说明行(有说明时占「加载器版本」标签那一行,否则显示标签)+ 版本菜单 + 实例名。
  钉死区高度不变(两者都是一行),列表仍 4 行;
- **钉死档保持旧结构**:把标签撤出会让原版面板矮 26px、列表涨到 302px(6.86 行、切出半行),
  实测把 `04` 从 531 挪到 563 —— 第一版就是这个坑,已按此回退。

实测:01/02/05 分隔线 437、列表 **176px = 4.00 整行**、说明墨迹带 y 576–587(真像素);
04 **531 未变**;03/06 268 未变;900 档 569 / 308px = 7.00 整行。
2026-11-10 19:34:14 +08:00
AskaEth cb2cffb3ca fix(app-iced,i18n): 分高改「面板先保预算」+ 说明压成单行(D30 五审一次性重分配)
五审:菜单文字/720 列表/44-97 标定/04 都过,但滚动区被压到 82px(宽)/24px(窄)——宽档切
「安装选项」字身、六枚 chip 0 像素。塔裁决停止逐轮调顺序,一次性重分配:

1. **分高从「对半分」改成「面板先保预算」**:`PANEL_BUDGET = 300`(滚动区 128 + 钉死区 136),
   `whole_rows = floor((column − LIST_CHROME − PANEL_BUDGET)/44) × 44`。
   1180×760:580−97−300 = 183 ⇒ **176px = 4 整行**(原 5 行),面板 307px ⇒ 滚动区 ≈150px;
   1180×900:323 ⇒ **308px = 7 整行**(未变)。720 档走 `None`(Fill),未动。
2. **说明压成单行**(`DownloadOptionsLiteLoaderVanillaOnly`):两行 28px 是滚动区最大的可压项。
   en `Vanilla parent only — Forge goes through the CLI` / zh `只叠原版;叠 Forge 请走命令行`;
   完整命令行说法留在 `ull help install`。单测断言随之改成「含该句 + 只有一行」并写明原因。

文档按六审要求改口:滚走的是说明/错误行(非控件);说明变单行后 760 档滚动区已能装下
head+标签+chips+单行说明,**无需滚动**。
2026-11-10 19:34:14 +08:00
AskaEth 6eb7132ab0 fix(app-iced): 加载器版本菜单搬进钉死区(裁切位置由钉死区高度决定,不是首屏分配)
四审点破根因:面板的裁切 y 由**钉死区高度**决定、与窗宽无关(1180 与 720 都切在同一处),
所以上一轮「把说明挪到菜单之后」只是换了被切的对象——菜单容器 642–706 只露 18px、条目文字
一个都没画,说明整块不可见。

按 tower 裁决改成结构方案:
- **滚动区**:head(版本 id + 安装按钮)+ 加载器 chips + 中性说明 + 错误行;
- **钉死区**:加载器版本菜单(标签 + 菜单,按条目数占高)+ 实例名输入框。

菜单不再依赖首屏剩余高度分配,可达性由结构保证;溢出时被滚走的是说明/错误行(非控件)。

实测(未变的部分也一并核对):01/02/05 分隔线 **481**、列表 **220px = 5 整行**;04 对照 **531 未变**;
03/06 窄窗 **268 未变**(本轮结构改动没有挤动列表);900 高加测 569 ⇒ **308px = 7 整行**。

文档按四审改正:`section` 的文档不再写「首屏能装下菜单/切点不再切字」,改成「裁切位置由钉死区
高度决定」;面板结构写清 head 在滚动区、菜单与实例名在钉死区。
2026-11-10 19:34:14 +08:00
AskaEth 0ca28d24ff fix(app-iced): 版本行节距/列表 chrome 标定重算(44/97)+ 中性说明挪到版本菜单之后
三审给了独立量测,两处都按它的数字收敛:

1. `ROW_PITCH` 278/6=46.33 → **44.0**(278 里含列表与面板之间的 8px `Space::X2`);
   `LIST_CHROME` 93 → **97**(列顶 156 → 列表顶 253)。1180×760 分隔线 492 → **481**、
   列表视口 **220px = 5 整行**;1180×900 加测 = 308px = 7 整行,三审报的 900 高残影不复发。
2. S7 钉页脚引入的新问题:面板上半块的滚动切点压在字形上(宽窗切「加载器版本」标签上沿、
   窄窗把 chips 切成 4px 条)。把中性说明从菜单**之前**移到**之后**——它两行高,排前面正是把
   唯一的加载器专属控件顶出首屏的元凶。

顺带纠正一处文档:面板结构不是「头部 + 可滚上半 + 钉页脚」,head(版本 id + 安装按钮)是上半块
的第一个子项、**安装按钮也在滚动区**,只有实例名是钉死的。
2026-11-10 19:34:14 +08:00
AskaEth 954281d71a fix(app-iced): 版本列表在三栏对半分档按整行取高(S1b/S9)
列表视口高度不再是「Fill 剩下的」:正文列高度由仓里已有的 `frame::body_height` 算出,
经 `view_sized` → `body` → `rails::three_column` → `rows::content` 传下来,Shared 档据此
`floor(((h − LIST_CHROME)/2) / ROW_PITCH) * ROW_PITCH` 取整行。

实测(1180×760):列表 245px → **239px**(分隔线 498 → 492,腾出的 6px 归面板),
`04` 对照**未变**(531 / 278px),720 窄窗**未变**(本来就是 4.0 整行、底缘 0 墨迹)。

两处修正写在提交里:
1. 第一版给所有三栏场合取整,把 `04` 也缩到 239px ⇒ 收窄为只在 `TailShare::Shared` 取整;
2. `rows.rs` 涨到 419 行撞 400 行门禁 ⇒ `TailShare` 与三个标定量纯搬移到 `rows/tail.rs`,
   `rows.rs` 回到 355 行(门禁 `components::gate` 与全仓扫描都绿)。

`view` 保留 4 参数(`#[cfg(test)]`,只有单测用),产品路径走 `view_sized`。
2026-11-10 19:34:13 +08:00
AskaEth f29ca609ec fix(app-iced,i18n): 实例名钉成面板页脚(S7)+ 中文占位符还原(S8)+ 注释实测值(S6)
D30 r2 视觉二审四条:

1. **S7(新回归)**:S1 的对半分让面板变矮之后实例名被挤出 1180×760 首屏。按 `install.rs`
   早已登记的方案改成**可滚上半块 + 钉死页脚**:`install::panel_with(.., shared)` →
   `options::section(.., scroll)`;`rows::content` 对 `PanelState::Options` 不再整块套
   scrollable(那层既把实例名卷进滚动区,也在窄窗产生第二条并列滚动条)。页脚不参与滚动,
   可达性由结构保证,S2 随之关闭。
2. **S8**:`catalog/download.rs` 与金表同一行的中文命令占位符 `<Forge 版本 id>` 照抄不可执行,
   还原成 `<forge id>`(两处一起改,金表顺序未动)。
3. **S6**:`rows.rs` 里「两半各 ≈215px / 列表 4.6 行」的估算换成实测(列表 245px = 5.29 行、
   面板 237px、分隔线 y=498),并写明 S1b 仍欠一项与其正确落点。

**S1b/S9(列表底缘整行)本轮没修成**:试过 `responsive` 拿「分给列表的高度」再 floor,
实测无效(`Fill` 子项上拿到非确定高度,量测显示列表区最后墨迹行仍在分隔线前一行),已回退,
不留无效代码。正确落点是把列表可用高度从 `pages/download.rs` / `rails.rs`(都在 scope 外)
传进 `rows::content`。

测试:`-p unlucky-app-iced` 862 passed、`-p unlucky-i18n` 36 passed(金表改动后仍绿)。
2026-11-10 19:34:13 +08:00