diff --git a/crates/app-iced/src/components/selection/paint.rs b/crates/app-iced/src/components/selection/paint.rs deleted file mode 100644 index 0563864..0000000 --- a/crates/app-iced/src/components/selection/paint.rs +++ /dev/null @@ -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, - /// 描边色;`None` = 不画描边。 - pub outline: Option, - /// 描边宽度(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, -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct SwitchPaint { - /// 轨道(52×32)。 - pub track: Frame, - /// 滑块。 - pub thumb: Color, - /// 滑块内图标(16);`None` = 不画(未选中时官方色与轨道同色,画了也看不见)。 - pub icon: Option, -} - -#[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 { - 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 { - 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 - } -} diff --git a/crates/app-iced/src/components/selection/paint/check.rs b/crates/app-iced/src/components/selection/paint/check.rs new file mode 100644 index 0000000..6fdab48 --- /dev/null +++ b/crates/app-iced/src/components/selection/paint/check.rs @@ -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, + } +} diff --git a/crates/app-iced/src/components/selection/paint/chip.rs b/crates/app-iced/src/components/selection/paint/chip.rs new file mode 100644 index 0000000..3277f33 --- /dev/null +++ b/crates/app-iced/src/components/selection/paint/chip.rs @@ -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), + } +} diff --git a/crates/app-iced/src/components/selection/paint/mod.rs b/crates/app-iced/src/components/selection/paint/mod.rs new file mode 100644 index 0000000..b52e4ee --- /dev/null +++ b/crates/app-iced/src/components/selection/paint/mod.rs @@ -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, + /// 描边色;`None` = 不画描边。 + pub outline: Option, + /// 描边宽度(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, +} + +/// Switch 的外观:轨道 + 滑块 + 滑块内图标。 +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct SwitchPaint { + /// 轨道(52×32)。 + pub track: Frame, + /// 滑块。 + pub thumb: Color, + /// 滑块内图标(16);`None` = 不画(未选中时官方色与轨道同色,画了也看不见)。 + pub icon: Option, +} + +/// 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 { + 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) + } +} diff --git a/crates/app-iced/src/components/selection/paint/radio.rs b/crates/app-iced/src/components/selection/paint/radio.rs new file mode 100644 index 0000000..7d09072 --- /dev/null +++ b/crates/app-iced/src/components/selection/paint/radio.rs @@ -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 + } +} diff --git a/crates/app-iced/src/components/selection/paint/switch.rs b/crates/app-iced/src/components/selection/paint/switch.rs new file mode 100644 index 0000000..d4b2520 --- /dev/null +++ b/crates/app-iced/src/components/selection/paint/switch.rs @@ -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 { + let track = if selected { + Role::Primary + } else { + Role::SurfaceContainerHighest + }; + interaction::overlay_color(ui, state, switch_layer_role(selected), track) +}