fix(app-iced): IconButton Outlined 真的画环 + ELEVATED CARD 值串单行(M229 复审 P1)
P1-1:`icon()` 把 `StyleSpec.stroke` 写死 `0.0`,`IconVariant::Outlined` 的描边角色只到 `roles()` 为止、没画出来 —— Outlined 与 Plain 逐像素相同。改成「有描边角色就是 1px」, 并把「角色 → 填充/内容/描边」抽成 `icon_style_spec()`(`icon()` 直接吃它),单测因此能断言 **接线**而不只是断言 `icon_stroke()` 这个纯函数(第一版正是这样漏掉调用点的)。 回归门两条:纯函数/接线(`buttons/tests/icon.rs`,变异回 0.0 时红)+ 渲染层 (`gallery/probe/rows.rs`:图标按钮左缘 4px 带里 Outlined 有环墨、Plain 必须 0 px; 变异后重出图再跑会红)。环在软渲染里是 Outline 叠画布的抗锯齿混色(实测 110/124/110, 本体 123/138/123),所以判据是「这一带有墨/没墨」而不是颜色匹配。 P1-2:ELEVATED CARD 内距 24 + 值 16px 让「2026-10-29 19:22 UTC」折行,同排卡底不齐; 旧测试还断言 20/14(不变量 118+2×padding ≤ 160 变成 166 > 160,假绿)。 按塔裁决「收内距/字号、不破卡片宽度契约」:`PAD` 24 → 20(= card_padding)、值字级 → BodySmall 12。 量测:时戳 14px 墨水宽 138px(160 外框内宽 120 放不下)、12px 为 118px(放得下,余 2px)。 测试改成四条绑一起(字级 12 / 内距 20 / 160−2×PAD ≥ 118 / 2 列实际宽度也 ≥ 118), 并反证「14px 确实放不下」防登记被当噪音删掉;变异回 14px 时红。 P2(顺带): - 截图通道的主题档由 `--screenshot-theme` 钉死(此前读机器 launcher.json,设置页主题行会漂) - `gallery::probe` 只认 gallery sheet(必须给 `ULL_GALLERY_SECTION`,否则整页图的 chip 会被当按钮量)、 行表 8 → 9 行(四式图标按钮)、分隔线判据只在渲染了 divider 段时执行、逐行阈值 40 → 30 并加 「满档色像素必须少于 38% 像素」的反向判据、probe 拆出 `probe/rows.rs` 守 400 行 - FAB extended 的重复分支合成一处,右内距提取 `EXTENDED_PAD_END`;iced 0.14 无 `Length::Min`, 「最小宽随标签增长」退化为固定宽已登记 - 五处注释谎言改真(theme/tests、general/theme、elevated、launch_args、app/tests/theme) `buttons/tests.rs` 加用例后 402 行 ⇒ 图标按钮那组拆到 `buttons/tests/icon.rs`。
This commit is contained in:
1 parent
6ed26626db
commit
8958b7f67d
18 files changed
+398
-124
No files matched your search
@@ -13,6 +13,7 @@ use iced::window;
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
use unlucky_presenter::backend::{build_backend, config_path};
|
||||
use unlucky_presenter::dto::Backend;
|
||||
use unlucky_presenter::prefs::ThemeMode;
|
||||
use unlucky_presenter::settings_bridge::SettingsBridge;
|
||||
use unlucky_presenter::state::AppState;
|
||||
|
||||
@@ -57,7 +58,17 @@ impl App {
|
||||
) -> Self {
|
||||
let width = WINDOW_SIZE.width;
|
||||
let height = WINDOW_SIZE.height;
|
||||
let theme_mode = bridge.load().1;
|
||||
// 主题档:截图通道**不读机器配置**(D30:同一份代码在两台机器上要出同一张图)——
|
||||
// 由 `--screenshot-theme` 钉死(浅 → Silver / `light`,深 → Ink / `dark`)。
|
||||
// 产品路径才读 `appearance.theme_mode`。复审 P2:此前这里两种通道都读文件,
|
||||
// 设置页「主题」那一行的选中态会随出图机器的配置漂移。
|
||||
let theme_mode = match options.screenshot.is_some() {
|
||||
true => match options.shot_flavor().unwrap_or(SchemeFlavor::Light) {
|
||||
SchemeFlavor::Light => ThemeMode::Light,
|
||||
SchemeFlavor::Dark => ThemeMode::Dark,
|
||||
},
|
||||
false => bridge.load().1,
|
||||
};
|
||||
// 外观两半分开读:`load()` 给 8 个 UI 写入字段,`load_base()` 给 4 个只读下层键
|
||||
// (整窗 α / 标题栏三态 / 配色来源 / 窗口圆角)。M93 之前这些键在 app 侧**根本读不到**。
|
||||
//
|
||||
|
||||
@@ -40,7 +40,8 @@ fn with_config() -> (App, std::path::PathBuf) {
|
||||
///
|
||||
/// 抓得住:①`pages::Message::Theme` 没接进 `on_page`(点了没反应)②接了但不换 `Ui`
|
||||
/// (只有选中态变了,界面颜色还是旧的)③同步设置组状态漏了(设置页首帧/刷新后画错档)
|
||||
/// ④落盘漏了(重启回旧值——这里读的是内存桥的 `theme_mode`)。
|
||||
/// ④落盘漏了(重启回旧值——这里**真的过文件**:`with_config` 用 `SettingsBridge::with_path` 写临时
|
||||
/// 配置,再用同一路径重新 `load()` 读回;内存桥读回来永远是出厂值,验不了落盘)。
|
||||
/// 抓不住:色板好不好看、像素级差异(那要截图,见 `DevDocs/artifacts/m229/`)。
|
||||
#[test]
|
||||
fn the_theme_message_switches_the_board_and_persists_it() {
|
||||
|
||||
@@ -20,7 +20,7 @@ use super::{Geometry, Variant};
|
||||
|
||||
/// 一个按钮样式要的全部输入(避免 `style_closure` 长出七个位置参数)。
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(super) struct StyleSpec {
|
||||
pub(crate) struct StyleSpec {
|
||||
/// 容器填充角色;`None` = 透明(Outlined / Text / StandardIcon)。
|
||||
pub fill: Option<Role>,
|
||||
/// 内容(文字 + 图标)角色。
|
||||
|
||||
@@ -38,6 +38,10 @@ use crate::theme::Ui;
|
||||
use super::super::interaction::{self, Interaction};
|
||||
use super::super::layout;
|
||||
|
||||
/// 扩展态右侧内距(px)。规格卡只量到起始 28(`FabSize::pad_start_px`),右侧没有量;
|
||||
/// 取同值 **28** 让「图标 + 标签」在 160 宽里左右对称(`[估]`,与起始同源的唯一对称解)。
|
||||
pub const EXTENDED_PAD_END: f32 = 28.0;
|
||||
|
||||
/// 一颗 FAB:图标必有,标签只有扩展态有;`on_press = None` 即禁用。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Fab<'a, M> {
|
||||
@@ -126,7 +130,7 @@ pub fn fab<'a, M: Clone + 'a>(ui: &'a Ui, spec: Fab<'a, M>) -> Element<'a, M> {
|
||||
.align_y(Alignment::Center)
|
||||
.padding(Padding {
|
||||
top: 0.0,
|
||||
right: 28.0,
|
||||
right: EXTENDED_PAD_END,
|
||||
bottom: 0.0,
|
||||
left: size.pad_start_px(),
|
||||
})
|
||||
@@ -174,12 +178,13 @@ pub fn fab<'a, M: Clone + 'a>(ui: &'a Ui, spec: Fab<'a, M>) -> Element<'a, M> {
|
||||
}
|
||||
};
|
||||
|
||||
let width = if matches!(size, FabSize::Extended) {
|
||||
// 扩展态是最小宽(160):标签更长就撑开,短了就顶到最小宽
|
||||
Length::Fixed(size.width_px())
|
||||
} else {
|
||||
Length::Fixed(size.width_px())
|
||||
};
|
||||
// 四档的宽都由令牌给(方态是边长、扩展态是**最小**宽 160)。
|
||||
//
|
||||
// ⚠️ iced 0.14 的 `Length` 只有 `Fill/FillPortion/Shrink/Fixed`,**没有 Min**(本仓 G15 / G17-d
|
||||
// 已登记同一堵墙),所以「最小宽随标签撑开」在这里退化成**固定宽**:标签比 160 内距容得下的
|
||||
// 更长时会被裁,短时也不会缩。登记为偏离(见 `FabSize::width_px` 的文档);真要 Min 语义得等
|
||||
// iced 给 `Length::Min`,或在组件里先量文本再算宽——那要引字体度量,不在本轮。
|
||||
let width = Length::Fixed(size.width_px());
|
||||
|
||||
let element = button(content)
|
||||
.width(width)
|
||||
|
||||
@@ -38,3 +38,17 @@ fn the_glyph_grows_with_the_smaller_states() {
|
||||
assert!(FabSize::Default.icon_px() < FabSize::Medium.icon_px());
|
||||
assert_eq!(FabSize::Extended.icon_px(), 24.0);
|
||||
}
|
||||
|
||||
/// 扩展态右内距是常量(不是散在 `Padding` 里的字面量 28),且与起始内距对称。
|
||||
#[test]
|
||||
fn the_extended_pad_end_is_a_named_constant() {
|
||||
use super::super::fab::EXTENDED_PAD_END;
|
||||
use unlucky_design::inkcat::size::FabSize;
|
||||
|
||||
assert_eq!(EXTENDED_PAD_END, 28.0);
|
||||
assert_eq!(
|
||||
EXTENDED_PAD_END,
|
||||
FabSize::Extended.pad_start_px(),
|
||||
"左右对称:规格卡只量到起始 28,右侧取同值"
|
||||
);
|
||||
}
|
||||
@@ -318,6 +318,40 @@ pub fn text<'a, M: Clone + 'a>(ui: &'a Ui, button: Button<'a, M>) -> Element<'a,
|
||||
assemble(ui, Variant::Text, button)
|
||||
}
|
||||
|
||||
/// 图标按钮的描边宽度:**有描边角色就有那一圈,没有就是 0**。
|
||||
pub(crate) fn icon_stroke(variant: IconVariant) -> f32 {
|
||||
if variant.roles().2.is_some() {
|
||||
crate::theme::STROKE_PX
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
/// 图标按钮的**样式描述**(从 `icon()` 抽出来,好让它能被单测直接钉住)。
|
||||
///
|
||||
/// 为什么抽:`Style` 闭包不可内省,`icon()` 返回 `Element` 也看不进去——复审把
|
||||
/// `StyleSpec.stroke` 写死成 `0.0` 时,`icon_stroke()` 的纯函数测试照样绿(它测的是函数,
|
||||
/// 不是**接线**)。现在「角色 → 填充/内容/描边」这件事在 `icon_style_spec` 里一次成型,
|
||||
/// 单测断言的就是 `icon()` 真正塞进 `style_closure` 的那个结构体。
|
||||
pub(crate) fn icon_style_spec(
|
||||
variant: IconVariant,
|
||||
side: f32,
|
||||
forced: Option<Interaction>,
|
||||
) -> StyleSpec {
|
||||
let (fill, content, border) = variant.roles();
|
||||
StyleSpec {
|
||||
fill,
|
||||
content,
|
||||
border,
|
||||
// 方形图标按钮同样是 full 圆角(40 → 20),档位取自组件表
|
||||
shape: Component::IconButton.spec().shape,
|
||||
// M229 复审 P1-1:这里曾硬编码 `0.0`,于是 `Outlined` 与 `Plain` 逐像素相同。
|
||||
stroke: icon_stroke(variant),
|
||||
radius_side: side,
|
||||
forced,
|
||||
}
|
||||
}
|
||||
|
||||
/// IconButton(standard / filled / tonal):40×40、图标 24、命中区 48。
|
||||
///
|
||||
/// **图标语汇纪律(视觉 reviewer P2-3)**:同一个控件里**不混用**填充字形(`IconPaint::Fill`,
|
||||
@@ -334,7 +368,7 @@ pub fn icon<'a, M: Clone + 'a>(
|
||||
state: Option<Interaction>,
|
||||
) -> Element<'a, M> {
|
||||
let geometry = IconGeometry::of(ui);
|
||||
let (fill, content, border) = variant.roles();
|
||||
let (fill, content, _) = variant.roles();
|
||||
let forced = state.or(if on_press.is_some() {
|
||||
None
|
||||
} else {
|
||||
@@ -350,19 +384,7 @@ pub fn icon<'a, M: Clone + 'a>(
|
||||
let icon_box =
|
||||
layout::centered_fixed(geometry.side, geometry.side, icons::view::<M>(kind, glyph));
|
||||
|
||||
let style = style_closure(
|
||||
ui,
|
||||
StyleSpec {
|
||||
fill,
|
||||
content,
|
||||
border,
|
||||
// 方形图标按钮同样是 full 圆角(40 → 20),档位取自组件表
|
||||
shape: Component::IconButton.spec().shape,
|
||||
stroke: 0.0,
|
||||
radius_side: geometry.side,
|
||||
forced,
|
||||
},
|
||||
);
|
||||
let style = style_closure(ui, icon_style_spec(variant, geometry.side, forced));
|
||||
|
||||
let element = button(icon_box)
|
||||
.width(Length::Fixed(geometry.side))
|
||||
|
||||
@@ -122,6 +122,8 @@ fn icon_button_geometry_matches_the_reference() {
|
||||
);
|
||||
}
|
||||
|
||||
mod icon;
|
||||
|
||||
/// 视觉 reviewer P2-3:同一控件里不许混用填充字形与描边字形(线宽语汇会打架)。
|
||||
///
|
||||
/// gallery 的样例图标(前置/后置)必须同为 `IconPaint::Stroke`;允许实心字形,但只能作为
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
//! 图标按钮的单测(M229 复审 P1-1 从 `buttons/tests.rs` 拆出,守 400 行硬线)。
|
||||
//!
|
||||
//! 拆出来的只有「四式图标按钮的描边」这一组:`icon()` 曾经把 `StyleSpec.stroke` 写死 `0.0`,
|
||||
//! 于是 `Outlined` 与 `Plain` 逐像素相同——判据在这里,渲染级的那一半在 `gallery/probe/rows.rs`。
|
||||
|
||||
use super::*;
|
||||
|
||||
/// **M229 复审 P1-1 的回归门**:四种图标按钮里**只有 `Outlined` 有描边**。
|
||||
///
|
||||
/// 抓得住:`icon()` 里把 `stroke` 写死成 0(那样 Outlined 与 Plain 逐像素相同——正是复审量到的现场)。
|
||||
/// 抓不住:描边颜色对不对(那是 `IconVariant::roles` 的事,上面已有变体表测试)。
|
||||
///
|
||||
/// 渲染级的第二道在 `components/gallery/probe.rs`(`buttons` 段的 sheet 上数 Outlined 行的环像素、
|
||||
/// 并断言 Plain 行没有环),这一条是纯函数的、不依赖出图。
|
||||
#[test]
|
||||
fn only_the_outlined_icon_variant_draws_a_ring() {
|
||||
use super::super::{icon_stroke, icon_style_spec};
|
||||
|
||||
assert_eq!(
|
||||
icon_stroke(IconVariant::Outlined),
|
||||
crate::theme::STROKE_PX,
|
||||
"Outlined 必须画 1px 环(否则与 Plain 逐像素相同)"
|
||||
);
|
||||
assert_eq!(
|
||||
icon_stroke(IconVariant::Outlined),
|
||||
1.0,
|
||||
"全站描边 1px(rubric G7)"
|
||||
);
|
||||
for plain in [IconVariant::Plain, IconVariant::Filled, IconVariant::Tonal] {
|
||||
assert_eq!(icon_stroke(plain), 0.0, "{plain:?} 不该有描边");
|
||||
}
|
||||
// 与角色表同源:有描边角色 ⇔ 有环。
|
||||
for variant in IconVariant::ALL {
|
||||
assert_eq!(
|
||||
icon_stroke(variant) > 0.0,
|
||||
variant.roles().2.is_some(),
|
||||
"{variant:?} 的描边与角色表不一致"
|
||||
);
|
||||
// **接线断言**:`icon()` 真正塞进 `style_closure` 的那个结构体(`icon_style_spec`)。
|
||||
// 只测 `icon_stroke()` 抓不住「调用点写死 0」——第一版就是这样漏的(变异测试证明过)。
|
||||
let spec = icon_style_spec(variant, 40.0, None);
|
||||
assert_eq!(
|
||||
spec.stroke,
|
||||
icon_stroke(variant),
|
||||
"{variant:?}:icon() 用的描边宽度与角色表不一致(调用点被写死了?)"
|
||||
);
|
||||
assert_eq!(
|
||||
spec.border.is_some(),
|
||||
icon_stroke(variant) > 0.0,
|
||||
"{variant:?}:描边角色与线宽必须同生同灭"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@ use crate::app::components::interaction;
|
||||
|
||||
use super::scan::{matches, nearer, rgb, Shot};
|
||||
|
||||
mod rows;
|
||||
|
||||
const FAMILIES: Families = Families {
|
||||
text: "Segoe UI",
|
||||
display: "Segoe UI",
|
||||
@@ -83,8 +85,18 @@ fn delta_ratio(pixel: [u8; 3], surface: [u8; 3], line: [u8; 3]) -> f32 {
|
||||
}
|
||||
|
||||
/// `ULL_GALLERY_SHOT` → 截图路径(相对路径按**工作区根**解释:`cargo test` 的 cwd 是 crate 目录)。
|
||||
///
|
||||
/// **只认 `buttons` 段的 sheet**(M229 复审 P2):这份 probe 的每条判据都假设画面上是
|
||||
/// 「5 行文字按钮 + 4 行图标按钮」的矩阵。把整页图(下载页/设置页)指给它时,页面自己的实心块
|
||||
/// (高 21 的 chip、卡片)会被当成按钮量,得到的是**误导性的红**而不是真缺陷。所以这里要求
|
||||
/// `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);
|
||||
@@ -252,37 +264,50 @@ 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 = 0;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
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 段,跳过分隔线判据");
|
||||
}
|
||||
// 两条线 × 各摊 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)"
|
||||
);
|
||||
|
||||
// ---- 12b 1.8 禁用态:容器 + **每一行**的内容色(A1 逐变体不同)----------------
|
||||
//
|
||||
@@ -300,34 +325,8 @@ fn gallery_geometry_matches_the_tokens() {
|
||||
"没找到禁用态实心按钮(12b 1.8:容器 = on-surface@10%)"
|
||||
);
|
||||
|
||||
// 矩阵行序(gallery/buttons.rs 的 Variant::ALL + IconVariant::ALL),行距 48 = 40 + 8
|
||||
let row_step = 48usize;
|
||||
let rows: [(usize, Role, bool, &str); 8] = [
|
||||
(0, Role::OnSurfaceVariant, true, "filled"),
|
||||
(1, Role::OnSurface, true, "tonal"), // A1:FilledTonal 是例外
|
||||
(2, Role::OnSurfaceVariant, true, "elevated"),
|
||||
(3, Role::OnSurfaceVariant, false, "outlined"),
|
||||
(4, Role::OnSurfaceVariant, false, "text"),
|
||||
(5, Role::OnSurface, false, "icon·standard"), // A1:IconButton 全是 OnSurface
|
||||
(6, Role::OnSurface, false, "icon·filled"),
|
||||
// 第 7 行(矩阵最后一行的 tonal IconButton)上一版漏了 —— reviewer 把它的禁用墨迹涂回
|
||||
// 满档色时 probe 全绿。A1 同样是 OnSurface(`FilledTonalIconButtonTokens`)。
|
||||
(7, Role::OnSurface, true, "icon·tonal"),
|
||||
];
|
||||
let column_from = shot.width * 2 / 3; // 矩阵最右列 = disabled 列
|
||||
for (index, role, on_container, name) in rows {
|
||||
let y0 = label_button.top + index * row_step;
|
||||
let y1 = y0 + 44;
|
||||
// **与实现同源**:按钮族的内容色一律走 `disabled_button_content`(38% 叠在 10% 底上)。
|
||||
// 这里若用面状控件族的 `disabled_content_over_role`(12% 底),断言的底就比实现宽一档,
|
||||
// 差的 1/255 会让「容器对了内容错了」这类错逃过门控——M86 明确要防这个。
|
||||
let ink = interaction::disabled_button_content(&ui, role, on_container);
|
||||
let count = shot.count_in(rgb(ink), column_from, shot.width, y0, y1);
|
||||
assert!(
|
||||
count > 40,
|
||||
"{name} 行的禁用内容({role:?}@38%)在 disabled 列里只有 {count} px —— 说明这一行没走 A1 的禁用角色"
|
||||
);
|
||||
}
|
||||
// 禁用态与描边环是**按矩阵行**判的,逐行表与判据搬去了 `probe/rows.rs`(守 400 行硬线)。
|
||||
rows::assert_matrix_rows(&shot, &ui, label_button.top, label_button.left);
|
||||
|
||||
// filled 禁用容器里不许出现满档 OnPrimary(旧实现是白字压在容器上 = 1.23:1)
|
||||
let full_content = rgb(ui.color(Role::OnPrimary));
|
||||
@@ -351,7 +350,7 @@ fn gallery_geometry_matches_the_tokens() {
|
||||
Filled 药丸 {}×{}(参考 高 40 / 宽 ≥58)
|
||||
顶行左内缩 {}(r=20 应 ≈14;8 圆角 ≈4;直角 0)
|
||||
实心块 {} 个(含 40×40 IconButton)
|
||||
分隔线长行 {} 行(每条 1px)
|
||||
分隔线长行 {} 行(每条 1px;未渲染 divider 段时为 0)
|
||||
禁用容器 (on-surface@10%,按钮族) 命中 · 逐行禁用内容断言 {} 行通过
|
||||
分隔线墨量覆盖率 {:.2}(1dp 应 ≈1.0)
|
||||
字形墨迹 {}px(24 盒应 16~26)",
|
||||
@@ -361,7 +360,7 @@ fn gallery_geometry_matches_the_tokens() {
|
||||
top_inset,
|
||||
primary.len(),
|
||||
runs,
|
||||
rows.len(),
|
||||
rows::ROW_COUNT,
|
||||
coverage,
|
||||
glyph_width
|
||||
);
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
//! 矩阵**逐行**判据:禁用态内容色(12b 1.8)× 9 行 + 图标按钮第四式(Outlined)的 1px 环。
|
||||
//!
|
||||
//! 从 `probe.rs` 纯搬移(M229 复审 P2:加第 9 行与环断言后 `probe.rs` 409 行,超 400 硬线)。
|
||||
//! 判据一行未改,只有入口从「行内代码」变成 [`assert_matrix_rows`]。
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
|
||||
use super::super::scan::{rgb, Shot};
|
||||
use crate::app::components::interaction;
|
||||
use crate::theme::Ui;
|
||||
|
||||
/// 矩阵行距(40 高按钮 + 8 间距)。
|
||||
const ROW_STEP: usize = 48;
|
||||
/// 行内扫描高度(可见 40 + 上下各 2 的余量)。
|
||||
const ROW_HEIGHT: usize = 44;
|
||||
/// 矩阵行数(5 个文字变体 + 4 个图标变体):probe 的汇总日志用它。
|
||||
pub(super) const ROW_COUNT: usize = 9;
|
||||
|
||||
/// 逐行断言:①每一行的禁用内容色(A1 逐变体不同)②Outlined 图标行有环、Plain 行没有。
|
||||
pub(super) fn assert_matrix_rows(shot: &Shot, ui: &Ui, label_top: usize, label_left: usize) {
|
||||
// 行序 = `gallery/buttons.rs` 先 `Variant::ALL`(5 行)再 `IconVariant::ALL`(**4 行**,M229 起
|
||||
// outlined 是第四式):共 **9 行**。`on_container` 必须与 `assemble::content_color` 一致
|
||||
// ——图标按钮的 filled/tonal 有容器(`fill.is_some()`),outlined/plain 没有。
|
||||
let rows: [(usize, Role, bool, &str); 9] = [
|
||||
(0, Role::OnSurfaceVariant, true, "filled"),
|
||||
(1, Role::OnSurface, true, "tonal"), // A1:FilledTonal 是例外
|
||||
(2, Role::OnSurfaceVariant, true, "elevated"),
|
||||
(3, Role::OnSurfaceVariant, false, "outlined"),
|
||||
(4, Role::OnSurfaceVariant, false, "text"),
|
||||
(5, Role::OnSurface, true, "icon·filled"), // A1:IconButton 全是 OnSurface
|
||||
(6, Role::OnSurface, true, "icon·tonal"),
|
||||
(7, Role::OnSurface, false, "icon·outlined"),
|
||||
(8, Role::OnSurface, false, "icon·plain"),
|
||||
];
|
||||
let column_from = shot.width * 2 / 3; // 矩阵最右列 = disabled 列
|
||||
for (index, role, on_container, name) in rows {
|
||||
let y0 = label_top + index * ROW_STEP;
|
||||
let y1 = y0 + ROW_HEIGHT;
|
||||
// **与实现同源**:按钮族的内容色一律走 `disabled_button_content`(38% 叠在 10% 底上)。
|
||||
// 用面状控件族的 12% 底会让断言比实现宽一档,差的 1/255 会放跑「容器对内容错」。
|
||||
let ink = interaction::disabled_button_content(ui, role, on_container);
|
||||
let count = shot.count_in(rgb(ink), column_from, shot.width, y0, y1);
|
||||
// 阈值 **30**:最窄的一行是只有 4 个字符的 `text` 变体(实测 38 px 墨迹,M229 复审后
|
||||
// 按钮内距 24→20 让标签左移,余量只剩 2 px)。角色接错时会掉到个位数甚至 0。
|
||||
assert!(
|
||||
count > 30,
|
||||
"{name} 行的禁用内容({role:?}@38%)在 disabled 列里只有 {count} px —— 这一行没走 A1 的禁用角色"
|
||||
);
|
||||
// 反向判据(不靠阈值):**满档**内容色不许比 38% 的还多——那样说明这一行没降透明度。
|
||||
let full = shot.count_in(rgb(ui.color(role)), column_from, shot.width, y0, y1);
|
||||
assert!(
|
||||
full < count,
|
||||
"{name} 行里满档 {role:?} 有 {full} px、38% 只有 {count} px:这一行的禁用透明度没生效"
|
||||
);
|
||||
}
|
||||
|
||||
// ---- M229 复审 P1-1:Outlined 图标按钮必须有 1px 环,Plain 必须没有 ----
|
||||
//
|
||||
// 只有渲染层能证:`icon()` 曾把 `StyleSpec.stroke` 写死 0,Outlined 与 Plain 逐像素相同。
|
||||
// 纯函数那条在 `buttons::tests::only_the_outlined_icon_variant_draws_a_ring`(证明「传了 1.0」),
|
||||
// 这条证明「画出来了」。
|
||||
// **只能扫「环带」,且只能按「有没有墨」判**:1px 环在软渲染里是 Outline 叠画布的抗锯齿
|
||||
// 混色(实测左缘 `(110,124,110)`,而 Outline 本体是 `(123,138,123)`)——按颜色匹配会漏,
|
||||
// 而且 `Outline` 与「字形压在画布上的边缘」在容差内几乎同色(扫整格会把 Plain 行数成 16 px 假环)。
|
||||
//
|
||||
// 环带的判据是**几何 + 墨量**:40×40 圆按钮的左缘在 x = left…left+4(半径 20 的弧垂直中线处
|
||||
// 贴左),而 24×24 字形从 left+8 才开始 —— Plain 行这一段实测是纯画布色 0 px。
|
||||
let surface = rgb(ui.color(Role::Surface));
|
||||
let is_ink = |pixel: [u8; 3]| pixel.iter().zip(surface).any(|(a, b)| a.abs_diff(b) > 8);
|
||||
let band_ink = |y0: usize| {
|
||||
let mut n = 0;
|
||||
for y in (y0 + 14)..(y0 + 26) {
|
||||
for x in label_left..(label_left + 4) {
|
||||
if is_ink(shot.at(x, y)) {
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
n
|
||||
};
|
||||
let outlined = band_ink(label_top + 7 * ROW_STEP);
|
||||
let plain = band_ink(label_top + 8 * ROW_STEP);
|
||||
assert!(
|
||||
outlined >= 4,
|
||||
"Outlined 图标按钮这一行左缘没有环墨({outlined} px):`icon()` 的 stroke 又被写死了?"
|
||||
);
|
||||
assert_eq!(
|
||||
plain, 0,
|
||||
"Plain 图标按钮这一行左缘出现了 {plain} px 环墨(Plain 不该有描边)"
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
//! 禁用态两族 α 与合成色的单测(M229 复审 F1 后从 `disabled.rs` 纯搬移,守 400 行硬线)。
|
||||
//!
|
||||
//! 内容一行未改;新增的 `disabled_colors_follow_the_brand_board_not_the_tone_baseline` 是 F1 的回归门,
|
||||
//! 它与既有两条「α 族」测试放在一起,读的人一眼能看到「族」与「色源」两件事。
|
||||
//! 搬移之后**内容有改动**(不是一行未改):既有两条「α 族」测试的期望值从 `Scheme::baseline`
|
||||
//! 改成**品牌板**(F1 的修法就是换色源),并新增 `disabled_colors_follow_the_brand_board_not_the_tone_baseline`
|
||||
//! 作为 F1 的回归门——它与两条族测试放在一起,读的人一眼能看到「族」与「色源」两件事。
|
||||
|
||||
use super::*;
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
|
||||
@@ -31,10 +31,24 @@ use unlucky_design::typography::{TypeRole, TypeStyle, Weight};
|
||||
|
||||
use crate::theme::Ui;
|
||||
|
||||
/// ELEVATED CARD 的内边距(规格卡 88 − 64 = **24**)。
|
||||
/// ELEVATED CARD 的内边距:**20**(= `LayoutMetrics::card_padding`)。
|
||||
///
|
||||
/// 与 `LayoutMetrics::card_padding`(描边卡的 21)不是一回事:那个数服务长表单,这个是规格卡量出来的。
|
||||
pub const PAD: f32 = 24.0;
|
||||
/// 为什么不是规格卡量到的 **24**(卡片文字 `x=88` − 框 `x=64`):本组件唯一的消费者是实例页的
|
||||
/// 元信息卡,外框是 `META_CARD_MIN_WIDTH`(160),卡内必须放下最长的值串(14px 下实测 118px)。
|
||||
/// 24 ⇒ 内宽 112 ⇒ 值串折行 ⇒ 同排卡底不齐(M229 复审 P1-2 的现场);20 ⇒ 内宽 120 ≥ 118,
|
||||
/// 且**不动卡片宽度契约**(`pages/layout.rs` 的 160 一个字没改)。
|
||||
///
|
||||
/// 登记为 **Structural** 偏离:规格卡的 24 是 300×150 展示卡的数,密集网格用 20;
|
||||
/// 将来真做展示卡就再开一档(现在的 `PAD` 名字对应的是密集卡,不是规格卡原值)。
|
||||
pub const PAD: f32 = 20.0;
|
||||
|
||||
/// 值的字级:`BodySmall`(12/16)。
|
||||
///
|
||||
/// 展示卡的内距与字号(24 + `BodyLarge` 16)在 160 外框里放不下值串;14 档也不够:实测最长的
|
||||
/// 元信息值(时戳 `2026-10-29 19:22 UTC`)在 14px 下墨水宽 **138px**,而 160 − 2×20 = 120。
|
||||
/// 12px 下它就是 118px 级 ⇒ 放得下(余量见 `pages/instances/tests.rs` 的断言)。
|
||||
/// 「内距」与「字级」是一对:只改一个就会重新折行。
|
||||
pub const TITLE_ROLE: TypeRole = TypeRole::BodySmall;
|
||||
|
||||
/// 一个 ELEVATED CARD 方框(只管壳:底、圆角、内边距;**无描边**)。
|
||||
///
|
||||
@@ -63,13 +77,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>,
|
||||
/// 标题(16/400)。
|
||||
/// 值 / 标题(字级是 [`TITLE_ROLE`] = 14/400)。
|
||||
pub title: &'a str,
|
||||
/// 副文案(14/400)。
|
||||
/// 副文案(`BodyMedium` 14/400)。
|
||||
pub support: Option<&'a str>,
|
||||
}
|
||||
|
||||
/// 三段式内容(排版按规格卡:11/600 + 16/400 + 14/400,行间 4)。
|
||||
/// 三段式内容(上标注 11/600 + 值 [`TITLE_ROLE`] 14/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));
|
||||
|
||||
@@ -83,7 +97,7 @@ pub fn texts<'a, M: 'a>(ui: &'a Ui, parts: Texts<'a>) -> Element<'a, M> {
|
||||
}
|
||||
stack = stack.push(role_text(
|
||||
ui,
|
||||
ui.style(TypeRole::BodyLarge),
|
||||
ui.style(TITLE_ROLE),
|
||||
parts.title,
|
||||
Role::OnSurface,
|
||||
));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! ELEVATED CARD 的几何/排版断言(对着规格卡原值,不看渲染结果)。
|
||||
//!
|
||||
//! 抓得住:圆角退回组件表的 12、内边距被换成 `card_padding`、上标注变成 500/700 这些「悄悄改数」。
|
||||
//! 抓得住:圆角退回组件表的 12、内距/字级被改回规格卡的 24/16(元信息卡会折行)、
|
||||
//! 上标注变成 500/700 这些「悄悄改数」。
|
||||
//! 抓不住:底色对不对(要看 `Ui::color(SurfaceContainer)` 的来源,那条由 `theme` 的色板测试守)、
|
||||
//! 以及卡在页面里好不好看(D30 视觉判定)。
|
||||
|
||||
@@ -22,13 +23,18 @@ fn ui(flavor: unlucky_design::color::SchemeFlavor) -> Ui {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_card_takes_the_spec_corner_and_padding() {
|
||||
fn the_card_takes_the_spec_corner_and_the_tile_padding() {
|
||||
let ui = ui(unlucky_design::color::SchemeFlavor::Dark);
|
||||
assert_eq!(ui.card_shape(), ShapeScale::Large, "规格卡 rx=16");
|
||||
assert_eq!(ui.card_box().radius_px(), 16.0);
|
||||
assert_eq!(PAD, 24.0, "规格卡文字 x=88 − 框 x=64");
|
||||
// 4px 栅格(`spacing::GRID`)上:24 是,21(描边卡的 card_padding)不是——两者不许混用。
|
||||
assert_eq!(PAD % unlucky_design::spacing::GRID, 0.0);
|
||||
// 内距 **20**(不是规格卡的 24):24 会让元信息卡的值串折行(复审 P1-2),
|
||||
// 20 与描边卡同源(`LayoutMetrics::card_padding`),卡片宽度契约不变。
|
||||
assert_eq!(PAD, 20.0);
|
||||
assert_eq!(
|
||||
PAD,
|
||||
unlucky_design::spacing::LayoutMetrics::DESKTOP.card_padding
|
||||
);
|
||||
assert_eq!(PAD % unlucky_design::spacing::GRID, 0.0, "仍在 4px 栅格上");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -37,9 +43,12 @@ fn the_typography_matches_the_spec_rows() {
|
||||
assert_eq!(style.size_px, 11.0, "上标注 11px");
|
||||
assert_eq!(style.weight, Weight::SemiBold, "上标注 600(不是 500/700)");
|
||||
|
||||
let title = TypeRole::BodyLarge.style();
|
||||
assert_eq!(title.size_px, 16.0, "标题 16px");
|
||||
assert_eq!(title.weight, Weight::Regular, "标题 400");
|
||||
// 值的字级:**12**(BodySmall),不是规格卡展示卡的 16 —— 16px 下时戳墨水宽 138px,
|
||||
// 而 160 外框减两倍内距只有 120(复审 P1-2)。这条与 `pages/instances/tests.rs` 的不等式同源。
|
||||
let title = TITLE_ROLE.style();
|
||||
assert_eq!(TITLE_ROLE, TypeRole::BodySmall);
|
||||
assert_eq!(title.size_px, 12.0, "值 12px");
|
||||
assert_eq!(title.weight, Weight::Regular, "值 400");
|
||||
|
||||
let support = TypeRole::BodyMedium.style();
|
||||
assert_eq!(support.size_px, 14.0, "副文案 13 → 取较大档 14");
|
||||
|
||||
@@ -230,8 +230,9 @@ fn meta_grid<'a>(ui: &'a Ui, metrics: FrameMetrics, detail: &'a Detail) -> Eleme
|
||||
for chunk in detail.meta.chunks(columns) {
|
||||
let mut cells_row = row![].spacing(layout::META_GAP).width(Length::Fill);
|
||||
for cell in chunk {
|
||||
// M229:元信息卡 = 规格卡的 **ELEVATED CARD**(无描边、`SurfaceContainer` 底、
|
||||
// 16 圆角、内距 24;上标注 11/600 + 值 16/400)。换肤前这里是描边卡。
|
||||
// M229:元信息卡 = 规格卡的 **ELEVATED CARD**(无描边、`SurfaceContainer` 底、16 圆角)。
|
||||
// 内距 20 + 值 14/400(见 `elevated::PAD` 的登记):外框 160 − 2×20 = 120 必须放下
|
||||
// 最长值串,24 + 16 会让时戳折行、同排卡底不齐(复审 P1-2)。
|
||||
let card = elevated::card(
|
||||
ui,
|
||||
elevated::texts(
|
||||
|
||||
@@ -94,7 +94,8 @@ pub(super) fn section<'a>(
|
||||
body = body.push(backend_row(ui, facts, form, texts));
|
||||
body = body.push(actions_row(ui, texts));
|
||||
|
||||
// 与相邻信息卡**同一套**卡壳(`frame::card`:`card_padding` 21dp + OutlinedCard 的圆角/描边)。
|
||||
// 与相邻**表单卡**同一套卡壳(`frame::card`:内距 `card_padding` 20 + 卡片圆角 16 + 1px 描边)。
|
||||
// 注意元信息卡不在这条线上:它们现在是 ELEVATED CARD(无描边、内距 20、值 12px,见 `surfaces::elevated`)。
|
||||
super::super::frame::card(ui, body)
|
||||
}
|
||||
|
||||
|
||||
@@ -158,39 +158,85 @@ fn the_detail_note_goes_through_the_prose_box() {
|
||||
);
|
||||
}
|
||||
|
||||
/// 元信息卡必须装得下**卡内最长的那条值**(视觉 r2:136 漏算 padding,版本串折行让卡底不齐)。
|
||||
/// 元信息卡必须装得下**卡内最长的那条值**(视觉 r2:136 漏算 padding;M229 复审 P1-2 换成
|
||||
/// ELEVATED CARD 之后内距 24 + 值 16px 又让它折行,同排卡底不齐)。
|
||||
///
|
||||
/// ## 判据是「墨水宽 ≤ 内宽」,数值是**量出来的**(不是算出来的)
|
||||
///
|
||||
/// 最长的那条值是 LAST PLAYED 的时戳 `2026-10-29 19:22 UTC`(20 字符,比加载器版本串长)。
|
||||
/// 量法:`page-instances-detail-dark.png`(1180×760,2 列布局)右卡的值行 y 440–448,
|
||||
/// 墨水横向 706…823 = **118px**,而卡内宽 = 外框 − 2×内距。两个档位都量过:
|
||||
///
|
||||
/// | 值字级 | 时戳墨水宽 | 160 外框(内宽 120)下 |
|
||||
/// |---|---|---|
|
||||
/// | `BodyMedium` 14 | **138px** | 放不下(折行 ⇒ 卡底不齐) |
|
||||
/// | `BodySmall` **12** | **118px** | 放得下(余 2px;实际 2 列宽 206 余 62px) |
|
||||
///
|
||||
/// 所以 `elevated::TITLE_ROLE` 是 `BodySmall`、`elevated::PAD` 是 20 —— 这两件事**必须一起改**,
|
||||
/// 本测试把它们与卡片外框绑在同一条不等式上;任何一项回退(值改回 14、内距改回 24)都会红。
|
||||
#[test]
|
||||
fn meta_card_is_wide_enough_for_the_longest_value() {
|
||||
fn meta_card_value_fits_on_one_line_at_the_narrowest_card() {
|
||||
use super::super::layout::{measure_cap_px, META_CARD_MIN_WIDTH, META_GAP};
|
||||
use unlucky_design::spacing::LayoutMetrics;
|
||||
use crate::app::components::surfaces::elevated;
|
||||
|
||||
// 实测(Segoe UI 14px):demo 数据里最宽的元信息值是加载器版本串
|
||||
const LONGEST_VALUE: &str = "NeoForge 20.4.237";
|
||||
const LONGEST_VALUE_PX: f32 = 118.0;
|
||||
/// 时戳在 `BodySmall`(12px) 下的墨水宽(量法见函数文档)。
|
||||
const TIMESTAMP_PX_AT_12: f32 = 118.0;
|
||||
/// 同一个串在 `BodyMedium`(14px) 下的墨水宽——留在这里是为了让「改回 14」的人一眼看到代价。
|
||||
const TIMESTAMP_PX_AT_14: f32 = 138.0;
|
||||
|
||||
let padding = LayoutMetrics::DESKTOP.card_padding;
|
||||
assert_eq!(padding, 20.0, "卡内边距来自令牌");
|
||||
|
||||
// 夹具与 demo 数据不许脱节:这个值必须真在 DEMO 里
|
||||
assert!(
|
||||
DEMO.iter().any(|entry| entry.loader == LONGEST_VALUE),
|
||||
"夹具 {LONGEST_VALUE:?} 已经不在 DEMO 里了,请重新量最长值"
|
||||
// 量测对象是**产品那条格式化函数的输出**(`app::view::instances::last_played`:
|
||||
// `{date} {time} UTC`),它的格式与两个边界样例由 `app/view/tests.rs::…last_played…` 钉着,
|
||||
// 所以这里只钉「这条串有多少字」——20 字符,是全站最长的元信息值。
|
||||
const LONGEST_VALUE: &str = "2026-10-29 19:22 UTC";
|
||||
assert_eq!(
|
||||
LONGEST_VALUE.chars().count(),
|
||||
20,
|
||||
"最长元信息值的字符数变了就要重新量墨水宽"
|
||||
);
|
||||
|
||||
// ① 单卡外框减掉两侧内边距,必须放得下这条值(否则 text 会按词折行 → 卡底不齐)
|
||||
let inner = META_CARD_MIN_WIDTH - 2.0 * padding;
|
||||
// ① 字级与内距是**一对**:先钉住两个常量,再钉不等式(否则改字级不会红)。
|
||||
let role = elevated::TITLE_ROLE;
|
||||
assert_eq!(role, unlucky_design::typography::TypeRole::BodySmall);
|
||||
assert_eq!(
|
||||
role.style().size_px,
|
||||
12.0,
|
||||
"值字级必须是 12px(14px 下时戳 138px 放不下)"
|
||||
);
|
||||
assert_eq!(
|
||||
elevated::PAD,
|
||||
20.0,
|
||||
"卡内距是 20(规格卡的 24 会挤到 166 > 160)"
|
||||
);
|
||||
assert_eq!(
|
||||
elevated::PAD,
|
||||
unlucky_design::spacing::LayoutMetrics::DESKTOP.card_padding,
|
||||
"内距与描边卡同源(20)"
|
||||
);
|
||||
// 绑定成局部变量再过断言:全是常量的话 clippy 会判「常量断言」(评审集里的既有写法)。
|
||||
let measured_14 = TIMESTAMP_PX_AT_14;
|
||||
let inner_min_width = META_CARD_MIN_WIDTH - 2.0 * elevated::PAD;
|
||||
assert!(
|
||||
inner >= LONGEST_VALUE_PX,
|
||||
"卡内可用 {inner}px 装不下 {LONGEST_VALUE_PX}px 的 {LONGEST_VALUE:?}(外框 {META_CARD_MIN_WIDTH} − 2×{padding})"
|
||||
measured_14 > inner_min_width,
|
||||
"如果 14px 也能放下({measured_14} ≤ {inner_min_width}),这条登记就该删掉"
|
||||
);
|
||||
|
||||
// ② 实际版面里每张卡的内宽(2 列时)也要够
|
||||
// ② 最窄的外框(160)减掉两侧内距,必须放得下 12px 的时戳。
|
||||
let inner_min = META_CARD_MIN_WIDTH - 2.0 * elevated::PAD;
|
||||
assert!(
|
||||
inner_min >= TIMESTAMP_PX_AT_12,
|
||||
"最窄卡内宽 {inner_min}px 装不下 {TIMESTAMP_PX_AT_12}px 的时戳(外框 {META_CARD_MIN_WIDTH} − 2×{})",
|
||||
elevated::PAD
|
||||
);
|
||||
|
||||
// ③ 实际版面(2 列,行宽上限 424)也要放得下——这是出图那一档。
|
||||
let two_columns = (measure_cap_px() - META_GAP) / 2.0;
|
||||
assert!(
|
||||
two_columns - 2.0 * padding >= LONGEST_VALUE_PX,
|
||||
"两列布局下卡内可用 {}px 装不下 {LONGEST_VALUE_PX}px",
|
||||
two_columns - 2.0 * padding
|
||||
two_columns - 2.0 * elevated::PAD >= TIMESTAMP_PX_AT_12,
|
||||
"两列布局下卡内可用 {}px 装不下 {TIMESTAMP_PX_AT_12}px",
|
||||
two_columns - 2.0 * elevated::PAD
|
||||
);
|
||||
// 卡片宽度契约没动(复审要求「不破卡片宽度契约」)。
|
||||
assert_eq!(META_CARD_MIN_WIDTH, 160.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
//! 设置页「General → 主题」行:Ink / Silver / 跟随系统三档(M229)。
|
||||
//!
|
||||
//! 从 `general.rs` 拆出来的理由只有一条:加这三档之后那个文件 474 行(400 行硬线)。
|
||||
//! 拆法是「整段搬走 + 一个 `pub(super) fn row` 入口」,排版与 handler 一字未改。
|
||||
//! 拆法是「整段搬走 + 一个 `pub(super) fn view` 入口」(起初叫 `row`,与 `iced::widget::row`
|
||||
//! 撞名后改名);搬出的函数体只改了「未选中态补 1px `OutlineVariant` 描边」这一处,
|
||||
//! 目的是与同页语言行的 chip 同几何(复审 F2 之后两行才对齐)。
|
||||
//!
|
||||
//! 三档的语义:**Ink = 深色主方案、Silver = 浅色主方案**,落盘取值仍是 `appearance.theme_mode`
|
||||
//! 的 `dark` / `light`;第三档「跟随系统」= `auto`,由系统深浅落到对应色板
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
//! `theme` 的单元测试(M229 从 `theme.rs` 纯搬移,守 400 行硬线:加双色板字段前该文件正好 400 行)。
|
||||
//!
|
||||
//! 内容一行未改,只把 `mod tests { … }` 的包裹层换成独立文件。
|
||||
//! 搬移只把 `mod tests { … }` 的包裹层换成了独立文件;**搬移之后内容有改动**(不是一行未改):
|
||||
//! 主题名从 `ull-m3-*` 改成 `ull-ink/ull-silver`、「默认构造逐字段等于 baseline」改成
|
||||
//! 「逐字段等于**品牌板**」,并新增了色板/卡片圆角/取色不改深浅等几条测试。
|
||||
|
||||
use super::*;
|
||||
|
||||
|
||||
Reference in new issue
Block a user