fix(settings): 改口逐实例覆盖的事实陈述,并把第三条守卫换成行为断言
复审 r1 的两条 P2: P2-1(`settings/mod.rs` 的 `form` 文档):原先写「『逐实例覆盖』既没有数据源也没有 消息落点」——这是错的。逐条核过:真控件在实例详情页的「启动参数」区(M157 `pages/instances/launch_args.rs`,由 `pages/instances/detail.rs` 渲染),落盘走 `app/update/instances.rs` 的 `AppCommand::set_launch_args` → presenter `host/launch_args.rs` 的 `resolved`/`apply`(写回 `config/instances/<id>.json`), 回落与落位在 core 的 `game/launch_overrides.rs` / `launch/overrides.rs`。真正无人产出 的只有 UI 变体 `Action::AddOverride`。改口后的事实:它是**放错了地方**,不是没接。 P2-2(守卫):删掉按名字扫源码的两条(`empty_state`/`overrides` 子串、`instance` 子串)—— 前者的假警报会挡住「某组确实需要自己的空态」,后者抓的根本不是它声明的不变量(真正让实例 选择进不来的是 `settings::view` 的签名),且日后一句正当英文注释就能把它弄红。换成: - 判据 ①(保留)守**收尾**:`form()` 里 `stack = stack.push(` 恰好一处、且以 `frame::scroll(ui, stack)` 收尾(整段重写让计数变 0 也红); - 判据 ②(新增,行为断言)守**生产**:把每组一列的循环抽成 `section_columns()`, 测试真调它数返回值——窄窗 == `GROUPS.len()`,单选各组 == 1,换任何名字都躲不掉。 文档里的「抓不住」段也不再指向静态图(静态图只能证明「现在没有」)。 `form` 的拼装形状没变(循环 + `frame::scroll` 收尾),分组表、分发顺序、正文内容一字未动。
This commit is contained in:
1 parent
7fdebba961
commit
e5a4b9bbd1
2 files changed
+117
-56
No files matched your search
@@ -225,38 +225,80 @@ fn setting_row<'a>(ui: &'a Ui, label: &'a str, value: &'a str) -> Element<'a, Me
|
||||
/// `settings/mod.rs` 的 `form()` 在分组循环之后**无条件**叠了一件固定 320dp 高的空态位
|
||||
/// (M84 骨架期的占位,M99 纯搬移时原样留下)——于是任何一组下面都长出同一张卡。
|
||||
///
|
||||
/// 这三条钉住的就是「尾部不许再多一件」,以及「本页不读实例选择」(于是「无选中实例」与
|
||||
/// 「有选中实例」两种状态下本页的渲染路径逐字相同)。
|
||||
/// ## 两条判据怎么合起来咬死它
|
||||
///
|
||||
/// 正文由两半拼成:`mod.rs` 的 [`section_columns`](super::section_columns) **生产**「每组一件」的列,
|
||||
/// `form` 只把它们堆起来再滚出去。于是:
|
||||
///
|
||||
/// - [`the_form_stops_at_its_last_group`] 守**收尾**:`form()` 里 `stack = stack.push(` 恰好一处、
|
||||
/// 且以 `frame::scroll(ui, stack)` 收尾 —— 尾部多一件、或整段换成别的拼装(push 数变 0)都红;
|
||||
/// - [`the_form_holds_exactly_one_column_per_group`] 守**生产**:真调 `section_columns` 数它交出的
|
||||
/// 列数(窄窗 == `GROUPS.len()`,单选各组 == 1)—— 多塞一件就红,**不管那件叫什么名字**。
|
||||
///
|
||||
/// 两半合起来 = 「正文恰好这些列,没有尾巴」。
|
||||
///
|
||||
/// ## 为什么住在 `frame.rs` 而不是 `pages/settings/tests/`
|
||||
///
|
||||
/// `tests/` 归四条 section 线所有(各改各的),而这里守的是**本文件与 `mod.rs` 之间的分工**:
|
||||
/// 壳层不提供空态位、分发点不在尾部多堆一件。判据要是哪天挪进 `tests/`,也请原样保留这三条。
|
||||
/// 壳层不提供空态位、分发点不在尾部多堆一件。判据要是哪天挪进 `tests/`,也请原样保留这两条。
|
||||
///
|
||||
/// ## 抓得住 / 抓不住(据实写)
|
||||
///
|
||||
/// 抓得住:把那张卡(或**任何**尾部元素)加回 `form()` —— 变异实测:在 `frame::scroll` 之前插一句
|
||||
/// `stack = stack.push(...)`,[`the_form_stops_at_its_last_group`] 立刻红;把空态件按名字写回去,
|
||||
/// [`the_form_never_renders_an_empty_state`] 红;把某件东西挂到「当前选中实例」上,
|
||||
/// [`the_settings_page_never_reads_the_installation_selection`] 红。
|
||||
/// 抓不住:**另起一套名字**重画的空态(文本断言不是行为断言)。那一档只有真图能看出来 ——
|
||||
/// 证据在 `DevDocs/artifacts/m196/`(Java / 助手 × 1180×760 / 1180×1100)。
|
||||
/// 抓得住(三种变异都实测过):①按原样把卡加回循环之后(`stack = stack.push(...)`)⇒ 收尾那条红;
|
||||
/// ②改成「塞进列清单」(`section_columns` 里多 push 一件)⇒ 列数那条红,换任何名字都躲不掉;
|
||||
/// ③把拼装整段重写成别的形状(不再用上面那个惯用法)⇒ 收尾那条数的 1 变成 0,同样红。
|
||||
/// 抓不住:**某一组自己的列里**换内容(比如某个分组真的需要自己的空态)。那不是本缺陷——本页允许
|
||||
/// 组内有空态,前提是它长在该组的列里、用本页语义的文案。`DevDocs/artifacts/m196/` 那组图是**当时**
|
||||
/// 的证据,不是将来的防线(静态图只能证明「现在没有」)。
|
||||
#[cfg(test)]
|
||||
mod no_stray_empty_state {
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
use unlucky_design::typography::Platform;
|
||||
|
||||
use crate::pages::update::Update;
|
||||
use crate::theme::{Families, Ui};
|
||||
|
||||
use super::super::{demo_groups, section_columns, Language, States, GROUPS};
|
||||
|
||||
/// 设置页的分发点(本文件是它的被调用方)。
|
||||
const FORM: &str = include_str!("mod.rs");
|
||||
|
||||
/// `form()` 的函数体:从签名取到文件尾(它是 `mod.rs` 最后一件)。
|
||||
const FAMILIES: Families = Families {
|
||||
text: "Segoe UI",
|
||||
display: "Segoe UI",
|
||||
mono: "Consolas",
|
||||
};
|
||||
|
||||
/// `form()` 的函数体:从它的签名取到下一个函数(`section_columns`)的签名之前。
|
||||
///
|
||||
/// 只取 `form` 自己:`section_columns` 里面有 `columns.push(`,那是另一条判据管的事。
|
||||
fn form_body() -> &'static str {
|
||||
let start = FORM
|
||||
.find("fn form<'a>(")
|
||||
.expect("settings/mod.rs 里找不到 form():分发点被改名了,本守门要跟着改");
|
||||
&FORM[start..]
|
||||
let rest = &FORM[start..];
|
||||
let end = rest
|
||||
.find("\nfn section_columns")
|
||||
.expect("settings/mod.rs 里找不到 section_columns():列生产被改名了,本守门要跟着改");
|
||||
&rest[..end]
|
||||
}
|
||||
|
||||
/// 判据 ①:循环之后**没有**任何额外的 `push`,且函数以滚出去收尾。
|
||||
fn ui() -> Ui {
|
||||
Ui::with_platform(SchemeFlavor::Light, Platform::Windows, FAMILIES)
|
||||
}
|
||||
|
||||
/// 演示 / harness 路径那份只读状态(与 `pages::page` 同源,不伪造第二份)。
|
||||
fn states() -> States<'static> {
|
||||
States {
|
||||
appearance: None,
|
||||
groups: demo_groups(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 判据 ①(**收尾**):循环之后没有任何额外的 `push`,且函数以滚出去收尾。
|
||||
///
|
||||
/// 循环体里只有一件(分组卡),所以「`stack = stack.push(` 恰好一次」等于「尾部空着」。
|
||||
/// 循环体里只有一件(一组一列),所以「`stack = stack.push(` 恰好一次」等于「尾部空着」;
|
||||
/// 拼装被整段重写成别的形状时计数变 0,同样红。
|
||||
#[test]
|
||||
fn the_form_stops_at_its_last_group() {
|
||||
let body = form_body();
|
||||
@@ -274,38 +316,35 @@ mod no_stray_empty_state {
|
||||
);
|
||||
}
|
||||
|
||||
/// 判据 ②:`form()` 不认任何空态件(名字层面)。
|
||||
/// 判据 ②(**生产**):真跑 `section_columns`,量它到底交出几列。
|
||||
///
|
||||
/// 抓得住「把那张卡原样加回来」;抓不住换名字重画的(见本模块文档)。
|
||||
/// 这是行为断言:调的是产品路径上那个真函数,**不认任何名字**。谁往正文里多塞一件(M196 那张
|
||||
/// 「没有逐实例覆盖」的卡当初就是这么塞的),列数就不等于组数。
|
||||
///
|
||||
/// 为什么不走 `tests/scroll_fit.rs` 那条「`()` 渲染器真跑布局」的路:`form` / `section_columns`
|
||||
/// 交出的是**产品渲染器**的 `Element`,要按那条路量子节点数,得把 `general` / `update` / `ai`
|
||||
/// 三条线的 `body` 一并泛型化(它们不在本 mission 的 scope 内)。量列数拿到的是同一个不变量,
|
||||
/// 且不需要泛型。
|
||||
#[test]
|
||||
fn the_form_never_renders_an_empty_state() {
|
||||
let body = form_body();
|
||||
for needle in ["empty_state", "EmptyState", "overrides"] {
|
||||
assert!(
|
||||
!body.contains(needle),
|
||||
"`form()` 里出现了 {needle:?}:设置页永远有内容(总有一组被选中),\
|
||||
空态件是实例 / 下载 / 日志页的东西(12b §15 的挡项就是不设空态的页),\
|
||||
设置页不该借它来充数"
|
||||
fn the_form_holds_exactly_one_column_per_group() {
|
||||
let ui = ui();
|
||||
let update = Update::default();
|
||||
|
||||
let narrow = section_columns(&ui, None, states(), Language::default(), &update);
|
||||
assert_eq!(
|
||||
narrow.len(),
|
||||
GROUPS.len(),
|
||||
"窄窗(按组堆叠)的正文应当是「每组一件」:多出来的那一件就是 M196 那张卡的位置"
|
||||
);
|
||||
|
||||
for index in 0..GROUPS.len() {
|
||||
let single = section_columns(&ui, Some(index), states(), Language::default(), &update);
|
||||
assert_eq!(
|
||||
single.len(),
|
||||
1,
|
||||
"supporting-pane 只该画被选中的那一组(下标 {index}),实际拿到 {} 列",
|
||||
single.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 判据 ③:本页**不读实例选择** —— 于是「无选中实例」与「有选中实例」两种状态下,
|
||||
/// 设置页走的是同一组函数、同一拼装顺序,那一张卡在两种状态下都不可能出现。
|
||||
///
|
||||
/// 抓得住:谁把某件东西改成「按当前选中实例决定画不画」(那正是这张卡当初被误读成
|
||||
/// 「实例详情的空态」的那条路)。
|
||||
#[test]
|
||||
fn the_settings_page_never_reads_the_installation_selection() {
|
||||
let needles = [concat!("inst", "ance"), concat!("Inst", "ance")];
|
||||
for (name, source) in [("mod.rs", FORM), ("frame.rs", include_str!("frame.rs"))] {
|
||||
for needle in needles {
|
||||
assert!(
|
||||
!source.contains(needle),
|
||||
"{name} 里出现了 {needle:?}:设置页不读实例选择(`settings::view` 的形参里\
|
||||
没有 `Model`)。一旦本页开始按选中实例分化,M196 那张卡就有回来的路"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -304,20 +304,19 @@ pub fn view<'a>(
|
||||
|
||||
// --------------------------------------------------------------- 内容分发
|
||||
|
||||
/// 表单列(正文列宽 ≤424)。`only = Some(i)` 只渲染一组(supporting-pane),
|
||||
/// `None` 渲染全部(窄窗:分组导航退化成按组堆叠)。
|
||||
/// 表单列(正文列宽 ≤424)。`only = Some(i)` 只渲染一组(supporting-pane),`None` 渲染全部(窄窗按组堆叠)。
|
||||
///
|
||||
/// ## M196:正文到最后一组就结束,尾部**什么**都不许挂
|
||||
///
|
||||
/// 这里原先在循环之后无条件叠了一张「没有逐实例覆盖」的空态卡(M84 骨架期的占位,M99 纯搬移时
|
||||
/// 原样留着),于是**每一组**下面都长出同一张卡:用户在 M174 的助手页与 M175 的 Java 页都看到了它
|
||||
/// (两张图 1180×1100、卡片位置完全一致、说明被窗口下沿截断)。设置页永远有内容(总有一组被选中),
|
||||
/// 「逐实例覆盖」既没有数据源也没有消息落点(全仓没有它的渲染点与处理器),所以它不是本页的空态:
|
||||
/// **删掉,不挪给任何一组**。守门见 `frame.rs` 末尾的三条用例。
|
||||
/// 这里原先在循环之后无条件叠了一张「没有逐实例覆盖」的空态卡(M84 骨架期的占位,M99 纯搬移时原样
|
||||
/// 留着),于是**每一组**下面都长出同一张卡(用户在 M174 的助手页与 M175 的 Java 页都看到了它:两张图
|
||||
/// 1180×1100、位置一致、说明被窗口下沿截断)。**删掉,不挪给任何一组。**
|
||||
///
|
||||
/// **分发点**:一组的正文由它自己文件里的 `GROUP.body` 决定——`Some(f)` 让 `f` 自己去
|
||||
/// [`States`] 里取它要的数据(`None` 返回值 ⇒ 退回演示行),`None` 直接画演示行
|
||||
/// ([`frame::demo_card`])。这里不出现任何一个分组名,也不比较下标,更不认识任何一组的字段。
|
||||
/// 它不是「还没接的活」,是**放错了地方**:逐实例覆盖(内存 / Java 运行时 / JVM 参数)的真控件在
|
||||
/// **实例详情页**的「启动参数」区(M157 `pages/instances/launch_args.rs` → `AppCommand::set_launch_args`
|
||||
/// → presenter `host/launch_args.rs` 的 `resolved`/`apply`,写回 `config/instances/<id>.json`;回落与
|
||||
/// 落位在 core 的 `game/launch_overrides.rs` / `launch/overrides.rs`)。设置页拿不到那份快照,也不该
|
||||
/// 重复一份([`view`] 的形参里没有 `Model`);真正无人产出的是 UI 变体 `Action::AddOverride`(死消息)。
|
||||
fn form<'a>(
|
||||
ui: &'a Ui,
|
||||
only: Option<usize>,
|
||||
@@ -325,9 +324,31 @@ fn form<'a>(
|
||||
language: Language,
|
||||
update: &'a Update,
|
||||
) -> Element<'a, Message> {
|
||||
let catalog = language.effective.catalog();
|
||||
let mut stack = column![].spacing(ui.space(Space::X6)).width(Length::Fill);
|
||||
|
||||
for section in section_columns(ui, only, states, language, update) {
|
||||
stack = stack.push(section);
|
||||
}
|
||||
|
||||
frame::scroll(ui, stack)
|
||||
}
|
||||
|
||||
/// 正文里**每组一件**的列(该组标题 + 正文),顺序 = [`GROUPS`] 顺序,只含被选中的那些。
|
||||
///
|
||||
/// **分发点**:正文由 `GROUP.body` 决定——`Some(f)` 自己去 [`States`] 取数(`None` 退回演示行);
|
||||
/// 这里不出现任何分组名、不比较下标、也不认识任何一组的字段。抽成独立函数是为了能被**行为断言**
|
||||
/// 量到(`frame.rs` 的 `the_form_holds_exactly_one_column_per_group` 数它的列数,换名字也红);
|
||||
/// 收尾留在 [`form`],由 push 计数守住——两半合起来 =「正文恰好这些列,没有尾巴」。
|
||||
fn section_columns<'a>(
|
||||
ui: &'a Ui,
|
||||
only: Option<usize>,
|
||||
states: States<'a>,
|
||||
language: Language,
|
||||
update: &'a Update,
|
||||
) -> Vec<Element<'a, Message>> {
|
||||
let catalog = language.effective.catalog();
|
||||
let mut columns = Vec::new();
|
||||
|
||||
for (index, group) in GROUPS.iter().enumerate() {
|
||||
if only.is_some_and(|selected| selected != index) {
|
||||
continue;
|
||||
@@ -354,7 +375,7 @@ fn form<'a>(
|
||||
.unwrap_or_else(|| frame::demo_card(ui, group.rows))
|
||||
};
|
||||
|
||||
stack = stack.push(
|
||||
columns.push(
|
||||
column![
|
||||
frame::text_role(
|
||||
ui,
|
||||
@@ -369,9 +390,10 @@ fn form<'a>(
|
||||
card,
|
||||
]
|
||||
.spacing(ui.space(Space::X2))
|
||||
.width(Length::Fill),
|
||||
.width(Length::Fill)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
frame::scroll(ui, stack)
|
||||
columns
|
||||
}
|
||||
Reference in new issue
Block a user