feat(motion2): 右栗折叠过渡 + 状态层 hover/pressed 过渡

A2 侧边栏(右栗 AI 栏)折叠/展开:
- 帧泵第三条轨道(pump/rail):展开 medium1 250ms + Emphasized、收起 short4 200ms +
  EmphasizedAccelerate;拆出档走 snap(不做过渡);MotionScale::NONE 下不插轨道
- 宽度在 metrics::rail_width 逐帧插值;面板内容按目标宽排版 + 按动画宽裁切
  (不重排 / 不溢出),宽度回到 48 时换成图标条(图标贴右端,位置不跳)
- pages/agent/mod.rs 的展开档面板纯搬移到 panel.rs(371 → 289 行)

A8 状态层过渡:
- interaction/layer:令牌化的进度时钟(short1 50ms),NONE 当帧落终值
- interaction/hover:取样控件把起跑搬到 Widget::update(早于订阅表求值)
- 按钮五式 + IconButton + 导航项药丸统一走 interaction::layer_color
  (没有取样控件时逐位退回旧口径,出图基线不变)
- components/motion/mod.rs 的 needs_frames / advance 各加一句委托,
  帧订阅仍然只有一处判断点(app/subscription.rs 未改)
This commit is contained in:
AskaEth committed 2026-11-12 20:05:58 +08:00
1 parent c4706c759c
commit b61f342431
22 files changed
+1837 -145

No files matched your search

@@ -10,17 +10,20 @@
//! |---|---|---|
//! | 页面 | 内容标识变了([`Pump::observe`]) | 两腿里长的那条走完 |
//! | 模态 | 弹窗开 / 关([`Pump::observe_modal`] / [`Pump::begin_modal_close`]) | 进场转 [`Modal::Open`](**不再要帧**)、退场转 [`Modal::Hidden`] |
//! | 折叠(M257) | 右栗档位变([`Pump::observe_rail`],拆出档走 [`Pump::snap_rail`]) | 主进度走完一条腿(`medium1` / `short4`) |
//!
//! D31:时长/缓动是**表**([`super::transition`]),页面只许按表取——它只能说「我要换页了」。
use unlucky_design::motion::{DurationToken, MotionScale};
mod modal;
mod rail;
use super::transition::{
enter_offset, exit_offset, modal_spec, page_spec, Kind, PageFrame, PageSpec,
};
pub(crate) use modal::Modal;
pub(crate) use rail::RailTrack;
/// 逐帧**推进时间轴**的最短间隔(16 ms ≈ 60 次/秒)。
///
@@ -61,6 +64,10 @@ pub struct Pump {
pending_ms: f64,
page: Option<PageTrack>,
modal: Modal,
/// 右栗折叠轨道(M257):`Some` = 宽度正在 `48 ⇄ 展开档` 之间走。
rail: Option<RailTrack>,
/// 右栗的目标档(`true` = 展开)。没有轨道时进度就是它的终值。
rail_open: bool,
/// 退场走完了、等调用方真正关掉那个框([`Pump::take_modal_closed`] 取走一次)。
close_pending: bool,
screen: Option<u64>,
@@ -81,6 +88,8 @@ impl Pump {
pending_ms: 0.0,
page: None,
modal: Modal::Hidden,
rail: None,
rail_open: false,
close_pending: false,
screen: None,
rank: None,
@@ -104,6 +113,9 @@ impl Pump {
self.detail = None;
self.pending_ms = 0.0;
self.modal = Modal::Hidden;
// 定档时右栗按出厂档(收起)复位:定档之后的第一帧画的就是 48,不会先画一次展开态。
self.rail = None;
self.rail_open = false;
self.close_pending = false;
if scale.is_off() {
// 关掉动效 = 直接落终态:把在跑的轨道清掉,而不是让它跑一遍 0 时长。
@@ -129,7 +141,9 @@ impl Pump {
/// 现在有没有动画在跑(= 要不要订阅帧)。
pub fn is_animating(&self) -> bool {
self.page.is_some() || matches!(self.modal, Modal::Opening { .. } | Modal::Closing { .. })
self.page.is_some()
|| self.rail.is_some()
|| matches!(self.modal, Modal::Opening { .. } | Modal::Closing { .. })
}
/// 轨道起点的口径:钉住相位时恒为 0(进度只取决于钉住值,与"谁在第几帧起跑"无关)。
@@ -286,6 +300,11 @@ impl Pump {
self.page = None;
}
}
if let Some(track) = self.rail {
if now - track.start_ms >= track.duration_ms {
self.rail = None;
}
}
let spec = modal_spec();
match self.modal {
Modal::Opening { start_ms } if now - start_ms >= self.leg_ms(spec.enter.duration) => {