Merge branch 'feat/m196-straycard-settings-page-renders-for'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
This commit is contained in:
commit
98262d3835
4 files changed
+172
-58
No files matched your search
@@ -1,4 +1,4 @@
|
||||
//! 设置页骨架:页头、左列分组导航、表单列的滚动区、演示卡片、空态位。
|
||||
//! 设置页骨架:页头、左列分组导航、表单列的滚动区、演示卡片。
|
||||
//!
|
||||
//! ## 为什么这里还再导出一批 `pages::frame` 的名字
|
||||
//!
|
||||
@@ -8,8 +8,8 @@
|
||||
//! 这不是新 API,只是一条转发(新增控件仍归 [`crate::pages::frame`])。
|
||||
|
||||
pub(super) use super::super::frame::{
|
||||
card, divider, empty_state, glyph_box, left_aligned, page, panes, scroll_style, single_column,
|
||||
text_action, text_role, Header, ICON_PX, SCROLLBAR_MARGIN, SCROLLBAR_WIDTH,
|
||||
card, divider, glyph_box, left_aligned, page, panes, scroll_style, single_column, text_action,
|
||||
text_role, Header, ICON_PX, SCROLLBAR_MARGIN, SCROLLBAR_WIDTH,
|
||||
};
|
||||
|
||||
use iced::widget::{column, container, mouse_area, row};
|
||||
@@ -18,7 +18,6 @@ use iced::{Alignment, Background, Border, Element, Length, Padding, Theme};
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::component_map::Component;
|
||||
use unlucky_design::elevation::MaterialLayer;
|
||||
use unlucky_design::icons::IconKind;
|
||||
use unlucky_design::shape::ShapeScale;
|
||||
use unlucky_design::spacing::Space;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
@@ -30,9 +29,6 @@ use crate::theme::Ui;
|
||||
|
||||
use super::Group;
|
||||
|
||||
/// 空态位高度:给图标块 96 + 标题 + 副文案 + 按钮留够空间(`[估]`,按 12b §15 的元素最小高度累加)。
|
||||
const EMPTY_SLOT_HEIGHT: f32 = 320.0;
|
||||
|
||||
/// 页头:标题 + 副标题 + 两个占位动作。
|
||||
///
|
||||
/// M106:三条文案走 catalog(`lang` 由 `settings::view` 传下来)。页标题复用 `NavSettings`——
|
||||
@@ -218,21 +214,137 @@ fn setting_row<'a>(ui: &'a Ui, label: &'a str, value: &'a str) -> Element<'a, Me
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 空态位:没有逐实例覆盖时的样子(12b §15 的口径)。
|
||||
pub(super) fn overrides_empty<'a>(ui: &'a Ui, lang: Lang) -> Element<'a, Message> {
|
||||
let catalog = lang.catalog();
|
||||
container(empty_state(
|
||||
ui,
|
||||
IconKind::Instances,
|
||||
catalog.text(Key::SettingsOverridesEmptyTitle),
|
||||
catalog.text(Key::SettingsOverridesEmptyBody),
|
||||
vec![text_action(
|
||||
ui,
|
||||
catalog.text(Key::SettingsOverridesAdd),
|
||||
Action::AddOverride,
|
||||
)],
|
||||
))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fixed(EMPTY_SLOT_HEIGHT))
|
||||
.into()
|
||||
// --------------------------------------------------------------- M196 守门
|
||||
|
||||
/// M196:设置页正文**不许**在最后一组之后挂一张空态卡。
|
||||
///
|
||||
/// ## 守的是哪一件事
|
||||
///
|
||||
/// 用户 2026-11-08 在 M174 的助手页与 M175 的 Java 页都看到同一张不属于本页的空态卡(四格图标 +
|
||||
/// 「没有逐实例覆盖」+ 说明被窗口下沿截断),两张图 1180×1100、卡片位置完全一致。根因是
|
||||
/// `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/`,也请原样保留这两条。
|
||||
///
|
||||
/// ## 抓得住 / 抓不住(据实写)
|
||||
///
|
||||
/// 抓得住(三种变异都实测过):①按原样把卡加回循环之后(`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");
|
||||
|
||||
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():分发点被改名了,本守门要跟着改");
|
||||
let rest = &FORM[start..];
|
||||
let end = rest
|
||||
.find("\nfn section_columns")
|
||||
.expect("settings/mod.rs 里找不到 section_columns():列生产被改名了,本守门要跟着改");
|
||||
&rest[..end]
|
||||
}
|
||||
|
||||
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(` 恰好一次」等于「尾部空着」;
|
||||
/// 拼装被整段重写成别的形状时计数变 0,同样红。
|
||||
#[test]
|
||||
fn the_form_stops_at_its_last_group() {
|
||||
let body = form_body();
|
||||
let pushes = body.matches("stack = stack.push(").count();
|
||||
assert_eq!(
|
||||
pushes, 1,
|
||||
"`form()` 里出现了 {pushes} 处 `stack = stack.push(`:分组循环里只该有一处。\
|
||||
尾部又被叠了一件东西的话,设置页每一组下面都会多出同一件(M196 那张\
|
||||
「没有逐实例覆盖」的卡就是这么来的)"
|
||||
);
|
||||
assert!(
|
||||
body.contains(" frame::scroll(ui, stack)\n}"),
|
||||
"`form()` 必须以 `frame::scroll(ui, stack)` 收尾(后面紧跟函数右花括号):\
|
||||
正文到最后一组就结束,别再往后叠东西"
|
||||
);
|
||||
}
|
||||
|
||||
/// 判据 ②(**生产**):真跑 `section_columns`,量它到底交出几列。
|
||||
///
|
||||
/// 这是行为断言:调的是产品路径上那个真函数,**不认任何名字**。谁往正文里多塞一件(M196 那张
|
||||
/// 「没有逐实例覆盖」的卡当初就是这么塞的),列数就不等于组数。
|
||||
///
|
||||
/// 为什么不走 `tests/scroll_fit.rs` 那条「`()` 渲染器真跑布局」的路:`form` / `section_columns`
|
||||
/// 交出的是**产品渲染器**的 `Element`,要按那条路量子节点数,得把 `general` / `update` / `ai`
|
||||
/// 三条线的 `body` 一并泛型化(它们不在本 mission 的 scope 内)。量列数拿到的是同一个不变量,
|
||||
/// 且不需要泛型。
|
||||
#[test]
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@
|
||||
//! | Downloads | [`download`] | download 线 |
|
||||
//! | Updates | [`update`] | update 线(M159,按下标分发) |
|
||||
//! | Assistant | [`ai_about`](正文在 [`ai`]) | ai 线(M174,按下标分发:本组要 `Lang`) |
|
||||
//! | 页头 / 侧列 / 滚动区 / 演示卡片 / 空态位 | [`frame`] | 谁都不该改(改了就是改版面) |
|
||||
//! | 页头 / 侧列 / 滚动区 / 演示卡片 | [`frame`] | 谁都不该改(改了就是改版面) |
|
||||
//!
|
||||
//! ## 测试也按 section 分(M100)
|
||||
//!
|
||||
@@ -106,16 +106,9 @@
|
||||
//! `app/view.rs:70`),四处都要补字段 ⇒ **3 个共享文件**:`settings/state.rs`、`pages/mod.rs`、
|
||||
//! **`app/view.rs`**(最后这个正是 M104 的落点,本 mission 反复设锚点要避开的就是它);
|
||||
//! - 给 `settings::view` **加一个 `model: &Model` 形参**(`pages/mod.rs` 的 `render` 本来就拿着
|
||||
//! `model`,直接往下传)⇒ **2 个共享文件**(`settings/mod.rs` + `pages/mod.rs`),
|
||||
//! `app/view.rs` 不用动 —— **推荐这个形状**。
|
||||
//!
|
||||
//! 顺带订正 r3 之前那句「在 `app/state.rs` 用真值初始化 `Groups.language_appearance`(1 个文件)」:
|
||||
//! **M106 起语言已经有真值源**——`App::lang: LangState`(`crates/app-iced/src/ui/lang.rs`,用户档位
|
||||
//! `LangChoice` + 探测结论 `Detection`),由 `App::refresh_view` 经 `view::model(…, lang)` 灌进
|
||||
//! `pages::Model.lang`,设置页再从 `settings::view` 的 `language` 形参拿到它。所以下一条线要给某一组
|
||||
//! 读「当前语言」时,**不需要**再造一层状态:要么走 `Model.lang`(给 `settings::view` 加形参,
|
||||
//! 与 M106 同形状),要么读 `Groups` 里本线的字段。两种形状都是「谁先撞谁付一次钱」的一次性代价,
|
||||
//! 不是永久性四路撞车。
|
||||
//! `model`,直接往下传)⇒ **2 个共享文件**(`settings/mod.rs` + `pages/mod.rs`),`app/view.rs`
|
||||
//! 不用动 —— **推荐这个形状**。语言也已有真值源(`ui/lang.rs` 的 `App::lang` → `view::model(…)`
|
||||
//! → `Model.lang`):某组读「当前语言」走它或 `Groups` 里本线字段,别再另造一层状态。
|
||||
//! 5. **首帧取数 / 异步发起**(同上,r2 复审 P1 补)—— [`Body`] 是
|
||||
//! `fn(&Ui, States, Lang) -> Option<Element>`,返回不了 `Task`;而且 `settings::view` → `pages::render`
|
||||
//! → `App::view` **一路都返回 `Element`**(iced 0.14 的 `view` 也只返回 `Element`,`view(&self)`
|
||||
@@ -314,18 +307,17 @@ pub fn view<'a>(
|
||||
|
||||
// --------------------------------------------------------------- 内容分发
|
||||
|
||||
/// 表单列(正文列宽 ≤424)。`only = Some(i)` 只渲染一组(supporting-pane),
|
||||
/// `None` 渲染全部(窄窗:分组导航退化成按组堆叠)。
|
||||
/// 表单列(正文列宽 ≤424)。`only = Some(i)` 只渲染一组(supporting-pane),`None` 渲染全部(窄窗按组堆叠)。
|
||||
///
|
||||
/// **分发点(两条路,M106 起就并存)**:
|
||||
/// **分发点(两条路,M106 起就并存)**:①`GROUP.body`——`Some(f)` 自己去 [`States`] 取数,并从形参拿
|
||||
/// **生效语言**(`None` 退回演示行);②**按下标分发**——General(语言档位)/ Update(presenter 快照)/
|
||||
/// Assistant(`Lang` + 测试结论)/ Java(`Lang` + 扫描槽)在 [`section_columns`] 里各有 `else if`,它们
|
||||
/// 的 `GROUP.body` 仍是 `None`(理由见各自的 `<组>_GROUP` 常量);分组名只作 `GROUPS` 元素的 id。
|
||||
///
|
||||
/// 1. `GROUP.body`——`Some(f)` 让 `f` 自己去 [`States`] 里取它要的数据、并从形参拿**生效语言**
|
||||
/// (`None` 返回值 ⇒ 退回演示行),`None` 直接画演示行([`frame::demo_card`]);
|
||||
/// 2. **按下标分发**——General(要语言档位)、Update(要 presenter 快照)、Assistant(要 `Lang`
|
||||
/// 与测试结论)、Java(要 `Lang` 与扫描槽)四组在下面各有 `else if`,它们的 `GROUP.body`
|
||||
/// 仍是 `None`(理由见各自的 `<组>_GROUP` 常量)。
|
||||
///
|
||||
/// 除此之外这里不认识任何一组的字段:分组名只作为 `GROUPS` 元素的 id 出现。
|
||||
/// **尾部不许再挂东西(M196)**:循环之后原先无条件叠了一张「没有逐实例覆盖」的空态卡(来历见
|
||||
/// `frame.rs`),于是**每一组**下面都长出同一张卡。它**放错了地方**:真控件在**实例详情页**的「启动
|
||||
/// 参数」区(M157 `pages/instances/launch_args.rs` → `AppCommand::set_launch_args`),设置页拿不到那份
|
||||
/// 快照([`view`] 没有 `Model`);无人产出的只有 `Action::AddOverride`。删掉,守门见 `frame.rs` 末尾。
|
||||
fn form<'a>(
|
||||
ui: &'a Ui,
|
||||
only: Option<usize>,
|
||||
@@ -333,9 +325,27 @@ 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`],只含被选中的那些;分发点见 [`form`] 的文档)。抽出来是
|
||||
/// 为了能被**行为断言**量到:`frame.rs` 的 `the_form_holds_exactly_one_column_per_group` 数它的列数。
|
||||
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;
|
||||
@@ -365,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,
|
||||
@@ -380,11 +390,10 @@ fn form<'a>(
|
||||
card,
|
||||
]
|
||||
.spacing(ui.space(Space::X2))
|
||||
.width(Length::Fill),
|
||||
.width(Length::Fill)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
stack = stack.push(frame::overrides_empty(ui, language.effective));
|
||||
|
||||
frame::scroll(ui, stack)
|
||||
columns
|
||||
}
|
||||
@@ -47,10 +47,6 @@ macro_rules! general_entries {
|
||||
SettingsGroupAppearance => ("Appearance", "外观"),
|
||||
SettingsGroupJava => ("Java", "Java 运行时"),
|
||||
SettingsGroupDownloads => ("Downloads", "下载"),
|
||||
// 逐实例覆盖的空态(M106)
|
||||
SettingsOverridesEmptyTitle => ("No per-instance overrides", "没有逐实例覆盖"),
|
||||
SettingsOverridesEmptyBody => ("Overrides let one instance keep its own Java runtime, memory or JVM flags while the rest of the launcher keeps the defaults above.", "逐实例覆盖让某个实例保留自己的 Java 运行时、内存或 JVM 参数,其余实例沿用上面的默认值。"),
|
||||
SettingsOverridesAdd => ("Add override", "新增覆盖"),
|
||||
// `Route::Agent` 的回退卡正文(M106:`app/view.rs` 里最后一条硬编码英文)
|
||||
AgentFallbackBody => ("The assistant page is not built yet. Chat lives in the docked panel once the AI mission lands; this screen is the M68 skeleton fallback.", "助手页还没做。AI 接线落地后对话会出现在右侧停靠面板;这一屏是 M68 的骨架回退。"),
|
||||
} }
|
||||
|
||||
@@ -321,9 +321,6 @@ pub(super) const ROWS: &[(&str, &str, &str)] = &[
|
||||
("SettingsGroupAppearance", "Appearance", "外观"),
|
||||
("SettingsGroupJava", "Java", "Java 运行时"),
|
||||
("SettingsGroupDownloads", "Downloads", "下载"),
|
||||
("SettingsOverridesEmptyTitle", "No per-instance overrides", "没有逐实例覆盖"),
|
||||
("SettingsOverridesEmptyBody", "Overrides let one instance keep its own Java runtime, memory or JVM flags while the rest of the launcher keeps the defaults above.", "逐实例覆盖让某个实例保留自己的 Java 运行时、内存或 JVM 参数,其余实例沿用上面的默认值。"),
|
||||
("SettingsOverridesAdd", "Add override", "新增覆盖"),
|
||||
("AgentFallbackBody", "The assistant page is not built yet. Chat lives in the docked panel once the AI mission lands; this screen is the M68 skeleton fallback.", "助手页还没做。AI 接线落地后对话会出现在右侧停靠面板;这一屏是 M68 的骨架回退。"),
|
||||
("VersionRailTitle", "Version type", "版本类型"),
|
||||
("VersionMajorTitle", "Major version", "主版本"),
|
||||
|
||||
Reference in new issue
Block a user