必修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 干净。
357 lines
13 KiB
Rust
357 lines
13 KiB
Rust
//! `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());
|
||
}
|