docs(app-iced): 值字级注释改真 + probe 只认 buttons/divider + 截图主题钉死回归测试(M229 复审 r2)
1) P1-2 的注释值与量测对齐(四处):`elevated.rs` 的 `PAD`/`Texts::title`/`texts()` 与
`pages/instances/detail.rs` 还写着「值 14/400」,而实现是 12/400;`PAD` 文档更把 12px 的
118px 量测挂在「14px 下」。全部改成真值,并把 14px=138px 与 12px=118px 两个量测并列写清。
2) probe 门禁只解决一半:`SECTION=buttons` 绿、`SECTION=all` 仍红(sizes 段的实心块撞「按钮
40 高」判据),断言文案却写「all 或 buttons」。现在:
- 只支持 `buttons`(按钮矩阵,实心块判据**只量矩阵区**,后续段不再参与)与 `divider`
(分隔线两条判据,独立出口);
- `all` / 其它 / 未给 → 明确失败并给理由(整页比 1180×760 高,分隔线段与按钮矩阵不可能同屏;
整页图的 chip/卡片会被当按钮量);
- 模块文档的复现命令补上 `ULL_GALLERY_SECTION`(以前照着跑会 panic);
- `gallery/mod.rs` 的「五段」→ 七段;`gallery/buttons.rs` 的「8 行 + icon·standard」→ 9 行 + 四式。
3) 截图通道的主题档钉死此前没有回归测试(复审 r2 P2-a)。新增
`the_screenshot_channel_pins_the_theme_mode_instead_of_reading_the_file`:两份不同的机器配置 +
同一组截图参数 ⇒ 同一个档位且等于参数;反证非截图通道下两份配置确实分出两档。
变异自证:把分支改回 `bridge.load().1` → 该测试红(left: Dark, right: Light)。
门禁:fmt --check 0 / check -p unlucky-app-iced --all-targets 0 / test -p unlucky-app-iced 0
(887 passed + 6 integration)/ clippy -p unlucky-app-iced --all-targets -D warnings 0 / 400 行扫描干净。
probe 亲跑:buttons 浅+深两张 sheet OK,divider 一张 OK,`all` 按预期被拒。
This commit is contained in:
1 parent
8958b7f67d
commit
f5a2610e4d
7 files changed
+205
-69
No files matched your search
@@ -84,3 +84,104 @@ fn the_theme_message_switches_the_board_and_persists_it() {
|
||||
assert_eq!(app.refreshes, refreshes, "同一档不该再重建视图模型");
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
/// **截图通道把主题档钉死在 `--screenshot-theme`**,不读机器上的 `launcher.json`(D30 可复现)。
|
||||
///
|
||||
/// 复审 r2 P2-a 指出:`construct.rs` 里新加的这条分支没有回归测试——于是「哪天有人把它改回
|
||||
/// `bridge.load().1`」不会有人发现,两次不同配置的机器又会出不同颜色的图。
|
||||
///
|
||||
/// 判据:**同一份截图参数 + 两份不同的机器配置 ⇒ 同一个档位**,且等于参数(浅→`Light`、深→`Dark`);
|
||||
/// 反证:非截图通道下同样两份配置**确实**分出两个档(否则这条测试等于没测到分支)。
|
||||
#[test]
|
||||
fn the_screenshot_channel_pins_the_theme_mode_instead_of_reading_the_file() {
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// 写一份只含 `appearance.theme_mode` 的临时配置,并用它构造 App。
|
||||
fn with_mode(
|
||||
dir_name: &str,
|
||||
json: &str,
|
||||
options: Options,
|
||||
flavor: SchemeFlavor,
|
||||
) -> (App, PathBuf) {
|
||||
let dir =
|
||||
std::env::temp_dir().join(format!("ull-shot-theme-{dir_name}-{}", std::process::id()));
|
||||
std::fs::create_dir_all(dir.join("config")).expect("临时目录");
|
||||
let path = dir.join("config/launcher.json");
|
||||
std::fs::write(&path, json).expect("写配置文件");
|
||||
let app = App::with_presenter(
|
||||
options,
|
||||
FAMILIES,
|
||||
flavor,
|
||||
Box::new(MockBackend::new()),
|
||||
SettingsBridge::with_path(&path),
|
||||
);
|
||||
(app, dir)
|
||||
}
|
||||
|
||||
fn cleanup(dir: &Path) {
|
||||
std::fs::remove_dir_all(dir).ok();
|
||||
}
|
||||
|
||||
let dark_file = r#"{"appearance":{"theme_mode":"dark"}}"#;
|
||||
let light_file = r#"{"appearance":{"theme_mode":"light"}}"#;
|
||||
|
||||
// ① 截图参数给浅色:机器配置是 dark 也一样出 Light(钉死值赢)
|
||||
let shot_light = Options {
|
||||
screenshot: Some(PathBuf::from("unused.png")),
|
||||
screenshot_theme: Some(SchemeFlavor::Light),
|
||||
..Options::default()
|
||||
};
|
||||
let (from_dark_file, d1) = with_mode(
|
||||
"light-a",
|
||||
dark_file,
|
||||
shot_light.clone(),
|
||||
SchemeFlavor::Light,
|
||||
);
|
||||
let (from_light_file, d2) = with_mode("light-b", light_file, shot_light, SchemeFlavor::Light);
|
||||
assert_eq!(
|
||||
from_dark_file.theme_mode(),
|
||||
ThemeMode::Light,
|
||||
"截图模式下档位必须由 --screenshot-theme 决定,不许读文件里的 dark"
|
||||
);
|
||||
assert_eq!(
|
||||
from_dark_file.theme_mode(),
|
||||
from_light_file.theme_mode(),
|
||||
"两份机器配置必须出同一个档位"
|
||||
);
|
||||
assert_eq!(from_dark_file.board_name(), "silver");
|
||||
assert_eq!(from_light_file.board_name(), "silver");
|
||||
|
||||
// ② 反方向:截图参数给深色 ⇒ 即使文件写 light 也是 Dark/Ink
|
||||
let shot_dark = Options {
|
||||
screenshot: Some(PathBuf::from("unused.png")),
|
||||
screenshot_theme: Some(SchemeFlavor::Dark),
|
||||
..Options::default()
|
||||
};
|
||||
let (from_light_file_dark, d3) = with_mode("dark-a", light_file, shot_dark, SchemeFlavor::Dark);
|
||||
assert_eq!(from_light_file_dark.theme_mode(), ThemeMode::Dark);
|
||||
assert_eq!(from_light_file_dark.board_name(), "ink");
|
||||
|
||||
// ③ 反证:非截图通道下,同样的两份配置**确实**分出两个档(证明这条分支真的存在)
|
||||
let (interactive_dark, d4) =
|
||||
with_mode("plain-a", dark_file, Options::default(), SchemeFlavor::Dark);
|
||||
let (interactive_light, d5) = with_mode(
|
||||
"plain-b",
|
||||
light_file,
|
||||
Options::default(),
|
||||
SchemeFlavor::Light,
|
||||
);
|
||||
assert_eq!(
|
||||
interactive_dark.theme_mode(),
|
||||
ThemeMode::Dark,
|
||||
"产品路径要读文件"
|
||||
);
|
||||
assert_eq!(
|
||||
interactive_light.theme_mode(),
|
||||
ThemeMode::Light,
|
||||
"产品路径要读文件"
|
||||
);
|
||||
|
||||
for dir in [d1, d2, d3, d4, d5] {
|
||||
cleanup(&dir);
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,16 @@
|
||||
//! elevated |
|
||||
//! outlined |
|
||||
//! text |
|
||||
//! icon·standard | [ico] | ...
|
||||
//! icon·filled |
|
||||
//! icon·filled | [ico] | ...
|
||||
//! icon·tonal |
|
||||
//! icon·outlined | ← M229 第四式(1px 环;此前与 plain 逐像素相同)
|
||||
//! icon·plain |
|
||||
//! ```
|
||||
//!
|
||||
//! 40 个单元格共 5 列 × 8 行,一屏(1180×760)装不下,靠 `gallery/mod.rs` 的 scrollable 滚动;
|
||||
//! 截图取的是**整窗**,所以首屏能看到按钮家族的全部 5 行(图标行可能需要滚动,报告里会说明)。
|
||||
//! 45 个单元格共 5 列 × **9 行**(5 个文字变体 + **4 个图标变体**),一屏(1180×760)装不下,
|
||||
//! 靠 `gallery/mod.rs` 的 scrollable 滚动;截图取的是**整窗**,所以首屏能看到前 8 行左右
|
||||
//! (最下面一行可能要滚动)。行序 = `Variant::ALL` 之后 `IconVariant::ALL`,`probe/rows.rs`
|
||||
//! 的逐行表按同一顺序排。
|
||||
//!
|
||||
//! ## 为什么每格都强制状态
|
||||
//!
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
//!
|
||||
//! ## 并行地基(后续四条线怎么加东西)
|
||||
//!
|
||||
//! 五个家族文件在下面 `mod` 里**全部预先声明**了,每个都有自己的 `section()`:
|
||||
//! 各家族文件在下面 `mod` 里**全部预先声明**了,每个都有自己的 `section()`:
|
||||
//!
|
||||
//! ```text
|
||||
//! gallery/buttons.rs ← COMPBASE(本 mission)
|
||||
@@ -38,12 +38,13 @@
|
||||
//!
|
||||
//! ## 段落过滤:环境变量 `ULL_GALLERY_SECTION`(M76 起)
|
||||
//!
|
||||
//! 五段拼成一列、外面套 `scrollable`,而截图只截**首屏** `1180×760`。段数一多,后面的家族
|
||||
//! **七段**(buttons / sizes / divider / selection / inputs / feedback / navigation)拼成一列、
|
||||
//! 外面套 `scrollable`,而截图只截**首屏** `1180×760`。段数一多,后面的家族
|
||||
//! 就落到折叠线以下(实测:selection 段从 y≈735 起,只剩标题一行)——那条线的
|
||||
//! 「渲染层像素断言」就没有一个像素可量。所以这里加一个**段落选择器**:
|
||||
//!
|
||||
//! ```text
|
||||
//! ULL_GALLERY_SECTION=all # 默认:五段全渲染(行为与加开关之前逐像素一致)
|
||||
//! ULL_GALLERY_SECTION=all # 默认:七段全渲染(行为与加开关之前逐像素一致)
|
||||
//! ULL_GALLERY_SECTION=buttons # 只渲染 buttons 段
|
||||
//! ULL_GALLERY_SECTION=selection # 只渲染 selection 段(本 mission 的取证与 probe 用它)
|
||||
//! ```
|
||||
@@ -105,7 +106,7 @@ struct Sections {
|
||||
}
|
||||
|
||||
impl Sections {
|
||||
/// 五段全渲染(默认)。
|
||||
/// 七段全渲染(默认)。
|
||||
const ALL: Self = Self {
|
||||
buttons: true,
|
||||
sizes: true,
|
||||
@@ -333,7 +334,7 @@ fn divider_proof<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> {
|
||||
|
||||
/// 一个家族小节的标题(`index.title`)+ 一行判据说明。
|
||||
///
|
||||
/// 五个家族文件共用(`pub(super)`),保证五段的节奏一致——这也是 rubric D4
|
||||
/// 各段共用(`pub(super)`),保证七段的节奏一致——这也是 rubric D4
|
||||
/// 「一屏字号档 ≤4」的落点:小节只有 `TitleMedium` + `BodySmall` 两档。
|
||||
pub(crate) fn heading<'a>(ui: &'a Ui, title: &'a str, note: &'a str) -> Element<'a, DemoMessage> {
|
||||
let note_style = ui.style(TypeRole::BodySmall);
|
||||
|
||||
@@ -92,11 +92,6 @@ fn delta_ratio(pixel: [u8; 3], surface: [u8; 3], line: [u8; 3]) -> f32 {
|
||||
/// `ULL_GALLERY_SECTION=buttons`,不满足就明确失败——「跳过」会把「指错了图」变成静默通过。
|
||||
fn shot_path() -> Option<std::path::PathBuf> {
|
||||
let raw = std::env::var_os("ULL_GALLERY_SHOT")?;
|
||||
let section = std::env::var("ULL_GALLERY_SECTION").unwrap_or_default();
|
||||
assert!(
|
||||
!section.trim().is_empty(),
|
||||
"这份 probe 只认 `--screen=gallery` 出的 sheet:请同时给 `ULL_GALLERY_SECTION` (`all` 或 `buttons`)。整页图(页面自己的 chip / 卡片,实心块高 21)会被当成按钮量, 得到的是误导性的红。"
|
||||
);
|
||||
let path = std::path::PathBuf::from(raw);
|
||||
if path.is_absolute() || path.exists() {
|
||||
return Some(path);
|
||||
@@ -132,6 +127,31 @@ fn gallery_geometry_matches_the_tokens() {
|
||||
};
|
||||
let ui = ui(flavor);
|
||||
|
||||
// 这份 probe **只认两种 sheet**(复审 r2 P2-b 的结论):
|
||||
//
|
||||
// - `ULL_GALLERY_SECTION=buttons`:按钮矩阵(五式 × 五态 + 图标四式)——本 probe 的主判据;
|
||||
// - `ULL_GALLERY_SECTION=divider`:分隔线的两条判据(1px / 墨量覆盖率)。
|
||||
//
|
||||
// **为什么不支持 `all`**:gallery 整页比 1180×760 的视口高,`all` 的 sheet 首屏里根本看不到
|
||||
// 分隔线段(实测 0 条长横线),而 sizes 段的实心块又会被「按钮 40 高」的判据量到。一张图装不下
|
||||
// 两段判据,所以按段出图、按段断言。
|
||||
//
|
||||
// **为什么必须给 SECTION**:整页图(下载页/设置页)的 chip 与卡片会被当成按钮量,
|
||||
// 那是误导性的红,不是缺陷。
|
||||
let section = std::env::var("ULL_GALLERY_SECTION")
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_ascii_lowercase();
|
||||
assert!(
|
||||
section == "buttons" || section == "divider",
|
||||
"这份 probe 只支持 `ULL_GALLERY_SECTION=buttons`(按钮矩阵)或 `divider`(分隔线); 当前 {section:?}。`all` 不行:整页比视口高,分隔线段与按钮矩阵不可能同屏。"
|
||||
);
|
||||
// 分隔线那段是**独立出口**:`divider` 段的 sheet 里没有按钮矩阵,判据也不同。
|
||||
if section == "divider" {
|
||||
check_divider(&shot, &ui);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- 12b 1.1 / 1.2:实心按钮 40 高、药丸圆角 20 ------------------------------
|
||||
let primary = shot.bbox_of(rgb(ui.color(Role::Primary)));
|
||||
assert!(
|
||||
@@ -140,12 +160,32 @@ fn gallery_geometry_matches_the_tokens() {
|
||||
primary.len()
|
||||
);
|
||||
|
||||
// **只判「按钮矩阵那一段」**:`--screen=gallery` 默认(`ULL_GALLERY_SECTION=all`)会把
|
||||
// sizes / divider / selection / inputs / feedback / navigation 也画进来,它们各自的实心块
|
||||
// (chips 底、卡片、stop indicator…)不该被「按钮 40 高」这条判据量到 —— 复审 r2 P2-b 的现场
|
||||
// 就是 `all` 模式下在 y≈715 撞到一块 21 高的实心块,probe 红在一个根本不在量程里的东西上。
|
||||
//
|
||||
// 矩阵区的上下界由**第一块文字按钮**(宽 > 60、高 40)的顶边决定,往下 9 行 + 8px 余量。
|
||||
let matrix_top = primary
|
||||
.iter()
|
||||
.filter(|bbox| {
|
||||
bbox.area() >= 800 && bbox.width() > 60 && (39..=41).contains(&bbox.height())
|
||||
})
|
||||
.map(|bbox| bbox.top)
|
||||
.min()
|
||||
.expect("没找到文字按钮的实心块(矩阵不在画面里?)");
|
||||
let matrix_bottom = matrix_top + rows::ROW_COUNT * rows::ROW_STEP + 8;
|
||||
|
||||
let mut label_button = None;
|
||||
for bbox in &primary {
|
||||
// 字形(OnPrimary)会把实心块割出小碎片,只判「成块的」(40×40 = 1600)
|
||||
if bbox.area() < 800 {
|
||||
continue;
|
||||
}
|
||||
// 矩阵之外(后续段)的实心块不在这条判据的量程里。
|
||||
if bbox.top >= matrix_bottom {
|
||||
continue;
|
||||
}
|
||||
assert!(
|
||||
(39..=41).contains(&bbox.height()),
|
||||
"实心块高 {}(参考 40,12b 1.1)@ {:?}",
|
||||
@@ -263,52 +303,6 @@ fn gallery_geometry_matches_the_tokens() {
|
||||
}
|
||||
assert!(measured_glyph, "没量到 IconButton 的字形");
|
||||
|
||||
// ---- 22.1:分隔线 1px(按「非底色的长横线」判,不按精确色:线被抗锯齿摊到两行)----
|
||||
//
|
||||
// **只在分隔线段被渲染时判**:`ULL_GALLERY_SECTION=buttons` 的 sheet 里没有分隔线,
|
||||
// 那一段是「没画」而不是「画错了」,断言只会误报(复审 P2 的同一类问题)。
|
||||
let section = std::env::var("ULL_GALLERY_SECTION")
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_ascii_lowercase();
|
||||
let has_divider = section.is_empty() || section == "all" || section == "divider";
|
||||
let surface = rgb(ui.color(Role::Surface));
|
||||
let mut runs = 0usize;
|
||||
let mut coverage = f32::NAN;
|
||||
if has_divider {
|
||||
for y in 0..shot.height {
|
||||
let mut longest = 0;
|
||||
let mut current = 0;
|
||||
for x in 0..shot.width {
|
||||
if !matches(shot.at(x, y), surface) {
|
||||
current += 1;
|
||||
longest = longest.max(current);
|
||||
} else {
|
||||
current = 0;
|
||||
}
|
||||
}
|
||||
if longest > 300 {
|
||||
runs += 1;
|
||||
}
|
||||
}
|
||||
// 两条线 × 各摊 1~2 行 ⇒ 2..=4 行
|
||||
assert!(
|
||||
(2..=4).contains(&runs),
|
||||
"分隔线应 2 条(每条 1px,抗锯齿最多摊 2 行)⇒ 2..=4 行,实测 {runs} 行"
|
||||
);
|
||||
// **墨量覆盖率**:一条 1dp 的线,其各行相对底色的不透明覆盖率之和应 ≈1.0。
|
||||
// 1.5dp 的线会到 ≈1.5,2dp 会到 ≈2.0 —— 这是「容忍 1.5 倍粗」的补丁。
|
||||
let full = rgb(ui.color(Role::OutlineVariant));
|
||||
// 屏上正好 2 条线(通栏 + 列表内缩),所以把总墨量除以 2 得到「单条线的覆盖率」
|
||||
coverage = line_coverage(&shot, surface, full) / 2.0;
|
||||
assert!(
|
||||
(0.8..=1.3).contains(&coverage),
|
||||
"单条分隔线墨量覆盖率应 ≈1.0(= 1dp),实测 {coverage:.2}(1.5dp 会到 ~1.5)"
|
||||
);
|
||||
} else {
|
||||
eprintln!("[probe] section={section:?} 未渲染 divider 段,跳过分隔线判据");
|
||||
}
|
||||
|
||||
// ---- 12b 1.8 禁用态:容器 + **每一行**的内容色(A1 逐变体不同)----------------
|
||||
//
|
||||
// 上一版把两个 reviewer 都指出的重复块留在这里(`disabled_colors` 被调两次、
|
||||
@@ -350,18 +344,53 @@ fn gallery_geometry_matches_the_tokens() {
|
||||
Filled 药丸 {}×{}(参考 高 40 / 宽 ≥58)
|
||||
顶行左内缩 {}(r=20 应 ≈14;8 圆角 ≈4;直角 0)
|
||||
实心块 {} 个(含 40×40 IconButton)
|
||||
分隔线长行 {} 行(每条 1px;未渲染 divider 段时为 0)
|
||||
禁用容器 (on-surface@10%,按钮族) 命中 · 逐行禁用内容断言 {} 行通过
|
||||
分隔线墨量覆盖率 {:.2}(1dp 应 ≈1.0)
|
||||
字形墨迹 {}px(24 盒应 16~26)",
|
||||
flavor,
|
||||
label_button.width(),
|
||||
true_height,
|
||||
top_inset,
|
||||
primary.len(),
|
||||
runs,
|
||||
rows::ROW_COUNT,
|
||||
coverage,
|
||||
glyph_width
|
||||
);
|
||||
}
|
||||
|
||||
/// `ULL_GALLERY_SECTION=divider` 的两条判据:分隔线 2 条(各摊 1~2 行)与单条墨量覆盖率 ≈1.0。
|
||||
///
|
||||
/// 单独成函数:`divider` 段的 sheet 没有按钮矩阵,走不到主流程的按钮判据;而主流程也不需要
|
||||
/// 分隔线(`buttons` 段的 sheet 里根本没画线,断言只会误报)。
|
||||
fn check_divider(shot: &Shot, ui: &Ui) {
|
||||
let surface = rgb(ui.color(Role::Surface));
|
||||
let mut runs = 0usize;
|
||||
for y in 0..shot.height {
|
||||
let mut longest = 0;
|
||||
let mut current = 0;
|
||||
for x in 0..shot.width {
|
||||
if !matches(shot.at(x, y), surface) {
|
||||
current += 1;
|
||||
longest = longest.max(current);
|
||||
} else {
|
||||
current = 0;
|
||||
}
|
||||
}
|
||||
if longest > 300 {
|
||||
runs += 1;
|
||||
}
|
||||
}
|
||||
// 两条线 × 各摊 1~2 行 ⇒ 2..=4 行
|
||||
assert!(
|
||||
(2..=4).contains(&runs),
|
||||
"分隔线应 2 条(每条 1px,抗锯齿最多摊 2 行)⇒ 2..=4 行,实测 {runs} 行"
|
||||
);
|
||||
// **墨量覆盖率**:一条 1dp 的线,其各行相对底色的不透明覆盖率之和应 ≈1.0。
|
||||
// 1.5dp 的线会到 ≈1.5,2dp 会到 ≈2.0 —— 这是「容忍 1.5 倍粗」的补丁。
|
||||
let full = rgb(ui.color(Role::OutlineVariant));
|
||||
// 屏上正好 2 条线(通栏 + 列表内缩),所以把总墨量除以 2 得到「单条线的覆盖率」
|
||||
let coverage = line_coverage(shot, surface, full) / 2.0;
|
||||
assert!(
|
||||
(0.8..=1.3).contains(&coverage),
|
||||
"单条分隔线墨量覆盖率应 ≈1.0(= 1dp),实测 {coverage:.2}(1.5dp 会到 ~1.5)"
|
||||
);
|
||||
eprintln!("[probe] OK section=divider:分隔线 {runs} 行、单条覆盖率 {coverage:.2}");
|
||||
}
|
||||
@@ -10,7 +10,7 @@ use crate::app::components::interaction;
|
||||
use crate::theme::Ui;
|
||||
|
||||
/// 矩阵行距(40 高按钮 + 8 间距)。
|
||||
const ROW_STEP: usize = 48;
|
||||
pub(super) const ROW_STEP: usize = 48;
|
||||
/// 行内扫描高度(可见 40 + 上下各 2 的余量)。
|
||||
const ROW_HEIGHT: usize = 44;
|
||||
/// 矩阵行数(5 个文字变体 + 4 个图标变体):probe 的汇总日志用它。
|
||||
|
||||
@@ -34,7 +34,8 @@ use crate::theme::Ui;
|
||||
/// ELEVATED CARD 的内边距:**20**(= `LayoutMetrics::card_padding`)。
|
||||
///
|
||||
/// 为什么不是规格卡量到的 **24**(卡片文字 `x=88` − 框 `x=64`):本组件唯一的消费者是实例页的
|
||||
/// 元信息卡,外框是 `META_CARD_MIN_WIDTH`(160),卡内必须放下最长的值串(14px 下实测 118px)。
|
||||
/// 元信息卡,外框是 `META_CARD_MIN_WIDTH`(160),卡内必须放下最长的值串(**12px 下实测 118px**;
|
||||
/// 同一个串在 14px 下是 138px,见 [`TITLE_ROLE`])。
|
||||
/// 24 ⇒ 内宽 112 ⇒ 值串折行 ⇒ 同排卡底不齐(M229 复审 P1-2 的现场);20 ⇒ 内宽 120 ≥ 118,
|
||||
/// 且**不动卡片宽度契约**(`pages/layout.rs` 的 160 一个字没改)。
|
||||
///
|
||||
@@ -77,13 +78,13 @@ pub fn card<'a, M: 'a>(ui: &'a Ui, content: impl Into<Element<'a, M>>) -> Elemen
|
||||
pub struct Texts<'a> {
|
||||
/// 上标注(`ELEVATED CARD` 那种小字标签)。
|
||||
pub overline: Option<&'a str>,
|
||||
/// 值 / 标题(字级是 [`TITLE_ROLE`] = 14/400)。
|
||||
/// 值 / 标题(字级是 [`TITLE_ROLE`] = **12/400**)。
|
||||
pub title: &'a str,
|
||||
/// 副文案(`BodyMedium` 14/400)。
|
||||
pub support: Option<&'a str>,
|
||||
}
|
||||
|
||||
/// 三段式内容(上标注 11/600 + 值 [`TITLE_ROLE`] 14/400 + 副文案 14/400,行间 4)。
|
||||
/// 三段式内容(上标注 11/600 + 值 [`TITLE_ROLE`] **12/400** + 副文案 14/400,行间 4)。
|
||||
pub fn texts<'a, M: 'a>(ui: &'a Ui, parts: Texts<'a>) -> Element<'a, M> {
|
||||
let mut stack = column![].spacing(ui.space(Space::X1));
|
||||
|
||||
|
||||
@@ -231,7 +231,8 @@ fn meta_grid<'a>(ui: &'a Ui, metrics: FrameMetrics, detail: &'a Detail) -> Eleme
|
||||
let mut cells_row = row![].spacing(layout::META_GAP).width(Length::Fill);
|
||||
for cell in chunk {
|
||||
// M229:元信息卡 = 规格卡的 **ELEVATED CARD**(无描边、`SurfaceContainer` 底、16 圆角)。
|
||||
// 内距 20 + 值 14/400(见 `elevated::PAD` 的登记):外框 160 − 2×20 = 120 必须放下
|
||||
// 内距 20 + 值 **12/400**(见 `elevated::PAD` / `elevated::TITLE_ROLE` 的登记):
|
||||
// 外框 160 − 2×20 = 120 必须放下
|
||||
// 最长值串,24 + 16 会让时戳折行、同排卡底不齐(复审 P1-2)。
|
||||
let card = elevated::card(
|
||||
ui,
|
||||
|
||||
Reference in new issue
Block a user