A1 定案(本机 gradle 缓存 material3.aar + javap):
FilledButtonTokens.<clinit>: DisabledContainerColor=OnSurface, DisabledContainerOpacity=0.1f
DisabledIconColor/DisabledLabelTextColor=OnSurfaceVariant, 两者 Opacity=0.38f
OutlinedButtonTokens/TextButtonTokens: LabelTextColor=OnSurfaceVariant(12b §1.9 原本就对)
据此:
- interaction::disabled_content_over / disabled_colors 的内容角色从 OnSurface 改成
OnSurfaceVariant(α 仍取令牌 DISABLED_CONTENT_ALPHA=0.38);设计层 color::state::disabled_content()
用的是 OnSurface,与 A1 不符 —— 令牌线修好前这里按 A1 直接求值,已登记。
- content_color 的「底」按有无容器分开:有容器叠 12% 容器、无容器叠 surface(视觉 r2 P2-1)。
- probe 加第二条护栏:无容器三式的 38% 墨迹单独计数,避免只护住有容器那四式(code r2 p2-1)。
- gallery 样例图标改描边(Download + ChevronRight),并加单测钉「同控件不混用 Fill/Stroke」(P2-3)。
- 滚动条:透明轨 + OutlineVariant thumb(P2-4;宽度 10px 是 iced 内部常量,只能改颜色)。
- probe 文档修好被 //! 打断的命令、shot_path 注释归位、去掉空转的重复断言(P2-5)。
174 lines
6.2 KiB
Rust
174 lines
6.2 KiB
Rust
//! 按钮家族的状态矩阵(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",
|
||
};
|
||
|
||
// 五态共用**同一套**装配,只有「有没有消息」随禁用变:
|
||
// 早先禁用格单独走 `Button::disabled(label)`,把前后图标一起丢了,于是 disabled 列看起来
|
||
// 像另一个组件(视觉 reviewer P2-3)。
|
||
let mut spec = if state.is_disabled() {
|
||
Button::disabled(label)
|
||
} else {
|
||
Button::new(label, DemoMessage::Pressed(label))
|
||
}
|
||
.state(state);
|
||
if variant != Variant::Text {
|
||
// 两个样例都必须是**描边**字形:`Play` 是填充字形,和 `ChevronRight` 混在一颗按钮里
|
||
// 会显得线宽不一致(视觉 reviewer P2-3)。主操作允许实心,但只能单独出现。
|
||
spec = spec
|
||
.leading(IconKind::Download)
|
||
.trailing(IconKind::ChevronRight);
|
||
}
|
||
|
||
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))
|
||
}
|