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
@@ -1,22 +1,322 @@
|
||||
//! gallery 里的反馈类小节(**桩**):后续 mission 只改本文件。
|
||||
//! gallery 里的反馈类小节:把状态矩阵摆进 harness(**断言不在这里**,在
|
||||
//! [`crate::app::components::feedback::probe`])。
|
||||
//!
|
||||
//! ## 段落切分:`ULL_FEEDBACK_PART`(本文件自己的开关)
|
||||
//!
|
||||
//! `ULL_GALLERY_SECTION=feedback` 只能把**整个家族**拎出来,而四件的高度超过 1180×760 首屏,
|
||||
//! 所以再加一层**家族内**切分(契约 §9.2 的口径:环境变量 + 默认向后兼容 + 未识别的值回落
|
||||
//! 默认并打 stderr;区别只是它住在**本家族自己的文件**里,不动 `gallery/mod.rs`):
|
||||
//!
|
||||
//! ```text
|
||||
//! ULL_FEEDBACK_PART=all # 默认:四件全渲染(progress → snackbar → empty → dialog)
|
||||
//! ULL_FEEDBACK_PART=progress # Linear 0/50/100 + 三相位 · Circular 0/25/50/75/100 + 不定态
|
||||
//! ULL_FEEDBACK_PART=snackbar # 四格:贴顶·上限宽 / 双行 / 宿主 300 / 宿主 344
|
||||
//! ULL_FEEDBACK_PART=dialog # 两形态并排 + 双动作 + 一屏 scrim
|
||||
//! ULL_FEEDBACK_PART=empty # 空态(带 CTA)
|
||||
//! ```
|
||||
//!
|
||||
//! ## 宿主尺寸为什么写死
|
||||
//!
|
||||
//! 量宽度必须有确定的宿主宽:`Snackbar` 的 600 是「宿主给 800 时的夹取值」、344 与 300 是
|
||||
//! 「宿主给多少就是多少」的反证(组件没有 min-width,G17-d)。`Linear` 轨道统一 320(一行 3 格 = 960 < 1128)。
|
||||
//!
|
||||
//! 复现(worktree 根目录):
|
||||
//!
|
||||
//! ```text
|
||||
//! ULL_GALLERY_SECTION=feedback ULL_FEEDBACK_PART=progress ./target/debug/unlucky-launcher-iced \
|
||||
//! --software-renderer --screen=gallery \
|
||||
//! --screenshot target/m78/progress-light.png --screenshot-theme=light
|
||||
//! ULL_GALLERY_SHOT=target/m78/progress-light.png cargo test -p app-iced feedback::probe
|
||||
//! ```
|
||||
|
||||
use iced::Element;
|
||||
use iced::widget::{column, container, row, text};
|
||||
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 super::super::feedback;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::feedback::gallery_parts::{Part, DIALOG, EMPTY, PROGRESS, SNACKBAR};
|
||||
use super::super::feedback::{self, Dialog, EmptyState, Placement, Progress, Snackbar};
|
||||
use super::super::interaction::Interaction;
|
||||
use super::{heading, DemoMessage};
|
||||
|
||||
/// 线性进度的宿主宽(格宽,不是组件尺寸)。
|
||||
const TRACK_HOST: f32 = 320.0;
|
||||
/// `Snackbar` 的宿主宽:**条宽 + 2×12 外边距**(12 是官方外边距,组件的定位层会扣掉)。
|
||||
///
|
||||
/// 800(条夹到上限 600)/ 368(条 344)/ 324(条 300,用来证明**没有**下限,G17-d)。
|
||||
const HOST_WIDE: f32 = 800.0;
|
||||
const HOST_MIN: f32 = 368.0;
|
||||
const HOST_NARROW: f32 = 324.0;
|
||||
/// `Snackbar` 宿主高(比 68 + 2×12 略高,留出贴顶/贴底的判定余量)。
|
||||
const HOST_TALL: f32 = 96.0;
|
||||
/// 单行提示条的宿主高(48 + 2×12 外边距 = 72,正好装下)。
|
||||
const HOST_SHORT: f32 = 72.0;
|
||||
/// 空态宿主高:内容本征高 ≈96+16+32+8+16+24+40 ≈ 232,留出上下各 40 的余量。
|
||||
const HOST_EMPTY: f32 = 320.0;
|
||||
|
||||
/// 反馈类家族的小节。
|
||||
pub(super) fn section<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> {
|
||||
iced::widget::column"
|
||||
)]
|
||||
.spacing(ui.space(Space::X3));
|
||||
|
||||
if part.has(PROGRESS) {
|
||||
body = body.push(progress_block(ui));
|
||||
}
|
||||
if part.has(SNACKBAR) {
|
||||
body = body.push(snackbar_block(ui));
|
||||
}
|
||||
if part.has(EMPTY) {
|
||||
body = body.push(empty_block(ui));
|
||||
}
|
||||
if part.has(DIALOG) {
|
||||
// 只有这一屏不画进度/提示条时,才把三形态与 scrim 都摆开(高度够)。
|
||||
let roomy = !part.has(PROGRESS) && !part.has(SNACKBAR);
|
||||
body = body.push(dialog_block(ui, roomy));
|
||||
}
|
||||
|
||||
body.into()
|
||||
}
|
||||
|
||||
/// 一行判据说明(`LabelSmall`,与矩阵的正文档分开)。
|
||||
fn caption<'a>(ui: &'a Ui, value: &'a str) -> Element<'a, DemoMessage> {
|
||||
let style = ui.style(TypeRole::LabelSmall);
|
||||
text(value)
|
||||
.size(style.size_px)
|
||||
.font(ui.font_of(style))
|
||||
.color(ui.color(Role::OnSurfaceVariant))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 固定宽宿主(量宽度用:宿主宽写死,组件内部的占比/夹取才有确定值)。
|
||||
fn host<'a>(
|
||||
width: f32,
|
||||
height: Option<f32>,
|
||||
content: impl Into<Element<'a, DemoMessage>>,
|
||||
) -> Element<'a, DemoMessage> {
|
||||
let mut box_ = container(content).width(Length::Fixed(width));
|
||||
if let Some(height) = height {
|
||||
box_ = box_.height(Length::Fixed(height));
|
||||
}
|
||||
box_.into()
|
||||
}
|
||||
|
||||
/// 一列:[进度条, 说明]。
|
||||
fn track<'a>(ui: &'a Ui, label: &'a str, progress: Progress) -> Element<'a, DemoMessage> {
|
||||
column![
|
||||
host(TRACK_HOST, None, feedback::linear(ui, progress)),
|
||||
caption(ui, label)
|
||||
]
|
||||
.spacing(ui.space(unlucky_design::spacing::Space::X2))
|
||||
.spacing(ui.space(Space::X2))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 一列:[环, 说明]。
|
||||
fn ring<'a>(ui: &'a Ui, label: &'a str, progress: Progress) -> Element<'a, DemoMessage> {
|
||||
column![feedback::circular(ui, progress), caption(ui, label)]
|
||||
.spacing(ui.space(Space::X2))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// §12:Linear 0/50/100 + 四个相位;Circular 0/25/50/75/100 + 三个相位。
|
||||
fn progress_block<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> {
|
||||
let determinate = row![
|
||||
track(ui, "0%", Progress::determinate(0.0)),
|
||||
track(ui, "50%", Progress::determinate(0.5)),
|
||||
track(ui, "100%", Progress::determinate(1.0)),
|
||||
]
|
||||
.spacing(ui.space(Space::X3));
|
||||
|
||||
let indeterminate = row![
|
||||
track(ui, "phase 0.15", Progress::indeterminate(0.15)),
|
||||
track(ui, "phase 0.35", Progress::indeterminate(0.35)),
|
||||
track(ui, "phase 0.55", Progress::indeterminate(0.55)),
|
||||
]
|
||||
.spacing(ui.space(Space::X3));
|
||||
|
||||
let rings = row![
|
||||
ring(ui, "0%", Progress::determinate(0.0)),
|
||||
ring(ui, "25%", Progress::determinate(0.25)),
|
||||
ring(ui, "50%", Progress::determinate(0.5)),
|
||||
ring(ui, "75%", Progress::determinate(0.75)),
|
||||
ring(ui, "100%", Progress::determinate(1.0)),
|
||||
ring(ui, "ind 0.30", Progress::indeterminate(0.30)),
|
||||
]
|
||||
.spacing(ui.space(Space::X4))
|
||||
.align_y(iced::Alignment::Start);
|
||||
|
||||
column![
|
||||
caption(
|
||||
ui,
|
||||
"Linear · determinate 0% / 50% / 100%(轨道 SecondaryContainer · 指示条 Primary)"
|
||||
),
|
||||
determinate,
|
||||
caption(
|
||||
ui,
|
||||
"Linear · indeterminate(官方 1750ms 双段追逐;下面是同一周期的三个相位)"
|
||||
),
|
||||
indeterminate,
|
||||
caption(
|
||||
ui,
|
||||
"Circular · 直径 40 · 线宽 4 · 弧长 = 数值 × 360°(第六格 = 不定态 phase 0.30)"
|
||||
),
|
||||
rings,
|
||||
]
|
||||
.spacing(ui.space(Space::X2))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 提示条的宿主:固定宽 + 提示条宿主高(比 68 + 2×12 略高,贴顶/贴底都量得到)。
|
||||
fn bar_host<'a>(
|
||||
width: f32,
|
||||
content: impl Into<Element<'a, DemoMessage>>,
|
||||
) -> Element<'a, DemoMessage> {
|
||||
host(width, Some(HOST_TALL), content)
|
||||
}
|
||||
|
||||
/// §14:四格 —— 贴顶·上限宽 / 双行 + action + 关闭 / 宿主 300 / 宿主 344。
|
||||
fn snackbar_block<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> {
|
||||
let press = |id| DemoMessage::Pressed(id);
|
||||
|
||||
let wide = bar_host(
|
||||
HOST_WIDE,
|
||||
feedback::snackbar(
|
||||
ui,
|
||||
Snackbar::new("版本清单已刷新")
|
||||
.placement(Placement::Top)
|
||||
.dismiss(press("snackbar/wide")),
|
||||
),
|
||||
);
|
||||
let two_lines = host(
|
||||
HOST_WIDE,
|
||||
Some(HOST_TALL * 2.0),
|
||||
feedback::snackbar(
|
||||
ui,
|
||||
Snackbar::new(
|
||||
"启动失败:找不到 Java 21。请在「设置 · Java」里指定运行时路径,或让启动器自动下载一个 21 的运行时。",
|
||||
)
|
||||
.two_lines()
|
||||
.placement(Placement::Top)
|
||||
.action("去设置", press("snackbar/settings"))
|
||||
.dismiss(press("snackbar/close"))
|
||||
.state(Interaction::Hovered),
|
||||
),
|
||||
);
|
||||
let mid = host(
|
||||
HOST_MIN,
|
||||
Some(HOST_SHORT),
|
||||
feedback::snackbar(
|
||||
ui,
|
||||
Snackbar::new("已删除实例")
|
||||
.placement(Placement::Bottom)
|
||||
.action("撤销", press("snackbar/undo")),
|
||||
),
|
||||
);
|
||||
let narrow = host(
|
||||
HOST_NARROW,
|
||||
Some(HOST_SHORT),
|
||||
feedback::snackbar(ui, Snackbar::new("宿主 324").placement(Placement::Bottom)),
|
||||
);
|
||||
|
||||
column![
|
||||
caption(
|
||||
ui,
|
||||
"Snackbar · 贴顶 + 上限宽(宿主 800 → 夹到 600)· 关闭键 InverseOnSurface"
|
||||
),
|
||||
wide,
|
||||
caption(
|
||||
ui,
|
||||
"Snackbar · 双行 68 + action InversePrimary + 关闭键(动作强制 hover 态)"
|
||||
),
|
||||
two_lines,
|
||||
caption(
|
||||
ui,
|
||||
"宿主 324 ⇒ 条 300:组件没有 min-width,宿主给多少就是多少(G17-d)"
|
||||
),
|
||||
narrow,
|
||||
caption(ui, "宿主 368 ⇒ 条 344:最小宽的取值口径([估])"),
|
||||
mid,
|
||||
]
|
||||
.spacing(ui.space(Space::X2))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// §15:空态(项目自定;浅/深两种主题各出一张图,主题来自 `--screenshot-theme`)。
|
||||
fn empty_block<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> {
|
||||
let press = |id| DemoMessage::Pressed(id);
|
||||
host(
|
||||
HOST_WIDE,
|
||||
Some(HOST_EMPTY),
|
||||
feedback::empty_state(
|
||||
ui,
|
||||
EmptyState::new(
|
||||
IconKind::Instances,
|
||||
"还没有任何实例",
|
||||
"建一个实例,或者从整合包导入。[项目自定 · 估]",
|
||||
)
|
||||
.action("新建实例", press("empty/create")),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/// §7:三形态(无动作 / 单动作 / 双动作)+ 一屏 scrim。
|
||||
///
|
||||
/// `full = true` 表示**这一屏只渲染 dialog 段**(`ULL_FEEDBACK_PART=dialog` 或 `=empty` 之类
|
||||
/// 单独出图):此时多摆一屏 scrim,并把三形态都摆开;全家族同屏时留高度给别的段。
|
||||
fn dialog_block<'a>(ui: &'a Ui, full: bool) -> Element<'a, DemoMessage> {
|
||||
let press = |id| DemoMessage::Pressed(id);
|
||||
|
||||
let plain = feedback::dialog(ui, Dialog::new("已是最新版本", "当前没有可用更新。"));
|
||||
let single = feedback::dialog(
|
||||
ui,
|
||||
Dialog::new("已导入整合包", "共 42 个模组,缺失 0 个。")
|
||||
.confirm("知道了", press("dialog/ok")),
|
||||
);
|
||||
// 强制 hover:动作件的状态层必须来自 `interaction`(不是组件里自己乘 8%)。
|
||||
let double = feedback::dialog(
|
||||
ui,
|
||||
Dialog::new("删除实例「1.20.1 生存」?", "存档与模组会一起删除。")
|
||||
.double(
|
||||
("取消", press("dialog/cancel")),
|
||||
("删除", press("dialog/delete")),
|
||||
)
|
||||
.state(Interaction::Hovered),
|
||||
);
|
||||
|
||||
let mut body = column![caption(
|
||||
ui,
|
||||
"Dialog · R28 · SurfaceContainerHigh + L3 · pad 24(标题下 16 / 正文下 24)· 动作 Primary 间距 8",
|
||||
)]
|
||||
.spacing(ui.space(Space::X2));
|
||||
|
||||
body = body.push(row![plain, single].spacing(ui.space(Space::X6)));
|
||||
if full {
|
||||
body = body.push(double);
|
||||
body = body.push(caption(
|
||||
ui,
|
||||
"Dialog · scrim = 黑@0.32 叠 canvas 的实色(D27 预合成;详见 dialog.rs 模块文档)",
|
||||
));
|
||||
body = body.push(host(
|
||||
HOST_WIDE,
|
||||
Some(HOST_EMPTY),
|
||||
feedback::scrimmed(
|
||||
ui,
|
||||
feedback::dialog(ui, Dialog::new("已是最新版本", "当前没有可用更新。")),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
body.into()
|
||||
}
|
||||
Reference in new issue
Block a user