WIP(app-iced): M75 组件基座 checkpoint —— 组件骨架/按钮五式/分隔线/gallery harness

已落地:
- components/{mod,interaction,layout,divider,gate}.rs + buttons/{mod,geometry,assemble,tests}.rs
- gallery/{mod,buttons,probe,scan}.rs + 四个家族桩(selection/inputs/feedback/navigation)
- cli.rs: --screen=shell|gallery + capture_flavor(截图默认 Light,删掉等系统主题的闸门)
- app.rs: gallery 路由 + DemoMessage 接线(400 行)

待办(下一个 commit):编译/测试全绿、浅深矩阵出图、1:1 并排与数值表。
This commit is contained in:
AskaEth committed 2026-11-02 04:50:44 +08:00
1 parent 22ddc74784
commit 4774e7b788
23 files changed
+3136 -133

No files matched your search

@@ -0,0 +1,172 @@
//! 按钮家族的状态矩阵(D30:浅/深 × 五态)。
//!
//! ## 矩阵形状
//!
//! ```text
//! | enabled | hovered | pressed | focused | disabled |
//! filled | [btn] | [btn] | [btn] | [btn] | [btn] |
//! tonal | ... (下同)
//! elevated |
//! outlined |
//! text |
//! icon·standard | [ico] | ...
//! icon·filled |
//! icon·tonal |
//! ```
//!
//! 40 个单元格共 5 列 × 8 行,一屏(1180×760)装不下,靠 `gallery/mod.rs` 的 scrollable 滚动;
//! 截图取的是**整窗**,所以首屏能看到按钮家族的全部 5 行(图标行可能需要滚动,报告里会说明)。
//!
//! ## 为什么每格都强制状态
//!
//! iced 的 `button::Status` 只由**真实鼠标**驱动,截图时没法让 40 个按钮同时 hover。
//! 组件层的 `Button::state(...)` 就是为这件事开的取证开关(`--force-state` 只能给整屏一种态)。
//! 强制值**只影响画什么**,不动任何状态机——所以矩阵图里的五态与运行期五态逐像素同源。
use iced::widget::{column, row};
use iced::{Element, Length};
use unlucky_design::color::Role;
use unlucky_design::icons::IconKind;
use unlucky_design::spacing::Space;
use unlucky_design::typography::TypeRole;
use crate::theme::Ui;
use super::super::buttons::{self, Button, IconVariant, Variant};
use super::super::interaction::Interaction;
use super::{cell_width, heading, row_label, state_cells, DemoMessage};
/// 按钮家族整段(标题 + 列头 + 5 行文字按钮 + 3 行图标按钮 + 一条图例说明)。
pub(super) fn section<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> {
let legend = ui.style(TypeRole::BodySmall);
let mut rows = column![header_row(ui)].spacing(ui.space(Space::X2));
for variant in Variant::ALL {
let cells = Interaction::ALL
.iter()
.map(|state| cell(ui, text_button(ui, variant, *state)))
.collect::<Vec<_>>();
rows = rows.push(
row![
row_label(ui, variant.name()),
row(cells).spacing(ui.space(Space::X2))
]
.spacing(ui.space(Space::X2)),
);
}
for variant in IconVariant::ALL {
let cells = Interaction::ALL
.iter()
.map(|state| cell(ui, icon_button(ui, variant, *state)))
.collect::<Vec<_>>();
rows = rows.push(
row![
row_label(ui, variant.name()),
row(cells).spacing(ui.space(Space::X2))
]
.spacing(ui.space(Space::X2)),
);
}
column![
heading(
ui,
"buttons · filled / tonal / elevated / outlined / text + icon",
"12b §1/§2:高 40 · 圆角 full(20) · 内边距 24(带图标 16/24)· 图标 16[G14] 间距 8 · \
hover 8% / focus 10% / pressed 10% / disabled 容器 12% 内容 38%"
),
rows,
iced::widget::text(
"图例:列 = 五态(focus 与 pressed 同 α 10%,看起来一样是规范要求);\
行 = 变体。虚线不表示任何东西,只是矩阵栅格。"
)
.size(legend.size_px)
.font(ui.font_of(legend))
.color(ui.color(Role::OnSurfaceVariant)),
]
.spacing(ui.space(Space::X2))
.into()
}
/// 列头(五态名)。
fn header_row<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> {
let style = ui.style(TypeRole::LabelSmall);
let mut head = row![row_label(ui, "")].spacing(ui.space(Space::X2));
for (name, width) in state_cells(ui) {
head = head.push(
iced::widget::container(
iced::widget::text(name)
.size(style.size_px)
.font(ui.font_of(style))
.color(ui.color(Role::OnSurfaceVariant)),
)
.width(Length::Fixed(width)),
);
}
head.into()
}
/// 一个矩阵单元格(固定宽,内容左对齐;高度只由按钮自己决定)。
fn cell<'a>(ui: &'a Ui, content: Element<'a, DemoMessage>) -> Element<'a, DemoMessage> {
iced::widget::container(content)
.width(Length::Fixed(cell_width(ui)))
.into()
}
/// 文字按钮的一格(强制成 `state`)。
fn text_button<'a>(ui: &'a Ui, variant: Variant, state: Interaction) -> Element<'a, DemoMessage> {
// 标签用变体名 + 首字母大写:图上要能一眼对上左栏,且不引入 i18n 依赖(gallery 不是产品页)
let label = match variant {
Variant::Filled => "Filled",
Variant::Tonal => "Tonal",
Variant::Elevated => "Elevated",
Variant::Outlined => "Outlined",
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)
} else {
lead
}
.state(state);
match variant {
Variant::Filled => buttons::filled(ui, spec),
Variant::Tonal => buttons::tonal(ui, spec),
Variant::Elevated => buttons::elevated(ui, spec),
Variant::Outlined => buttons::outlined(ui, spec),
Variant::Text => buttons::text(ui, spec),
}
}
/// 图标按钮的一格(强制成 `state`)。
fn icon_button<'a>(
ui: &'a Ui,
variant: IconVariant,
state: Interaction,
) -> Element<'a, DemoMessage> {
let press = if state.is_disabled() {
None
} else {
Some(DemoMessage::Pressed(variant.name()))
};
buttons::icon(ui, variant, IconKind::Settings, press, Some(state))
}