Add support for transitionrun events

These events are triggered as soon as a transition is added to the list
of running transitions. This will allow better test coverage while
reworking the transitions and animations processing model.
This commit is contained in:
Martin Robinson 2020-04-23 17:06:24 +02:00
parent 0540c4a284
commit d386d6d1f1
11 changed files with 105 additions and 93 deletions

View file

@ -131,6 +131,7 @@ toggle
track track
transitioncancel transitioncancel
transitionend transitionend
transitionrun
unhandledrejection unhandledrejection
unload unload
url url
@ -142,5 +143,6 @@ webkitAnimationEnd
webkitAnimationIteration webkitAnimationIteration
webkitAnimationStart webkitAnimationStart
webkitTransitionEnd webkitTransitionEnd
webkitTransitionRun
week week
width width

View file

@ -15,54 +15,62 @@ use script_traits::UntrustedNodeAddress;
use script_traits::{ use script_traits::{
AnimationState, ConstellationControlMsg, LayoutMsg as ConstellationMsg, TransitionEventType, AnimationState, ConstellationControlMsg, LayoutMsg as ConstellationMsg, TransitionEventType,
}; };
use style::animation::{update_style_for_animation, Animation, ElementAnimationState}; use style::animation::{
update_style_for_animation, Animation, ElementAnimationState, PropertyAnimation,
};
use style::dom::TElement; use style::dom::TElement;
use style::font_metrics::ServoMetricsProvider; use style::font_metrics::ServoMetricsProvider;
use style::selector_parser::RestyleDamage; use style::selector_parser::RestyleDamage;
use style::timer::Timer; use style::timer::Timer;
/// Collect newly transitioning nodes, which is used by the script process during
/// forced, synchronous reflows to root DOM nodes for the duration of their transitions.
pub fn collect_newly_transitioning_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)| {
let num_transitions = state
.new_animations
.iter()
.filter(|animation| animation.is_transition())
.count();
std::iter::repeat(node.to_untrusted_node_address()).take(num_transitions)
}));
}
}
/// Processes any new animations that were discovered after style recalculation. Also /// Processes any new animations that were discovered after style recalculation. Also
/// finish any animations that have completed, inserting them into `finished_animations`. /// finish any animations that have completed, inserting them into `finished_animations`.
pub fn update_animation_states<E>( pub fn update_animation_states(
constellation_chan: &IpcSender<ConstellationMsg>, constellation_chan: &IpcSender<ConstellationMsg>,
script_chan: &IpcSender<ConstellationControlMsg>, script_chan: &IpcSender<ConstellationControlMsg>,
animation_states: &mut FxHashMap<OpaqueNode, ElementAnimationState>, animation_states: &mut FxHashMap<OpaqueNode, ElementAnimationState>,
invalid_nodes: FxHashSet<OpaqueNode>, invalid_nodes: FxHashSet<OpaqueNode>,
mut newly_transitioning_nodes: Option<&mut Vec<UntrustedNodeAddress>>,
pipeline_id: PipelineId, pipeline_id: PipelineId,
timer: &Timer, timer: &Timer,
) where ) {
E: TElement,
{
let had_running_animations = animation_states let had_running_animations = animation_states
.values() .values()
.any(|state| !state.running_animations.is_empty()); .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 { for node in &invalid_nodes {
if let Some(mut state) = animation_states.remove(node) { if let Some(mut state) = animation_states.remove(node) {
state.cancel_all_animations(); state.cancel_all_animations();
send_events_for_cancelled_animations(script_chan, &mut state, pipeline_id);
} }
} }
let now = timer.seconds(); let now = timer.seconds();
let mut have_running_animations = false; let mut have_running_animations = false;
for (node, animation_state) in animation_states.iter_mut() { for (node, animation_state) in animation_states.iter_mut() {
// TODO(mrobinson): This should really be triggering transitionrun messages update_animation_state(script_chan, animation_state, pipeline_id, now, *node);
// on the script thread.
if let Some(ref mut newly_transitioning_nodes) = newly_transitioning_nodes {
let number_of_new_transitions = animation_state
.new_animations
.iter()
.filter(|animation| animation.is_transition())
.count();
for _ in 0..number_of_new_transitions {
newly_transitioning_nodes.push(node.to_untrusted_node_address());
}
}
update_animation_state::<E>(script_chan, animation_state, pipeline_id, now);
have_running_animations = have_running_animations =
have_running_animations || !animation_state.running_animations.is_empty(); have_running_animations || !animation_state.running_animations.is_empty();
} }
@ -85,16 +93,37 @@ pub fn update_animation_states<E>(
.unwrap(); .unwrap();
} }
pub fn update_animation_state<E>( pub fn update_animation_state(
script_chan: &IpcSender<ConstellationControlMsg>, script_channel: &IpcSender<ConstellationControlMsg>,
animation_state: &mut ElementAnimationState, animation_state: &mut ElementAnimationState,
pipeline_id: PipelineId, pipeline_id: PipelineId,
now: f64, now: f64,
) where node: OpaqueNode,
E: TElement, ) {
{ let send_transition_event = |property_animation: &PropertyAnimation, event_type| {
send_events_for_cancelled_animations(script_chan, animation_state, pipeline_id); script_channel
.send(ConstellationControlMsg::TransitionEvent {
pipeline_id,
event_type,
node: node.to_untrusted_node_address(),
property_name: property_animation.property_name().into(),
elapsed_time: property_animation.duration,
})
.unwrap()
};
handle_cancelled_animations(animation_state, send_transition_event);
handle_running_animations(animation_state, now, send_transition_event);
handle_new_animations(animation_state, send_transition_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_transition_event: impl FnMut(&PropertyAnimation, TransitionEventType),
) {
let mut running_animations = let mut running_animations =
std::mem::replace(&mut animation_state.running_animations, Vec::new()); std::mem::replace(&mut animation_state.running_animations, Vec::new());
for mut running_animation in running_animations.drain(..) { for mut running_animation in running_animations.drain(..) {
@ -115,46 +144,25 @@ pub fn update_animation_state<E>(
animation_state.running_animations.push(running_animation); animation_state.running_animations.push(running_animation);
} else { } else {
debug!("Finishing transition: {:?}", running_animation); debug!("Finishing transition: {:?}", running_animation);
if let Animation::Transition(node, _, ref property_animation) = running_animation { if let Animation::Transition(_, _, ref property_animation) = running_animation {
script_chan send_transition_event(property_animation, TransitionEventType::TransitionEnd);
.send(ConstellationControlMsg::TransitionEvent {
pipeline_id,
event_type: TransitionEventType::TransitionEnd,
node: node.to_untrusted_node_address(),
property_name: property_animation.property_name().into(),
elapsed_time: property_animation.duration,
})
.unwrap();
} }
animation_state.finished_animations.push(running_animation); animation_state.finished_animations.push(running_animation);
} }
} }
animation_state
.running_animations
.append(&mut animation_state.new_animations);
} }
/// Send events for cancelled animations. Currently this only handles cancelled /// Send events for cancelled animations. Currently this only handles cancelled
/// transitions, but eventually this should handle cancelled CSS animations as /// transitions, but eventually this should handle cancelled CSS animations as
/// well. /// well.
pub fn send_events_for_cancelled_animations( pub fn handle_cancelled_animations(
script_channel: &IpcSender<ConstellationControlMsg>,
animation_state: &mut ElementAnimationState, animation_state: &mut ElementAnimationState,
pipeline_id: PipelineId, mut send_transition_event: impl FnMut(&PropertyAnimation, TransitionEventType),
) { ) {
for animation in animation_state.cancelled_animations.drain(..) { for animation in animation_state.cancelled_animations.drain(..) {
match animation { match animation {
Animation::Transition(node, _, ref property_animation) => { Animation::Transition(_, _, ref property_animation) => {
script_channel send_transition_event(property_animation, TransitionEventType::TransitionCancel)
.send(ConstellationControlMsg::TransitionEvent {
pipeline_id,
event_type: TransitionEventType::TransitionCancel,
node: node.to_untrusted_node_address(),
property_name: property_animation.property_name().into(),
elapsed_time: property_animation.duration,
})
.unwrap();
}, },
Animation::Keyframes(..) => { Animation::Keyframes(..) => {
warn!("Got unexpected animation in finished transitions list.") warn!("Got unexpected animation in finished transitions list.")
@ -163,6 +171,21 @@ pub fn send_events_for_cancelled_animations(
} }
} }
pub fn handle_new_animations(
animation_state: &mut ElementAnimationState,
mut send_transition_event: impl FnMut(&PropertyAnimation, TransitionEventType),
) {
for animation in animation_state.new_animations.drain(..) {
match animation {
Animation::Transition(_, _, ref property_animation) => {
send_transition_event(property_animation, TransitionEventType::TransitionRun)
},
Animation::Keyframes(..) => {},
}
animation_state.running_animations.push(animation);
}
}
/// Recalculates style for a set of animations. This does *not* run with the DOM /// 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 /// lock held. Returns a set of nodes associated with animations that are no longer
/// valid. /// valid.

View file

@ -1762,13 +1762,18 @@ impl LayoutThread {
.map(|nodes| nodes.lock().unwrap()); .map(|nodes| nodes.lock().unwrap());
let newly_transitioning_nodes = let newly_transitioning_nodes =
newly_transitioning_nodes.as_mut().map(|nodes| &mut **nodes); newly_transitioning_nodes.as_mut().map(|nodes| &mut **nodes);
// Kick off animations if any were triggered, expire completed ones. let mut animation_states = self.animation_states.write();
animation::update_animation_states::<ServoLayoutElement>(
animation::collect_newly_transitioning_nodes(
&animation_states,
newly_transitioning_nodes,
);
animation::update_animation_states(
&self.constellation_chan, &self.constellation_chan,
&self.script_chan, &self.script_chan,
&mut *self.animation_states.write(), &mut *animation_states,
invalid_nodes, invalid_nodes,
newly_transitioning_nodes,
self.id, self.id,
&self.timer, &self.timer,
); );

View file

@ -647,6 +647,7 @@ fn invoke(
atom!("animationiteration") => Some(atom!("webkitAnimationIteration")), atom!("animationiteration") => Some(atom!("webkitAnimationIteration")),
atom!("animationstart") => Some(atom!("webkitAnimationStart")), atom!("animationstart") => Some(atom!("webkitAnimationStart")),
atom!("transitionend") => Some(atom!("webkitTransitionEnd")), atom!("transitionend") => Some(atom!("webkitTransitionEnd")),
atom!("transitionrun") => Some(atom!("webkitTransitionRun")),
_ => None, _ => None,
} { } {
let original_type = event.type_(); let original_type = event.type_();

View file

@ -498,6 +498,7 @@ macro_rules! global_event_handlers(
event_handler!(toggle, GetOntoggle, SetOntoggle); event_handler!(toggle, GetOntoggle, SetOntoggle);
event_handler!(transitioncancel, GetOntransitioncancel, SetOntransitioncancel); event_handler!(transitioncancel, GetOntransitioncancel, SetOntransitioncancel);
event_handler!(transitionend, GetOntransitionend, SetOntransitionend); event_handler!(transitionend, GetOntransitionend, SetOntransitionend);
event_handler!(transitionrun, GetOntransitionrun, SetOntransitionrun);
event_handler!(volumechange, GetOnvolumechange, SetOnvolumechange); event_handler!(volumechange, GetOnvolumechange, SetOnvolumechange);
event_handler!(waiting, GetOnwaiting, SetOnwaiting); event_handler!(waiting, GetOnwaiting, SetOnwaiting);
) )

View file

@ -92,6 +92,7 @@ interface mixin GlobalEventHandlers {
// https://drafts.csswg.org/css-transitions/#interface-globaleventhandlers-idl // https://drafts.csswg.org/css-transitions/#interface-globaleventhandlers-idl
partial interface mixin GlobalEventHandlers { partial interface mixin GlobalEventHandlers {
attribute EventHandler ontransitionrun;
attribute EventHandler ontransitionend; attribute EventHandler ontransitionend;
attribute EventHandler ontransitioncancel; attribute EventHandler ontransitioncancel;
}; };

View file

@ -2920,22 +2920,20 @@ impl ScriptThread {
let js_runtime = self.js_runtime.rt(); let js_runtime = self.js_runtime.rt();
let node = unsafe { from_untrusted_node_address(js_runtime, unsafe_node) }; let node = unsafe { from_untrusted_node_address(js_runtime, unsafe_node) };
let idx = self let node_index = self
.transitioning_nodes .transitioning_nodes
.borrow() .borrow()
.iter() .iter()
.position(|n| &**n as *const _ == &*node as *const _); .position(|n| &**n as *const _ == &*node as *const _);
match idx { let node_index = match node_index {
Some(idx) => { Some(node_index) => node_index,
self.transitioning_nodes.borrow_mut().remove(idx);
},
None => { None => {
// If no index is found, we can't know whether this node is safe to use. // If no index is found, we can't know whether this node is safe to use.
// It's better not to fire a DOM event than crash. // It's better not to fire a DOM event than crash.
warn!("Ignoring transition end notification for unknown node."); warn!("Ignoring transition end notification for unknown node.");
return; return;
}, },
} };
if self.closed_pipelines.borrow().contains(&pipeline_id) { if self.closed_pipelines.borrow().contains(&pipeline_id) {
warn!("Ignoring transition event for closed pipeline."); warn!("Ignoring transition event for closed pipeline.");
@ -2943,12 +2941,17 @@ impl ScriptThread {
} }
let event_atom = match event_type { let event_atom = match event_type {
TransitionEventType::TransitionRun => atom!("transitionrun"),
TransitionEventType::TransitionEnd => { TransitionEventType::TransitionEnd => {
// Not quite the right thing - see #13865. // Not quite the right thing - see #13865.
node.dirty(NodeDamage::NodeStyleDamaged); node.dirty(NodeDamage::NodeStyleDamaged);
self.transitioning_nodes.borrow_mut().remove(node_index);
atom!("transitionend") atom!("transitionend")
}, },
TransitionEventType::TransitionCancel => atom!("transitioncancel"), TransitionEventType::TransitionCancel => {
self.transitioning_nodes.borrow_mut().remove(node_index);
atom!("transitioncancel")
},
}; };
let event_init = TransitionEventInit { let event_init = TransitionEventInit {

View file

@ -283,8 +283,10 @@ pub enum UpdatePipelineIdReason {
} }
/// The type of transition event to trigger. /// The type of transition event to trigger.
#[derive(Clone, Copy, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub enum TransitionEventType { pub enum TransitionEventType {
/// The transition has started running.
TransitionRun,
/// The transition has ended by reaching the end of its animation. /// The transition has ended by reaching the end of its animation.
TransitionEnd, TransitionEnd,
/// The transition ended early for some reason, such as the property /// The transition ended early for some reason, such as the property

View file

@ -1,8 +1,4 @@
[before-load-001.html] [before-load-001.html]
expected: TIMEOUT
[transition height from 10px to 100px / events] [transition height from 10px to 100px / events]
expected: FAIL expected: FAIL
[CSS Transitions Test: Transitioning before load event]
expected: TIMEOUT

View file

@ -1,34 +1,16 @@
[idlharness.html] [idlharness.html]
[Document interface: document must inherit property "ontransitionrun" with the proper type]
expected: FAIL
[HTMLElement interface: attribute ontransitionstart] [HTMLElement interface: attribute ontransitionstart]
expected: FAIL expected: FAIL
[Document interface: document must inherit property "ontransitionstart" with the proper type] [Document interface: document must inherit property "ontransitionstart" with the proper type]
expected: FAIL expected: FAIL
[HTMLElement interface: attribute ontransitionrun]
expected: FAIL
[Window interface: attribute ontransitionrun]
expected: FAIL
[HTMLElement interface: document must inherit property "ontransitionstart" with the proper type] [HTMLElement interface: document must inherit property "ontransitionstart" with the proper type]
expected: FAIL expected: FAIL
[HTMLElement interface: document must inherit property "ontransitionrun" with the proper type]
expected: FAIL
[Window interface: window must inherit property "ontransitionrun" with the proper type]
expected: FAIL
[Document interface: attribute ontransitionstart] [Document interface: attribute ontransitionstart]
expected: FAIL expected: FAIL
[Document interface: attribute ontransitionrun]
expected: FAIL
[Window interface: window must inherit property "ontransitionstart" with the proper type] [Window interface: window must inherit property "ontransitionstart" with the proper type]
expected: FAIL expected: FAIL

View file

@ -1,8 +1,4 @@
[non-rendered-element-001.html] [non-rendered-element-001.html]
expected: TIMEOUT
[Transitions are canceled when an element is no longer rendered]
expected: TIMEOUT
[Transitions do not run for an element newly rendered] [Transitions do not run for an element newly rendered]
expected: FAIL expected: FAIL