feat(layout): M301 页面宽度跟随活轨宽(折叠 −64 / 展开 −88 / 动效插值)
用户口径「折叠腾出空间」只完成一半:M300 落了折叠态(ui.nav.collapsed + 一条并列宽度轨) 与折叠档令牌 nav_collapsed_width = 64,但 `pages/layout.rs` 的 `rail_width()` 仍是 `const fn` 恒返回 88 ⇒ 折叠后页面照旧按 88 扣宽,内容区右侧留一条 24px 空白带。 ## 轨宽从常量改成**当帧活值**(不存第二份状态) - 新增 `pages/layout/rail.rs`:thread-local `Option<f32>` 采样槽,`rail_width()` 读它, 未采样回落展开档令牌 88(不 panic、不 unwrap;NaN/inf/负数不写入)。 - 写入口**只有一个**:`app/view/rail.rs` 的 `rail_account()` —— M300 自己点名的「折叠档与 过渡进度的唯一构造点」,一次 `App::view()` 调用恰好一次,且排在页面树之前 (`app/view.rs` 的 `let account = self.rail_account();`),所以页面扣的宽度与外壳画的轨 是同一帧同一个数。折叠的**决定**仍在 presenter `prefs.nav_collapsed`,进度仍在 motion 轨。 - `rail_width()` 签名不变 ⇒ `pages/mod.rs`、`pages/agent/**`、20 个页面/测试文件的调用面 **零改动**(`git diff --stat` 里那两处都没有出现)。 - 动效期间自动插值:采样值就是 `RailAccount::nav_width` 的 64⇄88 插值结果。 - 门槛跟着动(公式里本来就是 `+ rail`):720/816 → 折叠 696/792。 ## 测试 - `pages/tests/rail_width.rs`(新):折叠=窗口−64、展开=窗口−88、差正好 24、块右缘贴死窗口; 无外壳通道回落 88;采样不粘滞;门槛跟活值;**唯一写入口**(扫 `src/**` 源码); **先写后读**(扫 `app/view.rs` 语句顺序)。期望值全从令牌取,不再写 88.0 / 64.0。 - `app/view/rail.rs` 新增接线用例:折叠→采样→展开→再采样必须回到 88(不粘滞)、 动效中间帧页面可用宽跟着插值(首尾逐位落在端值)。 - 原有钉 88 的三条改为「跟随模型」并逐条给了新算式(标题左缘 1180−88−48、块宽、 主列 pane);`pages/tests.rs` 400 行门禁前先拆出 `tests/demo_model.rs`。 ## 判据 - app-iced:1064 passed(M300 基线 1053 + 11 条新用例);no_hardcoded_tokens 6 passed。 - `clippy --workspace --all-targets -- -D warnings` / `fmt --all -- --check` 均 0。 - 变异:`rail_width()` 改回常量 88 ⇒ 2 条几何判据 + 门槛判据红(1068 vs 1044); 删掉外壳那一行写入 ⇒ 2 条接线用例红(折叠仍按 88 扣宽)。 - 像素(DevDocs/artifacts/m301/,本地不进仓):日志页折叠态内容右缘 1107 = 展开态 1107 (24px 全进内容),变异版折叠态 1083(那条 24px 空白带);`instances` 页因详情列被 424 封顶、列表列固定 336,改前改后**逐像素相同**,所以举证必须用单列页。
This commit is contained in:
1 parent
e9090d5668
commit
cb580a257e
7 files changed
+663
-69
No files matched your search
@@ -1,7 +1,16 @@
|
||||
//! 导航轨底部**账户头像位**的状态换算(M114)+ 轨自身的折叠档(M300)。
|
||||
//! 导航轨底部**账户头像位**的状态换算(M114)+ 轨自身的折叠档(M300)+ 轨宽的**当帧采样**(M301)。
|
||||
//!
|
||||
//! 为什么单独一个文件:`app/view.rs` 贴着 400 行硬线,而这块逻辑与页面树无关——
|
||||
//! 它只回答「轨底那个头像位现在该显示成未登录,还是某个账户」,以及「这一帧轨多宽」。
|
||||
//!
|
||||
//! ## 顺序契约(M301,给下一个改这里的人)
|
||||
//!
|
||||
//! [`App::rail_account`] 是轨宽的**唯一写入口**:它每次 `App::view()` 被调用**恰好一次**
|
||||
//! (`app/view.rs` 的 `let account = self.rail_account();`),且**排在页面树之前**。所以
|
||||
//! 页面几何(`pages::layout::rail::rail_width`)读到的值与这里递给 `shell::NavRail` 的
|
||||
//! **是同一个数、同一拍**——不存在「轨折叠了、页面还按 88 扣宽」的二段状态。
|
||||
//! 这条顺序是代码顺序,不是约定:谁把这次写挪到页面树之后再调用,`pages/tests/rail_width.rs`
|
||||
//! 的接线判据会红。
|
||||
|
||||
use crate::app::components::motion;
|
||||
use crate::shell;
|
||||
@@ -20,10 +29,12 @@ impl App {
|
||||
///
|
||||
/// M300:这里同时是轨的折叠档与过渡进度的**唯一构造点**——`shell::NavRail`(不在 scope 内)
|
||||
/// 只负责把它转交给 `nav::rail`,几何全部来自 [`shell::RailAccount::nav_width`]。
|
||||
///
|
||||
/// M301:**同时**是页面层「这一帧扣多宽」的写入口(同一个 `nav_width`,见模块文档的顺序契约)。
|
||||
pub(super) fn rail_account(&self) -> shell::RailAccount {
|
||||
let compact = self.height < RAIL_COMPACT_HEIGHT;
|
||||
let (collapsed, progress) = self.nav_state();
|
||||
match self.state.active_account() {
|
||||
let account = match self.state.active_account() {
|
||||
Some(account) => {
|
||||
shell::RailAccount::signed_in(account.monogram(), account.name.clone())
|
||||
.compact(compact)
|
||||
@@ -37,7 +48,11 @@ impl App {
|
||||
)
|
||||
.compact(compact)
|
||||
.nav(collapsed, progress),
|
||||
}
|
||||
};
|
||||
// M301:页面几何要扣的那一段(折叠 64 / 展开 88 / 动画中间帧插值)。**先写后读**——
|
||||
// 页面树紧跟着在同一次 `view()` 里建,所以两侧永远同一帧同一个数。
|
||||
crate::pages::layout::note_rail_width(account.nav_width(self.ui().metrics()));
|
||||
account
|
||||
}
|
||||
|
||||
/// 轨的折叠档 + **这一帧**的过渡进度(0 = 折叠 64,1 = 展开 88)。
|
||||
@@ -58,3 +73,138 @@ impl App {
|
||||
self.rail_account().nav_width(self.ui().metrics())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
//! **M301 的接线判据**:外壳这一帧采样 ⇒ 页面这一帧扣宽(同一个数、同一拍,含动效中间帧)。
|
||||
//!
|
||||
//! 几何面(折叠 −64 / 展开 −88 / 差 24 / 门檻 / 唯一写入口)在 `pages/tests/rail_width.rs`;
|
||||
//! 这里只守「外壳真的每帧都写、写了就立刻生效、不会粘住上一帧」。
|
||||
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
use unlucky_design::motion::MotionScale;
|
||||
use unlucky_presenter::mock::MockBackend;
|
||||
use unlucky_presenter::settings_bridge::SettingsBridge;
|
||||
|
||||
use crate::app::components::motion::{self, FRAME_MS};
|
||||
use crate::cli::Options;
|
||||
use crate::pages::layout::{self, WidthClass};
|
||||
use crate::theme::Families;
|
||||
|
||||
use super::*;
|
||||
|
||||
const FAMILIES: Families = Families {
|
||||
text: "Segoe UI",
|
||||
display: "Segoe UI",
|
||||
mono: "Consolas",
|
||||
};
|
||||
|
||||
/// 出厂态 App(桥指向一个不存在的临时文件 = 出厂偏好)。
|
||||
fn app() -> App {
|
||||
motion::reset();
|
||||
motion::configure_for_test(MotionScale::STANDARD, None);
|
||||
let path =
|
||||
std::env::temp_dir().join(format!("ull-m301-{}-{}.json", std::process::id(), line!()));
|
||||
let mut app = App::with_presenter(
|
||||
Options::default(),
|
||||
FAMILIES,
|
||||
SchemeFlavor::Light,
|
||||
Box::new(MockBackend::new()),
|
||||
SettingsBridge::with_path(path),
|
||||
);
|
||||
app.window = Some(iced::window::Id::unique());
|
||||
app
|
||||
}
|
||||
|
||||
/// 验收窗宽(与 `pages/tests.rs` 同一个对照尺寸)。
|
||||
const WINDOW: f32 = 1180.0;
|
||||
|
||||
/// 页面这一帧扣的宽度必须就是外壳这一帧采到的轨宽。
|
||||
fn assert_page_follows(app: &App) -> f32 {
|
||||
let _ = app.rail_account();
|
||||
let rail = layout::rail_width();
|
||||
assert_eq!(
|
||||
layout::available_width(WINDOW),
|
||||
WINDOW - rail - 2.0 * WidthClass::Expanded.gutter(),
|
||||
"页面扣的宽度必须等于外壳这一帧的轨宽({rail})"
|
||||
);
|
||||
rail
|
||||
}
|
||||
|
||||
/// **折叠 → 采样 → 展开 → 再采样**:第二次必须回到 88(不粘滞)。
|
||||
///
|
||||
/// **抓得住**:①写入口只在折叠方向生效(展开时还停在 64);②写进去的值被页面忽略;
|
||||
/// ③复位成常量(折叠也按 88 扣宽——这时折叠那条断言直接红)。
|
||||
/// **抓不住**:真画出来的像素(见 `DevDocs/artifacts/m301/`)。
|
||||
#[test]
|
||||
fn the_shell_sample_follows_the_collapse_state_and_reverts() {
|
||||
let mut app = app();
|
||||
let collapsed = layout::rail_width_collapsed();
|
||||
let expanded = layout::rail_width_expanded();
|
||||
|
||||
assert_eq!(
|
||||
assert_page_follows(&app),
|
||||
expanded,
|
||||
"出厂展开 ⇒ 页面按 88 扣宽"
|
||||
);
|
||||
|
||||
// 折叠:泵的目标与 prefs 不符 ⇒ `motion::nav(open)` 给终值 0.0 ⇒ 当帧就是 64(不靠动画)。
|
||||
app.state.prefs.nav_collapsed = true;
|
||||
assert_eq!(
|
||||
assert_page_follows(&app),
|
||||
collapsed,
|
||||
"折叠 ⇒ 当帧就按 64 扣宽"
|
||||
);
|
||||
|
||||
// 展开回去:第二次采样必须回到 88(删掉写入 / 只在折叠时写 ⇒ 这里绿不了)。
|
||||
app.state.prefs.nav_collapsed = false;
|
||||
assert_eq!(
|
||||
assert_page_follows(&app),
|
||||
expanded,
|
||||
"第二帧必须回到 88,不许粘住 64"
|
||||
);
|
||||
}
|
||||
|
||||
/// **动效期间页面跟着插值**(任务判据④):中间帧既不等于 88 也不等于 64,且首尾是端值。
|
||||
///
|
||||
/// **抓得住**:页面只在端值上跳变(中间帧列表里没有严格递减的中间值)、或是拿端值当宽度画
|
||||
/// (那会让 `available_width` 与轨宽在这一帧不等);**抓不住**:25 帧的观感(那要连拍出图)。
|
||||
#[test]
|
||||
fn the_page_width_interpolates_during_the_collapse_motion() {
|
||||
let mut app = app();
|
||||
let collapsed = layout::rail_width_collapsed();
|
||||
let expanded = layout::rail_width_expanded();
|
||||
motion::snap_nav(true);
|
||||
app.state.prefs.nav_collapsed = true;
|
||||
assert!(motion::observe_nav(false), "折叠必须起跑一条宽度轨道");
|
||||
|
||||
let mut widths = vec![assert_page_follows(&app)];
|
||||
let mut guard = 0;
|
||||
while motion::needs_frames() {
|
||||
motion::advance_by(FRAME_MS);
|
||||
widths.push(assert_page_follows(&app));
|
||||
guard += 1;
|
||||
assert!(guard < 200, "左轨轨道没跑完");
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
widths.first().copied(),
|
||||
Some(expanded),
|
||||
"起跑那一帧还在展开档"
|
||||
);
|
||||
assert_eq!(
|
||||
widths.last().copied(),
|
||||
Some(collapsed),
|
||||
"终值必须是折叠档,逐位"
|
||||
);
|
||||
assert!(
|
||||
widths
|
||||
.iter()
|
||||
.any(|width| *width > collapsed && *width < expanded),
|
||||
"必须有中间帧(页面跟插值走,不是端值跳变):{widths:?}"
|
||||
);
|
||||
for pair in widths.windows(2) {
|
||||
assert!(pair[1] <= pair[0], "折叠必须单调不增:{pair:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
//! ## 一屏的横向预算
|
||||
//!
|
||||
//! ```text
|
||||
//! 窗口宽 = 左轨 88 + [中栗] + [拖拽缝 8(仅展开档)] + [右栗]
|
||||
//! 窗口宽 = 左轨(**活值**:展开 88 / 折叠 64,见 [`layout::rail_width`])+ [中栗] + [拖拽缝 8(仅展开档)] + [右栗]
|
||||
//! ```
|
||||
//!
|
||||
//! - 右栗宽度 = [`rail_width`]:折叠 48 / 展开 280~360(吸附值)/ 拆出 0;
|
||||
@@ -124,7 +124,7 @@ pub fn effective_state(
|
||||
pub struct RailLayout {
|
||||
/// 窗口宽度(输入)。
|
||||
pub window: f32,
|
||||
/// 左侧导航轨宽度(令牌 `nav_rail_width` = 88)。
|
||||
/// 左侧导航轨宽度(**这一帧的活值**:展开 88 / 折叠 64,见 [`layout::rail_width`])。
|
||||
pub left: f32,
|
||||
/// 右栗面板宽度(0 / 48 / 280~360)。
|
||||
pub rail: f32,
|
||||
|
||||
@@ -27,10 +27,9 @@
|
||||
//! | `SupportingPane` | 主列 + 窄侧列(侧列是辅助,主列是阅读对象) | 设置(分组导航 240 + 表单列) | `w ≥ 600` 且 `w ≥ 720` | `SingleColumn` |
|
||||
//! | `ListDetail` | 固定列表列(320~360,**独立滚动**)+ 详情列 | 实例 | `w ≥ 840` 且 `w ≥ 816` | `SingleColumn` |
|
||||
//!
|
||||
//! 两个门槛都是**算出来的**,不是拍的(见 [`panes_min_window`]):
|
||||
//!
|
||||
//! - 设置:`240(drawer)+ 24(间隙)+ 320(表单列下限)+ 2×24(gutter)+ 88(rail)= 720`;
|
||||
//! - 实例:`336(列表列)+ 24(间隙)+ 320(详情列下限)+ 2×24(gutter)+ 88(rail)= 816`。
|
||||
//! 两个门槛都是**算出来的**,不是拍的(见 [`panes_min_window`]):`240/336 + 24 + 320 + 2×24 + rail`。
|
||||
//! rail 是**活值**(展开 88 / 折叠 64,见 [`rail::rail_width`])⇒ 门槛跟着动:720/816 降到
|
||||
//! 696/792——折叠真的把「两列版面」的成立线往下推了 24px。
|
||||
//!
|
||||
//! 实例的几何门槛 816 低于官方 expanded 断点 840 —— 所以**档位是更严的那一条**,
|
||||
//! 单测 `expanded_is_wide_enough_for_list_detail` 把这条「余量 24px」钉死。
|
||||
@@ -94,6 +93,12 @@ use unlucky_design::typography::TypeRole;
|
||||
pub mod block;
|
||||
pub mod wrap;
|
||||
|
||||
/// 轨宽的**当帧活值**(M301:折叠后按 64 而不是 88 扣宽,见模块文档)。
|
||||
pub(crate) mod rail;
|
||||
|
||||
pub(crate) use rail::note_rail_width;
|
||||
pub use rail::{rail_width, rail_width_collapsed, rail_width_expanded};
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod source;
|
||||
|
||||
@@ -153,11 +158,6 @@ pub const MAX_MEASURE_CHARS: f32 = 72.0;
|
||||
/// 行宽下限的字符数(再窄正文就碎成短行,45 是最低可读长度)。
|
||||
pub const MIN_MEASURE_CHARS: f32 = 45.0;
|
||||
|
||||
/// 导航轨宽度(与 `shell::nav` 同一令牌值;布局系统只读它,不重定义)。
|
||||
pub const fn rail_width() -> f32 {
|
||||
LayoutMetrics::DESKTOP.nav_rail_width
|
||||
}
|
||||
|
||||
/// **逐行**安全宽度(px):折行模拟下「最长一行 ≤72 字符」的最大 4px 栅格列宽 = **424**。
|
||||
///
|
||||
/// 见模块文档 §3 的模拟表:428 会出现 73 字符行,426 起降到 71,424 给到 67(留 5 字符余量)。
|
||||
@@ -268,12 +268,12 @@ pub fn panes_min_window(first: f32, second: f32) -> f32 {
|
||||
first + second + PANES_GAP + 2.0 * WidthClass::Expanded.gutter() + rail_width()
|
||||
}
|
||||
|
||||
/// `ListDetail` 的几何门槛:336 + 320 + 24 + 48 + 88 = **816**。
|
||||
/// `ListDetail` 的几何门槛:336 + 320 + 24 + 48 + 88 = **816**(折叠档 rail 64 ⇒ **792**)。
|
||||
pub fn list_detail_min_window() -> f32 {
|
||||
panes_min_window(LIST_COLUMN_WIDTH, DETAIL_MIN_WIDTH)
|
||||
}
|
||||
|
||||
/// `SupportingPane` 的几何门槛:240 + 320 + 24 + 48 + 88 = **720**。
|
||||
/// `SupportingPane` 的几何门槛:240 + 320 + 24 + 48 + 88 = **720**(折叠档 rail 64 ⇒ **696**)。
|
||||
pub fn supporting_pane_min_window() -> f32 {
|
||||
panes_min_window(SIDE_NAV_WIDTH, FORM_MIN_WIDTH)
|
||||
}
|
||||
@@ -298,7 +298,7 @@ pub fn resolve(kit: PaneKit, window_width: f32) -> PageLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/// 页面可用宽度:窗口 − 导航轨 − 两侧页边距(下限 0)。
|
||||
/// 页面可用宽度:窗口 − **这一帧的轨宽**([`rail_width`]:展开 88 / 折叠 64)− 两侧页边距(下限 0)。
|
||||
pub fn available_width(window_width: f32) -> f32 {
|
||||
let class = WidthClass::of(window_width);
|
||||
(window_width - rail_width() - 2.0 * class.gutter()).max(0.0)
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
//! 轨宽的**当帧活值**(M301):`available_width` / `frame_metrics` 扣掉的那一段宽度。
|
||||
//!
|
||||
//! ## 为什么不再是常量
|
||||
//!
|
||||
//! M300 给了导航轨两个宽度档:展开 `LayoutMetrics::nav_rail_width` = **88**、折叠
|
||||
//! `nav_collapsed_width` = **64**,中间按 `medium1` / `short4` 插值。若这里仍是
|
||||
//! `const fn` 恒返回 88,用户折叠后页面照旧按 88 扣宽 ⇒ 内容区右侧留一条 **24px 空白带**,
|
||||
//! 「折叠腾出空间」只完成一半(M300 登记的偏离,本条收口)。
|
||||
//!
|
||||
//! ## 顺序契约(谁必须在谁之前)
|
||||
//!
|
||||
//! | 时刻 | 谁 | 做什么 |
|
||||
//! |---|---|---|
|
||||
//! | 每次 `App::view()` 开头 | 外壳 `app/view/rail.rs` 的 `rail_account()` | **写**:`note_rail_width(RailAccount::nav_width)` |
|
||||
//! | 同一次 `view()` 里、之后 | 页面几何(`available_width` / `frame_metrics` / `pages/agent/metrics.rs`) | **读**:`rail_width()` |
|
||||
//!
|
||||
//! 「先写后读」是**代码顺序**保证的:`app/view.rs` 的 `let account = self.rail_account();`
|
||||
//! 排在页面树(`page_with_settings_transition`)之前,所以页面扣的宽度与外壳画的轨**同一帧同一个数**。
|
||||
//!
|
||||
//! ## 为什么这份采样不是「第二份状态」
|
||||
//!
|
||||
//! 折叠的**决定**只有一份(presenter 的 `UiPrefs::nav_collapsed`,落盘 `ui.nav.collapsed`);
|
||||
//! 过渡进度也只有一份(`components/motion` 的左轨轨道)。本模块存的是「这一帧外壳真画出来的那个
|
||||
//! 宽度」,是 [`super::super::layout`] 的**只读当帧输入**:没有任何代码把它写回状态、也不跨帧
|
||||
//! 做任何判断,所以两处不会漂。
|
||||
//!
|
||||
//! ## 为什么是 thread-local
|
||||
//!
|
||||
//! 它是**当帧采样**,不是应用状态:没有业务语义、不需要跨线程或跨进程,用例天然隔离(一个用例
|
||||
//! 一个线程)。仓内同样的先例:`components/motion`(时间轴)、`pages/appearance/probe.rs`。
|
||||
//!
|
||||
//! ## `None` 的语义:本线程还没有外壳采样过 ⇒ 回落展开档
|
||||
//!
|
||||
//! 没有外壳的通道(`pages_demo`、组件库 harness、直接调 `frame_metrics` 的单测)里,轨本身是
|
||||
//! **组件版** `pages/nav.rs` 画的(只有 88 / 240 两态,没有折叠档),所以页面也必须按 88 扣宽。
|
||||
//! 于是读不到值时回落 `nav_rail_width`(不 panic、不 unwrap),这条由
|
||||
//! `pages/tests/rail_width.rs::no_shell_channel_falls_back_to_the_expanded_token` 钉住。
|
||||
//!
|
||||
//! ## 为什么没有「一帧后自动失效」
|
||||
//!
|
||||
//! 自动失效要先知道「哪一拍是新帧」,而帧边界是 `App::view()` 的开头——那里不在本条的 scope 内。
|
||||
//! 折中成两条可断言的性质:①外壳**每帧覆盖**(写入口只有一个,见 `rail_account()`);
|
||||
//! ②没采过样的通道回落 88。副作用说明白:同一线程上先跑过 `App::view()`(折叠档)再跑无外壳
|
||||
//! 通道,会读到上一次采样——`#[cfg(test)]` 的 [`forget_rail_width`] 是给这种混跑场景的显式复位。
|
||||
|
||||
use std::cell::Cell;
|
||||
|
||||
use unlucky_design::spacing::LayoutMetrics;
|
||||
|
||||
/// 展开档(`nav_rail_width` = 88)。
|
||||
pub fn rail_width_expanded() -> f32 {
|
||||
LayoutMetrics::DESKTOP.nav_rail_width
|
||||
}
|
||||
|
||||
/// 折叠档(`nav_collapsed_width` = 64)。算式与「为什么不是 48」见 `design/spacing.rs` 的令牌文档。
|
||||
pub fn rail_width_collapsed() -> f32 {
|
||||
LayoutMetrics::DESKTOP.nav_collapsed_width
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// 这一帧的轨宽:`None` = 本线程还没有外壳采样过。
|
||||
///
|
||||
/// clippy 1.99 的 `missing_const_for_thread_local` 对**已经是** `const { … }` 的初始化也报
|
||||
/// (换过写法照报,属于误报;`components/motion` 等三处已有同样的 `allow`)。
|
||||
#[allow(clippy::missing_const_for_thread_local)]
|
||||
static SAMPLE: Cell<Option<f32>> = const { Cell::new(None) };
|
||||
}
|
||||
|
||||
/// 这一帧的轨宽(逻辑 px)。未采样 ⇒ 回落展开档令牌 88。
|
||||
pub fn rail_width() -> f32 {
|
||||
SAMPLE.with(Cell::get).unwrap_or_else(rail_width_expanded)
|
||||
}
|
||||
|
||||
/// 外壳每帧采一次(**唯一写入口**:`app/view/rail.rs` 的 `rail_account()`,
|
||||
/// `pages/tests/rail_width.rs` 扫源码守这条)。
|
||||
///
|
||||
/// 非有限值(NaN / inf)与负值**不写入**:布局跟着一条坏数走会算出负的可用宽,
|
||||
/// 而那比「这一帧沿用上一帧」糟得多。静默忽略而不是 `assert`——帧路径上不许 panic。
|
||||
pub(crate) fn note_rail_width(width: f32) {
|
||||
if width.is_finite() && width >= 0.0 {
|
||||
SAMPLE.with(|sample| sample.set(Some(width)));
|
||||
}
|
||||
}
|
||||
|
||||
/// 丢掉本线程的采样(回到「未采样」= 回落 88)。
|
||||
///
|
||||
/// 产品路径不用它(外壳每帧覆盖);它给的是两条判据:①`None` 的回落行为可断言;
|
||||
/// ②「先跑过一次外壳再跑无外壳通道」的单测可以显式复位,不必依赖线程调度。
|
||||
#[cfg(test)]
|
||||
pub(crate) fn forget_rail_width() {
|
||||
SAMPLE.with(|sample| sample.set(None));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
//! 采样槽自身的三条判据(几何面在 `pages/tests/rail_width.rs`)。
|
||||
|
||||
use super::*;
|
||||
|
||||
/// 未采样 ⇒ 回落 88,且**不 panic**(`None` 就是「本帧没有外壳」)。
|
||||
#[test]
|
||||
fn an_unwritten_sample_falls_back_to_the_expanded_token() {
|
||||
forget_rail_width();
|
||||
assert_eq!(rail_width(), rail_width_expanded());
|
||||
assert_eq!(rail_width(), 88.0, "回落值就是展开档令牌");
|
||||
}
|
||||
|
||||
/// 写进去什么就读到什么;两个端值分别等于两个令牌(不是手抄的字面量)。
|
||||
#[test]
|
||||
fn the_sample_is_read_back_verbatim() {
|
||||
forget_rail_width();
|
||||
note_rail_width(rail_width_collapsed());
|
||||
assert_eq!(rail_width(), rail_width_collapsed());
|
||||
note_rail_width(rail_width_expanded());
|
||||
assert_eq!(rail_width(), rail_width_expanded());
|
||||
forget_rail_width();
|
||||
}
|
||||
|
||||
/// 坏数不写:NaN / inf / 负数被忽略(沿用上一帧),而不是把几何拖成负宽。
|
||||
#[test]
|
||||
fn a_non_finite_or_negative_sample_is_ignored() {
|
||||
forget_rail_width();
|
||||
note_rail_width(f32::NAN);
|
||||
assert_eq!(rail_width(), rail_width_expanded(), "NaN 不许写进去");
|
||||
note_rail_width(f32::INFINITY);
|
||||
assert_eq!(rail_width(), rail_width_expanded(), "inf 不许写进去");
|
||||
note_rail_width(-8.0);
|
||||
assert_eq!(rail_width(), rail_width_expanded(), "负宽不许写进去");
|
||||
forget_rail_width();
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,15 @@
|
||||
//! 每条断言都注明了「抓得住什么 / 抓不住什么」——只测到纯函数,渲染真像素另见报告里的图与量测。
|
||||
|
||||
mod base_wiring;
|
||||
mod demo_model;
|
||||
/// M301:轨宽活值的几何判据(折叠 −64 / 展开 −88 / 差 24 + 唯一写入口)
|
||||
mod rail_width;
|
||||
|
||||
use super::layout::{self, frame_metrics, measure_cap_px, PaneKit, WidthClass};
|
||||
use super::nav::NAV;
|
||||
use super::*;
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
use unlucky_design::spacing::LayoutMetrics;
|
||||
use unlucky_design::typography::Platform;
|
||||
use unlucky_i18n::Lang;
|
||||
use unlucky_presenter::router::{NavSection, Route};
|
||||
@@ -125,10 +129,20 @@ fn body_block_starts_at_the_header_title_and_fills_the_page() {
|
||||
assert!(metrics.column <= measure_cap_px() + f32::EPSILON);
|
||||
}
|
||||
|
||||
// 验收尺寸 1180:标题左缘 112、块宽 1044(旧版居中时正文左缘是 422)
|
||||
// 验收尺寸 1180:标题左缘 = rail + gutter(展开档 88 + 24 = 112)、块宽 1044。
|
||||
// M301 的期望值从**令牌**算,不再手抄 88:`nav_rail_width + Expanded.gutter`。
|
||||
let wide = frame_metrics(PaneKit::Single, 1180.0);
|
||||
assert_eq!(layout::rail_width() + wide.gutter, 112.0, "页头标题左缘");
|
||||
assert_eq!(frame::body_width(wide), 1044.0, "正文块宽度");
|
||||
let expanded_title_x = LayoutMetrics::DESKTOP.nav_rail_width + WidthClass::Expanded.gutter();
|
||||
assert_eq!(
|
||||
layout::rail_width() + wide.gutter,
|
||||
expanded_title_x,
|
||||
"页头标题左缘"
|
||||
);
|
||||
assert_eq!(
|
||||
frame::body_width(wide),
|
||||
1180.0 - expanded_title_x - WidthClass::Expanded.gutter(),
|
||||
"正文块宽度 = 窗口 − 轨 − 两侧页边距(1180 − 88 − 48 = 1044)"
|
||||
);
|
||||
|
||||
// 极宽窗:块宽封顶 1440(tower 拍板),仍然贴左 ⇒ 右侧留白而不是居中
|
||||
let huge = frame_metrics(PaneKit::Single, 2560.0);
|
||||
@@ -140,7 +154,16 @@ fn body_block_starts_at_the_header_title_and_fills_the_page() {
|
||||
// 两列版面的块宽 = 主列 pane(实例页正文在 panes 的主列里,不铺整页)
|
||||
let two = frame_metrics(PaneKit::ListDetail, 1180.0);
|
||||
assert_eq!(frame::body_width(two), two.main_pane);
|
||||
assert_eq!(two.main_pane, 1180.0 - 88.0 - 48.0 - 336.0 - 24.0);
|
||||
let rail = LayoutMetrics::DESKTOP.nav_rail_width;
|
||||
assert_eq!(
|
||||
two.main_pane,
|
||||
1180.0
|
||||
- rail
|
||||
- 2.0 * WidthClass::Expanded.gutter()
|
||||
- layout::LIST_COLUMN_WIDTH
|
||||
- layout::PANES_GAP,
|
||||
"主列 pane = 窗口 − 轨 − 两侧页边距 − 列表列 − 间隙(1180 − 88 − 48 − 336 − 24)"
|
||||
);
|
||||
}
|
||||
|
||||
/// **源码结构**:`frame::body` 的盒子必须从「块宽 + 块偏移 + 贴左」这三个数建。
|
||||
@@ -284,15 +307,32 @@ fn every_page_any_width_stays_under_the_measure_cap() {
|
||||
}
|
||||
}
|
||||
|
||||
/// 断点 600 / 840 与 rail 88:M84 的三条硬值,接真组件后不许漂。
|
||||
/// 断点 600 / 840 与轨宽两档:M84 的三条硬值 + M301 的活值回落,接真组件后不许漂。
|
||||
///
|
||||
/// **抓得住**:布局系统的常量与 `WidthClass::of` 的分档;
|
||||
/// **抓不住**:导航轨真画出来是不是 88(那条要量像素;组件自己声明 88 见下一个断言)。
|
||||
/// **抓得住**:布局系统的常量与 `WidthClass::of` 的分档,以及「没有外壳采样时按展开档扣宽」;
|
||||
/// **抓不住**:导航轨真画出来是不是 88(那条要量像素;组件自己声明 88 见下一个断言),
|
||||
/// 也抓不住「折叠后是不是真的 64」——那是 `rail_width.rs` 那组判据 + 外壳接线的事。
|
||||
#[test]
|
||||
fn the_three_frozen_numbers_are_unchanged() {
|
||||
let lm = LayoutMetrics::DESKTOP;
|
||||
assert!((layout::BREAKPOINT_MEDIUM - 600.0).abs() < f32::EPSILON);
|
||||
assert!((layout::BREAKPOINT_EXPANDED - 840.0).abs() < f32::EPSILON);
|
||||
assert!((layout::rail_width() - 88.0).abs() < f32::EPSILON);
|
||||
// M301:`rail_width()` 不再是常量,断的是它的两个端点**令牌**与未采样时的回落值。
|
||||
assert_eq!(
|
||||
layout::rail_width_expanded(),
|
||||
lm.nav_rail_width,
|
||||
"展开档令牌 88"
|
||||
);
|
||||
assert_eq!(
|
||||
layout::rail_width_collapsed(),
|
||||
lm.nav_collapsed_width,
|
||||
"折叠档令牌 64"
|
||||
);
|
||||
assert_eq!(
|
||||
layout::rail_width(),
|
||||
lm.nav_rail_width,
|
||||
"未采样 ⇒ 回落展开档"
|
||||
);
|
||||
|
||||
assert_eq!(WidthClass::of(599.9), WidthClass::Compact);
|
||||
assert_eq!(WidthClass::of(600.0), WidthClass::Medium);
|
||||
@@ -300,29 +340,6 @@ fn the_three_frozen_numbers_are_unchanged() {
|
||||
assert_eq!(WidthClass::of(840.0), WidthClass::Expanded);
|
||||
}
|
||||
|
||||
/// **组件版**导航轨(M78 的 `NavigationRail`)声明的宽度必须与布局系统同值。
|
||||
///
|
||||
/// 注意这不是产品路径的实现(M90 r3 订正):产品页的 rail 由 `shell/nav.rs` 画,组件版只服务
|
||||
/// `pages::app`(harness 出口图)与单测。这条守的是「组件与布局系统没分叉」,**不是**「产品 rail
|
||||
/// 没问题」——两套实现的漂移风险见 `pages/mod.rs` 偏离表 #10。
|
||||
///
|
||||
/// **抓得住**:`NavPaneGeometry::rail().width` == `layout::rail_width()` == 88;
|
||||
/// **抓不住**:容器圆角/药丸摆放的像素级正确性(组件自己的 probe 管),也抓不住 `shell/nav.rs`。
|
||||
#[test]
|
||||
fn the_real_rail_agrees_with_the_layout_system() {
|
||||
let ui = ui();
|
||||
let geometry = crate::app::components::navigation::geometry::NavPaneGeometry::of(
|
||||
&ui,
|
||||
crate::app::components::navigation::NavLayout::Rail,
|
||||
);
|
||||
assert!(
|
||||
(geometry.width - layout::rail_width()).abs() < f32::EPSILON,
|
||||
"真组件 rail 宽 {} 与布局系统的 {} 分叉了",
|
||||
geometry.width,
|
||||
layout::rail_width()
|
||||
);
|
||||
}
|
||||
|
||||
/// 路由 → 页面:四个一级页各自落到自己那页,`Detail` 落在实例页,`Agent` 没有页面(app 层回退)。
|
||||
#[test]
|
||||
fn routes_map_onto_pages() {
|
||||
@@ -366,24 +383,3 @@ fn every_rail_item_points_at_a_route_that_highlights_it() {
|
||||
assert_eq!(Screen::Instances.nav(), NavSection::Instances);
|
||||
assert_eq!(Screen::Settings.nav(), NavSection::Settings);
|
||||
}
|
||||
|
||||
/// 演示模型:12 行 + 1 条选中详情(M84 的出图基线就靠这份数据)。
|
||||
#[test]
|
||||
fn the_demo_model_carries_the_m84_baseline() {
|
||||
let model = demo_model(Screen::Instances, 1180.0, 760.0);
|
||||
assert_eq!(model.instances.rows.len(), 12);
|
||||
let detail = model.instances.detail.as_ref().expect("演示数据有选中项");
|
||||
assert_eq!(detail.meta.len(), 6, "元信息 6 格");
|
||||
assert!(detail.note.is_some(), "演示数据带正文段");
|
||||
assert_eq!(model.active, Some(NavSection::Instances));
|
||||
|
||||
// 其余三页不带实例数据(占位视觉不读它)
|
||||
for screen in [Screen::Download, Screen::Settings, Screen::Logs] {
|
||||
let model = demo_model(screen, 1180.0, 760.0);
|
||||
assert!(
|
||||
model.instances.rows.is_empty(),
|
||||
"{} 不该带实例数据",
|
||||
screen.slug()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//! 演示模型(`pages::demo_model`)的基线判据(M84 出图基线)。
|
||||
//!
|
||||
//! M301 从 `pages/tests.rs` 搬出来:那份文件要装轨宽活值的判据,按语义拆一块正好守 400 行门禁。
|
||||
|
||||
use unlucky_presenter::router::NavSection;
|
||||
|
||||
use super::super::*;
|
||||
|
||||
/// 演示模型:12 行 + 1 条选中详情(M84 的出图基线就靠这份数据)。
|
||||
#[test]
|
||||
fn the_demo_model_carries_the_m84_baseline() {
|
||||
let model = demo_model(Screen::Instances, 1180.0, 760.0);
|
||||
assert_eq!(model.instances.rows.len(), 12);
|
||||
let detail = model.instances.detail.as_ref().expect("演示数据有选中项");
|
||||
assert_eq!(detail.meta.len(), 6, "元信息 6 格");
|
||||
assert!(detail.note.is_some(), "演示数据带正文段");
|
||||
assert_eq!(model.active, Some(NavSection::Instances));
|
||||
|
||||
// 其余三页不带实例数据(占位视觉不读它)
|
||||
for screen in [Screen::Download, Screen::Settings, Screen::Logs] {
|
||||
let model = demo_model(screen, 1180.0, 760.0);
|
||||
assert!(
|
||||
model.instances.rows.is_empty(),
|
||||
"{} 不该带实例数据",
|
||||
screen.slug()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
//! **M301 的几何判据**:页面可用宽跟着**这一帧的轨宽**走(折叠 −64 / 展开 −88 / 差值正好 24)。
|
||||
//!
|
||||
//! 这一组都打在「当帧采样」这一层(`layout::rail`),也就是产品里外壳写的那个值:采样写什么,
|
||||
//! 页面就扣什么。四条任务判据的落点:
|
||||
//!
|
||||
//! | 判据 | 用例 |
|
||||
//! |---|---|
|
||||
//! | ①折叠态可用宽 = 窗口 − 64 ②展开态 = 窗口 − 88 ③差正好 24 | [`the_page_width_follows_the_collapsed_rail`] |
|
||||
//! | 无外壳通道回落 88(harness / 组件 demo / 单测) | [`no_shell_channel_falls_back_to_the_expanded_token`] |
|
||||
//! | 第二帧必须回到 88(不粘滞上一帧的采样) | [`the_sampled_width_is_not_sticky`] |
|
||||
//! | 门槛跟着活值走(两列版面的成立线也降 24) | [`collapsing_lowers_the_pane_threshold_by_the_same_24`] |
|
||||
//! | 唯一写入口 = 外壳,且排在页面树之前 | [`only_the_shell_samples_the_rail_width`]、[`the_shell_samples_before_the_page_tree_is_built`] |
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use unlucky_design::spacing::LayoutMetrics;
|
||||
|
||||
use super::super::frame;
|
||||
use super::super::layout::{self, block, frame_metrics, PageLayout, PaneKit, WidthClass};
|
||||
|
||||
/// 两个档位一律**从令牌取**(本文件不许再出现 88.0 / 64.0 这种字面量)。
|
||||
fn ends() -> (f32, f32) {
|
||||
(
|
||||
layout::rail_width_collapsed(),
|
||||
layout::rail_width_expanded(),
|
||||
)
|
||||
}
|
||||
|
||||
/// 采一次样、跑一段几何、复位(复位用 `forget`:回到「本线程未采样」,不把值留给后面的断言)。
|
||||
fn with_rail<T>(width: f32, run: impl FnOnce() -> T) -> T {
|
||||
layout::note_rail_width(width);
|
||||
let out = run();
|
||||
layout::rail::forget_rail_width();
|
||||
out
|
||||
}
|
||||
|
||||
/// **判据③的对照组**:没有外壳的通道必须按展开档 88 扣宽。
|
||||
///
|
||||
/// **抓得住**:`None` 的回落值被改(比如回落成折叠档,或改成 panic);
|
||||
/// **抓不住**:产品路径(外壳采样之后)的宽度——那是下面几条与外壳接线判据的事。
|
||||
#[test]
|
||||
fn no_shell_channel_falls_back_to_the_expanded_token() {
|
||||
let (_, expanded) = ends();
|
||||
layout::rail::forget_rail_width();
|
||||
assert_eq!(layout::rail_width(), expanded, "未采样 ⇒ 展开档");
|
||||
|
||||
let window = 1180.0;
|
||||
let metrics = frame_metrics(PaneKit::Single, window);
|
||||
let gutter = WidthClass::Expanded.gutter();
|
||||
assert_eq!(
|
||||
metrics.available,
|
||||
window - expanded - 2.0 * gutter,
|
||||
"无外壳通道里页面按展开档扣宽(1180 − 88 − 48 = 1044)"
|
||||
);
|
||||
assert_eq!(
|
||||
metrics.available,
|
||||
window - LayoutMetrics::DESKTOP.nav_rail_width - 2.0 * gutter,
|
||||
"回落值就是 nav_rail_width 令牌"
|
||||
);
|
||||
}
|
||||
|
||||
/// **判据①②③**:折叠态 = 窗口 − 64、展开态 = 窗口 − 88、两者差**正好** 24。
|
||||
///
|
||||
/// 两侧都用同一个窗口(1180,验收尺寸),于是 diff 里窗口项与页边距项自己约掉——剩下的只能是
|
||||
/// 轨宽差。**抓得住**:有人把 `rail_width()` 改回常量 88(折叠态那条立刻红),或者把
|
||||
/// `available_width` 的减法换回硬编码;**抓不住**:外壳到底有没有真的写这个值(下一条 + 接线判据)、
|
||||
/// 真画出来的像素(`DevDocs/artifacts/m301/` 的折叠态全窗图)。
|
||||
#[test]
|
||||
fn the_page_width_follows_the_collapsed_rail() {
|
||||
let (collapsed, expanded) = ends();
|
||||
let window = 1180.0;
|
||||
let gutter = WidthClass::Expanded.gutter();
|
||||
|
||||
let open = with_rail(expanded, || frame_metrics(PaneKit::Single, window));
|
||||
let folded = with_rail(collapsed, || frame_metrics(PaneKit::Single, window));
|
||||
|
||||
// ① 折叠态:窗口 − 64 − 两侧页边距
|
||||
assert_eq!(
|
||||
folded.available,
|
||||
window - collapsed - 2.0 * gutter,
|
||||
"折叠态可用宽必须是窗口 − 64 − 48(1180 − 64 − 48 = 1068)"
|
||||
);
|
||||
assert_ne!(
|
||||
folded.available,
|
||||
window - expanded - 2.0 * gutter,
|
||||
"折叠态不许再按 88 扣宽——那正是 M300 留下的 24px 空白带"
|
||||
);
|
||||
// ② 展开态:窗口 − 88 − 两侧页边距
|
||||
assert_eq!(
|
||||
open.available,
|
||||
window - expanded - 2.0 * gutter,
|
||||
"展开态回到窗口 − 88 − 48(1044)"
|
||||
);
|
||||
// ③ 差值正好 24 = 88 − 64
|
||||
assert_eq!(folded.available - open.available, 24.0, "两态差值正好 24");
|
||||
assert_eq!(expanded - collapsed, 24.0, "差值来自两个令牌本身");
|
||||
|
||||
// 折叠真的多腾出 24px:折宽 = 轨 + 左页边距 + 块宽(+偏移) + 右页边距,右缘贴死窗口
|
||||
assert_eq!(
|
||||
collapsed + gutter + block::body_offset(folded) + frame::body_width(folded) + gutter,
|
||||
window,
|
||||
"折叠态正文块右缘必须贴到窗口右缘(右侧不许留 24px 空白带):64 + 24 + 1068 + 24 = 1180"
|
||||
);
|
||||
assert_eq!(
|
||||
frame::body_width(folded) - frame::body_width(open),
|
||||
24.0,
|
||||
"块宽也必须跟着长 24(不是只有 available 这个字段变了)"
|
||||
);
|
||||
}
|
||||
|
||||
/// 采样**不粘滞**:写完折叠档、再写展开档,第二次必须回到 88。
|
||||
///
|
||||
/// **抓得住**:只有一个方向的写入(折叠写、展开不写);`forget` 之后回到回落值;
|
||||
/// **抓不住**:外壳有没有每帧都调用写入口(那是 `only_the_shell_samples_the_rail_width` +
|
||||
/// `the_shell_samples_before_the_page_tree_is_built` 的事)。
|
||||
#[test]
|
||||
fn the_sampled_width_is_not_sticky() {
|
||||
let (collapsed, expanded) = ends();
|
||||
layout::rail::forget_rail_width();
|
||||
|
||||
layout::note_rail_width(collapsed);
|
||||
assert_eq!(layout::rail_width(), collapsed, "第一帧:折叠档");
|
||||
|
||||
layout::note_rail_width(expanded);
|
||||
assert_eq!(
|
||||
layout::rail_width(),
|
||||
expanded,
|
||||
"第二帧必须回到展开档,不许粘住 64"
|
||||
);
|
||||
|
||||
layout::rail::forget_rail_width();
|
||||
assert_eq!(
|
||||
layout::rail_width(),
|
||||
expanded,
|
||||
"复位后回到回落值(= 展开档)"
|
||||
);
|
||||
}
|
||||
|
||||
/// 两列版面的**门槛**也跟着活值走(少了 24 的轨宽,成立线也低 24)。
|
||||
///
|
||||
/// 设置页(supporting-pane):展开门槛 720 / 折叠 696 ⇒ 700 宽的窗口在展开态是单列、折叠态是两列。
|
||||
/// 实例页(list-detail)没有这条翻转带:它的几何门槛 816 低于官方 expanded 断点 840,档位是更严的
|
||||
/// 那一条——这条断言把「24px 改动不会让实例页在 840 线上反复翻版」写成判据。
|
||||
///
|
||||
/// **抓得住**:门槛被钉成常量(折叠后仍要求 720);**抓不住**:翻转瞬间的重排观感(要出动画)。
|
||||
#[test]
|
||||
fn collapsing_lowers_the_pane_threshold_by_the_same_24() {
|
||||
let (collapsed, expanded) = ends();
|
||||
let open_min = with_rail(expanded, layout::supporting_pane_min_window);
|
||||
let folded_min = with_rail(collapsed, layout::supporting_pane_min_window);
|
||||
assert_eq!(
|
||||
open_min - folded_min,
|
||||
24.0,
|
||||
"支撑栏门槛同样差 24(720 vs 696)"
|
||||
);
|
||||
|
||||
// 700 宽:展开态够不到 720 ⇒ 单列;折叠态跨过 696 ⇒ 两列
|
||||
assert_eq!(
|
||||
with_rail(expanded, || frame_metrics(PaneKit::Supporting, 700.0)
|
||||
.layout),
|
||||
PageLayout::SingleColumn
|
||||
);
|
||||
assert_eq!(
|
||||
with_rail(collapsed, || frame_metrics(PaneKit::Supporting, 700.0)
|
||||
.layout),
|
||||
PageLayout::SupportingPane,
|
||||
"少了 24 的轨宽,700 宽的窗口就放得下两列了"
|
||||
);
|
||||
|
||||
// 实例页:816 的几何门槛在 840 的档位之下,两态在 840 上都是 list-detail(没有翻转带)
|
||||
for rail in [expanded, collapsed] {
|
||||
assert_eq!(
|
||||
with_rail(rail, || frame_metrics(PaneKit::ListDetail, 840.0).layout),
|
||||
PageLayout::ListDetail,
|
||||
"840 窗宽下两态都成立两列:档位(840)比几何门槛(816 / 792)更严"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// **唯一写入口**:产品代码里只有外壳的 `app/view/rail.rs` 采样(其余是定义点与用例)。
|
||||
///
|
||||
/// 为什么扫源码:这条判据的本质是「不许有第二个写入口」——类型系统管不住一个 `pub(crate)` 函数。
|
||||
/// 仓内已有同样的先例(`layout::source`、`tests/no_hardcoded_tokens.rs` 都靠扫文本守形态)。
|
||||
///
|
||||
/// **抓得住**:谁在 update 侧 / 页面里偷写一份轨道宽度(那会与外壳漂成两个数);
|
||||
/// **抓不住**:外壳是不是在**每一帧**都写(下一条盯顺序)。
|
||||
#[test]
|
||||
fn only_the_shell_samples_the_rail_width() {
|
||||
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src");
|
||||
let mut files = Vec::new();
|
||||
rust_files(&root, &mut files);
|
||||
assert!(
|
||||
files.len() > 50,
|
||||
"只扫到 {} 个源文件:扫描逻辑坏了",
|
||||
files.len()
|
||||
);
|
||||
|
||||
let mut writers = Vec::new();
|
||||
for path in files {
|
||||
let text = std::fs::read_to_string(&path).expect("读源文件");
|
||||
if !text.contains("note_rail_width(") {
|
||||
continue;
|
||||
}
|
||||
let relative = path
|
||||
.strip_prefix(&root)
|
||||
.expect("相对路径")
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/");
|
||||
// 定义点 + 它自己的单测;以及任何用例文件(用例**应该**能摆采样,那是它们的输入)。
|
||||
if relative == "pages/layout/rail.rs" || is_test_path(&relative) {
|
||||
continue;
|
||||
}
|
||||
writers.push(relative);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
writers,
|
||||
vec!["app/view/rail.rs".to_string()],
|
||||
"轨宽的写入口只许有一个:外壳 `app/view/rail.rs` 的 `rail_account()`"
|
||||
);
|
||||
}
|
||||
|
||||
/// **顺序契约**:外壳的采样必须排在页面树之前(`app/view.rs` 里两条语句的先后)。
|
||||
///
|
||||
/// 为什么是源码判据而不是数值判据:iced 的 `Element` 没有几何内省接口,「同一帧里谁先跑」只能
|
||||
/// 从装配代码看。写反了 = 页面用上一帧的轨宽扣宽(动画最后一帧会停在 87 点几)。
|
||||
#[test]
|
||||
fn the_shell_samples_before_the_page_tree_is_built() {
|
||||
let view = include_str!("../../app/view.rs");
|
||||
let sampled = view
|
||||
.find("let account = self.rail_account();")
|
||||
.expect("`App::view` 里必须有外壳采样这一行");
|
||||
let page = view
|
||||
.find("page_with_settings_transition(")
|
||||
.expect("`App::view` 里必须建页面树");
|
||||
assert!(
|
||||
sampled < page,
|
||||
"外壳采样必须排在页面树之前:先写后读,页面才与轨同一拍"
|
||||
);
|
||||
}
|
||||
|
||||
/// 递归收集 `.rs`(与 `tests/no_hardcoded_tokens.rs` 同一手法)。
|
||||
fn rust_files(dir: &Path, collected: &mut Vec<PathBuf>) {
|
||||
let entries =
|
||||
std::fs::read_dir(dir).unwrap_or_else(|error| panic!("读不动 {}:{error}", dir.display()));
|
||||
for entry in entries {
|
||||
let path = entry.expect("目录项读失败").path();
|
||||
if path.is_dir() {
|
||||
rust_files(&path, collected);
|
||||
} else if path.extension().is_some_and(|ext| ext == "rs") {
|
||||
collected.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 用例文件:`*/tests.rs` 或 `*/tests/*.rs`。
|
||||
fn is_test_path(relative: &str) -> bool {
|
||||
relative.ends_with("/tests.rs") || relative.contains("/tests/")
|
||||
}
|
||||
|
||||
/// **组件版**导航轨(M78 的 `NavigationRail`)声明的宽度必须与布局系统同值。
|
||||
///
|
||||
/// 注意这不是产品路径的实现(M90 r3 订正):产品页的 rail 由 `shell/nav.rs` 画,组件版只服务
|
||||
/// `pages::app`(harness 出口图)与单测。这条守的是「组件与布局系统没分叉」,**不是**「产品 rail
|
||||
/// 没问题」——两套实现的漂移风险见 `pages/mod.rs` 偏离表 #10。
|
||||
///
|
||||
/// M301:组件版只有 88 / 240 两态(`pages/nav.rs` 的模块文档写了为什么不做第三档),所以这里比
|
||||
/// 的是**展开档**;本线程没有外壳采样 ⇒ `rail_width()` 正是展开档。
|
||||
///
|
||||
/// **抓得住**:`NavPaneGeometry::rail().width` 与 `layout::rail_width_expanded()` 分叉;
|
||||
/// **抓不住**:容器圆角/药丸摆放的像素级正确性(组件自己的 probe 管),也抓不住 `shell/nav.rs`。
|
||||
#[test]
|
||||
fn the_real_rail_agrees_with_the_layout_system() {
|
||||
let ui = super::ui();
|
||||
let geometry = crate::app::components::navigation::geometry::NavPaneGeometry::of(
|
||||
&ui,
|
||||
crate::app::components::navigation::NavLayout::Rail,
|
||||
);
|
||||
let expanded = layout::rail_width_expanded();
|
||||
assert!(
|
||||
(geometry.width - expanded).abs() < f32::EPSILON,
|
||||
"真组件 rail 宽 {} 与布局系统的展开档 {expanded} 分叉了",
|
||||
geometry.width
|
||||
);
|
||||
assert!(
|
||||
(layout::rail_width() - expanded).abs() < f32::EPSILON,
|
||||
"没有外壳采样时,页面扣宽也必须等于展开档"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user