diff options
author | Martin Robinson <mrobinson@igalia.com> | 2020-05-05 13:36:57 +0200 |
---|---|---|
committer | Martin Robinson <mrobinson@igalia.com> | 2020-05-05 20:08:44 +0200 |
commit | 3a74013abcec241d67d2685e52a031409dc59dd4 (patch) | |
tree | 0fc6791d087797120dd960aa0fb1e0a9a6ebce92 /components/script | |
parent | b585ce5b1f181996b2f8109a4e045eb6f5b3e2a0 (diff) | |
download | servo-3a74013abcec241d67d2685e52a031409dc59dd4.tar.gz servo-3a74013abcec241d67d2685e52a031409dc59dd4.zip |
Start having animations conform to the HTML spec
This is a small step toward fixing #19242. The main idea is that the
clock for animations should advance as the event loop ticks. We
accomplish this by moving the clock from layout and naming it the
"animation timeline" which is the spec language. This should fix
flakiness with animations and transitions tests where a reflow could
move animations forward while script was running.
This change also starts to break out transition and animation events
into their own data structure, because it's quite likely that the next
step in fixing #19242 is to no longer send these events through a
channel.
Diffstat (limited to 'components/script')
-rw-r--r-- | components/script/animation_timeline.rs | 49 | ||||
-rw-r--r-- | components/script/dom/document.rs | 21 | ||||
-rw-r--r-- | components/script/dom/window.rs | 53 | ||||
-rw-r--r-- | components/script/lib.rs | 1 | ||||
-rw-r--r-- | components/script/script_thread.rs | 96 |
5 files changed, 122 insertions, 98 deletions
diff --git a/components/script/animation_timeline.rs b/components/script/animation_timeline.rs new file mode 100644 index 00000000000..e0ad520db61 --- /dev/null +++ b/components/script/animation_timeline.rs @@ -0,0 +1,49 @@ +/* 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/. */ + +#![deny(missing_docs)] + +//! A timeline module, used to specify an `AnimationTimeline` which determines +//! the time used for synchronizing animations in the script thread. + +use time; + +/// A `AnimationTimeline` which is used to synchronize animations during the script +/// event loop. +#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf)] +pub struct AnimationTimeline { + current_value: f64, +} + +impl AnimationTimeline { + /// Creates a new "normal" timeline, i.e., a "Current" mode timer. + #[inline] + pub fn new() -> Self { + Self { + current_value: time::precise_time_s(), + } + } + + /// Creates a new "test mode" timeline, with initial time 0. + #[inline] + pub fn new_for_testing() -> Self { + Self { current_value: 0. } + } + + /// Returns the current value of the timeline in seconds. + pub fn current_value(&self) -> f64 { + self.current_value + } + + /// Updates the value of the `AnimationTimeline` to the current clock time. + pub fn update(&mut self) { + self.current_value = time::precise_time_s(); + } + + /// Increments the current value of the timeline by a specific number of seconds. + /// This is used for testing. + pub fn advance_specific(&mut self, by: f64) { + self.current_value += by; + } +} diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs index eacd6a11745..5e40f433da5 100644 --- a/components/script/dom/document.rs +++ b/components/script/dom/document.rs @@ -2,6 +2,7 @@ * 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/. */ +use crate::animation_timeline::AnimationTimeline; use crate::document_loader::{DocumentLoader, LoadType}; use crate::dom::attr::Attr; use crate::dom::beforeunloadevent::BeforeUnloadEvent; @@ -380,6 +381,9 @@ pub struct Document { csp_list: DomRefCell<Option<CspList>>, /// https://w3c.github.io/slection-api/#dfn-selection selection: MutNullableDom<Selection>, + /// A timeline for animations which is used for synchronizing animations. + /// https://drafts.csswg.org/web-animations/#timeline + animation_timeline: DomRefCell<AnimationTimeline>, } #[derive(JSTraceable, MallocSizeOf)] @@ -2904,6 +2908,11 @@ impl Document { dirty_webgl_contexts: DomRefCell::new(HashMap::new()), csp_list: DomRefCell::new(None), selection: MutNullableDom::new(None), + animation_timeline: if pref!(layout.animations.test.enabled) { + DomRefCell::new(AnimationTimeline::new_for_testing()) + } else { + DomRefCell::new(AnimationTimeline::new()) + }, } } @@ -3605,6 +3614,18 @@ impl Document { }) .collect() } + + pub fn advance_animation_timeline_for_testing(&self, delta: f64) { + self.animation_timeline.borrow_mut().advance_specific(delta); + } + + pub fn update_animation_timeline(&self) { + self.animation_timeline.borrow_mut().update(); + } + + pub fn current_animation_timeline_value(&self) -> f64 { + self.animation_timeline.borrow().current_value() + } } impl Element { diff --git a/components/script/dom/window.rs b/components/script/dom/window.rs index 481586a71ae..414ef0f2e45 100644 --- a/components/script/dom/window.rs +++ b/components/script/dom/window.rs @@ -173,8 +173,7 @@ pub enum ReflowReason { IFrameLoadEvent, MissingExplicitReflow, ElementStateChanged, - TickAnimations, - AdvanceClock(i32), + PendingReflow, } #[dom_struct] @@ -1550,12 +1549,9 @@ impl Window { /// layout animation clock. pub fn advance_animation_clock(&self, delta: i32) { let pipeline_id = self.upcast::<GlobalScope>().pipeline_id(); - ScriptThread::restyle_animating_nodes_for_advancing_clock(&pipeline_id); - self.force_reflow( - ReflowGoal::TickAnimations, - ReflowReason::AdvanceClock(delta), - None, - ); + self.Document() + .advance_animation_timeline_for_testing(delta as f64 / 1000.); + ScriptThread::handle_tick_all_animations_for_testing(pipeline_id); } /// Reflows the page unconditionally if possible and not suppressed. This @@ -1632,11 +1628,6 @@ impl Window { document.flush_dirty_canvases(); } - let advance_clock_delta = match reason { - ReflowReason::AdvanceClock(delta) => Some(delta), - _ => None, - }; - // Send new document and relevant styles to layout. let needs_display = reflow_goal.needs_display(); let reflow = ScriptReflow { @@ -1651,7 +1642,7 @@ impl Window { script_join_chan: join_chan, dom_count: document.dom_count(), pending_restyles: document.drain_pending_restyles(), - advance_clock_delta, + animation_timeline_value: document.current_animation_timeline_value(), }; self.layout_chan @@ -2453,8 +2444,7 @@ fn should_move_clip_rect(clip_rect: UntypedRect<Au>, new_viewport: UntypedRect<f } fn debug_reflow_events(id: PipelineId, reflow_goal: &ReflowGoal, reason: &ReflowReason) { - let mut debug_msg = format!("**** pipeline={}", id); - debug_msg.push_str(match *reflow_goal { + let goal_string = match *reflow_goal { ReflowGoal::Full => "\tFull", ReflowGoal::TickAnimations => "\tTickAnimations", ReflowGoal::LayoutQuery(ref query_msg, _) => match query_msg { @@ -2471,34 +2461,9 @@ fn debug_reflow_events(id: PipelineId, reflow_goal: &ReflowGoal, reason: &Reflow &QueryMsg::ElementInnerTextQuery(_) => "\tElementInnerTextQuery", &QueryMsg::InnerWindowDimensionsQuery(_) => "\tInnerWindowDimensionsQuery", }, - }); - - debug_msg.push_str(match *reason { - ReflowReason::CachedPageNeededReflow => "\tCachedPageNeededReflow", - ReflowReason::RefreshTick => "\tRefreshTick", - ReflowReason::FirstLoad => "\tFirstLoad", - ReflowReason::KeyEvent => "\tKeyEvent", - ReflowReason::MouseEvent => "\tMouseEvent", - ReflowReason::Query => "\tQuery", - ReflowReason::Timer => "\tTimer", - ReflowReason::Viewport => "\tViewport", - ReflowReason::WindowResize => "\tWindowResize", - ReflowReason::DOMContentLoaded => "\tDOMContentLoaded", - ReflowReason::DocumentLoaded => "\tDocumentLoaded", - ReflowReason::StylesheetLoaded => "\tStylesheetLoaded", - ReflowReason::ImageLoaded => "\tImageLoaded", - ReflowReason::RequestAnimationFrame => "\tRequestAnimationFrame", - ReflowReason::WebFontLoaded => "\tWebFontLoaded", - ReflowReason::WorkletLoaded => "\tWorkletLoaded", - ReflowReason::FramedContentChanged => "\tFramedContentChanged", - ReflowReason::IFrameLoadEvent => "\tIFrameLoadEvent", - ReflowReason::MissingExplicitReflow => "\tMissingExplicitReflow", - ReflowReason::ElementStateChanged => "\tElementStateChanged", - ReflowReason::TickAnimations => "\tTickAnimations", - ReflowReason::AdvanceClock(..) => "\tAdvanceClock", - }); - - println!("{}", debug_msg); + }; + + println!("**** pipeline={}\t{}\t{:?}", id, goal_string, reason); } impl Window { diff --git a/components/script/lib.rs b/components/script/lib.rs index 4eeca229e01..cd73045456a 100644 --- a/components/script/lib.rs +++ b/components/script/lib.rs @@ -47,6 +47,7 @@ extern crate servo_atoms; #[macro_use] extern crate style; +mod animation_timeline; #[warn(deprecated)] #[macro_use] mod task; diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index 5a99d800799..008274ded47 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -141,11 +141,12 @@ use script_traits::{ LayoutMsg, LoadData, LoadOrigin, MediaSessionActionType, MouseButton, MouseEventType, NewLayoutInfo, Painter, ProgressiveWebMetricType, ScriptMsg, ScriptThreadFactory, ScriptToConstellationChan, StructuredSerializedData, TimerSchedulerMsg, TouchEventType, - TouchId, TransitionOrAnimationEventType, UntrustedNodeAddress, UpdatePipelineIdReason, - WebrenderIpcSender, WheelDelta, WindowSizeData, WindowSizeType, + TouchId, TransitionOrAnimationEvent, TransitionOrAnimationEventType, UntrustedNodeAddress, + UpdatePipelineIdReason, WebrenderIpcSender, WheelDelta, WindowSizeData, WindowSizeType, }; use servo_atoms::Atom; use servo_config::opts; +use servo_config::pref; use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl}; use std::borrow::Cow; use std::cell::Cell; @@ -1498,7 +1499,6 @@ impl ScriptThread { }) }, FromConstellation(ConstellationControlMsg::TickAllAnimations(pipeline_id, _)) => { - // step 7.8 if !animation_ticks.contains(&pipeline_id) { animation_ticks.insert(pipeline_id); sequential.push(event); @@ -1544,6 +1544,9 @@ impl ScriptThread { } } + // Step 11.10 from https://html.spec.whatwg.org/multipage/#event-loops. + self.update_animations_and_send_events(); + // Process the gathered events. debug!("Processing events."); for msg in sequential { @@ -1603,7 +1606,7 @@ impl ScriptThread { let pending_reflows = window.get_pending_reflow_count(); if pending_reflows > 0 { - window.reflow(ReflowGoal::Full, ReflowReason::ImageLoaded); + window.reflow(ReflowGoal::Full, ReflowReason::PendingReflow); } else { // Reflow currently happens when explicitly invoked by code that // knows the document could have been modified. This should really @@ -1616,6 +1619,19 @@ impl ScriptThread { true } + // Perform step 11.10 from https://html.spec.whatwg.org/multipage/#event-loops. + // TODO(mrobinson): This should also update the current animations and send events + // to conform to the HTML specification. This might mean having events for rooting + // DOM nodes and ultimately all animations living in script. + fn update_animations_and_send_events(&self) { + for (_, document) in self.documents.borrow().iter() { + // Only update the time if it isn't being managed by a test. + if !pref!(layout.animations.test.enabled) { + document.update_animation_timeline(); + } + } + } + fn categorize_msg(&self, msg: &MixedMessage) -> ScriptThreadEventCategory { match *msg { MixedMessage::FromConstellation(ref inner_msg) => match *inner_msg { @@ -1905,20 +1921,8 @@ impl ScriptThread { ConstellationControlMsg::TickAllAnimations(pipeline_id, tick_type) => { self.handle_tick_all_animations(pipeline_id, tick_type) }, - ConstellationControlMsg::TransitionOrAnimationEvent { - pipeline_id, - event_type, - node, - property_or_animation_name, - elapsed_time, - } => { - self.handle_transition_or_animation_event( - pipeline_id, - event_type, - node, - property_or_animation_name, - elapsed_time, - ); + ConstellationControlMsg::TransitionOrAnimationEvent(ref event) => { + self.handle_transition_or_animation_event(event); }, ConstellationControlMsg::WebFontLoaded(pipeline_id) => { self.handle_web_font_loaded(pipeline_id) @@ -2914,22 +2918,12 @@ impl ScriptThread { debug!("Exited script thread."); } - fn restyle_animating_nodes(&self, id: &PipelineId) -> bool { - match self.animating_nodes.borrow().get(id) { - Some(nodes) => { - for node in nodes.iter() { - node.dirty(NodeDamage::NodeStyleDamaged); - } - true - }, - None => false, - } - } - - pub fn restyle_animating_nodes_for_advancing_clock(id: &PipelineId) { + /// Handles animation tick requested during testing. + pub fn handle_tick_all_animations_for_testing(id: PipelineId) { SCRIPT_THREAD_ROOT.with(|root| { let script_thread = unsafe { &*root.get().unwrap() }; - script_thread.restyle_animating_nodes(id); + script_thread + .handle_tick_all_animations(id, AnimationTickType::CSS_ANIMATIONS_AND_TRANSITIONS); }); } @@ -2943,38 +2937,32 @@ impl ScriptThread { document.run_the_animation_frame_callbacks(); } if tick_type.contains(AnimationTickType::CSS_ANIMATIONS_AND_TRANSITIONS) { - if !self.restyle_animating_nodes(&id) { - return; + match self.animating_nodes.borrow().get(&id) { + Some(nodes) => { + for node in nodes.iter() { + node.dirty(NodeDamage::NodeStyleDamaged); + } + }, + None => return, } - document.window().force_reflow( - ReflowGoal::TickAnimations, - ReflowReason::TickAnimations, - None, - ); + document.window().add_pending_reflow(); } } /// Handles firing of transition-related events. /// /// TODO(mrobinson): Add support for more events. - fn handle_transition_or_animation_event( - &self, - pipeline_id: PipelineId, - event_type: TransitionOrAnimationEventType, - unsafe_node: UntrustedNodeAddress, - property_or_animation_name: String, - elapsed_time: f64, - ) { + fn handle_transition_or_animation_event(&self, event: &TransitionOrAnimationEvent) { 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, event.node) }; // We limit the scope of the borrow here, so that we don't maintain this borrow // and then incidentally trigger another layout. That might result in a double // mutable borrow of `animating_nodes`. { let mut animating_nodes = self.animating_nodes.borrow_mut(); - let nodes = match animating_nodes.get_mut(&pipeline_id) { + let nodes = match animating_nodes.get_mut(&event.pipeline_id) { Some(nodes) => nodes, None => { return warn!( @@ -2996,12 +2984,12 @@ impl ScriptThread { }, }; - if event_type.finalizes_transition_or_animation() { + if event.event_type.finalizes_transition_or_animation() { nodes.remove(node_index); } } - let event_atom = match event_type { + let event_atom = match event.event_type { TransitionOrAnimationEventType::AnimationEnd => atom!("animationend"), TransitionOrAnimationEventType::TransitionCancel => atom!("transitioncancel"), TransitionOrAnimationEventType::TransitionEnd => atom!("transitionend"), @@ -3013,11 +3001,11 @@ impl ScriptThread { }; // TODO: Handle pseudo-elements properly - let property_or_animation_name = DOMString::from(property_or_animation_name); - let elapsed_time = Finite::new(elapsed_time as f32).unwrap(); + let property_or_animation_name = DOMString::from(event.property_or_animation_name.clone()); + let elapsed_time = Finite::new(event.elapsed_time as f32).unwrap(); let window = window_from_node(&*node); - if event_type.is_transition_event() { + if event.event_type.is_transition_event() { let event_init = TransitionEventInit { parent, propertyName: property_or_animation_name, |