feat(iced): 反馈类家族四件(Progress / Snackbar / Dialog / EmptyState)
D39 五家族并行的反馈类线(M78 / COMPFEED)。全部对消息类型泛型、不 import app::Message, 数值来自令牌或登记为 G17(令牌层没有反馈类的 ComponentMetrics 与骨架位)。 - Progress:linear(确定态 FillPortion 占比 + 官方 1750ms 双段不定态 keyframes)与 circular(40/4 环,弧长 = 数值×360°,不定态走官方 6000ms 弧长+旋转 keyframes)。 环形用几何 SVG(canvas feature 没开),组件不计时,相位由调用方给。 - Snackbar:48/68 两种高、上限宽 600(A1 ContainerMaxWidth)、12 外边距、贴顶/贴底、 action(InversePrimary)+ 关闭键(InverseOnSurface);**组件不计时**,超时归调用方。 iced 的 Length 没有 min-width ⇒ 344 的最小宽由宿主保证(登记 G17-d,与按钮 G15 同墙)。 - Dialog:scrim(黑@0.32 预合成实色,D27)+ R28 + SurfaceContainerHigh + pad 24 + 标题/正文/动作三区 + 无动作/单动作/双动作;官方 280..560 区间内取 480。 - EmptyState:**官方没有这个组件**,96 图标块/32 图标/HeadlineSmall 全部标 [估]。 - action.rs:Dialog/Snackbar 共用的文字/图标动作(内容角色作参数,状态层仍走 interaction)。 - gallery/feedback.rs:状态矩阵;家族内再切一段 ULL_FEEDBACK_PART(all|progress|snackbar| dialog|empty),开关本身住在 feedback/gallery_parts.rs,不动 gallery/mod.rs。 - feedback/probe.rs:对着真截图量像素(渲染层断言),四类几何各逐项钉住。 - feedback/tests.rs:几何/节奏/角色/scrim 的纯函数断言。 gates: fmt --all --check · clippy -p unlucky-app-iced --all-targets -D warnings · test -p unlucky-app-iced(144 通过 / 0 失败)
This commit is contained in:
1 parent
27f0d2a551
commit
36f58d7904
14 files changed
+2900
-39
No files matched your search
@@ -0,0 +1,134 @@
|
||||
//! 空态 EmptyState(`research/12b` §15):图标块 + 标题 + 说明 + 可选动作。
|
||||
//!
|
||||
//! ## ⚠️ 这是**项目自定**组件,官方 Material 3 没有它
|
||||
//!
|
||||
//! `research/14`(官方组件清单)里**没有** EmptyState 这一项,Compose `material3` 也没有
|
||||
//! 对应 token。所以本文件的数值**全部是 `[估]`**(`research/12b §15` + `11b R14` + M53 的空态锚点),
|
||||
//! 不得对外声称「按官方规格实现」:
|
||||
//!
|
||||
//! | # | 项 | 值 | 等级 |
|
||||
//! |---|---|---|---|
|
||||
//! | 15.1 | 图标块 | **96×96**,圆角 `Large(16)`,图标 **32** | `[估]`(圆角/图标档位是真令牌,96 是项目值) |
|
||||
//! | 15.2 | 标题 | **HeadlineSmall 24/32**(空态是唯一允许 Headline 级的场景) | `[规格]` `11b R14` |
|
||||
//! | 15.3 | 副文案 | **BodySmall 12/16** `OnSurfaceVariant`,最多 2 行 | `[规格]` |
|
||||
//! | 15.4 | 垂直留白 | 内容在可用区居中(上下留白差 ≤8) | `[估]` |
|
||||
//! | 15.5 | 快捷入口 | ≤ **3** 个 | `[估]`(本组件只给 1 个动作,见下) |
|
||||
//! | 15.6 | CTA | 走标准 Button(40 高 / full 圆角),实心 `Primary` 占「每屏一个」名额 | `[源]`(按钮家族) |
|
||||
//!
|
||||
//! ## 为什么 CTA 直接用 `buttons::filled`
|
||||
//!
|
||||
//! 15.6 说 CTA 就是标准按钮。复用按钮家族有两个好处:空态的 CTA 与全站按钮同日升级;
|
||||
//! 也顺带证明「组件库能跨家族组合」——本文件因此是唯一 import 兄弟家族的地方。
|
||||
|
||||
use iced::widget::{column, container, text};
|
||||
use iced::{Alignment, Background, Border, Element, Length, Padding};
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::elevation::MaterialLayer;
|
||||
use unlucky_design::icons::IconKind;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
|
||||
use crate::icons;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::buttons::{self, Button};
|
||||
use super::super::layout;
|
||||
use super::panel::EmptyStateGeometry;
|
||||
|
||||
/// 一个空态的装配描述(**不持有任何状态所有权**)。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EmptyState<'a, M> {
|
||||
icon: IconKind,
|
||||
title: &'a str,
|
||||
body: &'a str,
|
||||
action: Option<(&'a str, M)>,
|
||||
}
|
||||
|
||||
impl<'a, M> EmptyState<'a, M> {
|
||||
/// 图标 + 标题 + 说明,默认无动作。
|
||||
pub fn new(icon: IconKind, title: &'a str, body: &'a str) -> Self {
|
||||
Self {
|
||||
icon,
|
||||
title,
|
||||
body,
|
||||
action: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 一个主操作(15.6:标准 Filled 按钮)。
|
||||
pub fn action(mut self, label: &'a str, on_press: M) -> Self {
|
||||
self.action = Some((label, on_press));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// 画一个空态(内容整体在可用区里水平 + 垂直居中)。
|
||||
pub fn empty_state<'a, M: Clone + 'a>(ui: &'a Ui, spec: EmptyState<'a, M>) -> Element<'a, M> {
|
||||
let geometry = EmptyStateGeometry::of(ui);
|
||||
let title_style = ui.style(TypeRole::HeadlineSmall);
|
||||
let body_style = ui.style(TypeRole::BodySmall);
|
||||
|
||||
// Q9:图标必须落在固定尺寸的盒子里,否则 fill 约束会让 usvg 按 Contain 撑满 96 块。
|
||||
let glyph = layout::centered_fixed(
|
||||
geometry.icon,
|
||||
geometry.icon,
|
||||
icons::view::<M>(spec.icon, ui.color(Role::OnSurfaceVariant)),
|
||||
);
|
||||
let block_radius = iced::border::Radius::new(geometry.block_radius);
|
||||
// 块底色 `[估]`:取材质层 `Card`(`SurfaceContainerLow`)——空态块是「内容卡片」层级,
|
||||
// 不是浮层也不是画布;官方没有这个组件,所以这条是我们自己的口径。
|
||||
let block_color = ui.layer(MaterialLayer::Card);
|
||||
let block = container(glyph)
|
||||
.width(Length::Fixed(geometry.block))
|
||||
.height(Length::Fixed(geometry.block))
|
||||
.align_x(Alignment::Center)
|
||||
.align_y(Alignment::Center)
|
||||
.style(move |_theme| container::Style {
|
||||
background: Some(Background::Color(block_color)),
|
||||
border: Border {
|
||||
radius: block_radius,
|
||||
..Border::default()
|
||||
},
|
||||
..container::Style::default()
|
||||
});
|
||||
|
||||
let mut content = column![]
|
||||
.align_x(Alignment::Center)
|
||||
.spacing(geometry.gap_body)
|
||||
.push(block)
|
||||
.push(gap(geometry.gap_title))
|
||||
.push(
|
||||
text(spec.title)
|
||||
.size(title_style.size_px)
|
||||
.font(ui.font_of(title_style))
|
||||
.color(ui.color(Role::OnSurface)),
|
||||
)
|
||||
.push(
|
||||
text(spec.body)
|
||||
.size(body_style.size_px)
|
||||
.font(ui.font_of(body_style))
|
||||
.color(ui.color(Role::OnSurfaceVariant)),
|
||||
);
|
||||
|
||||
if let Some((label, message)) = spec.action {
|
||||
content = content
|
||||
.push(gap(geometry.gap_action))
|
||||
.push(buttons::filled(ui, Button::new(label, message)));
|
||||
}
|
||||
|
||||
container(content)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.align_x(Alignment::Center)
|
||||
.align_y(Alignment::Center)
|
||||
.padding(Padding::from(geometry.gap_action))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 固定高度的透明留白(列间距在 `spacing` 之外还要分档时用)。
|
||||
fn gap<'a, M: 'a>(height: f32) -> Element<'a, M> {
|
||||
iced::widget::Space::new()
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fixed(height))
|
||||
.into()
|
||||
}
|
||||
Reference in new issue
Block a user