mirror of
https://github.com/servo/servo.git
synced 2025-06-23 00:24:35 +01:00
This is triggered when an animation finishes. This is a high priority because it allows us to start rooting nodes with animations in the script thread. This doesn't yet cause a lot of tests to pass because they rely on the existence of `Document.getAnimations()` and the presence of `animationstart` and animationiteration` events.
262 lines
10 KiB
Rust
262 lines
10 KiB
Rust
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
|
|
//! CSS transitions and animations.
|
|
|
|
use crate::context::LayoutContext;
|
|
use crate::display_list::items::OpaqueNode;
|
|
use crate::flow::{Flow, GetBaseFlow};
|
|
use crate::opaque_node::OpaqueNodeMethods;
|
|
use fxhash::{FxHashMap, FxHashSet};
|
|
use ipc_channel::ipc::IpcSender;
|
|
use msg::constellation_msg::PipelineId;
|
|
use script_traits::UntrustedNodeAddress;
|
|
use script_traits::{
|
|
AnimationState, ConstellationControlMsg, LayoutMsg as ConstellationMsg,
|
|
TransitionOrAnimationEventType,
|
|
};
|
|
use servo_arc::Arc;
|
|
use style::animation::{update_style_for_animation, Animation, ElementAnimationState};
|
|
use style::dom::TElement;
|
|
use style::font_metrics::ServoMetricsProvider;
|
|
use style::selector_parser::RestyleDamage;
|
|
use style::timer::Timer;
|
|
|
|
/// Collect newly animating nodes, which is used by the script process during
|
|
/// forced, synchronous reflows to root DOM nodes for the duration of their
|
|
/// animations or transitions.
|
|
pub fn collect_newly_animating_nodes(
|
|
animation_states: &FxHashMap<OpaqueNode, ElementAnimationState>,
|
|
mut out: Option<&mut Vec<UntrustedNodeAddress>>,
|
|
) {
|
|
// This extends the output vector with an iterator that contains a copy of the node
|
|
// address for every new animation. This is a bit goofy, but the script thread
|
|
// currently stores a rooted node for every property that is transitioning.
|
|
if let Some(ref mut out) = out {
|
|
out.extend(animation_states.iter().flat_map(|(node, state)| {
|
|
std::iter::repeat(node.to_untrusted_node_address()).take(state.new_animations.len())
|
|
}));
|
|
}
|
|
}
|
|
|
|
/// Processes any new animations that were discovered after style recalculation. Also
|
|
/// finish any animations that have completed, inserting them into `finished_animations`.
|
|
pub fn update_animation_states(
|
|
constellation_chan: &IpcSender<ConstellationMsg>,
|
|
script_chan: &IpcSender<ConstellationControlMsg>,
|
|
animation_states: &mut FxHashMap<OpaqueNode, ElementAnimationState>,
|
|
invalid_nodes: FxHashSet<OpaqueNode>,
|
|
pipeline_id: PipelineId,
|
|
timer: &Timer,
|
|
) {
|
|
let had_running_animations = animation_states
|
|
.values()
|
|
.any(|state| !state.running_animations.is_empty());
|
|
|
|
// Cancel all animations on any invalid nodes. These entries will later
|
|
// be removed from the list of states, because their states will become
|
|
// empty.
|
|
for node in &invalid_nodes {
|
|
if let Some(mut state) = animation_states.remove(node) {
|
|
state.cancel_all_animations();
|
|
}
|
|
}
|
|
|
|
let now = timer.seconds();
|
|
let mut have_running_animations = false;
|
|
for (node, animation_state) in animation_states.iter_mut() {
|
|
update_animation_state(script_chan, animation_state, pipeline_id, now, *node);
|
|
have_running_animations =
|
|
have_running_animations || !animation_state.running_animations.is_empty();
|
|
}
|
|
|
|
// Remove empty states from our collection of states in order to free
|
|
// up space as soon as we are no longer tracking any animations for
|
|
// a node.
|
|
animation_states.retain(|_, state| !state.is_empty());
|
|
|
|
let present = match (had_running_animations, have_running_animations) {
|
|
(true, false) => AnimationState::NoAnimationsPresent,
|
|
(false, true) => AnimationState::AnimationsPresent,
|
|
_ => return,
|
|
};
|
|
constellation_chan
|
|
.send(ConstellationMsg::ChangeRunningAnimationsState(
|
|
pipeline_id,
|
|
present,
|
|
))
|
|
.unwrap();
|
|
}
|
|
|
|
pub fn update_animation_state(
|
|
script_channel: &IpcSender<ConstellationControlMsg>,
|
|
animation_state: &mut ElementAnimationState,
|
|
pipeline_id: PipelineId,
|
|
now: f64,
|
|
node: OpaqueNode,
|
|
) {
|
|
let send_event = |animation: &Animation, event_type, elapsed_time| {
|
|
let property_or_animation_name = match *animation {
|
|
Animation::Transition(_, _, ref property_animation) => {
|
|
property_animation.property_name().into()
|
|
},
|
|
Animation::Keyframes(_, _, ref name, _) => name.to_string(),
|
|
};
|
|
|
|
script_channel
|
|
.send(ConstellationControlMsg::TransitionOrAnimationEvent {
|
|
pipeline_id,
|
|
event_type,
|
|
node: node.to_untrusted_node_address(),
|
|
property_or_animation_name,
|
|
elapsed_time,
|
|
})
|
|
.unwrap()
|
|
};
|
|
|
|
handle_cancelled_animations(animation_state, now, send_event);
|
|
handle_running_animations(animation_state, now, send_event);
|
|
handle_new_animations(animation_state, send_event);
|
|
}
|
|
|
|
/// Walk through the list of running animations and remove all of the ones that
|
|
/// have ended.
|
|
pub fn handle_running_animations(
|
|
animation_state: &mut ElementAnimationState,
|
|
now: f64,
|
|
mut send_event: impl FnMut(&Animation, TransitionOrAnimationEventType, f64),
|
|
) {
|
|
let mut running_animations =
|
|
std::mem::replace(&mut animation_state.running_animations, Vec::new());
|
|
for mut running_animation in running_animations.drain(..) {
|
|
let still_running = match running_animation {
|
|
Animation::Transition(_, started_at, ref property_animation) => {
|
|
now < started_at + (property_animation.duration)
|
|
},
|
|
Animation::Keyframes(_, _, _, ref mut state) => {
|
|
// This animation is still running, or we need to keep
|
|
// iterating.
|
|
now < state.started_at + state.duration || state.tick()
|
|
},
|
|
};
|
|
|
|
// If the animation is still running, add it back to the list of running animations.
|
|
if still_running {
|
|
animation_state.running_animations.push(running_animation);
|
|
} else {
|
|
debug!("Finishing transition: {:?}", running_animation);
|
|
let (event_type, elapsed_time) = match running_animation {
|
|
Animation::Transition(_, _, ref property_animation) => (
|
|
TransitionOrAnimationEventType::TransitionEnd,
|
|
property_animation.duration,
|
|
),
|
|
Animation::Keyframes(_, _, _, ref mut state) => (
|
|
TransitionOrAnimationEventType::AnimationEnd,
|
|
state.active_duration(),
|
|
),
|
|
};
|
|
|
|
send_event(&running_animation, event_type, elapsed_time);
|
|
animation_state.finished_animations.push(running_animation);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Send events for cancelled animations. Currently this only handles cancelled
|
|
/// transitions, but eventually this should handle cancelled CSS animations as
|
|
/// well.
|
|
pub fn handle_cancelled_animations(
|
|
animation_state: &mut ElementAnimationState,
|
|
now: f64,
|
|
mut send_event: impl FnMut(&Animation, TransitionOrAnimationEventType, f64),
|
|
) {
|
|
for animation in animation_state.cancelled_animations.drain(..) {
|
|
match animation {
|
|
Animation::Transition(_, start_time, _) => {
|
|
// TODO(mrobinson): We need to properly compute the elapsed_time here
|
|
// according to https://drafts.csswg.org/css-transitions/#event-transitionevent
|
|
send_event(
|
|
&animation,
|
|
TransitionOrAnimationEventType::TransitionCancel,
|
|
(now - start_time).max(0.),
|
|
);
|
|
},
|
|
// TODO(mrobinson): We should send animationcancel events.
|
|
Animation::Keyframes(..) => {},
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn handle_new_animations(
|
|
animation_state: &mut ElementAnimationState,
|
|
mut send_event: impl FnMut(&Animation, TransitionOrAnimationEventType, f64),
|
|
) {
|
|
for animation in animation_state.new_animations.drain(..) {
|
|
match animation {
|
|
Animation::Transition(..) => {
|
|
// TODO(mrobinson): We need to properly compute the elapsed_time here
|
|
// according to https://drafts.csswg.org/css-transitions/#event-transitionevent
|
|
send_event(
|
|
&animation,
|
|
TransitionOrAnimationEventType::TransitionRun,
|
|
0.,
|
|
)
|
|
},
|
|
Animation::Keyframes(..) => {},
|
|
}
|
|
animation_state.running_animations.push(animation);
|
|
}
|
|
}
|
|
|
|
/// Recalculates style for a set of animations. This does *not* run with the DOM
|
|
/// lock held. Returns a set of nodes associated with animations that are no longer
|
|
/// valid.
|
|
pub fn recalc_style_for_animations<E>(
|
|
context: &LayoutContext,
|
|
flow: &mut dyn Flow,
|
|
animation_states: &FxHashMap<OpaqueNode, ElementAnimationState>,
|
|
) -> FxHashSet<OpaqueNode>
|
|
where
|
|
E: TElement,
|
|
{
|
|
let mut invalid_nodes = animation_states.keys().cloned().collect();
|
|
do_recalc_style_for_animations::<E>(context, flow, animation_states, &mut invalid_nodes);
|
|
invalid_nodes
|
|
}
|
|
|
|
fn do_recalc_style_for_animations<E>(
|
|
context: &LayoutContext,
|
|
flow: &mut dyn Flow,
|
|
animation_states: &FxHashMap<OpaqueNode, ElementAnimationState>,
|
|
invalid_nodes: &mut FxHashSet<OpaqueNode>,
|
|
) where
|
|
E: TElement,
|
|
{
|
|
let mut damage = RestyleDamage::empty();
|
|
flow.mutate_fragments(&mut |fragment| {
|
|
let animations = match animation_states.get(&fragment.node) {
|
|
Some(state) => &state.running_animations,
|
|
None => return,
|
|
};
|
|
|
|
invalid_nodes.remove(&fragment.node);
|
|
for animation in animations.iter() {
|
|
let old_style = fragment.style.clone();
|
|
update_style_for_animation::<E>(
|
|
&context.style_context,
|
|
animation,
|
|
Arc::make_mut(&mut fragment.style),
|
|
&ServoMetricsProvider,
|
|
);
|
|
let difference = RestyleDamage::compute_style_difference(&old_style, &fragment.style);
|
|
damage |= difference.damage;
|
|
}
|
|
});
|
|
|
|
let base = flow.mut_base();
|
|
base.restyle_damage.insert(damage);
|
|
for kid in base.children.iter_mut() {
|
|
do_recalc_style_for_animations::<E>(context, kid, animation_states, invalid_nodes)
|
|
}
|
|
}
|