- 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 文档:是**时长减半**(更快),不是半速。
187 lines
6.0 KiB
Rust
187 lines
6.0 KiB
Rust
//! 缩放层(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)
|
||
}
|
||
}
|