/* 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, mut out: Option<&mut Vec>, ) { // 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, script_chan: &IpcSender, animation_states: &mut FxHashMap, invalid_nodes: FxHashSet, 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, 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( context: &LayoutContext, flow: &mut dyn Flow, animation_states: &FxHashMap, ) -> FxHashSet where E: TElement, { let mut invalid_nodes = animation_states.keys().cloned().collect(); do_recalc_style_for_animations::(context, flow, animation_states, &mut invalid_nodes); invalid_nodes } fn do_recalc_style_for_animations( context: &LayoutContext, flow: &mut dyn Flow, animation_states: &FxHashMap, invalid_nodes: &mut FxHashSet, ) 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::( &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::(context, kid, animation_states, invalid_nodes) } }