1076 lines
33 KiB
Rust
1076 lines
33 KiB
Rust
use {
|
|
crate::{
|
|
cmm::{cmm_description::ColorDescription, cmm_render_intent::RenderIntent},
|
|
gfx_api::{GfxTexture, SampleRect},
|
|
ifs::wl_surface::{SurfaceBuffer, WlSurface},
|
|
rect::Rect,
|
|
state::State,
|
|
theme::Color,
|
|
tree::{LatchListener, NodeId, OutputNode},
|
|
utils::{clonecell::CloneCell, event_listener::EventListener},
|
|
},
|
|
ahash::AHashMap,
|
|
std::{
|
|
cell::{Cell, RefCell},
|
|
collections::VecDeque,
|
|
rc::{Rc, Weak},
|
|
},
|
|
};
|
|
|
|
pub mod multiphase;
|
|
|
|
pub use jay_layout_animation::{AnimationCurve, AnimationStyle};
|
|
|
|
const DEFAULT_DURATION_MS: u32 = 160;
|
|
|
|
pub struct AnimationState {
|
|
pub enabled: Cell<bool>,
|
|
pub duration_ms: Cell<u32>,
|
|
pub curve: Cell<AnimationCurve>,
|
|
pub style: Cell<AnimationStyle>,
|
|
windows: RefCell<AHashMap<NodeId, WindowAnimation>>,
|
|
phased: RefCell<AHashMap<NodeId, PhasedWindowAnimation>>,
|
|
exits: RefCell<Vec<ExitAnimation>>,
|
|
tick: CloneCell<Option<Rc<AnimationTick>>>,
|
|
}
|
|
|
|
pub struct RetainedToplevel {
|
|
pub offset: (i32, i32),
|
|
pub surface: RetainedSurface,
|
|
}
|
|
|
|
pub struct RetainedSurface {
|
|
pub offset: (i32, i32),
|
|
pub size: (i32, i32),
|
|
pub content: RetainedContent,
|
|
pub below: Vec<RetainedSurface>,
|
|
pub above: Vec<RetainedSurface>,
|
|
}
|
|
|
|
pub enum RetainedContent {
|
|
Texture {
|
|
texture: Rc<dyn GfxTexture>,
|
|
buffer: Rc<SurfaceBuffer>,
|
|
source: SampleRect,
|
|
alpha: Option<f32>,
|
|
color_description: Rc<ColorDescription>,
|
|
render_intent: RenderIntent,
|
|
alpha_mode: crate::gfx_api::AlphaMode,
|
|
opaque: bool,
|
|
},
|
|
Color {
|
|
color: Color,
|
|
alpha: Option<f32>,
|
|
color_description: Rc<ColorDescription>,
|
|
render_intent: RenderIntent,
|
|
},
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
|
pub enum RetainedExitLayer {
|
|
Tiled,
|
|
Floating,
|
|
}
|
|
|
|
pub struct RetainedExitFrame {
|
|
pub rect: Rect,
|
|
pub retained: Rc<RetainedToplevel>,
|
|
pub frame_inset: i32,
|
|
pub source_body_size: (i32, i32),
|
|
pub active: bool,
|
|
pub layer: RetainedExitLayer,
|
|
}
|
|
|
|
impl RetainedToplevel {
|
|
pub fn capture_surface(surface: &WlSurface, offset: (i32, i32)) -> Option<Rc<Self>> {
|
|
Some(Rc::new(Self {
|
|
offset,
|
|
surface: RetainedSurface::capture(surface, (0, 0))?,
|
|
}))
|
|
}
|
|
}
|
|
|
|
impl RetainedSurface {
|
|
fn capture(surface: &WlSurface, offset: (i32, i32)) -> Option<Self> {
|
|
let buffer = surface.buffer.get()?;
|
|
buffer.buffer.buf.update_texture_or_log(surface, true);
|
|
let size = surface.buffer_abs_pos.get().size();
|
|
let source = *surface.buffer_points_norm.borrow();
|
|
let color_description = surface.color_description();
|
|
let render_intent = surface.render_intent();
|
|
let alpha_mode = surface.alpha_mode();
|
|
let alpha = surface.alpha();
|
|
let content = match buffer.buffer.buf.get_texture(surface) {
|
|
Some(texture) => RetainedContent::Texture {
|
|
opaque: surface.opaque(),
|
|
texture,
|
|
buffer,
|
|
source,
|
|
alpha,
|
|
color_description,
|
|
render_intent,
|
|
alpha_mode,
|
|
},
|
|
None => {
|
|
let color = buffer.buffer.buf.color?;
|
|
RetainedContent::Color {
|
|
color: Color::from_u32(
|
|
color_description.eotf,
|
|
alpha_mode,
|
|
color[0],
|
|
color[1],
|
|
color[2],
|
|
color[3],
|
|
),
|
|
alpha,
|
|
color_description,
|
|
render_intent,
|
|
}
|
|
}
|
|
};
|
|
let mut below = vec![];
|
|
let mut above = vec![];
|
|
if let Some(children) = surface.children.borrow().as_deref() {
|
|
for child in children.below.iter() {
|
|
if child.pending.get() {
|
|
continue;
|
|
}
|
|
let pos = child.sub_surface.position.get();
|
|
if let Some(surface) = Self::capture(&child.sub_surface.surface, pos) {
|
|
below.push(surface);
|
|
}
|
|
}
|
|
for child in children.above.iter() {
|
|
if child.pending.get() {
|
|
continue;
|
|
}
|
|
let pos = child.sub_surface.position.get();
|
|
if let Some(surface) = Self::capture(&child.sub_surface.surface, pos) {
|
|
above.push(surface);
|
|
}
|
|
}
|
|
}
|
|
Some(Self {
|
|
offset,
|
|
size,
|
|
content,
|
|
below,
|
|
above,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Default for AnimationState {
|
|
fn default() -> Self {
|
|
Self {
|
|
enabled: Cell::new(false),
|
|
duration_ms: Cell::new(DEFAULT_DURATION_MS),
|
|
curve: Cell::new(AnimationCurve::from_config(3)),
|
|
style: Cell::new(AnimationStyle::Multiphase),
|
|
windows: Default::default(),
|
|
phased: Default::default(),
|
|
exits: Default::default(),
|
|
tick: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AnimationState {
|
|
pub fn clear(&self) {
|
|
self.windows.borrow_mut().clear();
|
|
self.phased.borrow_mut().clear();
|
|
self.exits.borrow_mut().clear();
|
|
if let Some(tick) = self.tick.take() {
|
|
tick.detach();
|
|
}
|
|
}
|
|
|
|
pub fn set_target(
|
|
&self,
|
|
node_id: NodeId,
|
|
old: Rect,
|
|
new: Rect,
|
|
_retained: Option<Rc<RetainedToplevel>>,
|
|
now_nsec: u64,
|
|
duration_ms: u32,
|
|
curve: AnimationCurve,
|
|
) -> bool {
|
|
if old == new || new.is_empty() || duration_ms == 0 {
|
|
self.windows.borrow_mut().remove(&node_id);
|
|
self.phased.borrow_mut().remove(&node_id);
|
|
return false;
|
|
}
|
|
let duration_nsec = duration_ms as u64 * 1_000_000;
|
|
let mut from = old;
|
|
{
|
|
let phased = self.phased.borrow();
|
|
if let Some(anim) = phased.get(&node_id) {
|
|
if anim.final_rect == new {
|
|
return false;
|
|
}
|
|
from = anim.rect_at(now_nsec);
|
|
}
|
|
}
|
|
{
|
|
let windows = self.windows.borrow();
|
|
if let Some(anim) = windows.get(&node_id) {
|
|
if anim.to == new {
|
|
return false;
|
|
}
|
|
from = anim.rect_at(now_nsec);
|
|
}
|
|
}
|
|
if from == new {
|
|
self.windows.borrow_mut().remove(&node_id);
|
|
self.phased.borrow_mut().remove(&node_id);
|
|
return false;
|
|
}
|
|
self.phased.borrow_mut().remove(&node_id);
|
|
self.windows.borrow_mut().insert(
|
|
node_id,
|
|
WindowAnimation {
|
|
from,
|
|
to: new,
|
|
start_nsec: now_nsec,
|
|
duration_nsec,
|
|
curve,
|
|
last_damage: from,
|
|
retained: None,
|
|
},
|
|
);
|
|
true
|
|
}
|
|
|
|
pub fn set_phased_target(
|
|
&self,
|
|
node_id: NodeId,
|
|
phases: Vec<(Rect, Rect)>,
|
|
_retained: Option<Rc<RetainedToplevel>>,
|
|
now_nsec: u64,
|
|
duration_ms: u32,
|
|
curve: AnimationCurve,
|
|
) -> bool {
|
|
if phases.is_empty() || duration_ms == 0 {
|
|
return false;
|
|
}
|
|
let Some((from, _)) = phases.first().copied() else {
|
|
return false;
|
|
};
|
|
let Some((_, final_rect)) = phases.last().copied() else {
|
|
return false;
|
|
};
|
|
if from.is_empty() || final_rect.is_empty() || from == final_rect {
|
|
return false;
|
|
}
|
|
let segments: Vec<_> = phases
|
|
.into_iter()
|
|
.map(|(from, to)| PhasedSegment { from, to })
|
|
.collect();
|
|
let mut route_edges = route_edges_from_segments(&segments);
|
|
if let Some(anim) = self.phased.borrow().get(&node_id)
|
|
&& !anim.done(now_nsec)
|
|
{
|
|
for &(from, to) in &anim.route_edges {
|
|
push_unique_route_edge(&mut route_edges, from, to);
|
|
}
|
|
}
|
|
self.windows.borrow_mut().remove(&node_id);
|
|
self.phased.borrow_mut().insert(
|
|
node_id,
|
|
PhasedWindowAnimation {
|
|
segments,
|
|
start_nsec: now_nsec,
|
|
duration_nsec: duration_ms as u64 * 1_000_000,
|
|
curve,
|
|
last_damage: from,
|
|
final_rect,
|
|
route_edges,
|
|
retained: None,
|
|
},
|
|
);
|
|
true
|
|
}
|
|
|
|
pub fn set_spawn_in(
|
|
&self,
|
|
node_id: NodeId,
|
|
target: Rect,
|
|
retained: Option<Rc<RetainedToplevel>>,
|
|
now_nsec: u64,
|
|
duration_ms: u32,
|
|
curve: AnimationCurve,
|
|
) -> bool {
|
|
let start = spawn_in_start_rect(target);
|
|
self.set_target(
|
|
node_id,
|
|
start,
|
|
target,
|
|
retained,
|
|
now_nsec,
|
|
duration_ms,
|
|
curve,
|
|
)
|
|
}
|
|
|
|
pub fn set_spawn_out(
|
|
&self,
|
|
from: Rect,
|
|
frame_inset: i32,
|
|
retained: Rc<RetainedToplevel>,
|
|
active: bool,
|
|
layer: RetainedExitLayer,
|
|
now_nsec: u64,
|
|
duration_ms: u32,
|
|
curve: AnimationCurve,
|
|
) -> bool {
|
|
if from.is_empty() || duration_ms == 0 {
|
|
return false;
|
|
}
|
|
let to = spawn_in_start_rect(from);
|
|
if to == from {
|
|
return false;
|
|
}
|
|
let source_body_size = body_size_for_frame(from, frame_inset);
|
|
if source_body_size.0 <= 0 || source_body_size.1 <= 0 {
|
|
return false;
|
|
}
|
|
self.exits.borrow_mut().push(ExitAnimation {
|
|
from,
|
|
to,
|
|
start_nsec: now_nsec,
|
|
duration_nsec: duration_ms as u64 * 1_000_000,
|
|
curve,
|
|
last_damage: from,
|
|
retained,
|
|
frame_inset,
|
|
source_body_size,
|
|
active,
|
|
layer,
|
|
});
|
|
true
|
|
}
|
|
|
|
pub fn visual_rect(&self, node_id: NodeId, layout: Rect, now_nsec: u64) -> Rect {
|
|
let phased = self.phased.borrow();
|
|
if let Some(anim) = phased.get(&node_id)
|
|
&& !anim.done(now_nsec)
|
|
{
|
|
return anim.rect_at(now_nsec);
|
|
}
|
|
drop(phased);
|
|
let windows = self.windows.borrow();
|
|
match windows.get(&node_id) {
|
|
Some(anim) if !anim.done(now_nsec) => anim.rect_at(now_nsec),
|
|
_ => layout,
|
|
}
|
|
}
|
|
|
|
pub fn retained_snapshot(
|
|
&self,
|
|
node_id: NodeId,
|
|
now_nsec: u64,
|
|
) -> Option<Rc<RetainedToplevel>> {
|
|
let phased = self.phased.borrow();
|
|
if let Some(anim) = phased.get(&node_id)
|
|
&& !anim.done(now_nsec)
|
|
{
|
|
return anim.retained.clone();
|
|
}
|
|
drop(phased);
|
|
let windows = self.windows.borrow();
|
|
match windows.get(&node_id) {
|
|
Some(anim) if !anim.done(now_nsec) => anim.retained.clone(),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn phased_route_to(
|
|
&self,
|
|
node_id: NodeId,
|
|
target: Rect,
|
|
now_nsec: u64,
|
|
) -> Option<Vec<(Rect, Rect)>> {
|
|
let phased = self.phased.borrow();
|
|
let anim = phased.get(&node_id)?;
|
|
if anim.done(now_nsec) {
|
|
return None;
|
|
}
|
|
anim.route_to(target, now_nsec)
|
|
}
|
|
|
|
pub fn exit_frames(&self, now_nsec: u64) -> Vec<RetainedExitFrame> {
|
|
self.exits
|
|
.borrow()
|
|
.iter()
|
|
.filter(|exit| !exit.done(now_nsec))
|
|
.map(|exit| RetainedExitFrame {
|
|
rect: exit.rect_at(now_nsec),
|
|
retained: exit.retained.clone(),
|
|
frame_inset: exit.frame_inset,
|
|
source_body_size: exit.source_body_size,
|
|
active: exit.active,
|
|
layer: exit.layer,
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn damage_active(&self, state: &State, now_nsec: u64) -> bool {
|
|
let mut damages = vec![];
|
|
let mut any_active = false;
|
|
{
|
|
let mut windows = self.windows.borrow_mut();
|
|
windows.retain(|_, anim| {
|
|
let current = anim.rect_at(now_nsec);
|
|
let damage = anim.last_damage.union(current).union(anim.to);
|
|
damages.push(expand_damage_rect(
|
|
damage,
|
|
state.theme.sizes.border_width.get().max(0),
|
|
));
|
|
anim.last_damage = current;
|
|
let active = !anim.done(now_nsec);
|
|
any_active |= active;
|
|
active
|
|
});
|
|
self.phased.borrow_mut().retain(|_, anim| {
|
|
let current = anim.rect_at(now_nsec);
|
|
let damage = anim.last_damage.union(current).union(anim.final_rect);
|
|
damages.push(expand_damage_rect(
|
|
damage,
|
|
state.theme.sizes.border_width.get().max(0),
|
|
));
|
|
anim.last_damage = current;
|
|
let active = !anim.done(now_nsec);
|
|
any_active |= active;
|
|
active
|
|
});
|
|
self.exits.borrow_mut().retain_mut(|exit| {
|
|
let current = exit.rect_at(now_nsec);
|
|
let damage = exit.last_damage.union(current).union(exit.to);
|
|
damages.push(expand_damage_rect(
|
|
damage,
|
|
state.theme.sizes.border_width.get().max(0),
|
|
));
|
|
exit.last_damage = current;
|
|
let active = !exit.done(now_nsec);
|
|
any_active |= active;
|
|
active
|
|
});
|
|
}
|
|
for damage in damages {
|
|
state.damage(damage);
|
|
}
|
|
any_active
|
|
}
|
|
|
|
pub(crate) fn tick_is_active(&self) -> bool {
|
|
self.tick.is_some()
|
|
}
|
|
|
|
pub(crate) fn set_tick(&self, tick: Rc<AnimationTick>) {
|
|
self.tick.set(Some(tick));
|
|
}
|
|
|
|
pub(crate) fn clear_tick(&self) {
|
|
self.tick.take();
|
|
}
|
|
}
|
|
|
|
struct WindowAnimation {
|
|
from: Rect,
|
|
to: Rect,
|
|
start_nsec: u64,
|
|
duration_nsec: u64,
|
|
curve: AnimationCurve,
|
|
last_damage: Rect,
|
|
retained: Option<Rc<RetainedToplevel>>,
|
|
}
|
|
|
|
impl WindowAnimation {
|
|
fn done(&self, now_nsec: u64) -> bool {
|
|
now_nsec.saturating_sub(self.start_nsec) >= self.duration_nsec
|
|
}
|
|
|
|
fn rect_at(&self, now_nsec: u64) -> Rect {
|
|
if self.duration_nsec == 0 {
|
|
return self.to;
|
|
}
|
|
let elapsed = now_nsec.saturating_sub(self.start_nsec);
|
|
let t = (elapsed as f64 / self.duration_nsec as f64).clamp(0.0, 1.0);
|
|
let t = self.curve.sample(t);
|
|
lerp_rect(self.from, self.to, t)
|
|
}
|
|
}
|
|
|
|
struct PhasedWindowAnimation {
|
|
segments: Vec<PhasedSegment>,
|
|
start_nsec: u64,
|
|
duration_nsec: u64,
|
|
curve: AnimationCurve,
|
|
last_damage: Rect,
|
|
final_rect: Rect,
|
|
route_edges: Vec<(Rect, Rect)>,
|
|
retained: Option<Rc<RetainedToplevel>>,
|
|
}
|
|
|
|
struct PhasedSegment {
|
|
from: Rect,
|
|
to: Rect,
|
|
}
|
|
|
|
impl PhasedWindowAnimation {
|
|
fn done(&self, now_nsec: u64) -> bool {
|
|
let total_duration = self
|
|
.duration_nsec
|
|
.saturating_mul(self.segments.len() as u64);
|
|
now_nsec.saturating_sub(self.start_nsec) >= total_duration
|
|
}
|
|
|
|
fn rect_at(&self, now_nsec: u64) -> Rect {
|
|
if self.duration_nsec == 0 {
|
|
return self.final_rect;
|
|
}
|
|
let elapsed = now_nsec.saturating_sub(self.start_nsec);
|
|
let phase = (elapsed / self.duration_nsec) as usize;
|
|
let Some(segment) = self.segments.get(phase) else {
|
|
return self.final_rect;
|
|
};
|
|
let phase_elapsed = elapsed % self.duration_nsec;
|
|
let t = (phase_elapsed as f64 / self.duration_nsec as f64).clamp(0.0, 1.0);
|
|
let t = self.curve.sample(t);
|
|
lerp_rect(segment.from, segment.to, t)
|
|
}
|
|
|
|
fn phase_at(&self, now_nsec: u64) -> Option<usize> {
|
|
if self.duration_nsec == 0 || self.segments.is_empty() {
|
|
return None;
|
|
}
|
|
let elapsed = now_nsec.saturating_sub(self.start_nsec);
|
|
let phase = (elapsed / self.duration_nsec) as usize;
|
|
(phase < self.segments.len()).then_some(phase)
|
|
}
|
|
|
|
fn route_to(&self, target: Rect, now_nsec: u64) -> Option<Vec<(Rect, Rect)>> {
|
|
let phase = self.phase_at(now_nsec)?;
|
|
let current = self.rect_at(now_nsec);
|
|
if current == target {
|
|
return Some(vec![]);
|
|
}
|
|
let segment = self.segments.get(phase)?;
|
|
route_through_edges(current, target, segment.from, segment.to, &self.route_edges)
|
|
}
|
|
}
|
|
|
|
fn route_edges_from_segments(segments: &[PhasedSegment]) -> Vec<(Rect, Rect)> {
|
|
let mut edges = vec![];
|
|
for segment in segments {
|
|
push_unique_route_edge(&mut edges, segment.from, segment.to);
|
|
}
|
|
edges
|
|
}
|
|
|
|
fn push_unique_route_edge(edges: &mut Vec<(Rect, Rect)>, from: Rect, to: Rect) {
|
|
if from == to {
|
|
return;
|
|
}
|
|
if edges
|
|
.iter()
|
|
.any(|&(a, b)| (a == from && b == to) || (a == to && b == from))
|
|
{
|
|
return;
|
|
}
|
|
edges.push((from, to));
|
|
}
|
|
|
|
fn route_through_edges(
|
|
current: Rect,
|
|
target: Rect,
|
|
current_from: Rect,
|
|
current_to: Rect,
|
|
known_edges: &[(Rect, Rect)],
|
|
) -> Option<Vec<(Rect, Rect)>> {
|
|
let mut edges = known_edges.to_vec();
|
|
push_unique_route_edge(&mut edges, current, current_from);
|
|
push_unique_route_edge(&mut edges, current, current_to);
|
|
rect_graph_route(current, target, &edges)
|
|
}
|
|
|
|
fn rect_graph_route(
|
|
start: Rect,
|
|
target: Rect,
|
|
edges: &[(Rect, Rect)],
|
|
) -> Option<Vec<(Rect, Rect)>> {
|
|
let mut nodes = vec![];
|
|
let mut adjacency: Vec<Vec<usize>> = vec![];
|
|
let start_idx = rect_graph_node(&mut nodes, &mut adjacency, start);
|
|
let target_idx = rect_graph_node(&mut nodes, &mut adjacency, target);
|
|
for &(from, to) in edges {
|
|
let from_idx = rect_graph_node(&mut nodes, &mut adjacency, from);
|
|
let to_idx = rect_graph_node(&mut nodes, &mut adjacency, to);
|
|
if !adjacency[from_idx].contains(&to_idx) {
|
|
adjacency[from_idx].push(to_idx);
|
|
}
|
|
if !adjacency[to_idx].contains(&from_idx) {
|
|
adjacency[to_idx].push(from_idx);
|
|
}
|
|
}
|
|
|
|
let mut previous = vec![None; nodes.len()];
|
|
let mut queue = VecDeque::from([start_idx]);
|
|
previous[start_idx] = Some(start_idx);
|
|
while let Some(idx) = queue.pop_front() {
|
|
if idx == target_idx {
|
|
break;
|
|
}
|
|
for &next in &adjacency[idx] {
|
|
if previous[next].is_none() {
|
|
previous[next] = Some(idx);
|
|
queue.push_back(next);
|
|
}
|
|
}
|
|
}
|
|
previous[target_idx]?;
|
|
|
|
let mut reversed_nodes = vec![target_idx];
|
|
let mut idx = target_idx;
|
|
while idx != start_idx {
|
|
idx = previous[idx]?;
|
|
reversed_nodes.push(idx);
|
|
}
|
|
reversed_nodes.reverse();
|
|
|
|
let mut route = vec![];
|
|
for pair in reversed_nodes.windows(2) {
|
|
push_non_empty_segment(&mut route, nodes[pair[0]], nodes[pair[1]]);
|
|
}
|
|
Some(route)
|
|
}
|
|
|
|
fn rect_graph_node(nodes: &mut Vec<Rect>, adjacency: &mut Vec<Vec<usize>>, rect: Rect) -> usize {
|
|
if let Some(idx) = nodes.iter().position(|&node| node == rect) {
|
|
return idx;
|
|
}
|
|
let idx = nodes.len();
|
|
nodes.push(rect);
|
|
adjacency.push(vec![]);
|
|
idx
|
|
}
|
|
|
|
fn push_non_empty_segment(route: &mut Vec<(Rect, Rect)>, from: Rect, to: Rect) {
|
|
if from != to {
|
|
route.push((from, to));
|
|
}
|
|
}
|
|
|
|
struct ExitAnimation {
|
|
from: Rect,
|
|
to: Rect,
|
|
start_nsec: u64,
|
|
duration_nsec: u64,
|
|
curve: AnimationCurve,
|
|
last_damage: Rect,
|
|
retained: Rc<RetainedToplevel>,
|
|
frame_inset: i32,
|
|
source_body_size: (i32, i32),
|
|
active: bool,
|
|
layer: RetainedExitLayer,
|
|
}
|
|
|
|
impl ExitAnimation {
|
|
fn done(&self, now_nsec: u64) -> bool {
|
|
now_nsec.saturating_sub(self.start_nsec) >= self.duration_nsec
|
|
}
|
|
|
|
fn rect_at(&self, now_nsec: u64) -> Rect {
|
|
if self.duration_nsec == 0 {
|
|
return self.to;
|
|
}
|
|
let elapsed = now_nsec.saturating_sub(self.start_nsec);
|
|
let t = (elapsed as f64 / self.duration_nsec as f64).clamp(0.0, 1.0);
|
|
let t = self.curve.sample(t);
|
|
lerp_rect(self.from, self.to, t)
|
|
}
|
|
}
|
|
|
|
pub struct AnimationTick {
|
|
state: Weak<State>,
|
|
slf: Weak<dyn LatchListener>,
|
|
latch_listeners: RefCell<Vec<EventListener<dyn LatchListener>>>,
|
|
}
|
|
|
|
impl AnimationTick {
|
|
pub fn new(state: &Rc<State>, slf: &Weak<Self>) -> Self {
|
|
let slf: Weak<dyn LatchListener> = slf.clone();
|
|
Self {
|
|
state: Rc::downgrade(state),
|
|
slf,
|
|
latch_listeners: Default::default(),
|
|
}
|
|
}
|
|
|
|
pub fn attach(&self, output: &OutputNode) {
|
|
let listener = EventListener::new(self.slf.clone());
|
|
listener.attach(&output.latch_event);
|
|
self.latch_listeners.borrow_mut().push(listener);
|
|
}
|
|
|
|
pub fn detach(&self) {
|
|
for listener in self.latch_listeners.borrow_mut().drain(..) {
|
|
listener.detach();
|
|
}
|
|
}
|
|
}
|
|
|
|
impl LatchListener for AnimationTick {
|
|
fn after_latch(self: Rc<Self>, _on: &OutputNode, _tearing: bool) {
|
|
let Some(state) = self.state.upgrade() else {
|
|
self.detach();
|
|
return;
|
|
};
|
|
let active = state.animations.damage_active(&state, state.now_nsec());
|
|
if !active {
|
|
self.detach();
|
|
state.animations.clear_tick();
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn spawn_in_start_rect(target: Rect) -> Rect {
|
|
let (cx, cy) = target.center();
|
|
Rect::new_empty(cx, cy)
|
|
}
|
|
|
|
fn body_size_for_frame(rect: Rect, frame_inset: i32) -> (i32, i32) {
|
|
(
|
|
rect.width().saturating_sub(2 * frame_inset),
|
|
rect.height().saturating_sub(2 * frame_inset),
|
|
)
|
|
}
|
|
|
|
fn lerp_rect(from: Rect, to: Rect, t: f64) -> Rect {
|
|
fn lerp(from: i32, to: i32, t: f64) -> i32 {
|
|
(from as f64 + (to as f64 - from as f64) * t).round() as i32
|
|
}
|
|
Rect::new_saturating(
|
|
lerp(from.x1(), to.x1(), t),
|
|
lerp(from.y1(), to.y1(), t),
|
|
lerp(from.x2(), to.x2(), t),
|
|
lerp(from.y2(), to.y2(), t),
|
|
)
|
|
}
|
|
|
|
pub(crate) fn expand_damage_rect(rect: Rect, width: i32) -> Rect {
|
|
Rect::new_saturating(
|
|
rect.x1().saturating_sub(width),
|
|
rect.y1().saturating_sub(width),
|
|
rect.x2().saturating_add(width),
|
|
rect.y2().saturating_add(width),
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
use crate::cmm::cmm_manager::ColorManager;
|
|
|
|
fn retained_for_tests() -> Rc<RetainedToplevel> {
|
|
let color_manager = ColorManager::new();
|
|
Rc::new(RetainedToplevel {
|
|
offset: (0, 0),
|
|
surface: RetainedSurface {
|
|
offset: (0, 0),
|
|
size: (100, 100),
|
|
content: RetainedContent::Color {
|
|
color: Color::SOLID_BLACK,
|
|
alpha: None,
|
|
color_description: color_manager.srgb_gamma22().clone(),
|
|
render_intent: RenderIntent::Perceptual,
|
|
},
|
|
below: vec![],
|
|
above: vec![],
|
|
},
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn linear_rect_interpolation_is_symmetric() {
|
|
let a = Rect::new_sized_saturating(0, 0, 100, 100);
|
|
let b = Rect::new_sized_saturating(100, 40, 200, 80);
|
|
assert_eq!(lerp_rect(a, b, 0.25), lerp_rect(b, a, 0.75));
|
|
}
|
|
|
|
#[test]
|
|
fn custom_cubic_bezier_curve_is_prepared() {
|
|
let curve = AnimationCurve::from_cubic_bezier(0.0, 0.0, 1.0, 1.0).unwrap();
|
|
assert_eq!(curve.sample(0.0), 0.0);
|
|
assert_eq!(curve.sample(1.0), 1.0);
|
|
assert!((curve.sample(0.5) - 0.5).abs() < 0.001);
|
|
|
|
let ease_out = AnimationCurve::from_cubic_bezier(0.0, 0.0, 0.58, 1.0).unwrap();
|
|
let mid = ease_out.sample(0.5);
|
|
assert!(mid > 0.5);
|
|
assert!(mid < 1.0);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_custom_cubic_bezier_curve_is_rejected() {
|
|
assert!(AnimationCurve::from_cubic_bezier(-0.1, 0.0, 0.58, 1.0).is_none());
|
|
assert!(AnimationCurve::from_cubic_bezier(0.0, 0.0, 1.1, 1.0).is_none());
|
|
assert!(AnimationCurve::from_cubic_bezier(0.0, f32::NAN, 0.58, 1.0).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn spawn_out_frames_use_configured_curve_and_expire() {
|
|
let state = AnimationState::default();
|
|
let retained = retained_for_tests();
|
|
let from = Rect::new_sized_saturating(10, 20, 100, 80);
|
|
let to = spawn_in_start_rect(from);
|
|
let curve = AnimationCurve::from_config(3);
|
|
assert!(state.set_spawn_out(
|
|
from,
|
|
2,
|
|
retained.clone(),
|
|
true,
|
|
RetainedExitLayer::Floating,
|
|
0,
|
|
160,
|
|
curve
|
|
));
|
|
|
|
let start = state.exit_frames(0);
|
|
assert_eq!(start.len(), 1);
|
|
assert_eq!(start[0].rect, from);
|
|
assert_eq!(start[0].source_body_size, (96, 76));
|
|
assert!(start[0].active);
|
|
assert_eq!(start[0].layer, RetainedExitLayer::Floating);
|
|
assert!(Rc::ptr_eq(&start[0].retained, &retained));
|
|
|
|
let middle = state.exit_frames(80_000_000);
|
|
assert_eq!(middle.len(), 1);
|
|
assert_eq!(middle[0].rect, lerp_rect(from, to, curve.sample(0.5)));
|
|
assert_ne!(middle[0].rect, lerp_rect(from, to, 0.5));
|
|
assert!(state.exit_frames(160_000_000).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn normal_window_animations_do_not_retain_content() {
|
|
let state = AnimationState::default();
|
|
let id = NodeId(1);
|
|
let from = Rect::new_sized_saturating(0, 0, 100, 100);
|
|
let to = Rect::new_sized_saturating(100, 0, 100, 100);
|
|
assert!(state.set_target(
|
|
id,
|
|
from,
|
|
to,
|
|
Some(retained_for_tests()),
|
|
0,
|
|
160,
|
|
AnimationCurve::Linear
|
|
));
|
|
|
|
assert!(state.retained_snapshot(id, 80_000_000).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn phased_window_animations_do_not_retain_content() {
|
|
let state = AnimationState::default();
|
|
let id = NodeId(1);
|
|
let a = Rect::new_sized_saturating(0, 0, 100, 100);
|
|
let b = Rect::new_sized_saturating(0, 0, 100, 50);
|
|
let c = Rect::new_sized_saturating(100, 0, 100, 50);
|
|
assert!(state.set_phased_target(
|
|
id,
|
|
vec![(a, b), (b, c)],
|
|
Some(retained_for_tests()),
|
|
0,
|
|
100,
|
|
AnimationCurve::Linear
|
|
));
|
|
|
|
assert!(state.retained_snapshot(id, 50_000_000).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn phased_animation_uses_full_duration_per_phase() {
|
|
let state = AnimationState::default();
|
|
let id = NodeId(1);
|
|
let a = Rect::new_sized_saturating(0, 0, 100, 100);
|
|
let b = Rect::new_sized_saturating(0, 0, 100, 50);
|
|
let c = Rect::new_sized_saturating(100, 0, 100, 50);
|
|
assert!(state.set_phased_target(
|
|
id,
|
|
vec![(a, b), (b, c)],
|
|
None,
|
|
0,
|
|
100,
|
|
AnimationCurve::Linear
|
|
));
|
|
assert_eq!(state.visual_rect(id, c, 0), a);
|
|
assert_eq!(state.visual_rect(id, c, 50_000_000), lerp_rect(a, b, 0.5));
|
|
assert_eq!(state.visual_rect(id, c, 100_000_000), b);
|
|
assert_eq!(state.visual_rect(id, c, 150_000_000), lerp_rect(b, c, 0.5));
|
|
assert_eq!(state.visual_rect(id, c, 200_000_000), c);
|
|
}
|
|
|
|
#[test]
|
|
fn phased_route_reverses_to_existing_endpoint() {
|
|
let state = AnimationState::default();
|
|
let id = NodeId(1);
|
|
let a = Rect::new_sized_saturating(0, 0, 100, 100);
|
|
let b = Rect::new_sized_saturating(0, 0, 100, 50);
|
|
let c = Rect::new_sized_saturating(100, 0, 100, 50);
|
|
assert!(state.set_phased_target(
|
|
id,
|
|
vec![(a, b), (b, c)],
|
|
None,
|
|
0,
|
|
100,
|
|
AnimationCurve::Linear
|
|
));
|
|
|
|
let current = lerp_rect(b, c, 0.5);
|
|
assert_eq!(
|
|
state.phased_route_to(id, a, 150_000_000).unwrap(),
|
|
vec![(current, b), (b, a)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn phased_route_continues_to_existing_endpoint() {
|
|
let state = AnimationState::default();
|
|
let id = NodeId(1);
|
|
let a = Rect::new_sized_saturating(0, 0, 100, 100);
|
|
let b = Rect::new_sized_saturating(0, 0, 100, 50);
|
|
let c = Rect::new_sized_saturating(100, 0, 100, 50);
|
|
assert!(state.set_phased_target(
|
|
id,
|
|
vec![(a, b), (b, c)],
|
|
None,
|
|
0,
|
|
100,
|
|
AnimationCurve::Linear
|
|
));
|
|
|
|
let current = lerp_rect(a, b, 0.5);
|
|
assert_eq!(
|
|
state.phased_route_to(id, c, 50_000_000).unwrap(),
|
|
vec![(current, b), (b, c)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn phased_route_remembers_original_path_after_retarget() {
|
|
let state = AnimationState::default();
|
|
let id = NodeId(1);
|
|
let a = Rect::new_sized_saturating(0, 0, 100, 100);
|
|
let b = Rect::new_sized_saturating(0, 0, 100, 50);
|
|
let c = Rect::new_sized_saturating(100, 0, 100, 50);
|
|
assert!(state.set_phased_target(
|
|
id,
|
|
vec![(a, b), (b, c)],
|
|
None,
|
|
0,
|
|
100,
|
|
AnimationCurve::Linear
|
|
));
|
|
|
|
let current = lerp_rect(b, c, 0.5);
|
|
let reverse = state.phased_route_to(id, a, 150_000_000).unwrap();
|
|
assert_eq!(reverse, vec![(current, b), (b, a)]);
|
|
assert!(state.set_phased_target(
|
|
id,
|
|
reverse,
|
|
None,
|
|
150_000_000,
|
|
100,
|
|
AnimationCurve::Linear
|
|
));
|
|
|
|
let current = lerp_rect(current, b, 0.5);
|
|
assert_eq!(
|
|
state.phased_route_to(id, c, 200_000_000).unwrap(),
|
|
vec![(current, b), (b, c)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn linear_retarget_interrupts_phased_animation_from_current_rect() {
|
|
let state = AnimationState::default();
|
|
let id = NodeId(1);
|
|
let a = Rect::new_sized_saturating(0, 0, 100, 100);
|
|
let b = Rect::new_sized_saturating(0, 0, 100, 50);
|
|
let c = Rect::new_sized_saturating(100, 0, 100, 50);
|
|
let d = Rect::new_sized_saturating(100, 100, 100, 50);
|
|
assert!(state.set_phased_target(
|
|
id,
|
|
vec![(a, b), (b, c)],
|
|
None,
|
|
0,
|
|
100,
|
|
AnimationCurve::Linear
|
|
));
|
|
let current = lerp_rect(a, b, 0.5);
|
|
assert!(state.set_target(id, a, d, None, 50_000_000, 100, AnimationCurve::Linear));
|
|
assert_eq!(state.visual_rect(id, d, 50_000_000), current);
|
|
assert_eq!(
|
|
state.visual_rect(id, d, 100_000_000),
|
|
lerp_rect(current, d, 0.5)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unchanged_target_does_not_restart() {
|
|
let state = AnimationState::default();
|
|
let id = NodeId(1);
|
|
let a = Rect::new_sized_saturating(0, 0, 100, 100);
|
|
let b = Rect::new_sized_saturating(100, 0, 100, 100);
|
|
assert!(state.set_target(id, a, b, None, 0, 160, AnimationCurve::Linear));
|
|
assert!(!state.set_target(id, a, b, None, 80_000_000, 160, AnimationCurve::Linear));
|
|
assert_eq!(
|
|
state.visual_rect(id, b, 80_000_000),
|
|
Rect::new_sized_saturating(50, 0, 100, 100)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn changed_target_restarts_from_current_visual_rect() {
|
|
let state = AnimationState::default();
|
|
let id = NodeId(1);
|
|
let a = Rect::new_sized_saturating(0, 0, 100, 100);
|
|
let b = Rect::new_sized_saturating(100, 0, 100, 100);
|
|
let c = Rect::new_sized_saturating(200, 0, 100, 100);
|
|
assert!(state.set_target(id, a, b, None, 0, 160, AnimationCurve::Linear));
|
|
assert!(state.set_target(id, a, c, None, 80_000_000, 160, AnimationCurve::Linear));
|
|
assert_eq!(
|
|
state.visual_rect(id, c, 80_000_000),
|
|
Rect::new_sized_saturating(50, 0, 100, 100)
|
|
);
|
|
assert_eq!(
|
|
state.visual_rect(id, c, 160_000_000),
|
|
Rect::new_sized_saturating(125, 0, 100, 100)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn spawn_in_start_rect_is_centered_and_empty() {
|
|
let target = Rect::new_sized_saturating(10, 20, 100, 50);
|
|
assert_eq!(spawn_in_start_rect(target), Rect::new_empty(60, 45));
|
|
}
|
|
|
|
#[test]
|
|
fn spawn_in_uses_configured_curve() {
|
|
let state = AnimationState::default();
|
|
let id = NodeId(1);
|
|
let target = Rect::new_sized_saturating(10, 20, 100, 50);
|
|
let curve = AnimationCurve::from_config(3);
|
|
assert!(state.set_spawn_in(id, target, None, 0, 160, curve));
|
|
assert_eq!(
|
|
state.visual_rect(id, target, 80_000_000),
|
|
lerp_rect(spawn_in_start_rect(target), target, curve.sample(0.5))
|
|
);
|
|
assert_ne!(
|
|
state.visual_rect(id, target, 80_000_000),
|
|
Rect::new_sized_saturating(35, 33, 50, 25)
|
|
);
|
|
}
|
|
}
|