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:
AskaEth committed 2026-11-02 06:52:10 +08:00
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)
}