Files
Unlucky-Launcher/crates/app-iced/src/components/gallery/buttons.rs
T
AskaEth 4ce24d6fbd fix(app-iced): 禁用态与遮罩改从品牌板合成(M229 复审 F1)+ 选择类图注回填(F2)
F1(核心):三处 `Scheme::baseline(ui.flavor())` 自己重算了一份方案,绕开 `Ui` 里注入的品牌板,
于是 Ink 板上的禁用控件与对话框遮罩仍按 tone 派生的天蓝中性合成(复审量到滑块轨道 #58646C、
填充框 #222E36、遮罩 #060E13,全是蓝系)。修法:`Ui::scheme()` 公开已注入的那份,
`interaction/disabled.rs`(两族合成 + 内容色)、`feedback/dialog.rs`(scrim)、
`feedback/action.rs`(禁用动作文字)改为从它取。

修完 Ink 下实测:禁用容器(12%)#292F28、按钮容器(10%)#252B24、遮罩 #0A0E0A(绿系中性,
R = B);Silver 档遮罩 #A9A9AA。旧三个蓝系值在 7 张深色图里出现次数均为 0。

回归门:`disabled_colors_follow_the_brand_board_not_the_tone_baseline`(按板合成 + 与 baseline
不等 + 深色档 G ≥ B)、`scrim_is_scrimalpha_precomposited_onto_the_canvas` 增加「不许退回 baseline」。

F2:`selection/{matrix,mod,checkbox,radio,geometry}.rs` 里「20×20 / rx2 / 外圈 20」回填成实际渲染的
「18×18 / rx4 / 外圈 18」(勾形比例不变 ⇒ 10.8dp),并按塔裁决把搜索药丸容器从
`SurfaceContainerHigh` 抬到 **`SurfaceContainerHighest`**(贴规格卡,与 filled 字段同底);
`search.rs` / `probe/search.rs` / gallery `inputs` 图注同步。展开的搜索面板与菜单是浮层,
仍留 High 档(已在文档里写明不是漏改)。

`interaction/disabled.rs` 加测试后 405 行 ⇒ 单测纯搬移到 `interaction/disabled/tests.rs`。
2026-11-11 01:49:14 +08:00

174 lines
6.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 按钮家族的状态矩阵(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:S 档高 40 · 圆角 full(20) · 内边距 20(带图标 16/20;M229 起按 ControlSize::pad_x)· 图标 18 间距 8 · \
hover 8% / focus 10% / pressed 10% / disabled 容器 10% 内容 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))
}