fix(app-iced): M75 复审 P1-A/P1-B/P2 —— 内容色跟态走 + 断言收紧 + 矩阵排版
P1-A 禁用内容色没下发(视觉 reviewer:text/standard 的 enabled 与 disabled 逐像素相同): assemble 里内容色恒取 ui.color(content) 再被 text().color() 钉死,Style.text_color 的 38% 永远下发不出去。新增 assemble::content_color(ui, forced, content),文字按钮与 IconButton 共用;probe 补两条像素断言(禁用 38% 墨迹必须存在、禁用容器内不得有满档 OnPrimary)。 P1-B 渲染层断言抓不住 2px 级回归(reviewer 变异验证:高度 42 仍绿): 高度改成精确 == 40;补「带图标左内边距 16(墨迹 ≈21)」断言;分隔线从「行数」改成 「墨量覆盖率 ≈1.0」(1.5dp 会到 ≈1.5)。 P2-1 probe 给了图却读不到时硬失败(只有完全没给 ULL_GALLERY_SHOT 才跳过)。 P2-2 Geometry::stroke 真正接到 Border.width;IconGeometry::icon 改由 side−2×padding 推导。 P2-3 矩阵 cell 宽 192→160(总宽 1040<1180,disabled 列不再被裁);禁用格保留前后图标。 P2-4 补注:snap 仅 wgpu 实现,CPU 验收图不受它影响。
This commit is contained in:
1 parent
8072f1cc7e
commit
e905fa3922
8 files changed
+195
-68
No files matched your search
@@ -13,7 +13,7 @@ use unlucky_design::shape::ShapeScale;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
|
||||
use crate::icons;
|
||||
use crate::theme::{Ui, STROKE_PX};
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::interaction::{self, Interaction};
|
||||
use super::super::layout;
|
||||
@@ -30,6 +30,8 @@ pub(super) struct StyleSpec {
|
||||
pub border: Option<Role>,
|
||||
/// 圆角档位(来自组件表,五式统一 `Full`)。
|
||||
pub shape: ShapeScale,
|
||||
/// 描边宽度(`Geometry::stroke`:有描边的变体 = `stroke::HAIRLINE`,其余 0)。
|
||||
pub stroke: f32,
|
||||
/// 圆角折算用的边长(药丸 = 可见高)。
|
||||
pub radius_side: f32,
|
||||
/// 强制态(gallery 取证);`None` = 跟随运行期的 `button::Status`。
|
||||
@@ -48,6 +50,11 @@ pub(super) fn assemble<'a, M: Clone + 'a>(
|
||||
) -> Element<'a, M> {
|
||||
let geometry = Geometry::of(ui, variant, spec.leading.is_some());
|
||||
let (fill, content, border) = variant.roles();
|
||||
let forced = spec.state.or(if spec.on_press.is_none() {
|
||||
Some(Interaction::Disabled)
|
||||
} else {
|
||||
None
|
||||
});
|
||||
|
||||
// 按钮家族没有浮层级高程:L1(Elevated)只用容器色调差表达(rubric §7 不画阴影)
|
||||
debug_assert!(
|
||||
@@ -57,7 +64,11 @@ pub(super) fn assemble<'a, M: Clone + 'a>(
|
||||
),
|
||||
"按钮家族不许有浮层高程"
|
||||
);
|
||||
let content_color = ui.color(content);
|
||||
// **内容色必须跟着态走**:禁用时文字/图标是 on-surface@38%,不是满档内容色。
|
||||
// 早先这里恒取 `ui.color(content)`,再被 `text(...).color(..)` 钉死,`Style.text_color`
|
||||
// 里的 38% 永远下发不出去 —— 表现为「text / standard icon 的 enabled 格与 disabled 格
|
||||
// 逐像素相同」(视觉 reviewer 实测 changed_px = 0;filled 禁用标签 1.23:1)。
|
||||
let ink = content_color(ui, forced, content);
|
||||
|
||||
let label = text(spec.label)
|
||||
.size(ui.style(TypeRole::LabelLarge).size_px)
|
||||
@@ -65,7 +76,7 @@ pub(super) fn assemble<'a, M: Clone + 'a>(
|
||||
ui.style(TypeRole::LabelLarge).line_height_px,
|
||||
)))
|
||||
.font(ui.font_of(ui.emphasized(TypeRole::LabelLarge)))
|
||||
.color(content_color);
|
||||
.color(ink);
|
||||
|
||||
let mut row = Row::new().align_y(Alignment::Center);
|
||||
if let Some(kind) = spec.leading {
|
||||
@@ -73,7 +84,7 @@ pub(super) fn assemble<'a, M: Clone + 'a>(
|
||||
row = row.push(layout::fixed(
|
||||
geometry.icon,
|
||||
geometry.icon,
|
||||
icons::view::<M>(kind, content_color),
|
||||
icons::view::<M>(kind, ink),
|
||||
));
|
||||
}
|
||||
row = row.push(label);
|
||||
@@ -81,7 +92,7 @@ pub(super) fn assemble<'a, M: Clone + 'a>(
|
||||
row = row.push(layout::fixed(
|
||||
geometry.icon,
|
||||
geometry.icon,
|
||||
icons::view::<M>(kind, content_color),
|
||||
icons::view::<M>(kind, ink),
|
||||
));
|
||||
}
|
||||
let row = row.spacing(geometry.gap);
|
||||
@@ -93,12 +104,6 @@ pub(super) fn assemble<'a, M: Clone + 'a>(
|
||||
.height(Length::Fill)
|
||||
.align_y(iced::Alignment::Center);
|
||||
|
||||
let forced = spec.state.or(if spec.on_press.is_none() {
|
||||
Some(Interaction::Disabled)
|
||||
} else {
|
||||
None
|
||||
});
|
||||
|
||||
let style = style_closure(
|
||||
ui,
|
||||
StyleSpec {
|
||||
@@ -106,6 +111,7 @@ pub(super) fn assemble<'a, M: Clone + 'a>(
|
||||
content,
|
||||
border,
|
||||
shape: variant.shape(),
|
||||
stroke: geometry.stroke,
|
||||
radius_side: geometry.height,
|
||||
forced,
|
||||
},
|
||||
@@ -122,6 +128,17 @@ pub(super) fn assemble<'a, M: Clone + 'a>(
|
||||
}
|
||||
}
|
||||
|
||||
/// 内容色(文字与图标共用):禁用态走令牌的 38%,其余态就是角色色。
|
||||
///
|
||||
/// 单独成函数是为了让「内容色跟态走」这件事只有一处实现——`assemble`(文字按钮)与 `icon`
|
||||
/// (图标按钮)都调它,两条路径不会再各自钉死满档色。
|
||||
pub(super) fn content_color(ui: &Ui, forced: Option<Interaction>, content: Role) -> iced::Color {
|
||||
match forced {
|
||||
Some(Interaction::Disabled) => interaction::disabled_colors(ui, Role::Surface).1,
|
||||
_ => ui.color(content),
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成 `button::style` 闭包:状态层 / 禁用 / 描边全在这里落地。
|
||||
pub(super) fn style_closure(
|
||||
ui: &Ui,
|
||||
@@ -166,15 +183,16 @@ pub(super) fn style_closure(
|
||||
} else {
|
||||
iced::Color::TRANSPARENT
|
||||
},
|
||||
width: if spec.border.is_some() {
|
||||
STROKE_PX
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
// 线宽来自 `Geometry::stroke`(有描边的变体 = HAIRLINE,其余 0);
|
||||
// `spec.border.is_none()` 时颜色是透明的,宽度也就无所谓了。
|
||||
width: spec.stroke,
|
||||
radius: ui.radius(spec.shape, spec.radius_side),
|
||||
},
|
||||
// rubric §7:L0/L1 不用阴影(Elevated 的 L1 由容器色调差表达)
|
||||
shadow: Shadow::default(),
|
||||
// ⚠️ `snap` 目前**只有 wgpu 后端实现**;我们的验收图走 tiny-skia(CPU),
|
||||
// 所以这个 `true` 在验收图上不起作用——分隔线的「1dp 摊成 2 行」正是这个原因,
|
||||
// 不是「iced 没有 snap」(reviewer P2-4 纠正的正是我上一版报告里的说法)。
|
||||
snap: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,12 +82,15 @@ pub struct IconGeometry {
|
||||
}
|
||||
|
||||
impl IconGeometry {
|
||||
/// 从令牌求值。
|
||||
/// 从令牌求值。图标边长**由 `side − 2×padding` 推导**(40 − 16 = 24),
|
||||
/// 与 `IconSize::Toolbar`(24)互为校验:两者不等说明令牌层动了,单测会先红。
|
||||
pub fn of(ui: &Ui) -> Self {
|
||||
let side = ui.metrics().control_height;
|
||||
let padding = ui.space(Space::X2);
|
||||
Self {
|
||||
side: ui.metrics().control_height,
|
||||
icon: IconSize::Toolbar.px(),
|
||||
padding: ui.space(Space::X2),
|
||||
side,
|
||||
icon: side - 2.0 * padding,
|
||||
padding,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -289,11 +289,10 @@ pub fn icon<'a, M: Clone + 'a>(
|
||||
// 撑满内容区(M68:24 的字形渲染成 46px、线宽 1.5 → 2.9)。
|
||||
// 这里把盒子做成整个 40×40(而不是 24×24):`button::layout` 把内容放左上角、不居中,
|
||||
// 所以「居中」这件事必须由容器自己写死,别指望 button。
|
||||
let icon_box = layout::centered_fixed(
|
||||
geometry.side,
|
||||
geometry.side,
|
||||
icons::view::<M>(kind, ui.color(content)),
|
||||
);
|
||||
// 内容色跟态走(禁用 = on-surface@38%),见 `assemble::content_color` 的说明
|
||||
let glyph = assemble::content_color(ui, forced, content);
|
||||
let icon_box =
|
||||
layout::centered_fixed(geometry.side, geometry.side, icons::view::<M>(kind, glyph));
|
||||
|
||||
let style = style_closure(
|
||||
ui,
|
||||
@@ -303,6 +302,7 @@ pub fn icon<'a, M: Clone + 'a>(
|
||||
border,
|
||||
// 方形图标按钮同样是 full 圆角(40 → 20),档位取自组件表
|
||||
shape: Component::IconButton.spec().shape,
|
||||
stroke: 0.0,
|
||||
radius_side: geometry.side,
|
||||
forced,
|
||||
},
|
||||
|
||||
@@ -219,6 +219,7 @@ fn spec_of(variant: Variant, forced: Option<Interaction>) -> StyleSpec {
|
||||
content,
|
||||
border,
|
||||
shape: variant.shape(),
|
||||
stroke: Geometry::of(&light(), variant, false).stroke,
|
||||
radius_side: Geometry::of(&light(), variant, false).height,
|
||||
forced,
|
||||
}
|
||||
|
||||
@@ -131,21 +131,20 @@ fn text_button<'a>(ui: &'a Ui, variant: Variant, state: Interaction) -> Element<
|
||||
Variant::Text => "Text",
|
||||
};
|
||||
|
||||
let lead = if variant == Variant::Text {
|
||||
Button::new(label, DemoMessage::Pressed("text"))
|
||||
} else {
|
||||
Button::new(label, DemoMessage::Pressed(label))
|
||||
.leading(IconKind::Play)
|
||||
.trailing(IconKind::ChevronRight)
|
||||
};
|
||||
|
||||
// 禁用格必须真的没有消息(语义正确),再强制成 Disabled 的画法
|
||||
let spec = if state.is_disabled() {
|
||||
// 五态共用**同一套**装配,只有「有没有消息」随禁用变:
|
||||
// 早先禁用格单独走 `Button::disabled(label)`,把前后图标一起丢了,于是 disabled 列看起来
|
||||
// 像另一个组件(视觉 reviewer P2-3)。
|
||||
let mut spec = if state.is_disabled() {
|
||||
Button::disabled(label)
|
||||
} else {
|
||||
lead
|
||||
Button::new(label, DemoMessage::Pressed(label))
|
||||
}
|
||||
.state(state);
|
||||
if variant != Variant::Text {
|
||||
spec = spec
|
||||
.leading(IconKind::Play)
|
||||
.trailing(IconKind::ChevronRight);
|
||||
}
|
||||
|
||||
match variant {
|
||||
Variant::Filled => buttons::filled(ui, spec),
|
||||
|
||||
@@ -188,9 +188,13 @@ pub(super) fn state_cells(ui: &Ui) -> Vec<(&'static str, f32)> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 矩阵单元格宽度(只影响 harness 的取景,不是组件尺寸;216 = 5 列正好铺满 1092 内容宽)。
|
||||
/// 矩阵单元格宽度(只影响 harness 的取景,不是组件尺寸)。
|
||||
///
|
||||
/// 160 = `Space::X8 × 5`:5 列 + 1 个标签列 + 4 个列间距 + 页左右各 24
|
||||
/// = 6×160 + 4×8 + 48 = **1040 < 1180**,所以第 5 列(disabled)不再被窗口右缘裁掉。
|
||||
/// 早先是 192(×6),总宽 1232 撑破窗口,disabled 列被裁掉一半(视觉 reviewer P2-3)。
|
||||
pub(super) fn cell_width(ui: &Ui) -> f32 {
|
||||
ui.space(Space::X8) * 6.0
|
||||
ui.space(Space::X8) * 5.0
|
||||
}
|
||||
|
||||
/// 一个矩阵行的左侧标签(变体名)。
|
||||
|
||||
@@ -35,6 +35,50 @@ fn ui(flavor: SchemeFlavor) -> Ui {
|
||||
}
|
||||
|
||||
/// `ULL_GALLERY_SHOT` → 截图路径(相对路径按**工作区根**解释:`cargo test` 的 cwd 是 crate 目录)。
|
||||
/// 一条 1dp 细线在竖直方向的「墨量覆盖率」:各行相对底色的不透明度之和(1dp ⇒ ≈1.0)。
|
||||
fn line_coverage(shot: &Shot, surface: [u8; 3], line: [u8; 3]) -> f32 {
|
||||
let mut total = 0.0;
|
||||
for y in 0..shot.height {
|
||||
let mut sum = 0.0f32;
|
||||
let mut count = 0usize;
|
||||
for x in 0..shot.width {
|
||||
let pixel = shot.at(x, y);
|
||||
if !matches(pixel, surface) {
|
||||
count += 1;
|
||||
sum += delta_ratio(pixel, surface, line);
|
||||
}
|
||||
}
|
||||
if count > 300 {
|
||||
total += sum / count as f32;
|
||||
}
|
||||
}
|
||||
total
|
||||
}
|
||||
|
||||
/// `pixel` 相对 `surface` 的偏移占「满档 line 色偏移」的比例(0..=1)。
|
||||
fn delta_ratio(pixel: [u8; 3], surface: [u8; 3], line: [u8; 3]) -> f32 {
|
||||
let channel = |i: usize| {
|
||||
let full = f32::from(line[i]) - f32::from(surface[i]);
|
||||
if full.abs() < 1.0 {
|
||||
return 0.0;
|
||||
}
|
||||
((f32::from(pixel[i]) - f32::from(surface[i])) / full).clamp(0.0, 1.0)
|
||||
};
|
||||
let (dr, dg, db) = (channel(0), channel(1), channel(2));
|
||||
(dr + dg + db) / 3.0
|
||||
}
|
||||
|
||||
/// 图上某个精确色的像素数(带容差)。
|
||||
fn count_pixels(shot: &Shot, color: [u8; 3]) -> usize {
|
||||
(0..shot.height)
|
||||
.map(|y| {
|
||||
(0..shot.width)
|
||||
.filter(|x| matches(shot.at(*x, y), color))
|
||||
.count()
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn shot_path() -> Option<std::path::PathBuf> {
|
||||
let raw = std::env::var_os("ULL_GALLERY_SHOT")?;
|
||||
let path = std::path::PathBuf::from(raw);
|
||||
@@ -138,7 +182,33 @@ fn gallery_geometry_matches_the_tokens() {
|
||||
"药丸圆角应 = 20(12b 1.2):顶部行左内缩应 ≈14,实测 {top_inset}(8 圆角 ≈4,直角 0)"
|
||||
);
|
||||
|
||||
// ---- Q9:IconButton 的字形不许撑满 40 盒 --- // ---- Q9:IconButton 的字形不许撑满 40 盒 -----------------------------------
|
||||
// ---- 12b 1.4 / 1.6:内边距 16、图标↔文字 8(用带前置图标的 Filled 格量)----
|
||||
let ink = shot.ink_inside(&label_button, [255, 255, 255]);
|
||||
if let Some(ink) = ink {
|
||||
let icon_pad = ink.left - label_button.left;
|
||||
// 图标盒左缘 = 16;字形在 24 viewBox 里自带 ≈5.3 的左边距 ⇒ 墨迹落在 21 附近
|
||||
assert!(
|
||||
(19..=23).contains(&icon_pad),
|
||||
"12b 1.4:带图标左内边距应 16(墨迹 ≈21),实测 {icon_pad}"
|
||||
);
|
||||
} else {
|
||||
panic!("Filled 格应带前置图标(量内边距用)");
|
||||
}
|
||||
|
||||
// ---- Q9:IconButton 的字形不许撑满 40 盒 --- // ---- 12b 1.4 / 1.6:内边距 16、图标↔文字 8(用带前置图标的 Filled 格量)----
|
||||
let ink = shot.ink_inside(&label_button, [255, 255, 255]);
|
||||
if let Some(ink) = ink {
|
||||
let icon_pad = ink.left - label_button.left;
|
||||
// 图标盒左缘 = 16;字形在 24 viewBox 里自带 ≈5.3 的左边距 ⇒ 墨迹落在 21 附近
|
||||
assert!(
|
||||
(19..=23).contains(&icon_pad),
|
||||
"12b 1.4:带图标左内边距应 16(墨迹 ≈21),实测 {icon_pad}"
|
||||
);
|
||||
} else {
|
||||
panic!("Filled 格应带前置图标(量内边距用)");
|
||||
}
|
||||
|
||||
// ---- Q9:IconButton 的字形不许撑满 40 盒 -----------------------------------
|
||||
let tonal = rgb(ui.color(Role::SecondaryContainer));
|
||||
let tonal_boxes = shot.bbox_of(tonal);
|
||||
let icon_states = tonal_boxes
|
||||
@@ -191,10 +261,18 @@ fn gallery_geometry_matches_the_tokens() {
|
||||
runs += 1;
|
||||
}
|
||||
}
|
||||
// 两条分隔线 × 各摊在 1~2 行上(抗锯齿)⇒ 2..=6 行;>6 说明线粗了或画了第三条
|
||||
// 两条线 × 各摊 1~2 行 ⇒ 2..=4 行
|
||||
assert!(
|
||||
(2..=6).contains(&runs),
|
||||
"分隔线应 2 条 × 1px(抗锯齿时各占 2 行),实测 {runs} 行"
|
||||
(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));
|
||||
let coverage = line_coverage(&shot, surface, full);
|
||||
assert!(
|
||||
(0.8..=1.3).contains(&coverage),
|
||||
"分隔线墨量覆盖率应 ≈1.0(= 1dp),实测 {coverage:.2}(1.5dp 会到 ~1.5)"
|
||||
);
|
||||
|
||||
// ---- 12b 1.8:禁用格必须与静息格不同 ------------------------------------
|
||||
@@ -207,20 +285,47 @@ fn gallery_geometry_matches_the_tokens() {
|
||||
"没找到禁用态实心按钮(12b 1.8:容器 = on-surface@12%)"
|
||||
);
|
||||
|
||||
// ---- 12b 1.8(视觉 reviewer 的 P1-A):禁用态的内容色必须真的下发 ----
|
||||
let (disabled_container, disabled_ink) =
|
||||
crate::app::components::interaction::disabled_colors(&ui, Role::Surface);
|
||||
let ink_px = count_pixels(&shot, rgb(disabled_ink));
|
||||
assert!(
|
||||
ink_px > 200,
|
||||
"禁用内容色(on-surface@38%)在图上只出现 {ink_px} 像素: 说明 text / icon 的字形没走 38%(旧实现 enabled 与 disabled 逐像素相同)"
|
||||
);
|
||||
|
||||
// filled 禁用容器里不许出现满档 OnPrimary(旧实现是 #FFFFFF 压在 12% 容器上 = 1.23:1)
|
||||
let full_content = rgb(ui.color(Role::OnPrimary));
|
||||
for bbox in shot.bbox_of(rgb(disabled_container)) {
|
||||
if bbox.width() < 60 {
|
||||
continue;
|
||||
}
|
||||
for y in (bbox.top + 2)..=(bbox.bottom - 2) {
|
||||
for x in (bbox.left + 2)..=(bbox.right - 2) {
|
||||
assert!(
|
||||
!matches(shot.at(x, y), full_content),
|
||||
"禁用容器内出现满档 OnPrimary @({x},{y}):禁用标签没降到 38%"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"[probe] OK flavor={:?}:
|
||||
Filled 药丸 {}×{}(参考 高 40 / 宽 ≥58)
|
||||
顶行左内缩 {}(r=20 应 ≈14;8 圆角 ≈4;直角 0)
|
||||
实心块 {} 个(含 40×40 IconButton)
|
||||
分隔线长行 {} 行(每条 1px)
|
||||
禁用容器 (on-surface@12%) 命中
|
||||
禁用容器 (on-surface@12%) 命中 · 禁用内容 38% 墨迹 {} px
|
||||
分隔线墨量覆盖率 {:.2}(1dp 应 ≈1.0)
|
||||
字形墨迹 {}px(24 盒应 16~26)",
|
||||
flavor,
|
||||
label_button.width(),
|
||||
true_height,
|
||||
top_inset,
|
||||
primary.len(),
|
||||
runs,
|
||||
ink_px,
|
||||
coverage,
|
||||
glyph_width
|
||||
);
|
||||
}
|
||||
@@ -211,36 +211,33 @@ fn every_component_file_stays_under_the_line_limit() {
|
||||
);
|
||||
}
|
||||
|
||||
/// 四个家族桩必须**只有桩**(不许顺手把别人的组件写了)。
|
||||
/// 四个家族目录必须存在且结构完整。
|
||||
///
|
||||
/// 判据:文件里 `pub fn` 只允许出现一次,且它是 `demo`。
|
||||
/// **这里刻意不再限制「pub fn 有几个」**:M76 往 `selection/` 里加 Checkbox/Radio 是它的本职工作,
|
||||
/// 而家族线被明确要求不许改 `gate.rs`——把「桩还在不在」写成 pub fn 计数,等于给 M76 埋一个
|
||||
/// 它无权拆的地雷(reviewer P1-C)。越界与否由 merge gate 的 scope 判定(各家族目录只归对应
|
||||
/// mission),门禁只管「地基还在」:
|
||||
///
|
||||
/// - 四个家族目录各有 `mod.rs`;
|
||||
/// - `components/mod.rs` 仍声明这五个家族 + gallery(谁把声明删了,这里先红)。
|
||||
#[test]
|
||||
fn the_four_family_stubs_are_still_stubs() {
|
||||
fn the_five_families_are_declared_and_present() {
|
||||
let declared =
|
||||
std::fs::read_to_string(components_dir().join("mod.rs")).expect("读 components/mod.rs");
|
||||
|
||||
for family in ["selection", "inputs", "feedback", "navigation"] {
|
||||
let path = components_dir().join(family).join("mod.rs");
|
||||
let text = std::fs::read_to_string(&path)
|
||||
.unwrap_or_else(|error| panic!("家族桩 {} 不见了:{error}", path.display()));
|
||||
let product = product_part(&text);
|
||||
|
||||
let pub_fns: Vec<&str> = product
|
||||
.lines()
|
||||
.filter(|line| line.trim_start().starts_with("pub fn "))
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
pub_fns.len(),
|
||||
1,
|
||||
"{} 只该有占位 demo(),实际有 {} 个 pub fn:{pub_fns:?}(别顺手实现别人的家族)",
|
||||
path.display(),
|
||||
pub_fns.len()
|
||||
);
|
||||
let dir = components_dir().join(family);
|
||||
let file = dir.join("mod.rs");
|
||||
assert!(file.is_file(), "家族目录 {} 缺 mod.rs", dir.display());
|
||||
assert!(
|
||||
pub_fns[0].contains("fn demo"),
|
||||
"{} 里唯一那个 pub fn 应该是 demo,实际是 {:?}",
|
||||
path.display(),
|
||||
pub_fns[0]
|
||||
declared.contains(&format!("pub mod {family};")),
|
||||
"components/mod.rs 少了 `pub mod {family};` 声明"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
declared.contains("pub mod gallery;") && declared.contains("pub mod buttons;"),
|
||||
"components/mod.rs 的声明被动了"
|
||||
);
|
||||
}
|
||||
|
||||
/// gallery 的五个家族文件必须都在(`mod.rs` 声明了它们,缺一个就编译不过——
|
||||
|
||||
Reference in new issue
Block a user