fix(settings): 设置页尾部不再渲染不属于本页的空态卡
用户 2026-11-08 在 M174 的助手页与 M175 的 Java 页截图里看到同一张卡(四格图标 + 「没有逐实例覆盖」+ 说明被窗口下沿截断):两张图 1180×1100、卡片位置完全一致、 独立居中在内容下方。 根因是 `pages/settings/mod.rs` 的 `form()` 在分组循环之后**无条件** push 了 `pages/settings/frame.rs` 的 `overrides_empty()`(M84 骨架期的占位,M99 纯搬移时原样 留下,固定 320dp 高):于是六个分组下面都长出同一张卡,与实例选中态无关。 修法:删掉该空态位及其唯一的三个 i18n key(`SettingsOverrides*`,金表同步删三行)。 设置页永远有内容(总有一组被选中),12b §15 的空态件是给实例/下载/日志页的; 「逐实例覆盖」全仓既无数据源也无消息落点,所以不挪给任何一组,直接不渲染。 守门(`settings/frame.rs` 的 `no_stray_empty_state` 三条,变异实测会红): 尾部不许再出现额外 push、`form()` 不认任何空态件名、设置页不读实例选择。
This commit is contained in:
1 parent
30b61e2684
commit
7fdebba961
4 files changed
+106
-34
No files matched your search
@@ -1,4 +1,4 @@
|
|||||||
//! 设置页骨架:页头、左列分组导航、表单列的滚动区、演示卡片、空态位。
|
//! 设置页骨架:页头、左列分组导航、表单列的滚动区、演示卡片。
|
||||||
//!
|
//!
|
||||||
//! ## 为什么这里还再导出一批 `pages::frame` 的名字
|
//! ## 为什么这里还再导出一批 `pages::frame` 的名字
|
||||||
//!
|
//!
|
||||||
@@ -8,8 +8,8 @@
|
|||||||
//! 这不是新 API,只是一条转发(新增控件仍归 [`crate::pages::frame`])。
|
//! 这不是新 API,只是一条转发(新增控件仍归 [`crate::pages::frame`])。
|
||||||
|
|
||||||
pub(super) use super::super::frame::{
|
pub(super) use super::super::frame::{
|
||||||
card, divider, empty_state, glyph_box, left_aligned, page, panes, scroll_style, single_column,
|
card, divider, glyph_box, left_aligned, page, panes, scroll_style, single_column, text_action,
|
||||||
text_action, text_role, Header, ICON_PX, SCROLLBAR_MARGIN, SCROLLBAR_WIDTH,
|
text_role, Header, ICON_PX, SCROLLBAR_MARGIN, SCROLLBAR_WIDTH,
|
||||||
};
|
};
|
||||||
|
|
||||||
use iced::widget::{column, container, mouse_area, row};
|
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::color::Role;
|
||||||
use unlucky_design::component_map::Component;
|
use unlucky_design::component_map::Component;
|
||||||
use unlucky_design::elevation::MaterialLayer;
|
use unlucky_design::elevation::MaterialLayer;
|
||||||
use unlucky_design::icons::IconKind;
|
|
||||||
use unlucky_design::shape::ShapeScale;
|
use unlucky_design::shape::ShapeScale;
|
||||||
use unlucky_design::spacing::Space;
|
use unlucky_design::spacing::Space;
|
||||||
use unlucky_design::typography::TypeRole;
|
use unlucky_design::typography::TypeRole;
|
||||||
@@ -30,9 +29,6 @@ use crate::theme::Ui;
|
|||||||
|
|
||||||
use super::Group;
|
use super::Group;
|
||||||
|
|
||||||
/// 空态位高度:给图标块 96 + 标题 + 副文案 + 按钮留够空间(`[估]`,按 12b §15 的元素最小高度累加)。
|
|
||||||
const EMPTY_SLOT_HEIGHT: f32 = 320.0;
|
|
||||||
|
|
||||||
/// 页头:标题 + 副标题 + 两个占位动作。
|
/// 页头:标题 + 副标题 + 两个占位动作。
|
||||||
///
|
///
|
||||||
/// M106:三条文案走 catalog(`lang` 由 `settings::view` 传下来)。页标题复用 `NavSettings`——
|
/// M106:三条文案走 catalog(`lang` 由 `settings::view` 传下来)。页标题复用 `NavSettings`——
|
||||||
@@ -218,21 +214,98 @@ fn setting_row<'a>(ui: &'a Ui, label: &'a str, value: &'a str) -> Element<'a, Me
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 空态位:没有逐实例覆盖时的样子(12b §15 的口径)。
|
// --------------------------------------------------------------- M196 守门
|
||||||
pub(super) fn overrides_empty<'a>(ui: &'a Ui, lang: Lang) -> Element<'a, Message> {
|
|
||||||
let catalog = lang.catalog();
|
/// M196:设置页正文**不许**在最后一组之后挂一张空态卡。
|
||||||
container(empty_state(
|
///
|
||||||
ui,
|
/// ## 守的是哪一件事
|
||||||
IconKind::Instances,
|
///
|
||||||
catalog.text(Key::SettingsOverridesEmptyTitle),
|
/// 用户 2026-11-08 在 M174 的助手页与 M175 的 Java 页都看到同一张不属于本页的空态卡(四格图标 +
|
||||||
catalog.text(Key::SettingsOverridesEmptyBody),
|
/// 「没有逐实例覆盖」+ 说明被窗口下沿截断),两张图 1180×1100、卡片位置完全一致。根因是
|
||||||
vec![text_action(
|
/// `settings/mod.rs` 的 `form()` 在分组循环之后**无条件**叠了一件固定 320dp 高的空态位
|
||||||
ui,
|
/// (M84 骨架期的占位,M99 纯搬移时原样留下)——于是任何一组下面都长出同一张卡。
|
||||||
catalog.text(Key::SettingsOverridesAdd),
|
///
|
||||||
Action::AddOverride,
|
/// 这三条钉住的就是「尾部不许再多一件」,以及「本页不读实例选择」(于是「无选中实例」与
|
||||||
)],
|
/// 「有选中实例」两种状态下本页的渲染路径逐字相同)。
|
||||||
))
|
///
|
||||||
.width(Length::Fill)
|
/// ## 为什么住在 `frame.rs` 而不是 `pages/settings/tests/`
|
||||||
.height(Length::Fixed(EMPTY_SLOT_HEIGHT))
|
///
|
||||||
.into()
|
/// `tests/` 归四条 section 线所有(各改各的),而这里守的是**本文件与 `mod.rs` 之间的分工**:
|
||||||
|
/// 壳层不提供空态位、分发点不在尾部多堆一件。判据要是哪天挪进 `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)。
|
||||||
|
#[cfg(test)]
|
||||||
|
mod no_stray_empty_state {
|
||||||
|
/// 设置页的分发点(本文件是它的被调用方)。
|
||||||
|
const FORM: &str = include_str!("mod.rs");
|
||||||
|
|
||||||
|
/// `form()` 的函数体:从签名取到文件尾(它是 `mod.rs` 最后一件)。
|
||||||
|
fn form_body() -> &'static str {
|
||||||
|
let start = FORM
|
||||||
|
.find("fn form<'a>(")
|
||||||
|
.expect("settings/mod.rs 里找不到 form():分发点被改名了,本守门要跟着改");
|
||||||
|
&FORM[start..]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 判据 ①:循环之后**没有**任何额外的 `push`,且函数以滚出去收尾。
|
||||||
|
///
|
||||||
|
/// 循环体里只有一件(分组卡),所以「`stack = stack.push(` 恰好一次」等于「尾部空着」。
|
||||||
|
#[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)` 收尾(后面紧跟函数右花括号):\
|
||||||
|
正文到最后一组就结束,别再往后叠东西"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 判据 ②:`form()` 不认任何空态件(名字层面)。
|
||||||
|
///
|
||||||
|
/// 抓得住「把那张卡原样加回来」;抓不住换名字重画的(见本模块文档)。
|
||||||
|
#[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 的挡项就是不设空态的页),\
|
||||||
|
设置页不该借它来充数"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 判据 ③:本页**不读实例选择** —— 于是「无选中实例」与「有选中实例」两种状态下,
|
||||||
|
/// 设置页走的是同一组函数、同一拼装顺序,那一张卡在两种状态下都不可能出现。
|
||||||
|
///
|
||||||
|
/// 抓得住:谁把某件东西改成「按当前选中实例决定画不画」(那正是这张卡当初被误读成
|
||||||
|
/// 「实例详情的空态」的那条路)。
|
||||||
|
#[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 那张卡就有回来的路"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
//! | Downloads | [`download`] | download 线 |
|
//! | Downloads | [`download`] | download 线 |
|
||||||
//! | Updates | [`update`] | update 线(M159,按下标分发) |
|
//! | Updates | [`update`] | update 线(M159,按下标分发) |
|
||||||
//! | Assistant | [`ai_about`](正文在 [`ai`]) | ai 线(M174,按下标分发:本组要 `Lang`) |
|
//! | Assistant | [`ai_about`](正文在 [`ai`]) | ai 线(M174,按下标分发:本组要 `Lang`) |
|
||||||
//! | 页头 / 侧列 / 滚动区 / 演示卡片 / 空态位 | [`frame`] | 谁都不该改(改了就是改版面) |
|
//! | 页头 / 侧列 / 滚动区 / 演示卡片 | [`frame`] | 谁都不该改(改了就是改版面) |
|
||||||
//!
|
//!
|
||||||
//! ## 测试也按 section 分(M100)
|
//! ## 测试也按 section 分(M100)
|
||||||
//!
|
//!
|
||||||
@@ -307,6 +307,14 @@ pub fn view<'a>(
|
|||||||
/// 表单列(正文列宽 ≤424)。`only = Some(i)` 只渲染一组(supporting-pane),
|
/// 表单列(正文列宽 ≤424)。`only = Some(i)` 只渲染一组(supporting-pane),
|
||||||
/// `None` 渲染全部(窄窗:分组导航退化成按组堆叠)。
|
/// `None` 渲染全部(窄窗:分组导航退化成按组堆叠)。
|
||||||
///
|
///
|
||||||
|
/// ## M196:正文到最后一组就结束,尾部**什么**都不许挂
|
||||||
|
///
|
||||||
|
/// 这里原先在循环之后无条件叠了一张「没有逐实例覆盖」的空态卡(M84 骨架期的占位,M99 纯搬移时
|
||||||
|
/// 原样留着),于是**每一组**下面都长出同一张卡:用户在 M174 的助手页与 M175 的 Java 页都看到了它
|
||||||
|
/// (两张图 1180×1100、卡片位置完全一致、说明被窗口下沿截断)。设置页永远有内容(总有一组被选中),
|
||||||
|
/// 「逐实例覆盖」既没有数据源也没有消息落点(全仓没有它的渲染点与处理器),所以它不是本页的空态:
|
||||||
|
/// **删掉,不挪给任何一组**。守门见 `frame.rs` 末尾的三条用例。
|
||||||
|
///
|
||||||
/// **分发点**:一组的正文由它自己文件里的 `GROUP.body` 决定——`Some(f)` 让 `f` 自己去
|
/// **分发点**:一组的正文由它自己文件里的 `GROUP.body` 决定——`Some(f)` 让 `f` 自己去
|
||||||
/// [`States`] 里取它要的数据(`None` 返回值 ⇒ 退回演示行),`None` 直接画演示行
|
/// [`States`] 里取它要的数据(`None` 返回值 ⇒ 退回演示行),`None` 直接画演示行
|
||||||
/// ([`frame::demo_card`])。这里不出现任何一个分组名,也不比较下标,更不认识任何一组的字段。
|
/// ([`frame::demo_card`])。这里不出现任何一个分组名,也不比较下标,更不认识任何一组的字段。
|
||||||
@@ -365,7 +373,5 @@ fn form<'a>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
stack = stack.push(frame::overrides_empty(ui, language.effective));
|
|
||||||
|
|
||||||
frame::scroll(ui, stack)
|
frame::scroll(ui, stack)
|
||||||
}
|
}
|
||||||
@@ -47,10 +47,6 @@ macro_rules! general_entries {
|
|||||||
SettingsGroupAppearance => ("Appearance", "外观"),
|
SettingsGroupAppearance => ("Appearance", "外观"),
|
||||||
SettingsGroupJava => ("Java", "Java 运行时"),
|
SettingsGroupJava => ("Java", "Java 运行时"),
|
||||||
SettingsGroupDownloads => ("Downloads", "下载"),
|
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` 里最后一条硬编码英文)
|
// `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 的骨架回退。"),
|
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 的骨架回退。"),
|
||||||
} }
|
} }
|
||||||
|
|||||||
@@ -190,9 +190,6 @@ pub(super) const ROWS: &[(&str, &str, &str)] = &[
|
|||||||
("SettingsGroupAppearance", "Appearance", "外观"),
|
("SettingsGroupAppearance", "Appearance", "外观"),
|
||||||
("SettingsGroupJava", "Java", "Java 运行时"),
|
("SettingsGroupJava", "Java", "Java 运行时"),
|
||||||
("SettingsGroupDownloads", "Downloads", "下载"),
|
("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 的骨架回退。"),
|
("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", "版本类型"),
|
("VersionRailTitle", "Version type", "版本类型"),
|
||||||
("VersionMajorTitle", "Major version", "主版本"),
|
("VersionMajorTitle", "Major version", "主版本"),
|
||||||
|
|||||||
Reference in new issue
Block a user