Files
Unlucky-Launcher/crates/app-iced/src/pages/layout/tests.rs
T
AskaEth 0de8e63ad6 fix(app-iced): 必修1 跟进 —— 旧断言 435→424 + clippy 常量断言
必修1 改完 cap 之后跑门禁抓到的两处自身的漏改:

1. `settings_pane_keeps_the_drawer_width_and_a_readable_form_column` 里还留着
   `assert_eq!(measure_cap_px(), 435.0)`(P2-1 轮加的锚点),cap 收到 424 后它必红。
   已改 424 —— 这正是「锚点断言」该有的行为:常量一改就红,而不是悄悄跟着漂。
2. clippy `assertions_on_constants`:`assert!(SIM_SAFE_MAX_CHARS <= MAX_MEASURE_CHARS)`
   两边都是 const,改成 `const { assert!(..) }`。

门禁(本机私有 target,CPU 档):86 lib passed + 6 集成 passed;clippy -D warnings 干净。
2026-11-02 02:42:58 +08:00

357 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `layout` 的断言:断点表、版面决议、行宽上限、列宽几何。
//!
//! 这些是**门禁级**断言:阈值改错必须立刻红(tower 会故意改错来验)。
use super::*;
/// 断点表:官方 600 / 840 两个硬值,边界闭开区间逐点钉死。
#[test]
fn breakpoints_are_the_official_600_and_840() {
assert_eq!(BREAKPOINT_MEDIUM, 600.0, "官方 medium 断点");
assert_eq!(BREAKPOINT_EXPANDED, 840.0, "官方 expanded 断点");
for (width, expected) in [
(320.0, WidthClass::Compact),
(599.0, WidthClass::Compact),
(599.9, WidthClass::Compact),
(600.0, WidthClass::Medium),
(720.0, WidthClass::Medium),
(839.0, WidthClass::Medium),
(839.9, WidthClass::Medium),
(840.0, WidthClass::Expanded),
(900.0, WidthClass::Expanded),
(1180.0, WidthClass::Expanded),
(2560.0, WidthClass::Expanded),
] {
assert_eq!(
WidthClass::of(width),
expected,
"{width} 宽应该判为 {expected:?}"
);
}
}
/// 版面决议表(tower 会照这张表对文档):一列写死「宽度 → 版面」。
#[test]
fn layout_table_matches_the_documented_thresholds() {
// (窗口宽度, 实例页, 设置页, 下载/日志页)
let table = [
(
320.0,
PageLayout::SingleColumn,
PageLayout::SingleColumn,
PageLayout::SingleColumn,
),
(
599.0,
PageLayout::SingleColumn,
PageLayout::SingleColumn,
PageLayout::SingleColumn,
),
(
700.0,
PageLayout::SingleColumn,
PageLayout::SingleColumn,
PageLayout::SingleColumn,
),
(
720.0,
PageLayout::SingleColumn,
PageLayout::SupportingPane,
PageLayout::SingleColumn,
),
(
800.0,
PageLayout::SingleColumn,
PageLayout::SupportingPane,
PageLayout::SingleColumn,
),
(
839.0,
PageLayout::SingleColumn,
PageLayout::SupportingPane,
PageLayout::SingleColumn,
),
(
840.0,
PageLayout::ListDetail,
PageLayout::SupportingPane,
PageLayout::SingleColumn,
),
(
900.0,
PageLayout::ListDetail,
PageLayout::SupportingPane,
PageLayout::SingleColumn,
),
(
1180.0,
PageLayout::ListDetail,
PageLayout::SupportingPane,
PageLayout::SingleColumn,
),
(
1920.0,
PageLayout::ListDetail,
PageLayout::SupportingPane,
PageLayout::SingleColumn,
),
];
for (width, instances, settings, single) in table {
assert_eq!(
resolve(PaneKit::ListDetail, width),
instances,
"{width} 宽实例页版面不对"
);
assert_eq!(
resolve(PaneKit::Supporting, width),
settings,
"{width} 宽设置页版面不对"
);
assert_eq!(
resolve(PaneKit::Single, width),
single,
"{width} 宽单列页版面对不"
);
}
}
#[test]
fn pane_thresholds_are_the_sum_of_their_columns() {
// 336 + 320 + 24 + 2×24 + 88 = 816
assert_eq!(list_detail_min_window(), 816.0);
// 240 + 320 + 24 + 2×24 + 88 = 720
assert_eq!(supporting_pane_min_window(), 720.0);
// 官方 expanded 断点必须**宽于** list-detail 的几何门槛,否则「840 起用 list-detail」是空话
assert!(
BREAKPOINT_EXPANDED >= list_detail_min_window(),
"840 档必须放得下 336 + 24 + 320 的三列和"
);
// 设置页在 medium 档里就有能用的窗口宽度(720 < 840)
assert!(supporting_pane_min_window() < BREAKPOINT_EXPANDED);
assert!(WidthClass::of(supporting_pane_min_window()) == WidthClass::Medium);
}
/// 实测样本:**实例页详情那段正文**(`pages/instances/data.rs` 的 `NOTE`)。
///
/// 口径(`layout` 模块文档 §3):Segoe UI Regular @14px、无 letter-spacing、
/// FreeType `getlength` 求整段排版宽。**这是外部实测数据,不是从常量推出来的**——
/// 下面的断言拿它反算「一行能放几个字符」,所以把 `CHAR_ADVANCE_EM` 改回估算值会立刻红。
const MEASURE_SAMPLE_CHARS: f32 = 361.0;
const MEASURE_SAMPLE_WIDTH_PX: f32 = 2185.0;
/// 样本的实测平均步进(px/字符)。
fn measured_advance_px() -> f32 {
MEASURE_SAMPLE_WIDTH_PX / MEASURE_SAMPLE_CHARS
}
/// 给定列宽,按**实测步进**能放多少字符。
fn chars_per_line(width: f32) -> f32 {
width / measured_advance_px()
}
/// **逐行**折行模拟结果(FreeType `getlength` 量宽 + 贪心词折行,脚本见模块文档 §3)。
///
/// 这两组数是外部实测:在同一段正文(361 字符)上扫列宽得到——
/// `SAFE` 是交付宽度下的最长一行,`BREAK` 是第一档会冒出 73 字符行的宽度。
/// 有了它,Rust 侧不用字体引擎也能把「上限必须夹在这两者之间」写成断言。
const SIM_SAFE_MAX_CHARS: f32 = 67.0;
const SIM_SAFE_MAX_INK: f32 = 402.0;
const SIM_BREAK_INK: f32 = 428.0;
const SIM_BREAK_CHARS: f32 = 73.0;
/// 行宽上限必须真的停在 72 字符:名字、常量、**逐行模拟**三者一致。
#[test]
fn line_length_is_capped_at_72_characters() {
// ① 常量对得上实测步进(14px 下的平均步进)
let body_size = TypeRole::BodyMedium.style().size_px;
assert_eq!(body_size, 14.0);
assert!(
(measured_advance_px() - body_size * CHAR_ADVANCE_EM).abs() < 0.02,
"CHAR_ADVANCE_EM = {CHAR_ADVANCE_EM} 与实测步进 {:.4}px/字符对不上",
measured_advance_px()
);
assert_eq!(measure_floor_px(), 272.0, "floor(14 × 0.43233 × 45)");
// ② 上限是**逐行**模拟出来的 424,不是「平均步进 × 72」的算术值 435
assert_eq!(measure_cap_px(), 424.0, "逐行安全宽(4px 栅格)");
assert_eq!(
measure_average_bound_px(),
435.0,
"floor(14 × 0.43233 × 72)"
);
assert!(
measure_cap_px() < measure_average_bound_px(),
"平均口径上界必须比逐行安全宽**更宽**——这说明 cap 不是拿平均算出来的"
);
// ③ 交付宽度下,模拟出来的最长一行装得下,且字符数 ≤72
assert!(
SIM_SAFE_MAX_INK <= measure_cap_px(),
"模拟最长行 {}px 超过上限 {}px",
SIM_SAFE_MAX_INK,
measure_cap_px()
);
const { assert!(SIM_SAFE_MAX_CHARS <= MAX_MEASURE_CHARS) };
// ④ **两侧夹逼**:这是本条真正的护栏。
// 放大到 SIM_BREAK_INK(428)逐行模拟就会重新出现 73 字符行;
// 压到 SAFE 以下则纯属白丢宽度。两个方向都会红。
assert!(
measure_cap_px() < SIM_BREAK_INK,
"上限 ≥{SIM_BREAK_INK}px 时逐行模拟会再次出现 {SIM_BREAK_CHARS} 字符行——请重跑折行模拟"
);
assert!(
measure_cap_px() >= SIM_SAFE_MAX_INK,
"上限 {}px 比模拟出来的最长行 {}px 还窄,白丢宽度",
measure_cap_px(),
SIM_SAFE_MAX_INK
);
// ⑤ 平均口径的回归护栏:旧 0.5em 的 504px 在实测步进下能放 83 字符
assert!(
chars_per_line(504.0) > MAX_MEASURE_CHARS + 10.0,
"504px 在实测步进下应能放 >82 字符(旧口径的破绽),现在算出来 {:.1}",
chars_per_line(504.0)
);
for width in [600.0, 840.0, 900.0, 1180.0, 1440.0, 2560.0] {
let column = content_column_width(width);
// 平均口径只是次要护栏(逐行口径见上面的模拟常数)
assert!(
chars_per_line(column) <= MAX_MEASURE_CHARS,
"{width} 宽窗口的平均行宽 {:.1} 字符,超过 {MAX_MEASURE_CHARS}",
chars_per_line(column)
);
assert!(column <= measure_cap_px() + f32::EPSILON);
}
// 1180 —— M68 就是在这条上翻的车(712px),现在必须是 435
let wide = content_column_width(1180.0);
assert_eq!(wide, measure_cap_px());
assert!(wide < 712.0, "不许回到 M68 的全宽单卡(712px)");
}
#[test]
fn line_length_tracks_width_until_it_hits_the_cap() {
// 窄窗:正文列跟着可用宽度走(480 宽还在上限以内),撞上上限就锁死
let narrow = content_column_width(480.0);
assert!(narrow < measure_cap_px(), "480 宽时正文列应窄于上限");
assert_eq!(narrow, available_width(480.0));
// 单调不减:窗口变宽,正文列不变窄
let mut previous = 0.0;
for width in [320.0, 480.0, 600.0, 720.0, 840.0, 1024.0, 1180.0, 1600.0] {
let column = content_column_width(width);
assert!(column >= previous, "{width} 宽的正文列比更窄时还小");
previous = column;
}
// 一旦撞上上限就锁死(宽窗不会继续变宽)
assert_eq!(content_column_width(1180.0), measure_cap_px());
assert_eq!(content_column_width(1920.0), measure_cap_px());
}
#[test]
fn list_detail_geometry_keeps_the_list_column_in_range() {
let metrics = frame_metrics(PaneKit::ListDetail, 1180.0);
assert_eq!(metrics.layout, PageLayout::ListDetail);
let list = metrics.secondary.expect("list-detail 必须有列表列");
assert!(
(320.0..=360.0).contains(&list),
"列表列 {list} 不在 320~360 参考区间"
);
assert_eq!(list, LIST_COLUMN_WIDTH);
// 详情正文列:≤ 行宽上限,且明显窄于「窗口全宽」
assert!(metrics.column <= measure_cap_px() + f32::EPSILON);
assert!(metrics.column < metrics.available, "详情列必须窄于可用宽度");
// 列表列 + 间隙 + 详情 pane = 可用宽度(不留缝)
assert_eq!(
list + PANES_GAP + metrics.main_pane,
metrics.available,
"两列几何必须铺满可用宽度"
);
}
#[test]
fn list_detail_degrades_to_one_column_below_the_threshold() {
for width in [320.0, 599.0, 700.0, 816.0, 839.0] {
assert_eq!(
frame_metrics(PaneKit::ListDetail, width).layout,
PageLayout::SingleColumn,
"{width} 宽应该退化成单列"
);
assert!(frame_metrics(PaneKit::ListDetail, width)
.secondary
.is_none());
}
// 840 = 官方 expanded 断点,list-detail 从这里成立
assert_eq!(
frame_metrics(PaneKit::ListDetail, 840.0).layout,
PageLayout::ListDetail
);
// 840 宽下详情正文列仍要够宽(≥ 下限),否则等于假分栏
let at_breakpoint = frame_metrics(PaneKit::ListDetail, 840.0);
assert!(
at_breakpoint.column >= measure_floor_px(),
"840 宽下详情列 {:?} 低于行宽下限",
at_breakpoint.column
);
}
#[test]
fn settings_pane_keeps_the_drawer_width_and_a_readable_form_column() {
let metrics = frame_metrics(PaneKit::Supporting, 1180.0);
assert_eq!(metrics.layout, PageLayout::SupportingPane);
assert_eq!(metrics.secondary, Some(SIDE_NAV_WIDTH));
assert_eq!(SIDE_NAV_WIDTH, 240.0, "D33:展开态是 240 的 drawer");
assert_eq!(metrics.column, measure_cap_px(), "表单列吃满行宽上限");
assert_eq!(measure_cap_px(), 424.0);
let narrow = frame_metrics(PaneKit::Supporting, 700.0);
assert_eq!(narrow.layout, PageLayout::SingleColumn);
assert!(narrow.secondary.is_none());
}
#[test]
fn meta_grid_drops_columns_as_the_detail_narrows() {
// 160 宽卡:3 列要 3×160 + 2×12 = 504(超出行宽上限),所以上限处就是 2 列;2 列要 332
assert_eq!(meta_columns(measure_cap_px()), 2, "2×160 + 12 = 332");
assert_eq!(meta_columns(340.0), 2);
assert_eq!(meta_columns(330.0), 1);
// 1180 与 900 都是 2 列(160 宽卡三列塞不进行宽上限),差别落在详情列宽
let wide = frame_metrics(PaneKit::ListDetail, 1180.0);
let mid = frame_metrics(PaneKit::ListDetail, 900.0);
assert_eq!(wide.meta_columns, 2);
assert_eq!(mid.meta_columns, 2);
assert!(mid.column < wide.column, "900 宽下详情列必须真的变窄");
}
#[test]
fn gutters_follow_the_class_and_are_on_the_grid() {
assert_eq!(WidthClass::Compact.gutter(), 16.0);
assert_eq!(WidthClass::Medium.gutter(), 24.0);
assert_eq!(WidthClass::Expanded.gutter(), 24.0);
for class in [
WidthClass::Compact,
WidthClass::Medium,
WidthClass::Expanded,
] {
assert_eq!(class.gutter() % 4.0, 0.0, "{class:?} 的页边距不在 4px 栅格");
}
}
#[test]
fn page_layout_names_are_stable() {
assert_eq!(PageLayout::SingleColumn.name(), "single-column");
assert_eq!(PageLayout::SupportingPane.name(), "supporting-pane");
assert_eq!(PageLayout::ListDetail.name(), "list-detail");
assert!(!PageLayout::SingleColumn.is_two_pane());
assert!(PageLayout::ListDetail.is_two_pane());
}