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,191 @@
|
||||
//! 装配与样式:五式共用的一条产线(尺寸 / 内边距 / 图标盒 / 状态层 / 描边)。
|
||||
//!
|
||||
//! 单独成文件的原因:`mod.rs` 已经是「对外契约 + 变体角色表」,把绘制细节放一起会顶到 400 行红线;
|
||||
//! 而且这条产线是**唯一**会把「令牌 → 像素」翻一次的地方,独立出来才好审。
|
||||
|
||||
use iced::widget::text::LineHeight;
|
||||
use iced::widget::{button, text, Row};
|
||||
use iced::{Alignment, Background, Border, Element, Length, Pixels, Shadow};
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::elevation::Elevation;
|
||||
use unlucky_design::shape::ShapeScale;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
|
||||
use crate::icons;
|
||||
use crate::theme::{Ui, STROKE_PX};
|
||||
|
||||
use super::super::interaction::{self, Interaction};
|
||||
use super::super::layout;
|
||||
use super::{Geometry, Variant};
|
||||
|
||||
/// 一个按钮样式要的全部输入(避免 `style_closure` 长出七个位置参数)。
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(super) struct StyleSpec {
|
||||
/// 容器填充角色;`None` = 透明(Outlined / Text / StandardIcon)。
|
||||
pub fill: Option<Role>,
|
||||
/// 内容(文字 + 图标)角色。
|
||||
pub content: Role,
|
||||
/// 描边角色;`None` = 无描边。
|
||||
pub border: Option<Role>,
|
||||
/// 圆角档位(来自组件表,五式统一 `Full`)。
|
||||
pub shape: ShapeScale,
|
||||
/// 圆角折算用的边长(药丸 = 可见高)。
|
||||
pub radius_side: f32,
|
||||
/// 强制态(gallery 取证);`Enabled` 时跟随运行期的 `button::Status`。
|
||||
pub forced: Interaction,
|
||||
}
|
||||
|
||||
/// 五式共用的装配:**只画一处**,五个变体函数都走这里。
|
||||
pub(super) fn assemble<'a, M: Clone + 'a>(
|
||||
ui: &'a Ui,
|
||||
variant: Variant,
|
||||
spec: super::Button<'a, M>,
|
||||
) -> Element<'a, M> {
|
||||
let geometry = Geometry::of(ui, variant, spec.leading.is_some());
|
||||
let (fill, content, border) = variant.roles();
|
||||
|
||||
// 按钮家族没有浮层级高程:L1(Elevated)只用容器色调差表达(rubric §7 不画阴影)
|
||||
debug_assert!(
|
||||
!matches!(
|
||||
variant.elevation(),
|
||||
Elevation::Level3 | Elevation::Level4 | Elevation::Level5
|
||||
),
|
||||
"按钮家族不许有浮层高程"
|
||||
);
|
||||
let content_color = ui.color(content);
|
||||
|
||||
let label = text(spec.label)
|
||||
.size(ui.style(TypeRole::LabelLarge).size_px)
|
||||
.line_height(LineHeight::Absolute(Pixels(
|
||||
ui.style(TypeRole::LabelLarge).line_height_px,
|
||||
)))
|
||||
.font(ui.font_of(ui.emphasized(TypeRole::LabelLarge)))
|
||||
.color(content_color);
|
||||
|
||||
let mut row = Row::new().align_y(Alignment::Center);
|
||||
if let Some(kind) = spec.leading {
|
||||
// Q9:图标一律经固定尺寸容器(按钮图标 16、IconButton 24 都不例外)
|
||||
row = row.push(layout::fixed(
|
||||
geometry.icon,
|
||||
geometry.icon,
|
||||
icons::view::<M>(kind, content_color),
|
||||
));
|
||||
}
|
||||
row = row.push(label);
|
||||
if let Some(kind) = spec.trailing {
|
||||
row = row.push(layout::fixed(
|
||||
geometry.icon,
|
||||
geometry.icon,
|
||||
icons::view::<M>(kind, content_color),
|
||||
));
|
||||
}
|
||||
let row = row.spacing(geometry.gap);
|
||||
|
||||
// **垂直居中必须在内容侧做**:iced 的 `button::layout` 把内容放在 padding-box 的左上角
|
||||
// (`layout::padded` 只 `move_to(padding.top)`,不做居中),所以内容自己撑满高度再居中。
|
||||
// 少了这一层,40 高的按钮里文字会贴上沿 10px(实测首版就是这样)。
|
||||
let content_box = iced::widget::container(row)
|
||||
.height(Length::Fill)
|
||||
.align_y(iced::Alignment::Center);
|
||||
|
||||
let forced = spec.state.unwrap_or(if spec.on_press.is_none() {
|
||||
Interaction::Disabled
|
||||
} else {
|
||||
Interaction::Enabled
|
||||
});
|
||||
|
||||
let style = style_closure(
|
||||
ui,
|
||||
StyleSpec {
|
||||
fill,
|
||||
content,
|
||||
border,
|
||||
shape: variant.shape(),
|
||||
radius_side: geometry.height,
|
||||
forced,
|
||||
},
|
||||
);
|
||||
|
||||
let element = button(content_box)
|
||||
.height(Length::Fixed(geometry.height))
|
||||
.padding(geometry.padding)
|
||||
.style(style);
|
||||
|
||||
match spec.on_press {
|
||||
Some(message) => element.on_press(message).into(),
|
||||
None => element.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成 `button::style` 闭包:状态层 / 禁用 / 描边全在这里落地。
|
||||
pub(super) fn style_closure(
|
||||
ui: &Ui,
|
||||
spec: StyleSpec,
|
||||
) -> impl Fn(&iced::Theme, button::Status) -> button::Style + '_ {
|
||||
move |_theme: &iced::Theme, status| {
|
||||
// 强制态优先(gallery 状态矩阵),否则把 iced 的四态翻成 M3 五态
|
||||
let state = match spec.forced {
|
||||
Interaction::Enabled => Interaction::from_status(status),
|
||||
forced => forced,
|
||||
};
|
||||
|
||||
let (background, text_color) = if state.is_disabled() {
|
||||
let (container, content) = interaction::disabled_colors(ui, Role::Surface);
|
||||
// 透明容器在禁用态依然透明(官方只改内容透明度)
|
||||
(spec.fill.map(|_| container), content)
|
||||
} else {
|
||||
// 有填充:状态层叠在**容器角色**上;透明容器:叠在表面色上(否则 hover 看不见)
|
||||
let background = match spec.fill {
|
||||
Some(role) => match state.layer() {
|
||||
Some(_) => interaction::overlay_color(ui, state, spec.content, role)
|
||||
.or(Some(ui.color(role))),
|
||||
None => Some(ui.color(role)),
|
||||
},
|
||||
None => transparent_state_color(ui, state, spec.content),
|
||||
};
|
||||
(background, ui.color(spec.content))
|
||||
};
|
||||
|
||||
let border_color = if state.is_disabled() {
|
||||
interaction::disabled_colors(ui, Role::Surface).0
|
||||
} else {
|
||||
spec.border
|
||||
.map_or_else(|| ui.color(Role::OutlineVariant), |role| ui.color(role))
|
||||
};
|
||||
|
||||
button::Style {
|
||||
background: background.map(Background::Color),
|
||||
text_color,
|
||||
border: Border {
|
||||
color: if spec.border.is_some() {
|
||||
border_color
|
||||
} else {
|
||||
iced::Color::TRANSPARENT
|
||||
},
|
||||
width: if spec.border.is_some() {
|
||||
STROKE_PX
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
radius: ui.radius(spec.shape, spec.radius_side),
|
||||
},
|
||||
// rubric §7:L0/L1 不用阴影(Elevated 的 L1 由容器色调差表达)
|
||||
shadow: Shadow::default(),
|
||||
snap: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 透明容器的状态层:`Outlined` / `Text` / `StandardIcon` 没有填充,可见反馈只能来自
|
||||
/// 叠在**内容角色**上的状态层——把它们当作「叠在表面色上」求值,得到不透明实色。
|
||||
///
|
||||
/// 之所以单独一个函数:这是 M3 里唯一「容器不存在但状态层存在」的情形,
|
||||
/// 三个变体共用一条口径,免得各自发挥。
|
||||
pub(super) fn transparent_state_color(
|
||||
ui: &Ui,
|
||||
state: Interaction,
|
||||
content: Role,
|
||||
) -> Option<iced::Color> {
|
||||
interaction::overlay_color(ui, state, content, Role::Surface)
|
||||
}
|
||||
Reference in new issue
Block a user