feat(motion): M305 —— 列表增删(错位淡入/退场后才移除)+ 进度插值(到点一次收束)
- 新轨道两条:components/motion/pump/{rows,progress}.rs(纯算术,连拍 advance(dt) 可判)
· rows:两条清单(画出来的顺序 / 真值)+ 逐行可见度;退场行钉在原位、走完才移除
· progress:真实读数一跳,画出来的值沿 short4 追上去;到 100% 一次脉冲(0→1→0,不循环)
- 接线:pages/download/model.rs::from_snapshot(scope 内唯一 update 侧入口)
· 行清单 observe → 差分;退场行的数据进 leaving 池(否则退场行画不出来)
· 进度 observe(key = 版本 id)
- 视图:rows/list.rs 换成 keyed::Column(按 id 做连续匹配)+ 逐行 revealed(裁切框收放)
+ 同色遮罩淡出;install.rs 画插值后的值 + 收束
- MotionScale::NONE:不插轨道、不留幽灵,值严格等于最终值、零采样
- 顺手:rows/tests.rs 重新挂上 mod(此前一直没编译),i18n 扫描补两个新文件
判据:motion::tests::{rows,progress}、rows/motion_tests(Tree::diff 后未变行状态盒不变)、
出图 DevDocs/artifacts/m305(probe.py 量行距与墨色)
This commit is contained in:
1 parent
b9536c5833
commit
ae33674293
24 files changed
+2175
-153
No files matched your search
@@ -0,0 +1,244 @@
|
||||
//! **纵向裁切容器**(M305):占的格子高可以逐帧变,**内容排版高不变**。
|
||||
//!
|
||||
//! ## 为什么需要它(与 `clip.rs` 是同一课,换了一个轴)
|
||||
//!
|
||||
//! 列表增删要「高度也一起收/放」:不收,退场行删掉的那一帧下面所有行整体上移一格 = **整表瞬间
|
||||
//! 重排闪跳**(任务里点名禁止);收,就必须让一行的高从 44 走到 0。而直接把 44 喂给行内容会让
|
||||
//! 它按「半行高」重新排版(文字折行 / 尾部按钮挤掉),那 150ms 里行内会「沸腾」——`clip.rs`
|
||||
//! 的模块文档记的正是这件事(M257 的取证图里空态文案在过渡中折成两行)。
|
||||
//!
|
||||
//! 所以两件事拆开:
|
||||
//!
|
||||
//! | 谁 | 用多大的高 |
|
||||
//! |---|---|
|
||||
//! | 子内容的**排版** | `content_height`(整段过渡里不变 ⇒ 不重排) |
|
||||
//! | 自己占的**格子**(= 裁切框) | `visible_height`(逐帧变,由行的可见度给) |
|
||||
//!
|
||||
//! ## 绘制
|
||||
//!
|
||||
//! 走 `Renderer::with_layer(bounds)`(tiny-skia 是 `push_clip`,wgpu 是同族的层栈),超出的部分
|
||||
//! **画不出来**——「收起到 0」在像素上就是「一格都不占」,不需要半透明(D27 的单一口径)。
|
||||
//! 淡出是另一件事,在 `motion/fade.rs`(色板预合成)。
|
||||
//!
|
||||
//! ## 与 `clip.rs` 的区别(为什么不再写一个通用件)
|
||||
//!
|
||||
//! | | `clip.rs` | 本文件 |
|
||||
//! |---|---|---|
|
||||
//! | 轴 | 横向(宽度逐帧变) | 纵向(高度逐帧变) |
|
||||
//! | 内容锚点 | **右**端对齐(右栗贴窗口右缘) | **上**端对齐 + 可给一个纵向偏移 |
|
||||
//! | 子内容排版宽 | 显式 `content_width` | 吃父级给的最大宽(行本来就是整宽) |
|
||||
//!
|
||||
//! 两处的布局约束方向不同(一个是宽、一个是高),合成一个带 `enum Axis` 的控件会让两个调用点
|
||||
//! 都得读对方的注释才知道自己在传什么——分开写反而更短。
|
||||
//!
|
||||
//! ## 已知局限(与 `clip.rs` 同一条,登记在报告里)
|
||||
//!
|
||||
//! **命中测试用排版矩形**:过渡期间一行的可点区域是它的**排版**高(内容高),不是裁切后的可视高。
|
||||
//! 只在 150~200ms 内成立;末帧 `visible_height == content_height` 且偏移为 0 时两者重合,
|
||||
//! 逐像素与静息相同。
|
||||
|
||||
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, Point, Rectangle, Size, Vector};
|
||||
|
||||
/// 把 `content` 按 `content_height` 排版、按 `visible_height` 裁切显示(内容**上端对齐**)。
|
||||
///
|
||||
/// `offset_y` 是内容在裁切框里的纵向位移(正 = 向下):入场从下方浮上来时给正值,
|
||||
/// 退场上卷时给负值——两端都是 `0`(末帧与静息逐像素相同)。
|
||||
pub fn revealed<'a, Message, Theme, Renderer>(
|
||||
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
visible_height: f32,
|
||||
content_height: f32,
|
||||
offset_y: f32,
|
||||
veil: Option<iced::Color>,
|
||||
) -> Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: 'a,
|
||||
Renderer: iced::advanced::Renderer + 'a,
|
||||
{
|
||||
Element::new(Revealed {
|
||||
content: content.into(),
|
||||
visible_height,
|
||||
content_height,
|
||||
offset_y,
|
||||
veil,
|
||||
})
|
||||
}
|
||||
|
||||
struct Revealed<'a, Message, Theme, Renderer> {
|
||||
content: Element<'a, Message, Theme, Renderer>,
|
||||
/// 这一帧占的格子高(= 裁切框高)。
|
||||
visible_height: f32,
|
||||
/// 内容排版高(整段过渡里不变)。
|
||||
content_height: f32,
|
||||
/// 内容在裁切框里的纵向位移。
|
||||
offset_y: f32,
|
||||
/// 收尾遮罩(与底色同色;`None` = 静息,不画)。见模块文档「淡出」。
|
||||
veil: Option<iced::Color>,
|
||||
}
|
||||
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Revealed<'_, 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 {
|
||||
// 排版高**只**认 `content_height`:父级给的 max 高不参与(那正是「半行高重排」的来源)。
|
||||
let child_limits = layout::Limits::new(
|
||||
Size::ZERO,
|
||||
Size::new(limits.max().width, self.content_height),
|
||||
);
|
||||
let node = self
|
||||
.content
|
||||
.as_widget_mut()
|
||||
.layout(tree, renderer, &child_limits);
|
||||
let width = if limits.max().width.is_finite() {
|
||||
limits.max().width
|
||||
} else {
|
||||
node.size().width
|
||||
};
|
||||
layout::Node::with_children(
|
||||
Size::new(width, self.visible_height.max(0.0)),
|
||||
vec![node.move_to(Point::new(0.0, self.offset_y))],
|
||||
)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let Some(child) = layout.children().next() else {
|
||||
return;
|
||||
};
|
||||
let bounds = layout.bounds();
|
||||
let content = self.content.as_widget();
|
||||
// `with_layer` 在两个后端都裁到 `bounds`(见模块文档)。
|
||||
renderer.with_layer(bounds, |renderer| {
|
||||
content.draw(tree, renderer, theme, style, child, cursor, viewport);
|
||||
});
|
||||
if let Some(veil) = self.veil.filter(|veil| veil.a > 0.0) {
|
||||
// **必须自成一层**:tiny-skia 的绘制按「四边形 → 三角 → 图 → 文字」分批(`lib.rs` 的
|
||||
// `present`),同一层里文字永远排在四边形之后 ⇒ 直接把遮罩塞进当前层的话,
|
||||
// 它只盖得住底色,一个字都盖不住(M305 实测:红色 α=0.3 的遮罩,文字像素一像素没变)。
|
||||
// 换成子层之后,子层的批排在父层文字**之后** ⇒ 真的盖住行内容。
|
||||
renderer.with_layer(bounds, |renderer| {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
iced::Background::Color(veil),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn iced::advanced::widget::Operation,
|
||||
) {
|
||||
let Some(child) = layout.children().next() else {
|
||||
return;
|
||||
};
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.operate(tree, child, 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,
|
||||
) {
|
||||
let Some(child) = layout.children().next() else {
|
||||
return;
|
||||
};
|
||||
// 收起到底(一格都不占)时不再收事件:退场行不该抢到一个已经点不到的按钮。
|
||||
if self.visible_height < f32::EPSILON {
|
||||
return;
|
||||
}
|
||||
self.content.as_widget_mut().update(
|
||||
tree, event, child, cursor, renderer, clipboard, shell, viewport,
|
||||
);
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
let Some(child) = layout.children().next() else {
|
||||
return mouse::Interaction::None;
|
||||
};
|
||||
if self.visible_height < f32::EPSILON {
|
||||
return mouse::Interaction::None;
|
||||
}
|
||||
self.content
|
||||
.as_widget()
|
||||
.mouse_interaction(tree, child, 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>> {
|
||||
let child = layout.children().next()?;
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.overlay(tree, child, renderer, viewport, translation)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user