From a878fc6a5837a0173373d17e1468acb40248c87a Mon Sep 17 00:00:00 2001 From: AskaEth Date: Mon, 2 Nov 2026 02:31:23 +0800 Subject: [PATCH] =?UTF-8?q?refactor(app-iced):=20M75=20r4=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20interaction=20=E6=8B=86=E6=88=90=20mod.rs(287)=20+?= =?UTF-8?q?=20tracker.rs(159)=EF=BC=8C=E6=81=A2=E5=A4=8D=E8=A2=AB=E5=8E=8B?= =?UTF-8?q?=E6=8E=89=E7=9A=84=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit interaction.rs 连压三笔才回到 400 行(靠删文档腾空间),而它会被四条家族线各扩一次。 - interaction/mod.rs:五态枚举 + overlay_color / disabled_colors / disabled_content_over(态→颜色) - interaction/tracker.rs:事件→态的状态机 + 它的两条单测;Tracker 用法示例(ignore 代码块)恢复 - 对外路径不变:components::interaction::* 照旧(mod.rs 里再导出 Tracker) - 两份文件都写了「加东西前先看行数余额」 顺带修两处门禁:注释里去掉 hex 色值(守门连注释一起扫)、再导出显式 allow。 --- crates/app-iced/src/components/gallery/mod.rs | 2 +- .../{interaction.rs => interaction/mod.rs} | 160 +++--------------- .../src/components/interaction/tracker.rs | 155 +++++++++++++++++ 3 files changed, 180 insertions(+), 137 deletions(-) rename crates/app-iced/src/components/{interaction.rs => interaction/mod.rs} (66%) create mode 100644 crates/app-iced/src/components/interaction/tracker.rs diff --git a/crates/app-iced/src/components/gallery/mod.rs b/crates/app-iced/src/components/gallery/mod.rs index 3a512a8..eaac1e7 100644 --- a/crates/app-iced/src/components/gallery/mod.rs +++ b/crates/app-iced/src/components/gallery/mod.rs @@ -111,7 +111,7 @@ pub fn view<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> { .spacing(ui.space(Space::X8)) .width(Length::Fill); - // 滚动条:默认是主题给的 10px 深灰 thumb(`#6A7985`),压过组件本体(视觉 reviewer P2-4)。 + // 滚动条:默认是主题给的 10px 深灰 thumb,压过组件本体(视觉 reviewer P2-4)。 // 这里把它改成 **透明轨 + OutlineVariant thumb**;宽度 10px 是 `scrollable` 的内部常量 // (该控件不暴露 Scrollbar 配置),所以只做了「低对比」这一半,已登记为偏离 D7。 let body = scrollable(sections) diff --git a/crates/app-iced/src/components/interaction.rs b/crates/app-iced/src/components/interaction/mod.rs similarity index 66% rename from crates/app-iced/src/components/interaction.rs rename to crates/app-iced/src/components/interaction/mod.rs index e7b40c6..0a0e73b 100644 --- a/crates/app-iced/src/components/interaction.rs +++ b/crates/app-iced/src/components/interaction/mod.rs @@ -7,12 +7,17 @@ //! `if hovered { 0.08 }`,第一周就会出现 0.08 / 0.1 / 0.12 三种 hover—— //! 这一层存在的意义就是让「组件里出现 α 字面量」变成不可能: //! -//! | 态 | α(令牌) | -//! |---|---| -//! | hover 0.08 / focus 0.10 / pressed 0.10 / dragged 0.16 | [`StateLayer`] | -//! | disabled:容器 0.12 / 内容 0.38 | [`DISABLED_CONTAINER_ALPHA`] / [`DISABLED_CONTENT_ALPHA`] | +//! | 态 | α | 令牌来源 | +//! |---|---|---| +//! | hover | 0.08 | [`StateLayer::Hover`] | +//! | focus | 0.10 | [`StateLayer::Focus`] | +//! | pressed | 0.10 | [`StateLayer::Pressed`] | +//! | dragged | 0.16 | [`StateLayer::Dragged`](按钮家族不用,列表拖拽用) | +//! | disabled 容器 | 0.12 | [`DISABLED_CONTAINER_ALPHA`](A1 是 0.10,差异登记 D8) | +//! | disabled 内容 | 0.38 | [`DISABLED_CONTENT_ALPHA`] | //! -//! 表里的数字**只能从令牌拿**,代码里不许出现(`components::gate` 会扫)。 +//! 上表右列是**唯一**允许的取值口;左列只是给人看的,代码里一个数字都不许出现 +//! (`components::gate` 会扫)。 //! //! ## 两个消费形态 //! @@ -26,16 +31,28 @@ //! 逐像素一致,D27)。本模块只做「哪一态叠哪一层」的映射,不做任何颜色算术。 use iced::widget::button; -use iced::{Color, Event}; +use iced::Color; use unlucky_design::color::state::{DISABLED_CONTAINER_ALPHA, DISABLED_CONTENT_ALPHA}; use unlucky_design::color::{Role, Scheme, StateLayer}; use crate::theme::{to_iced, Ui}; +mod tracker; + +// 本 mission 没有自绘控件,所以这条再导出在 crate 内暂时无人使用; +// 它是给四条家族线(Slider / Switch 轨道要自绘)的公共入口,路径与拆分前一致。 +#[allow(unused_imports)] +pub use tracker::Tracker; + +// 本模块会被四条家族线各扩一次:加东西前先看行数余额(单文件 ≤400,AGENTS.md §3-1)。 +// `mod.rs` 只装「五态 + 颜色映射」这类纯函数;事件 → 态的**状态机在 [`tracker`]**, +// 需要新的状态机就加到那边,别把本文件再撑到红线上。 + /// 一个控件的视觉态(五态,M3 的完整集合)。 /// -/// `Enabled` = 静息(不叠状态层);`Focused` / `Pressed` 的 α 相同(都 10%),所以图上一样是规范。 +/// `Enabled` 是**静息态**(不叠状态层);`Focused` / `Pressed` 在 M3 里 α 相同(都 10%), +/// 所以两者在图上会一模一样——这是规范,不是漏画(12b §1.8 判据只要求 α 对得上)。 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Interaction { /// 静息:可交互,不叠状态层。 @@ -153,93 +170,6 @@ pub fn disabled_colors(ui: &Ui, surface: Role) -> (Color, Color) { /// 自定义 `Widget` 的状态机:把 iced 事件折成 [`Interaction`]。 /// -/// D39 §3 说的「状态机的公共部分」。自定义 `Widget` 的 `update` 里: -/// `if tracker.event(event, cursor.is_over(bounds)) { shell.request_redraw(); }`, -/// 画的时候读 `tracker.state()`。`event()` 只在**视觉态真的变了**时返回 `true`。 -// 本 mission 的按钮家族全部走 iced 的 `button`,没有自绘控件,所以 Tracker 目前只有单测在用; -// 它在这里是给四个后续家族(Slider / Switch 轨道这类要自绘的)准备的公共状态机。 -#[allow(dead_code)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct Tracker { - enabled: bool, - hovered: bool, - focused: bool, - pressed: bool, -} - -#[allow(dead_code)] -impl Tracker { - /// 新建;`enabled` 由组件自己决定(例如 `on_press.is_some()`)。 - pub fn new(enabled: bool) -> Self { - Self { - enabled, - ..Self::default() - } - } - - /// 启用/禁用热切换(禁用时清掉悬停与按下:禁用元素不该留着 hover 高亮)。 - pub fn set_enabled(&mut self, enabled: bool) -> bool { - let changed = self.enabled != enabled; - self.enabled = enabled; - if !enabled { - self.hovered = false; - self.pressed = false; - self.focused = false; - } - changed - } - - /// 键盘焦点(由 `operation`/`shell` 侧的聚焦消息驱动)。 - pub fn set_focused(&mut self, focused: bool) -> bool { - let next = focused && self.enabled; - let changed = self.focused != next; - self.focused = next; - changed - } - - /// 折进一个事件;返回「视觉态变了,要重绘吗」。 - /// - /// `over` = 指针是否在控件 bounds 内(`Cursor::is_over(bounds)` 的结果,由调用方算—— - /// 这样本函数不依赖 `Layout`,可以在单测里直接喂)。 - pub fn event(&mut self, event: &Event, over: bool) -> bool { - if !self.enabled { - return false; - } - - let before = self.state(); - match event { - Event::Mouse(iced::mouse::Event::CursorMoved { .. }) => self.hovered = over, - Event::Mouse(iced::mouse::Event::CursorLeft) => { - self.hovered = false; - self.pressed = false; - } - Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left)) => { - self.pressed = over; - } - Event::Mouse(iced::mouse::Event::ButtonReleased(iced::mouse::Button::Left)) => { - self.pressed = false; - } - _ => {} - } - self.state() != before - } - - /// 当前态(优先级:按下 > 焦点 > 悬停 > 静息;禁用最高)。 - pub const fn state(&self) -> Interaction { - if !self.enabled { - Interaction::Disabled - } else if self.pressed { - Interaction::Pressed - } else if self.focused { - Interaction::Focused - } else if self.hovered { - Interaction::Hovered - } else { - Interaction::Enabled - } - } -} - #[cfg(test)] mod tests { use super::*; @@ -355,46 +285,4 @@ mod tests { assert_eq!(container.a, 1.0, "禁用容器也是实色(CPU/GPU 一致)"); assert_eq!(content.a, 1.0, "禁用内容也是实色"); } - - #[test] - fn tracker_folds_events_into_the_five_states() { - let mut tracker = Tracker::new(true); - assert_eq!(tracker.state(), Interaction::Enabled); - - let moved = Event::Mouse(iced::mouse::Event::CursorMoved { - position: iced::Point::ORIGIN, - }); - assert!(tracker.event(&moved, true), "首次悬停要报变化"); - assert_eq!(tracker.state(), Interaction::Hovered); - assert!(!tracker.event(&moved, true), "重复悬停不该再报变化"); - - let press = Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left)); - assert!(tracker.event(&press, true)); - assert_eq!(tracker.state(), Interaction::Pressed); - - let release = Event::Mouse(iced::mouse::Event::ButtonReleased( - iced::mouse::Button::Left, - )); - assert!(tracker.event(&release, true)); - assert_eq!(tracker.state(), Interaction::Hovered); - - assert!(tracker.set_focused(true)); - assert_eq!(tracker.state(), Interaction::Focused, "焦点态优先于悬停"); - - let left = Event::Mouse(iced::mouse::Event::CursorLeft); - tracker.event(&left, false); - assert_eq!(tracker.state(), Interaction::Focused, "鼠标离开不影响焦点"); - } - - #[test] - fn disabled_tracker_swallows_everything() { - let mut tracker = Tracker::new(true); - assert!(tracker.set_enabled(false)); - assert_eq!(tracker.state(), Interaction::Disabled); - - let press = Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left)); - assert!(!tracker.event(&press, true), "禁用态不响应事件"); - assert_eq!(tracker.state(), Interaction::Disabled); - assert!(!tracker.set_focused(true), "禁用态拿不到焦点"); - } } diff --git a/crates/app-iced/src/components/interaction/tracker.rs b/crates/app-iced/src/components/interaction/tracker.rs new file mode 100644 index 0000000..8139171 --- /dev/null +++ b/crates/app-iced/src/components/interaction/tracker.rs @@ -0,0 +1,155 @@ +//! 自定义 `Widget` 的状态机([`Tracker`]):把 iced 事件折成 [`Interaction`] 五态。 +//! +//! 与 [`super`] 的分工:`super` 只有「态 → 颜色」的纯映射;**事件 → 态**在这里。 +//! 本 mission 的按钮家族全走 iced 的 `button`(自带四态),所以 `Tracker` 目前只有单测在用—— +//! 它给四个后续家族里**要自绘**的控件(Slider 轨道 / Switch 轨道 / 进度条)准备。 +//! +//! 加东西前先看行数余额(单文件 ≤400,AGENTS.md §3-1)。 + +use iced::Event; + +use super::Interaction; + +/// 自定义 `Widget` 的状态机:把 iced 事件折成 [`Interaction`]。 +/// +/// 这是 D39 §3 说的「状态机的公共部分」。用法(自定义 Widget 的 `update` 里): +/// +/// ```ignore +/// if self.tracker.event(event, cursor.is_over(layout.bounds())) { +/// shell.request_redraw(); // 只有真变了才重绘 +/// } +/// let style = style_of(self.tracker.state()); +/// ``` +#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Tracker { + enabled: bool, + hovered: bool, + focused: bool, + pressed: bool, +} + +#[allow(dead_code)] +impl Tracker { + /// 新建;`enabled` 由组件自己决定(例如 `on_press.is_some()`)。 + pub fn new(enabled: bool) -> Self { + Self { + enabled, + ..Self::default() + } + } + + /// 启用/禁用热切换(禁用时清掉悬停与按下:禁用元素不该留着 hover 高亮)。 + pub fn set_enabled(&mut self, enabled: bool) -> bool { + let changed = self.enabled != enabled; + self.enabled = enabled; + if !enabled { + self.hovered = false; + self.pressed = false; + self.focused = false; + } + changed + } + + /// 键盘焦点(由 `operation`/`shell` 侧的聚焦消息驱动)。 + pub fn set_focused(&mut self, focused: bool) -> bool { + let next = focused && self.enabled; + let changed = self.focused != next; + self.focused = next; + changed + } + + /// 折进一个事件;返回「视觉态变了,要重绘吗」。 + /// + /// `over` = 指针是否在控件 bounds 内(`Cursor::is_over(bounds)` 的结果,由调用方算—— + /// 这样本函数不依赖 `Layout`,可以在单测里直接喂)。 + pub fn event(&mut self, event: &Event, over: bool) -> bool { + if !self.enabled { + return false; + } + + let before = self.state(); + match event { + Event::Mouse(iced::mouse::Event::CursorMoved { .. }) => self.hovered = over, + Event::Mouse(iced::mouse::Event::CursorLeft) => { + self.hovered = false; + self.pressed = false; + } + Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left)) => { + self.pressed = over; + } + Event::Mouse(iced::mouse::Event::ButtonReleased(iced::mouse::Button::Left)) => { + self.pressed = false; + } + _ => {} + } + self.state() != before + } + + /// 当前态(优先级:按下 > 焦点 > 悬停 > 静息;禁用最高)。 + pub const fn state(&self) -> Interaction { + if !self.enabled { + Interaction::Disabled + } else if self.pressed { + Interaction::Pressed + } else if self.focused { + Interaction::Focused + } else if self.hovered { + Interaction::Hovered + } else { + Interaction::Enabled + } + } +} + +/// `Tracker` 的行为用例(自绘控件用,与颜色无关)。 +#[cfg(test)] +mod tests { + use super::*; + use iced::Event; + + fn moved() -> Event { + Event::Mouse(iced::mouse::Event::CursorMoved { + position: iced::Point::ORIGIN, + }) + } + + #[test] + fn tracker_folds_events_into_the_five_states() { + let mut tracker = Tracker::new(true); + assert_eq!(tracker.state(), Interaction::Enabled); + + assert!(tracker.event(&moved(), true), "首次悬停要报变化"); + assert_eq!(tracker.state(), Interaction::Hovered); + assert!(!tracker.event(&moved(), true), "重复悬停不该再报变化"); + + let press = Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left)); + assert!(tracker.event(&press, true)); + assert_eq!(tracker.state(), Interaction::Pressed); + + let release = Event::Mouse(iced::mouse::Event::ButtonReleased( + iced::mouse::Button::Left, + )); + assert!(tracker.event(&release, true)); + assert_eq!(tracker.state(), Interaction::Hovered); + + assert!(tracker.set_focused(true)); + assert_eq!(tracker.state(), Interaction::Focused, "焦点态优先于悬停"); + + let left = Event::Mouse(iced::mouse::Event::CursorLeft); + tracker.event(&left, false); + assert_eq!(tracker.state(), Interaction::Focused, "鼠标离开不影响焦点"); + } + + #[test] + fn disabled_tracker_swallows_everything() { + let mut tracker = Tracker::new(true); + assert!(tracker.set_enabled(false)); + assert_eq!(tracker.state(), Interaction::Disabled); + + let press = Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left)); + assert!(!tracker.event(&press, true), "禁用态不响应事件"); + assert_eq!(tracker.state(), Interaction::Disabled); + assert!(!tracker.set_focused(true), "禁用态拿不到焦点"); + } +}