feat(app-iced): 页面切换按导航深度定向 + 模态缩放淡入(M256 MOTION1)
- transition.rs:唯一一张动效表。同级换页走水平(±12 / 旧页 ∓8)、进深层走纵向
(新页 +16 上浮 / 旧页 -8),进腿 medium2/medium1(300/250),退腿 short4→medium1(更短),
进用 Standard、退用 StandardAccelerate。幅度全部取自 Space 令牌。
- pump.rs:Slot 三互斥槽 → Kind(Boot/Peer{forward}/Push/Pop)+ 两腿轨道;新增模态状态机
(Opening/Open/Closing,展开后不占帧,退场走完交回一次 take_modal_closed)。
- components/motion/zoom.rs:绕中心缩放的自绘控件(renderer.with_transformation,
tiny-skia 与 wgpu 都实现了,文字也真的跟着缩放);scale==1.0 时不套变换 ⇒ 出图基线不变。
- pages/mod.rs:page_with_settings_transition(ui, current, previous, …) 叠
stack![opaque(旧页@退场位移), 新页@进场位移];旧函数只剩一句委托(blast radius 不变)。
- App.previous_view:换页时用 std::mem::replace **移动**旧模型(冻结快照),过渡结束当拍
置 None(不随换页次数增长)。
- 模态:逻辑状态当拍关(state.dialogs 立刻空),画面多留 150ms;退场期间弹窗消息被闸门吞掉。
- platform/motion.rs:系统「减少动效」三态探针(Windows=SystemParametersInfoW,
Linux/macOS=子进程+300ms 超时),OnceLock 缓存;Unknown **不降级**(探针失败不静默关动效)。
- MotionScale::HALF 语义核清并写进 design 文档:是**时长减半**(更快),不是半速。
This commit is contained in:
1 parent
5a182e80de
commit
b1c09c4c91
21 files changed
+2168
-658
No files matched your search
@@ -0,0 +1,186 @@
|
||||
//! 缩放层(M256):把一棵子树整体缩放(模态卡片进出场用)。
|
||||
//!
|
||||
//! ## 为什么自己写一个
|
||||
//!
|
||||
//! iced 0.14 的控件清单里**没有**通用的 transform / opacity 控件(`iced_widget` 只有 `stack` /
|
||||
//! `opaque` / `float` / `pin`)。但渲染器层是有的:`Renderer::with_transformation` 在**两个后端
|
||||
//! 都实现了**——`iced_tiny_skia-0.14.1/src/lib.rs:211-217` 与 `iced_wgpu-0.14.0/src/lib.rs:682-688`
|
||||
//! 各是一行 `layers.push_transformation(...)`。而且 tiny_skia 对文字也乘了 `transformation`:
|
||||
//! `layer.rs:73-96` 把字号、行高、bounds 都按 `scale_factor()` 缩过 ⇒ **文字是真缩放**,
|
||||
//! 不是"只缩背景、字不动"。
|
||||
//!
|
||||
//! ## 静息态**逐像素不变**
|
||||
//!
|
||||
//! `scale == 1.0` 时直接 `content.draw(..)`,**不套**变换:出图基线(D30)不受这层影响,
|
||||
//! 也不给渲染器多压一层矩阵。
|
||||
//!
|
||||
//! ## 已知局限(登记在报告里)
|
||||
//!
|
||||
//! 命中测试用的是**布局矩形**(未缩放),所以缩放途中的点击落点仍按 1.0 的框算。
|
||||
//! 模态只在开/关那 150~200 ms 里缩放,而那两段本来就不该被点击(`app/update/dialog.rs`
|
||||
//! 在退场期间直接吞掉弹窗消息),所以这件事没有可观察的后果。
|
||||
|
||||
use iced::advanced::layout::{self, Layout};
|
||||
use iced::advanced::renderer;
|
||||
use iced::advanced::widget::{tree, Tree};
|
||||
use iced::advanced::{mouse, Clipboard, Shell, Widget};
|
||||
use iced::{Element, Event, Length, Rectangle, Size, Transformation, Vector};
|
||||
|
||||
/// 把 `content` 缩放 `scale` 倍(**绕自身中心**),布局尺寸不变。
|
||||
pub fn scaled<'a, Message, Theme, Renderer>(
|
||||
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
scale: f32,
|
||||
) -> Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: 'a,
|
||||
Renderer: iced::advanced::Renderer + 'a,
|
||||
{
|
||||
Element::new(Scaled {
|
||||
content: content.into(),
|
||||
scale,
|
||||
})
|
||||
}
|
||||
|
||||
/// 绕**中心**缩放的变换:`T(-c) · S · T(c)`(`Mul` 的乘法序 = 先右后左,见 iced_core 的
|
||||
/// `impl Mul for Transformation`)。
|
||||
fn about_center(bounds: Rectangle, scale: f32) -> Transformation {
|
||||
let (cx, cy) = (
|
||||
bounds.x + bounds.width / 2.0,
|
||||
bounds.y + bounds.height / 2.0,
|
||||
);
|
||||
Transformation::translate(-cx, -cy)
|
||||
* Transformation::scale(scale)
|
||||
* Transformation::translate(cx, cy)
|
||||
}
|
||||
|
||||
struct Scaled<'a, Message, Theme, Renderer> {
|
||||
content: Element<'a, Message, Theme, Renderer>,
|
||||
scale: f32,
|
||||
}
|
||||
|
||||
impl<Message, Theme, Renderer> Scaled<'_, Message, Theme, Renderer> {
|
||||
/// 这条变换要不要真的套(`1.0` 时什么都不做)。
|
||||
fn transformation(&self, bounds: Rectangle) -> Option<Transformation> {
|
||||
// 0.1% 以下视为到位:亚像素缩放只会让文字发虚,没有观感收益。
|
||||
if (self.scale - 1.0).abs() < 1.0e-3 {
|
||||
return None;
|
||||
}
|
||||
Some(about_center(bounds, self.scale.max(0.01)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Scaled<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: iced::advanced::Renderer,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
self.content.as_widget().tag()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
self.content.as_widget().state()
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
self.content.as_widget().children()
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
self.content.as_widget().diff(tree);
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<Length> {
|
||||
self.content.as_widget().size()
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> Size<Length> {
|
||||
self.content.as_widget().size_hint()
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
self.content.as_widget_mut().layout(tree, renderer, limits)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
// 先算变换(借 layout 的 bounds),再借用 content —— 两个借用互不重叠。
|
||||
let transformation = self.transformation(layout.bounds());
|
||||
let content = self.content.as_widget();
|
||||
|
||||
match transformation {
|
||||
None => content.draw(tree, renderer, theme, style, layout, cursor, viewport),
|
||||
Some(transformation) => renderer.with_transformation(transformation, |renderer| {
|
||||
content.draw(tree, renderer, theme, style, layout, cursor, viewport);
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn iced::advanced::widget::Operation,
|
||||
) {
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.operate(tree, layout, renderer, operation);
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.content.as_widget_mut().update(
|
||||
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
|
||||
);
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.content
|
||||
.as_widget()
|
||||
.mouse_interaction(tree, layout, cursor, viewport, renderer)
|
||||
}
|
||||
|
||||
fn overlay<'a>(
|
||||
&'a mut self,
|
||||
tree: &'a mut Tree,
|
||||
layout: Layout<'a>,
|
||||
renderer: &Renderer,
|
||||
viewport: &Rectangle,
|
||||
translation: Vector,
|
||||
) -> Option<iced::overlay::Element<'a, Message, Theme, Renderer>> {
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.overlay(tree, layout, renderer, viewport, translation)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user