refactor(app-iced): paint.rs 按组件拆成 paint/ 目录(复审 P2-5)
- paint.rs 原本正好 400 行、零余量:再加一个态就得动它(和 app.rs 同一个问题) - 拆成 mod.rs(Frame / 状态层 / 禁用色 / 标签色)+ check.rs / radio.rs / switch.rs / chip.rs - 对外 API 一字不改(selection::paint::* 路径全部保持) - 顺带把「选中且禁用的 Checkbox 容器」从 12% 改成 A1 的 38%(复审 P2-3):这一行就在被移动的 check.rs 里,单独切 hunk 只会让历史不完整;断言在下一条提交
This commit is contained in:
1 parent
b69a2cdd7f
commit
17394b73ff
6 files changed
+460
-400
No files matched your search
@@ -1,400 +0,0 @@
|
||||
//! 选择类家族的**角色映射**:`Interaction × 选中 × 错误 → 颜色`。
|
||||
//!
|
||||
//! ## 出处(全部 A1:`material3-android:1.4.0` 的 token 类,本机 AAR `javap` 复核,见报告)
|
||||
//!
|
||||
//! | 组件 | 静息 | hover / focus / pressed | 禁用 |
|
||||
//! |---|---|---|---|
|
||||
//! | Checkbox 未选中 | 描边 `OnSurfaceVariant` 2px | 描边提到 **`OnSurface`**(宽度仍 2) | 描边 `OnSurface`@38% |
|
||||
//! | Checkbox 选中 | 填 `Primary` + 勾 `OnPrimary` | 填 `Primary`(不变) | 填 `OnSurface`@38% + 勾 `Surface` |
|
||||
//! | Radio | 环 `OnSurfaceVariant` / 选中环+点 `Primary` | 未选中环提到 **`OnSurface`**;**选中态仍是 `Primary`** | 环+点 `OnSurface`@38% |
|
||||
//! | Switch | 轨 `Primary`(选中)/`SurfaceContainerHighest`+2px `Outline`;滑块 `OnPrimary`/`Outline` | 滑块 `PrimaryContainer`/`OnSurfaceVariant` | 轨 `OnSurface`@12%;滑块 `Surface`/@38% |
|
||||
//! | Chip | 未选中:透明+1px `OutlineVariant`,文字 `OnSurfaceVariant`;选中:`SecondaryContainer` **无描边** | 描边不变,状态层成为底色 | 描边 `OnSurface`@12%;文字/图标 @38% |
|
||||
//!
|
||||
//! **A1 改过参考表的三处**:`UnselectedHover/Focus/PressedOutlineColor` 与 Radio 的
|
||||
//! `UnselectedHover/Focus/PressedIconColor` 都是 `OnSurface`(不是 `OnSurfaceVariant`);
|
||||
//! `SelectedDisabledIconColor` 是 `Surface`。α 一律从 [`interaction`] 取。
|
||||
|
||||
use iced::widget::container;
|
||||
use iced::{Background, Border, Color};
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::shape::ShapeScale;
|
||||
|
||||
use crate::theme::{Ui, STROKE_PX};
|
||||
|
||||
use super::super::interaction::{self, Interaction};
|
||||
use super::geometry::SwitchGeometry;
|
||||
|
||||
/// 一个「方框」的外观:填色 / 描边 / 线宽 / 半径。
|
||||
///
|
||||
/// Checkbox 的框、Radio 的环、Switch 的轨道、Chip 的容器都用它——差别只在颜色与线宽。
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Frame {
|
||||
/// 填充色;`None` = 透明(未选中的 Checkbox / Radio 外圈、Chip 的 flat 底)。
|
||||
pub fill: Option<Color>,
|
||||
/// 描边色;`None` = 不画描边。
|
||||
pub outline: Option<Color>,
|
||||
/// 描边宽度(Checkbox/Radio 2,Chip 1,Switch 未选中 2)。
|
||||
pub stroke: f32,
|
||||
/// 圆角半径(Checkbox 2 / Radio 10 / Switch 16 / Chip 8)。
|
||||
pub radius: f32,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
/// 翻成 iced 的 `container::Style`(唯一把 Frame 变成像素的地方)。
|
||||
pub fn container_style(self) -> container::Style {
|
||||
container::Style {
|
||||
background: self.fill.map(Background::Color),
|
||||
border: Border {
|
||||
color: self.outline.unwrap_or(Color::TRANSPARENT),
|
||||
width: self.outline.map_or(0.0, |_| self.stroke),
|
||||
radius: iced::border::Radius::new(self.radius),
|
||||
},
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checkbox 的外观:框 + 勾的颜色。
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct CheckboxPaint {
|
||||
/// 框(20×20)。
|
||||
pub frame: Frame,
|
||||
/// 勾 / 短横。
|
||||
pub glyph: Color,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct RadioPaint {
|
||||
/// 外圈(20×20,环线向内收 → 外缘恒 20)。
|
||||
pub ring: Frame,
|
||||
/// 内点(Ø10);`None` = 未选中(不画点)。
|
||||
pub dot: Option<Color>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct SwitchPaint {
|
||||
/// 轨道(52×32)。
|
||||
pub track: Frame,
|
||||
/// 滑块。
|
||||
pub thumb: Color,
|
||||
/// 滑块内图标(16);`None` = 不画(未选中时官方色与轨道同色,画了也看不见)。
|
||||
pub icon: Option<Color>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct ChipPaint {
|
||||
/// 容器。
|
||||
pub frame: Frame,
|
||||
/// 文字。
|
||||
pub label: Color,
|
||||
/// 前/后置图标。
|
||||
pub icon: Color,
|
||||
}
|
||||
|
||||
/// 状态层颜色:`content` 角色 @ 该态的 α 叠在 `surface` 上(不透明;α 的唯一实现点是
|
||||
/// [`interaction::overlay_color`],本文件零 α 字面量)。
|
||||
pub fn state_layer(ui: &Ui, state: Interaction, content: Role) -> Option<Color> {
|
||||
interaction::overlay_color(ui, state, content, Role::Surface)
|
||||
}
|
||||
|
||||
/// `OnSurface`@12% 叠 surface(**容器**禁用色:Switch 轨道、Chip 选中底)。
|
||||
fn disabled_container(ui: &Ui) -> Color {
|
||||
interaction::disabled_colors(ui, Role::Surface).0
|
||||
}
|
||||
|
||||
/// 内容禁用色:`role` @38% 叠在 surface 上(`on_container = false`,见 [`disabled_container`])。
|
||||
fn disabled_ink(ui: &Ui, role: Role) -> Color {
|
||||
interaction::disabled_content_over_role(ui, role, false)
|
||||
}
|
||||
|
||||
fn lifted(state: Interaction) -> bool {
|
||||
matches!(
|
||||
state,
|
||||
Interaction::Hovered | Interaction::Focused | Interaction::Pressed
|
||||
)
|
||||
}
|
||||
|
||||
/// Checkbox 的外观(§18.2 / §18.7 / §18.8)。
|
||||
///
|
||||
/// **描边宽度恒 2**(禁用态也是 2,§18.8:禁用不是「变淡变细」);
|
||||
/// 选中态没有描边(`SelectedOutlineWidth = 0`,走填充分支)。
|
||||
pub fn checkbox(
|
||||
ui: &Ui,
|
||||
selected: bool,
|
||||
error: bool,
|
||||
state: Interaction,
|
||||
radius: f32,
|
||||
stroke: f32,
|
||||
) -> CheckboxPaint {
|
||||
if state.is_disabled() {
|
||||
return CheckboxPaint {
|
||||
frame: Frame {
|
||||
fill: selected.then(|| disabled_container(ui)),
|
||||
outline: (!selected).then(|| disabled_ink(ui, Role::OnSurface)),
|
||||
stroke,
|
||||
radius,
|
||||
},
|
||||
// `SelectedDisabledIconColor = Surface`:勾是「透出底色」的浅色
|
||||
glyph: ui.color(Role::Surface),
|
||||
};
|
||||
}
|
||||
|
||||
match (selected, error) {
|
||||
(true, false) => CheckboxPaint {
|
||||
frame: Frame {
|
||||
fill: Some(ui.color(Role::Primary)),
|
||||
outline: None,
|
||||
stroke: 0.0,
|
||||
radius,
|
||||
},
|
||||
glyph: ui.color(Role::OnPrimary),
|
||||
},
|
||||
(true, true) => CheckboxPaint {
|
||||
frame: Frame {
|
||||
fill: Some(ui.color(Role::Error)),
|
||||
outline: None,
|
||||
stroke: 0.0,
|
||||
radius,
|
||||
},
|
||||
glyph: ui.color(Role::OnError),
|
||||
},
|
||||
(false, error) => {
|
||||
let role = if error {
|
||||
Role::Error
|
||||
} else if lifted(state) {
|
||||
// `UnselectedHover/Focus/PressedOutlineColor = OnSurface`(A1;不是 OnSurfaceVariant)
|
||||
Role::OnSurface
|
||||
} else {
|
||||
Role::OnSurfaceVariant
|
||||
};
|
||||
CheckboxPaint {
|
||||
frame: Frame {
|
||||
fill: None,
|
||||
outline: Some(ui.color(role)),
|
||||
stroke,
|
||||
radius,
|
||||
},
|
||||
glyph: ui.color(role),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Radio 的外观(§19.3 / §19.4 / §19.5)。
|
||||
///
|
||||
/// **选中态 hover 不换色**:环与点在四态里都是 `Primary`(只有状态层变)。
|
||||
pub fn radio(ui: &Ui, selected: bool, state: Interaction, radius: f32, stroke: f32) -> RadioPaint {
|
||||
let ink = if state.is_disabled() {
|
||||
disabled_ink(ui, Role::OnSurface)
|
||||
} else if selected {
|
||||
ui.color(Role::Primary)
|
||||
} else if lifted(state) {
|
||||
ui.color(Role::OnSurface)
|
||||
} else {
|
||||
ui.color(Role::OnSurfaceVariant)
|
||||
};
|
||||
|
||||
RadioPaint {
|
||||
ring: Frame {
|
||||
fill: None,
|
||||
outline: Some(ink),
|
||||
stroke,
|
||||
radius,
|
||||
},
|
||||
dot: selected.then_some(ink),
|
||||
}
|
||||
}
|
||||
|
||||
/// Switch 的外观(§10.3 / §10.4 / §10.7)。
|
||||
pub fn switch(ui: &Ui, selected: bool, state: Interaction) -> SwitchPaint {
|
||||
let geometry = SwitchGeometry::of(ui);
|
||||
let radius = ShapeScale::Full.radius_px(geometry.track_height);
|
||||
|
||||
if state.is_disabled() {
|
||||
let container = disabled_container(ui);
|
||||
return if selected {
|
||||
SwitchPaint {
|
||||
track: Frame {
|
||||
fill: Some(container),
|
||||
outline: None,
|
||||
stroke: 0.0,
|
||||
radius,
|
||||
},
|
||||
// `DisabledSelectedHandleColor = Surface` 且不透明度 1(A1 的 fconst_1)
|
||||
thumb: ui.color(Role::Surface),
|
||||
icon: Some(disabled_ink(ui, Role::OnSurface)),
|
||||
}
|
||||
} else {
|
||||
SwitchPaint {
|
||||
track: Frame {
|
||||
fill: Some(ui.color(Role::SurfaceContainerHighest)),
|
||||
outline: Some(container),
|
||||
stroke: geometry.track_stroke,
|
||||
radius,
|
||||
},
|
||||
thumb: disabled_ink(ui, Role::OnSurface),
|
||||
icon: None,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if selected {
|
||||
SwitchPaint {
|
||||
track: Frame {
|
||||
fill: Some(ui.color(Role::Primary)),
|
||||
outline: None,
|
||||
stroke: 0.0,
|
||||
radius,
|
||||
},
|
||||
// `SelectedHover/Focus/PressedHandleColor = PrimaryContainer`(A1,1.4.0 起)
|
||||
thumb: if lifted(state) {
|
||||
ui.color(Role::PrimaryContainer)
|
||||
} else {
|
||||
ui.color(Role::OnPrimary)
|
||||
},
|
||||
icon: Some(ui.color(Role::OnPrimaryContainer)),
|
||||
}
|
||||
} else {
|
||||
SwitchPaint {
|
||||
track: Frame {
|
||||
fill: Some(ui.color(Role::SurfaceContainerHighest)),
|
||||
outline: Some(ui.color(Role::Outline)),
|
||||
stroke: geometry.track_stroke,
|
||||
radius,
|
||||
},
|
||||
// `UnselectedHover/Focus/PressedHandleColor = OnSurfaceVariant`(A1)
|
||||
thumb: if lifted(state) {
|
||||
ui.color(Role::OnSurfaceVariant)
|
||||
} else {
|
||||
ui.color(Role::Outline)
|
||||
},
|
||||
icon: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Chip 的三种(§13.6 / §13.7):Assist / Filter / Input。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ChipVariant {
|
||||
/// Assist:**永远未选中**,前置图标 `Primary`(`AssistChipTokens.IconColor`)。
|
||||
Assist,
|
||||
/// Filter:可选中(`SecondaryContainer`),选中态无描边。
|
||||
Filter,
|
||||
/// Input:可选中 + 后置关闭图标。
|
||||
Input,
|
||||
}
|
||||
|
||||
/// Chip 的外观。
|
||||
///
|
||||
/// 与 Checkbox/Radio 的区别:Chip 的**状态层就是它自己的容器**(M3 的 chip 没有圆形状态层),
|
||||
/// 所以未选中 + hover 的 fill 是「状态层叠在 surface 上」的实色。
|
||||
pub fn chip(
|
||||
ui: &Ui,
|
||||
variant: ChipVariant,
|
||||
selected: bool,
|
||||
state: Interaction,
|
||||
radius: f32,
|
||||
) -> ChipPaint {
|
||||
let selected = selected && !matches!(variant, ChipVariant::Assist);
|
||||
let label_role = if selected {
|
||||
Role::OnSecondaryContainer
|
||||
} else if matches!(variant, ChipVariant::Assist) {
|
||||
Role::OnSurface
|
||||
} else {
|
||||
Role::OnSurfaceVariant
|
||||
};
|
||||
|
||||
if state.is_disabled() {
|
||||
let ink = disabled_ink(ui, Role::OnSurface);
|
||||
return ChipPaint {
|
||||
frame: Frame {
|
||||
fill: selected.then(|| disabled_container(ui)),
|
||||
outline: (!selected).then(|| disabled_container(ui)),
|
||||
stroke: STROKE_PX,
|
||||
radius,
|
||||
},
|
||||
label: ink,
|
||||
icon: ink,
|
||||
};
|
||||
}
|
||||
|
||||
let outline = (!selected).then(|| ui.color(Role::OutlineVariant));
|
||||
let fill = if selected {
|
||||
Some(ui.color(Role::SecondaryContainer))
|
||||
} else {
|
||||
state_layer(ui, state, label_role)
|
||||
};
|
||||
// Assist / 选中态的前置图标是 `Primary`(`AssistChipTokens.IconColor` /
|
||||
// `InputChipTokens.SelectedLeadingIconColor`);其余跟文字同色。
|
||||
let icon_role = if matches!(variant, ChipVariant::Assist) || selected {
|
||||
Role::Primary
|
||||
} else {
|
||||
label_role
|
||||
};
|
||||
|
||||
ChipPaint {
|
||||
frame: Frame {
|
||||
fill,
|
||||
outline,
|
||||
stroke: STROKE_PX,
|
||||
radius,
|
||||
},
|
||||
label: ui.color(label_role),
|
||||
icon: ui.color(icon_role),
|
||||
}
|
||||
}
|
||||
|
||||
/// 状态层的 content 角色(选中 = 滑块色 `OnPrimary`,未选中 = `Outline`)。
|
||||
pub fn switch_layer_role(selected: bool) -> Role {
|
||||
if selected {
|
||||
Role::OnPrimary
|
||||
} else {
|
||||
Role::Outline
|
||||
}
|
||||
}
|
||||
|
||||
/// Switch 状态层的颜色:**叠在轨道色上**,不是叠在 surface 上。
|
||||
///
|
||||
/// switch 的状态层圆(Ø40)有一大半压在轨道(52×32)上:若照别的控件那样叠 surface,
|
||||
/// 圆会在轨道里画出一块「更亮的补丁」(实测第一版就是这个样子,轨道像被啃掉一块)。
|
||||
pub fn switch_layer(ui: &Ui, state: Interaction, selected: bool) -> Option<Color> {
|
||||
let track = if selected {
|
||||
Role::Primary
|
||||
} else {
|
||||
Role::SurfaceContainerHighest
|
||||
};
|
||||
interaction::overlay_color(ui, state, switch_layer_role(selected), track)
|
||||
}
|
||||
|
||||
/// Checkbox 状态层的 content 角色(选中/部分选中 = `Primary`,错误 = `Error`,否则 `OnSurfaceVariant`)。
|
||||
///
|
||||
/// 出处:Compose 的 ripple 色就是 `selectedContainerColor` / `unselectedOutlineColor`。
|
||||
pub fn checkbox_layer_role(filled: bool, error: bool) -> Role {
|
||||
match (filled, error) {
|
||||
(true, false) => Role::Primary,
|
||||
(true, true) | (false, true) => Role::Error,
|
||||
(false, false) => Role::OnSurfaceVariant,
|
||||
}
|
||||
}
|
||||
|
||||
/// 控件旁的标签色:`OnSurface`;禁用 → `OnSurface`@38%。
|
||||
///
|
||||
/// A1 里 checkbox/radio 都没有「标签色随 hover 变」的字段(标签不属于控件本体),
|
||||
/// 所以这里只看禁用与否。
|
||||
pub fn label(ui: &Ui, state: Interaction) -> Color {
|
||||
if state.is_disabled() {
|
||||
disabled_ink(ui, Role::OnSurface)
|
||||
} else {
|
||||
ui.color(Role::OnSurface)
|
||||
}
|
||||
}
|
||||
|
||||
/// Radio 状态层的 content 角色(选中 = `Primary`,未选中 = `OnSurfaceVariant`)。
|
||||
pub fn radio_layer_role(selected: bool) -> Role {
|
||||
if selected {
|
||||
Role::Primary
|
||||
} else {
|
||||
Role::OnSurfaceVariant
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
//! Checkbox 的角色映射(`research/15` §18.2 / §18.7 / §18.8)。
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::super::interaction::Interaction;
|
||||
use super::{disabled_ink, lifted, CheckboxPaint, Frame};
|
||||
|
||||
/// Checkbox 的外观。
|
||||
///
|
||||
/// **描边宽度恒 2**(禁用态也是 2,§18.8:禁用不是「变淡变细」);
|
||||
/// 选中态没有描边(`SelectedOutlineWidth = 0`,走填充分支)。
|
||||
pub fn checkbox(
|
||||
ui: &Ui,
|
||||
selected: bool,
|
||||
error: bool,
|
||||
state: Interaction,
|
||||
radius: f32,
|
||||
stroke: f32,
|
||||
) -> CheckboxPaint {
|
||||
if state.is_disabled() {
|
||||
return CheckboxPaint {
|
||||
frame: Frame {
|
||||
// A1:`SelectedDisabledContainerColor = OnSurface` +
|
||||
// `SelectedDisabledContainerOpacity = 0.38` —— **不是** 12%(复审 P2-3)。
|
||||
// 与未选中禁用的描边是同一个 38% 叠色,两格看起来才成对。
|
||||
fill: selected.then(|| disabled_ink(ui, Role::OnSurface)),
|
||||
outline: (!selected).then(|| disabled_ink(ui, Role::OnSurface)),
|
||||
stroke,
|
||||
radius,
|
||||
},
|
||||
// `SelectedDisabledIconColor = Surface`:勾是「透出底色」的浅色
|
||||
glyph: ui.color(Role::Surface),
|
||||
};
|
||||
}
|
||||
|
||||
match (selected, error) {
|
||||
(true, false) => CheckboxPaint {
|
||||
frame: Frame {
|
||||
fill: Some(ui.color(Role::Primary)),
|
||||
outline: None,
|
||||
stroke: 0.0,
|
||||
radius,
|
||||
},
|
||||
glyph: ui.color(Role::OnPrimary),
|
||||
},
|
||||
(true, true) => CheckboxPaint {
|
||||
frame: Frame {
|
||||
fill: Some(ui.color(Role::Error)),
|
||||
outline: None,
|
||||
stroke: 0.0,
|
||||
radius,
|
||||
},
|
||||
glyph: ui.color(Role::OnError),
|
||||
},
|
||||
(false, error) => {
|
||||
let role = if error {
|
||||
Role::Error
|
||||
} else if lifted(state) {
|
||||
// `UnselectedHover/Focus/PressedOutlineColor = OnSurface`(A1;不是 OnSurfaceVariant)
|
||||
Role::OnSurface
|
||||
} else {
|
||||
Role::OnSurfaceVariant
|
||||
};
|
||||
CheckboxPaint {
|
||||
frame: Frame {
|
||||
fill: None,
|
||||
outline: Some(ui.color(role)),
|
||||
stroke,
|
||||
radius,
|
||||
},
|
||||
glyph: ui.color(role),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checkbox 状态层的 content 角色(选中/部分选中 = `Primary`,错误 = `Error`,否则 `OnSurfaceVariant`)。
|
||||
///
|
||||
/// 出处:Compose 的 ripple 色就是 `selectedContainerColor` / `unselectedOutlineColor`。
|
||||
pub fn checkbox_layer_role(filled: bool, error: bool) -> Role {
|
||||
match (filled, error) {
|
||||
(true, false) => Role::Primary,
|
||||
(true, true) | (false, true) => Role::Error,
|
||||
(false, false) => Role::OnSurfaceVariant,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
//! Chip 的角色映射(`research/12b` §13.5 / §13.6 / §13.7)。
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
|
||||
use crate::theme::{Ui, STROKE_PX};
|
||||
|
||||
use super::super::super::interaction::Interaction;
|
||||
use super::{disabled_container, disabled_ink, state_layer, ChipPaint, Frame};
|
||||
|
||||
/// Chip 的三种(§13.6 / §13.7):Assist / Filter / Input。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ChipVariant {
|
||||
/// Assist:**永远未选中**,前置图标 `Primary`(`AssistChipTokens.IconColor`)。
|
||||
Assist,
|
||||
/// Filter:可选中(`SecondaryContainer`),选中态无描边。
|
||||
Filter,
|
||||
/// Input:可选中 + 后置关闭图标。
|
||||
Input,
|
||||
}
|
||||
|
||||
/// Chip 的外观。
|
||||
///
|
||||
/// 与 Checkbox/Radio 的区别:Chip 的**状态层就是它自己的容器**(M3 的 chip 没有圆形状态层),
|
||||
/// 所以未选中 + hover 的 fill 是「状态层叠在 surface 上」的实色。
|
||||
pub fn chip(
|
||||
ui: &Ui,
|
||||
variant: ChipVariant,
|
||||
selected: bool,
|
||||
state: Interaction,
|
||||
radius: f32,
|
||||
) -> ChipPaint {
|
||||
let selected = selected && !matches!(variant, ChipVariant::Assist);
|
||||
let label_role = if selected {
|
||||
Role::OnSecondaryContainer
|
||||
} else if matches!(variant, ChipVariant::Assist) {
|
||||
Role::OnSurface
|
||||
} else {
|
||||
Role::OnSurfaceVariant
|
||||
};
|
||||
|
||||
if state.is_disabled() {
|
||||
let ink = disabled_ink(ui, Role::OnSurface);
|
||||
return ChipPaint {
|
||||
frame: Frame {
|
||||
fill: selected.then(|| disabled_container(ui)),
|
||||
outline: (!selected).then(|| disabled_container(ui)),
|
||||
stroke: STROKE_PX,
|
||||
radius,
|
||||
},
|
||||
label: ink,
|
||||
icon: ink,
|
||||
};
|
||||
}
|
||||
|
||||
let outline = (!selected).then(|| ui.color(Role::OutlineVariant));
|
||||
let fill = if selected {
|
||||
Some(ui.color(Role::SecondaryContainer))
|
||||
} else {
|
||||
state_layer(ui, state, label_role)
|
||||
};
|
||||
// Assist / 选中态的前置图标是 `Primary`(`AssistChipTokens.IconColor` /
|
||||
// `InputChipTokens.SelectedLeadingIconColor`);其余跟文字同色。
|
||||
let icon_role = if matches!(variant, ChipVariant::Assist) || selected {
|
||||
Role::Primary
|
||||
} else {
|
||||
label_role
|
||||
};
|
||||
|
||||
ChipPaint {
|
||||
frame: Frame {
|
||||
fill,
|
||||
outline,
|
||||
stroke: STROKE_PX,
|
||||
radius,
|
||||
},
|
||||
label: ui.color(label_role),
|
||||
icon: ui.color(icon_role),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
//! 选择类家族的**角色映射**:`Interaction × 选中 × 错误 → 颜色`(公共件 + 各组件分文件)。
|
||||
//!
|
||||
//! ## 出处(全部 A1:`material3-android:1.4.0` 的 token 类,本机 AAR `javap` 复核,见报告)
|
||||
//!
|
||||
//! | 组件 | 静息 | hover / focus / pressed | 禁用 |
|
||||
//! |---|---|---|---|
|
||||
//! | Checkbox 未选中 | 描边 `OnSurfaceVariant` 2px | 描边提到 **`OnSurface`**(宽度仍 2) | 描边 `OnSurface`@38% |
|
||||
//! | Checkbox 选中 | 填 `Primary` + 勾 `OnPrimary` | 填 `Primary`(不变) | 填 `OnSurface`@38% + 勾 `Surface` |
|
||||
//! | Radio | 环 `OnSurfaceVariant` / 选中环+点 `Primary` | 未选中环提到 **`OnSurface`**;**选中态仍是 `Primary`** | 环+点 `OnSurface`@38% |
|
||||
//! | Switch | 轨 `Primary`(选中)/`SurfaceContainerHighest`+2px `Outline`;滑块 `OnPrimary`/`Outline` | 滑块 `PrimaryContainer`/`OnSurfaceVariant` | 轨 `OnSurface`@12%;滑块 `Surface`/@38% |
|
||||
//! | Chip | 未选中:透明+1px `OutlineVariant`,文字 `OnSurfaceVariant`;选中:`SecondaryContainer` **无描边** | 描边不变,状态层成为底色 | 描边 `OnSurface`@12%;文字/图标 @38% |
|
||||
//!
|
||||
//! **A1 改过参考表的三处**:`UnselectedHover/Focus/PressedOutlineColor` 与 Radio 的
|
||||
//! `UnselectedHover/Focus/PressedIconColor` 都是 `OnSurface`(不是 `OnSurfaceVariant`);
|
||||
//! `SelectedDisabledIconColor` 是 `Surface`。α 一律从 [`interaction`] 取。
|
||||
//!
|
||||
//! ## 为什么拆成目录(复审 P2-5)
|
||||
//!
|
||||
//! 原来一个文件正好 400 行、零余量:再加一个态就得动它。按组件拆成
|
||||
//! `check.rs` / `radio.rs` / `switch.rs` / `chip.rs`,公共件([`Frame`]、状态层、禁用色)留在本文件,
|
||||
//! **对外 API 一字不改**(`selection::paint::*` 的路径全部保持)。
|
||||
|
||||
use iced::widget::container;
|
||||
use iced::{Background, Border, Color};
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::interaction::{self, Interaction};
|
||||
|
||||
mod check;
|
||||
mod chip;
|
||||
mod radio;
|
||||
mod switch;
|
||||
|
||||
pub use check::{checkbox, checkbox_layer_role};
|
||||
pub use chip::{chip, ChipVariant};
|
||||
pub use radio::{radio, radio_layer_role};
|
||||
pub use switch::{switch, switch_layer};
|
||||
|
||||
/// 一个「方框」的外观:填色 / 描边 / 线宽 / 半径。
|
||||
///
|
||||
/// Checkbox 的框、Radio 的环、Switch 的轨道、Chip 的容器都用它——差别只在颜色与线宽。
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Frame {
|
||||
/// 填充色;`None` = 透明(未选中的 Checkbox / Radio 外圈、Chip 的 flat 底)。
|
||||
pub fill: Option<Color>,
|
||||
/// 描边色;`None` = 不画描边。
|
||||
pub outline: Option<Color>,
|
||||
/// 描边宽度(Checkbox/Radio 2,Chip 1,Switch 未选中 2)。
|
||||
pub stroke: f32,
|
||||
/// 圆角半径(Checkbox 2 / Radio 10 / Switch 16 / Chip 8)。
|
||||
pub radius: f32,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
/// 翻成 iced 的 `container::Style`(唯一把 Frame 变成像素的地方)。
|
||||
pub fn container_style(self) -> container::Style {
|
||||
container::Style {
|
||||
background: self.fill.map(Background::Color),
|
||||
border: Border {
|
||||
color: self.outline.unwrap_or(Color::TRANSPARENT),
|
||||
width: self.outline.map_or(0.0, |_| self.stroke),
|
||||
radius: iced::border::Radius::new(self.radius),
|
||||
},
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checkbox 的外观:框 + 勾的颜色。
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct CheckboxPaint {
|
||||
/// 框(20×20)。
|
||||
pub frame: Frame,
|
||||
/// 勾 / 短横。
|
||||
pub glyph: Color,
|
||||
}
|
||||
|
||||
/// Radio 的外观:环 + 内点。
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct RadioPaint {
|
||||
/// 外圈(20×20,环线向内收 → 外缘恒 20)。
|
||||
pub ring: Frame,
|
||||
/// 内点(Ø10);`None` = 未选中(不画点)。
|
||||
pub dot: Option<Color>,
|
||||
}
|
||||
|
||||
/// Switch 的外观:轨道 + 滑块 + 滑块内图标。
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct SwitchPaint {
|
||||
/// 轨道(52×32)。
|
||||
pub track: Frame,
|
||||
/// 滑块。
|
||||
pub thumb: Color,
|
||||
/// 滑块内图标(16);`None` = 不画(未选中时官方色与轨道同色,画了也看不见)。
|
||||
pub icon: Option<Color>,
|
||||
}
|
||||
|
||||
/// Chip 的外观:容器 + 文字/图标。
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct ChipPaint {
|
||||
/// 容器。
|
||||
pub frame: Frame,
|
||||
/// 文字。
|
||||
pub label: Color,
|
||||
/// 前/后置图标。
|
||||
pub icon: Color,
|
||||
}
|
||||
|
||||
/// 状态层颜色:`content` 角色 @ 该态的 α 叠在 `surface` 上(不透明;α 的唯一实现点是
|
||||
/// [`interaction::overlay_color`],本目录零 α 字面量)。
|
||||
pub fn state_layer(ui: &Ui, state: Interaction, content: Role) -> Option<Color> {
|
||||
interaction::overlay_color(ui, state, content, Role::Surface)
|
||||
}
|
||||
|
||||
/// `OnSurface`@12% 叠 surface(**容器**禁用色:Switch 轨道、Chip 未选中描边)。
|
||||
///
|
||||
/// 注意:Checkbox「选中且禁用」的**填充**不是这个 12%,A1 的
|
||||
/// `SelectedDisabledContainerOpacity = 0.38` → 见 [`disabled_ink`](复审 P2-3)。
|
||||
pub(super) fn disabled_container(ui: &Ui) -> Color {
|
||||
interaction::disabled_colors(ui, Role::Surface).0
|
||||
}
|
||||
|
||||
/// 内容禁用色:`role` @38% 叠在 surface 上(`on_container = false`)。
|
||||
pub(super) fn disabled_ink(ui: &Ui, role: Role) -> Color {
|
||||
interaction::disabled_content_over_role(ui, role, false)
|
||||
}
|
||||
|
||||
/// hover / focus / pressed(「抬起来」的三态)。
|
||||
pub(super) fn lifted(state: Interaction) -> bool {
|
||||
matches!(
|
||||
state,
|
||||
Interaction::Hovered | Interaction::Focused | Interaction::Pressed
|
||||
)
|
||||
}
|
||||
|
||||
/// 控件旁的标签色:`OnSurface`;禁用 → `OnSurface`@38%。
|
||||
///
|
||||
/// A1 里 checkbox/radio 都没有「标签色随 hover 变」的字段(标签不属于控件本体),
|
||||
/// 所以这里只看禁用与否。
|
||||
pub fn label(ui: &Ui, state: Interaction) -> Color {
|
||||
if state.is_disabled() {
|
||||
disabled_ink(ui, Role::OnSurface)
|
||||
} else {
|
||||
ui.color(Role::OnSurface)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
//! Radio 的角色映射(`research/15` §19.3 / §19.4 / §19.5)。
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::super::interaction::Interaction;
|
||||
use super::{disabled_ink, lifted, Frame, RadioPaint};
|
||||
|
||||
/// Radio 的外观。
|
||||
///
|
||||
/// **选中态 hover 不换色**:环与点在四态里都是 `Primary`(只有状态层变)。
|
||||
pub fn radio(ui: &Ui, selected: bool, state: Interaction, radius: f32, stroke: f32) -> RadioPaint {
|
||||
let ink = if state.is_disabled() {
|
||||
disabled_ink(ui, Role::OnSurface)
|
||||
} else if selected {
|
||||
ui.color(Role::Primary)
|
||||
} else if lifted(state) {
|
||||
ui.color(Role::OnSurface)
|
||||
} else {
|
||||
ui.color(Role::OnSurfaceVariant)
|
||||
};
|
||||
|
||||
RadioPaint {
|
||||
ring: Frame {
|
||||
fill: None,
|
||||
outline: Some(ink),
|
||||
stroke,
|
||||
radius,
|
||||
},
|
||||
dot: selected.then_some(ink),
|
||||
}
|
||||
}
|
||||
|
||||
/// Radio 状态层的 content 角色(选中 = `Primary`,未选中 = `OnSurfaceVariant`)。
|
||||
pub fn radio_layer_role(selected: bool) -> Role {
|
||||
if selected {
|
||||
Role::Primary
|
||||
} else {
|
||||
Role::OnSurfaceVariant
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
//! Switch 的角色映射(`research/12b` §10.3 / §10.4 / §10.6 / §10.7)。
|
||||
|
||||
use iced::Color;
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::shape::ShapeScale;
|
||||
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::super::interaction::{self, Interaction};
|
||||
use super::super::geometry::SwitchGeometry;
|
||||
use super::{disabled_container, disabled_ink, lifted, Frame, SwitchPaint};
|
||||
|
||||
/// Switch 的外观。
|
||||
pub fn switch(ui: &Ui, selected: bool, state: Interaction) -> SwitchPaint {
|
||||
let geometry = SwitchGeometry::of(ui);
|
||||
let radius = ShapeScale::Full.radius_px(geometry.track_height);
|
||||
|
||||
if state.is_disabled() {
|
||||
let container = disabled_container(ui);
|
||||
return if selected {
|
||||
SwitchPaint {
|
||||
track: Frame {
|
||||
fill: Some(container),
|
||||
outline: None,
|
||||
stroke: 0.0,
|
||||
radius,
|
||||
},
|
||||
// `DisabledSelectedHandleColor = Surface` 且不透明度 1(A1 的 fconst_1)
|
||||
thumb: ui.color(Role::Surface),
|
||||
icon: Some(disabled_ink(ui, Role::OnSurface)),
|
||||
}
|
||||
} else {
|
||||
SwitchPaint {
|
||||
track: Frame {
|
||||
fill: Some(ui.color(Role::SurfaceContainerHighest)),
|
||||
outline: Some(container),
|
||||
stroke: geometry.track_stroke,
|
||||
radius,
|
||||
},
|
||||
thumb: disabled_ink(ui, Role::OnSurface),
|
||||
icon: None,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if selected {
|
||||
SwitchPaint {
|
||||
track: Frame {
|
||||
fill: Some(ui.color(Role::Primary)),
|
||||
outline: None,
|
||||
stroke: 0.0,
|
||||
radius,
|
||||
},
|
||||
// `SelectedHover/Focus/PressedHandleColor = PrimaryContainer`(A1,1.4.0 起)
|
||||
thumb: if lifted(state) {
|
||||
ui.color(Role::PrimaryContainer)
|
||||
} else {
|
||||
ui.color(Role::OnPrimary)
|
||||
},
|
||||
icon: Some(ui.color(Role::OnPrimaryContainer)),
|
||||
}
|
||||
} else {
|
||||
SwitchPaint {
|
||||
track: Frame {
|
||||
fill: Some(ui.color(Role::SurfaceContainerHighest)),
|
||||
outline: Some(ui.color(Role::Outline)),
|
||||
stroke: geometry.track_stroke,
|
||||
radius,
|
||||
},
|
||||
// `UnselectedHover/Focus/PressedHandleColor = OnSurfaceVariant`(A1)
|
||||
thumb: if lifted(state) {
|
||||
ui.color(Role::OnSurfaceVariant)
|
||||
} else {
|
||||
ui.color(Role::Outline)
|
||||
},
|
||||
icon: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 状态层的 content 角色(选中 = 滑块色 `OnPrimary`,未选中 = `Outline`)。
|
||||
pub fn switch_layer_role(selected: bool) -> Role {
|
||||
if selected {
|
||||
Role::OnPrimary
|
||||
} else {
|
||||
Role::Outline
|
||||
}
|
||||
}
|
||||
|
||||
/// Switch 状态层的颜色:**叠在轨道色上**,不是叠在 surface 上。
|
||||
///
|
||||
/// switch 的状态层圆(Ø40)有一大半压在轨道(52×32)上:若照别的控件那样叠 surface,
|
||||
/// 圆会在轨道里画出一块「更亮的补丁」(实测第一版就是这个样子,轨道像被啃掉一块)。
|
||||
pub fn switch_layer(ui: &Ui, state: Interaction, selected: bool) -> Option<Color> {
|
||||
let track = if selected {
|
||||
Role::Primary
|
||||
} else {
|
||||
Role::SurfaceContainerHighest
|
||||
};
|
||||
interaction::overlay_color(ui, state, switch_layer_role(selected), track)
|
||||
}
|
||||
Reference in new issue
Block a user