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:
1 parent
22ddc74784
commit
4774e7b788
23 files changed
+3136
-133
No files matched your search
@@ -0,0 +1,208 @@
|
||||
//! `--screen=gallery` 的壳:**视觉验收 harness**(D30 / D39 §5),不是产品页面。
|
||||
//!
|
||||
//! ## 它是什么、不是什么
|
||||
//!
|
||||
//! - 是:**状态矩阵的取景框**。每个家族在自己的 `gallery/<family>.rs` 里摆出
|
||||
//! 「五变体 × 五态」的矩阵,一张截图就能判完该家族(浅/深各一张 = 10 张证据)。
|
||||
//! - 不是:页面。gallery 允许堆叠、不分页、不做空态,**难看不算问题**;
|
||||
//! 但**每个组件本身**必须按官方口径做到位。
|
||||
//!
|
||||
//! ## 并行地基(后续四条线怎么加东西)
|
||||
//!
|
||||
//! 五个家族文件在下面 `mod` 里**全部预先声明**了,每个都有自己的 `section()`:
|
||||
//!
|
||||
//! ```text
|
||||
//! gallery/buttons.rs ← COMPBASE(本 mission)
|
||||
//! gallery/selection.rs ← 后续 mission(只改这个文件)
|
||||
//! gallery/inputs.rs ← 同上
|
||||
//! gallery/feedback.rs ← 同上
|
||||
//! gallery/navigation.rs ← 同上
|
||||
//! ```
|
||||
//!
|
||||
//! 加组件 = 只改自己那个文件里的 `section()`,**不用碰本文件**(这是「五条线并行不冲突」的
|
||||
//! 具体保证,也是 tower 会亲手验的一条)。
|
||||
//!
|
||||
//! ## DemoMessage 为什么独立
|
||||
//!
|
||||
//! 契约 §2:组件不 import `crate::app::Message`。gallery 用自己定义的消息枚举,
|
||||
//! 正好当「组件能脱离业务验收」的示范——`app.rs` 只做一次 `.map(Message::Demo)` 就接上了。
|
||||
//!
|
||||
//! ## 截图命令(D30)
|
||||
//!
|
||||
//! ```text
|
||||
//! unlucky-launcher-iced --software-renderer --screen=gallery \
|
||||
//! --screenshot out.png --screenshot-theme=light
|
||||
//! ```
|
||||
//!
|
||||
//! `--screenshot` 不带主题时**默认 Light**(可复现;见 `cli::Options::capture_flavor`)。
|
||||
|
||||
use iced::widget::{column, container, scrollable, text};
|
||||
use iced::{Alignment, Background, Element, Length};
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::elevation::MaterialLayer;
|
||||
use unlucky_design::spacing::Space;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
|
||||
use crate::shot::flavor_slug;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::divider;
|
||||
use super::interaction::Interaction;
|
||||
use super::layout;
|
||||
|
||||
mod buttons;
|
||||
mod feedback;
|
||||
mod inputs;
|
||||
mod navigation;
|
||||
#[cfg(test)]
|
||||
mod probe;
|
||||
#[cfg(test)]
|
||||
mod scan;
|
||||
mod selection;
|
||||
|
||||
/// gallery 自己的消息(与业务 `Message` 解耦,契约 §2)。
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum DemoMessage {
|
||||
/// 某个演示按钮被按下(`&'static str` = 演示 id,只用于日志)。
|
||||
Pressed(&'static str),
|
||||
}
|
||||
|
||||
impl DemoMessage {
|
||||
/// 演示 id(日志用;将来加变体时这里跟着改,`app.rs` 不用动)。
|
||||
pub fn id(&self) -> &'static str {
|
||||
match self {
|
||||
DemoMessage::Pressed(id) => id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// gallery 根视图(整窗:不带标题栏与导航轨,验收图上只有组件)。
|
||||
pub fn view<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> {
|
||||
let page = ui.style(TypeRole::BodySmall);
|
||||
|
||||
let header = column![
|
||||
text("Component gallery")
|
||||
.size(ui.emphasized(TypeRole::TitleLarge).size_px)
|
||||
.font(ui.emphasized_font(TypeRole::TitleLarge))
|
||||
.color(ui.color(Role::OnSurface)),
|
||||
text(format!(
|
||||
"M3 Expressive component base · flavor={} · 1180×760 @100% DPI · \
|
||||
focus 与 pressed 同为 10%(令牌口径,图上一样是对的)",
|
||||
flavor_slug(ui)
|
||||
))
|
||||
.size(page.size_px)
|
||||
.font(ui.font_of(page))
|
||||
.color(ui.color(Role::OnSurfaceVariant)),
|
||||
]
|
||||
.spacing(ui.space(Space::X1));
|
||||
|
||||
// 小节之间**不画分隔线**:rubric(15 §22.4)要求同屏同类线 ≤3,
|
||||
// 而验收图里要给分隔线自己留证据(见 `divider_proof`)。分组靠 32px 留白 + 标题。
|
||||
let sections = column![
|
||||
buttons::section(ui),
|
||||
divider_proof(ui),
|
||||
selection::section(ui),
|
||||
inputs::section(ui),
|
||||
feedback::section(ui),
|
||||
navigation::section(ui),
|
||||
]
|
||||
.spacing(ui.space(Space::X8))
|
||||
.width(Length::Fill);
|
||||
|
||||
let body = scrollable(sections)
|
||||
.height(Length::Fill)
|
||||
.width(Length::Fill);
|
||||
|
||||
// 页面留白走 `layout::inset`(左右/上下各 24):这是 Q8/Q9 那两个 helper 的第三个兄弟,
|
||||
// 存在意义是「只改内边距、不改尺寸」,也顺手避免把 Padding 的上右下左写反。
|
||||
let page_padding = ui.space(Space::X6);
|
||||
container(layout::inset(
|
||||
page_padding,
|
||||
page_padding,
|
||||
column![header, body].spacing(ui.space(Space::X4)),
|
||||
))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.style(|_theme| container::Style {
|
||||
background: Some(Background::Color(ui.layer(MaterialLayer::Canvas))),
|
||||
..container::Style::default()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 分隔线的视觉证据(§22):一条通栏 + 一条列表内缩进,各自带说明。
|
||||
///
|
||||
/// 单独一节而不是散落各处:`15 §22.4` 说「同屏同类线 ≤3」,这两条是**唯一**计入的两条。
|
||||
fn divider_proof<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> {
|
||||
let caption = ui.style(TypeRole::LabelSmall);
|
||||
let label = |text_value: &'a str| {
|
||||
text(text_value)
|
||||
.size(caption.size_px)
|
||||
.font(ui.font_of(caption))
|
||||
.color(ui.color(Role::OnSurfaceVariant))
|
||||
};
|
||||
|
||||
column![
|
||||
heading(
|
||||
ui,
|
||||
"divider · full / list-inset",
|
||||
"15 §22:厚度 1px(stroke::HAIRLINE)· 颜色 OutlineVariant · 列表内左缩进 16 · 同屏 ≤3 条"
|
||||
),
|
||||
label("full (通栏,区块分隔)"),
|
||||
divider::full::<DemoMessage>(ui),
|
||||
label("list (左缩进 16)"),
|
||||
divider::list::<DemoMessage>(ui),
|
||||
]
|
||||
.spacing(ui.space(Space::X2))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 一个家族小节的标题(`index.title`)+ 一行判据说明。
|
||||
///
|
||||
/// 五个家族文件共用(`pub(super)`),保证五段的节奏一致——这也是 rubric D4
|
||||
/// 「一屏字号档 ≤4」的落点:小节只有 `TitleMedium` + `BodySmall` 两档。
|
||||
pub(super) fn heading<'a>(ui: &'a Ui, title: &'a str, note: &'a str) -> Element<'a, DemoMessage> {
|
||||
let note_style = ui.style(TypeRole::BodySmall);
|
||||
column![
|
||||
text(title)
|
||||
.size(ui.emphasized(TypeRole::TitleMedium).size_px)
|
||||
.font(ui.emphasized_font(TypeRole::TitleMedium))
|
||||
.color(ui.color(Role::OnSurface)),
|
||||
text(note)
|
||||
.size(note_style.size_px)
|
||||
.font(ui.font_of(note_style))
|
||||
.color(ui.color(Role::OnSurfaceVariant)),
|
||||
]
|
||||
.spacing(ui.space(Space::X1))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 状态矩阵的列头:五态的固定次序(`Interaction::ALL` 的口径)。
|
||||
///
|
||||
/// 列宽 = 按钮最常见的自然宽 132(用作矩阵栅格,不是组件尺寸)。
|
||||
pub(super) fn state_cells(ui: &Ui) -> Vec<(&'static str, f32)> {
|
||||
Interaction::ALL
|
||||
.iter()
|
||||
.map(|state| (state.name(), cell_width(ui)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 矩阵单元格宽度(只影响 harness 的取景,不是组件尺寸;216 = 5 列正好铺满 1092 内容宽)。
|
||||
pub(super) fn cell_width(ui: &Ui) -> f32 {
|
||||
ui.space(Space::X8) * 6.0
|
||||
}
|
||||
|
||||
/// 一个矩阵行的左侧标签(变体名)。
|
||||
pub(super) fn row_label<'a>(ui: &'a Ui, name: &'a str) -> Element<'a, DemoMessage> {
|
||||
let style = ui.style(TypeRole::BodySmall);
|
||||
container(
|
||||
text(name)
|
||||
.size(style.size_px)
|
||||
.font(ui.font_of(style))
|
||||
.color(ui.color(Role::OnSurfaceVariant)),
|
||||
)
|
||||
.width(Length::Fixed(cell_width(ui)))
|
||||
.align_y(Alignment::Center)
|
||||
.into()
|
||||
}
|
||||
Reference in new issue
Block a user