refactor(app-iced): M75 r4 —— interaction 拆成 mod.rs(287) + tracker.rs(159),恢复被压掉的文档

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。
This commit is contained in:
AskaEth committed 2026-11-02 04:51:01 +08:00
1 parent ab5f5802a8
commit a878fc6a58
3 files changed
+180 -137

No files matched your search

@@ -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)
@@ -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), "禁用态拿不到焦点");
}
}
@@ -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), "禁用态拿不到焦点");
}
}