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
@@ -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]
|
||||
|
||||
Reference in new issue
Block a user