Files
Unlucky-Launcher/crates/app-iced/src/components/selection/chip.rs
T
AskaEth b69a2cdd7f feat(app-iced): M76 选择类家族 —— Checkbox / Radio / Switch / Chip(15 §18/§19 + 12b §10/§13)
- selection/:geometry(令牌→像素,缺档登记 G16)/ paint(Interaction×选中×错误→角色)/
  shell(48 透明外壳 + 帧内状态通道 + 勾形 SVG)/ checkbox·radio·switch·chip / matrix(12×5 状态矩阵)/ tests
- gallery/selection.rs:小节 + **渲染层像素断言**(20×20 选中与未选中同尺寸 · Radio 内点 10 ·
  Ø40 状态层是圆 · 勾形 14.8 · 短横 · 轨道 52×32 · Chip 32 · 禁用 38%),按颜色+墨迹块定位、不假设行号
- gallery/mod.rs:新增 ULL_GALLERY_SECTION 段落过滤(tower 批准,M77 零成本复用)+ 四个 helper 放宽到 pub(crate)

A1 证据(本机 AAR javap):未选中 hover/focus/pressed 描边是 OnSurface(不是 OnSurfaceVariant)、
Radio 选中态 hover 仍 Primary、Checkbox SelectedDisabledIconColor=Surface、Switch hover 滑块 PrimaryContainer。
2026-11-02 06:11:21 +08:00

189 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.
//! Chip(`research/12b` §13):高 **32** / 圆角 **8** / 水平内边距 **8** / 元素间距 **8** /
//! 图标 **16**(官方 18 → **G14**,与按钮家族同一登记)/ 描边 1px `OutlineVariant`。
//!
//! ## 三类(§13 的 Assist / Filter / Input)
//!
//! | 类 | 选中态 | 容器 | 文字 | 前置图标 |
//! |---|---|---|---|---|
//! | Assist | **无**(一次动作,不是开关) | 透明 + 1px 描边 | `OnSurface` | `Primary` |
//! | Filter | 有 | 未选透明+描边;选中 `SecondaryContainer` **无描边** | `OnSurfaceVariant` / `OnSecondaryContainer` | 选中态才给勾 |
//! | Input | 有 | 同 Filter | 同 Filter | 可给 + 后置关闭图标 |
//!
//! ## 为什么 Chip 不用 `selection::shell` 那条透明外壳
//!
//! M3 的 chip **没有圆形状态层**:hover / pressed 就是把状态层叠在**自己的容器**上,
//! 而容器尺寸 = 命中区尺寸。所以 chip 直接走 `button` 的样式闭包(与按钮家族同一条产线),
//! 不需要「48 外壳 + 40 圆」那套。
use iced::widget::{button, container, row, text};
use iced::{Alignment, Background, Border, Element, Length, Padding, Shadow};
use unlucky_design::icons::IconKind;
use unlucky_design::typography::TypeRole;
use crate::icons;
use crate::theme::Ui;
use super::super::interaction::Interaction;
use super::geometry::ChipGeometry;
use super::paint::{self, ChipVariant};
/// 一个 chip(可带前后图标;`on_press` 为 `None` = 禁用)。
#[derive(Debug, Clone)]
pub struct Chip<'a, M> {
label: &'a str,
variant: ChipVariant,
selected: bool,
leading: Option<IconKind>,
trailing: Option<IconKind>,
on_press: Option<M>,
state: Option<Interaction>,
}
impl<'a, M> Chip<'a, M> {
/// Assist chip:辅助动作,**没有**选中态。
pub fn assist(label: &'a str, on_press: M) -> Self {
Self::new(label, ChipVariant::Assist, false, on_press)
}
/// Filter chip:筛选开关(`selected` 决定容器是否 `SecondaryContainer`)。
pub fn filter(label: &'a str, selected: bool, on_press: M) -> Self {
Self::new(label, ChipVariant::Filter, selected, on_press)
}
/// Input chip:用户输入的实体(可选中 + 可带关闭图标)。
pub fn input(label: &'a str, on_press: M) -> Self {
Self::new(label, ChipVariant::Input, false, on_press)
}
/// 禁用的 chip(无消息)。
pub fn disabled(label: &'a str, variant: ChipVariant) -> Self {
Self {
label,
variant,
selected: false,
leading: None,
trailing: None,
on_press: None,
state: None,
}
}
fn new(label: &'a str, variant: ChipVariant, selected: bool, on_press: M) -> Self {
Self {
label,
variant,
selected,
leading: None,
trailing: None,
on_press: Some(on_press),
state: None,
}
}
/// 前置图标(Assist / Filter 用)。
pub fn leading(mut self, kind: IconKind) -> Self {
self.leading = Some(kind);
self
}
/// 后置图标(Input chip 的关闭键)。
pub fn trailing(mut self, kind: IconKind) -> Self {
self.trailing = Some(kind);
self
}
/// 强制视觉态(gallery 取证用;运行期不要调)。
pub fn state(mut self, state: Interaction) -> Self {
self.state = Some(state);
self
}
}
/// 画一个 chip。
pub fn chip<'a, M: Clone + 'a>(ui: &'a Ui, spec: Chip<'a, M>) -> Element<'a, M> {
let geometry = ChipGeometry::of(ui);
let variant = spec.variant;
let selected = spec.selected;
let disabled = spec.on_press.is_none();
let forced = spec.state;
// 文字/图标色只看「选中 + 禁用」(A1:Filter/Input 的 label 色在 hover/focus/pressed 不变),
// 所以这里静态求值;容器(含状态层)在样式闭包里跟态走。
let at_rest = paint::chip(
ui,
variant,
selected,
if disabled {
Interaction::Disabled
} else {
forced.unwrap_or(Interaction::Enabled)
},
geometry.radius,
);
let mut content = row![].align_y(Alignment::Center).spacing(geometry.gap);
if let Some(kind) = spec.leading {
// Q9:图标一律经固定尺寸容器
content = content.push(super::super::layout::centered_fixed(
geometry.icon,
geometry.icon,
icons::view::<M>(kind, at_rest.icon),
));
}
let style = ui.style(TypeRole::LabelLarge);
content = content.push(
text(spec.label)
.size(style.size_px)
.font(ui.font_of(ui.emphasized(TypeRole::LabelLarge)))
.color(at_rest.label),
);
if let Some(kind) = spec.trailing {
content = content.push(super::super::layout::centered_fixed(
geometry.icon,
geometry.icon,
icons::view::<M>(kind, at_rest.icon),
));
}
// 垂直居中必须在内容侧做(`button` 把内容放左上角)
let body = container(content)
.height(Length::Fill)
.align_y(Alignment::Center);
let element = button(body)
.height(Length::Fixed(geometry.height))
.padding(Padding {
top: 0.0,
right: geometry.pad,
bottom: 0.0,
left: geometry.pad,
})
.style(move |_theme, status| {
let state = forced.unwrap_or_else(|| Interaction::from_status(status));
let paint = paint::chip(ui, variant, selected, state, geometry.radius);
button::Style {
background: paint.frame.fill.map(Background::Color),
text_color: paint.label,
border: Border {
color: paint.frame.outline.unwrap_or(iced::Color::TRANSPARENT),
width: if paint.frame.outline.is_some() {
paint.frame.stroke
} else {
0.0
},
radius: iced::border::Radius::new(paint.frame.radius),
},
shadow: Shadow::default(),
snap: true,
}
});
match spec.on_press {
Some(message) => element.on_press(message).into(),
None => element.into(),
}
}