diff --git a/components/style/context.rs b/components/style/context.rs index f89674e8d04..bf12841f8a4 100644 --- a/components/style/context.rs +++ b/components/style/context.rs @@ -19,6 +19,7 @@ use font_metrics::FontMetricsProvider; #[cfg(feature = "gecko")] use gecko_bindings::structs; use matching::StyleSharingCandidateCache; use parking_lot::RwLock; +#[cfg(feature = "gecko")] use properties::ComputedValues; #[cfg(feature = "gecko")] use selector_parser::PseudoElement; use selectors::matching::ElementSelectorFlags; #[cfg(feature = "servo")] use servo_config::opts; @@ -285,10 +286,22 @@ bitflags! { pub enum SequentialTask { /// Entry to avoid an unused type parameter error on servo. Unused(SendElement), + + /// Performs one of a number of possible tasks related to updating animations based on the + /// |tasks| field. These include updating CSS animations/transitions that changed as part + /// of the non-animation style traversal, and updating the computed effect properties. #[cfg(feature = "gecko")] - /// Marks that we need to update CSS animations, update effect properties of - /// any type of animations after the normal traversal. - UpdateAnimations(SendElement, Option, UpdateAnimationsTasks), + UpdateAnimations { + /// The target element. + el: SendElement, + /// The target pseudo element. + pseudo: Option, + /// The before-change style for transitions. We use before-change style as the initial + /// value of its Keyframe. Required if |tasks| includes CSSTransitions. + before_change_style: Option>, + /// The tasks which are performed in this SequentialTask. + tasks: UpdateAnimationsTasks + }, } impl SequentialTask { @@ -299,18 +312,24 @@ impl SequentialTask { match self { Unused(_) => unreachable!(), #[cfg(feature = "gecko")] - UpdateAnimations(el, pseudo, tasks) => { - unsafe { el.update_animations(pseudo.as_ref(), tasks) }; + UpdateAnimations { el, pseudo, before_change_style, tasks } => { + unsafe { el.update_animations(pseudo.as_ref(), before_change_style, tasks) }; } } } + /// Creates a task to update various animation-related state on + /// a given (pseudo-)element. #[cfg(feature = "gecko")] - /// Creates a task to update various animation state on a given (pseudo-)element. - pub fn update_animations(el: E, pseudo: Option, + pub fn update_animations(el: E, + pseudo: Option, + before_change_style: Option>, tasks: UpdateAnimationsTasks) -> Self { use self::SequentialTask::*; - UpdateAnimations(unsafe { SendElement::new(el) }, pseudo, tasks) + UpdateAnimations { el: unsafe { SendElement::new(el) }, + pseudo: pseudo, + before_change_style: before_change_style, + tasks: tasks } } } diff --git a/components/style/dom.rs b/components/style/dom.rs index f56194fb14b..ffc9f67ef8d 100644 --- a/components/style/dom.rs +++ b/components/style/dom.rs @@ -14,12 +14,15 @@ use data::ElementData; use element_state::ElementState; use font_metrics::FontMetricsProvider; use properties::{ComputedValues, PropertyDeclarationBlock}; +#[cfg(feature = "gecko")] use properties::animated_properties::AnimationValue; +#[cfg(feature = "gecko")] use properties::animated_properties::TransitionProperty; use rule_tree::CascadeLevel; use selector_parser::{ElementExt, PreExistingComputedValues, PseudoElement}; use selectors::matching::ElementSelectorFlags; use shared_lock::Locked; use sink::Push; use std::fmt; +#[cfg(feature = "gecko")] use std::collections::HashMap; use std::fmt::Debug; use std::hash::Hash; use std::ops::Deref; @@ -462,6 +465,7 @@ pub trait TElement : Eq + PartialEq + Debug + Hash + Sized + Copy + Clone + /// Creates a task to update various animation state on a given (pseudo-)element. #[cfg(feature = "gecko")] fn update_animations(&self, _pseudo: Option<&PseudoElement>, + before_change_style: Option>, tasks: UpdateAnimationsTasks); /// Returns true if the element has relevant animations. Relevant @@ -472,7 +476,8 @@ pub trait TElement : Eq + PartialEq + Debug + Hash + Sized + Copy + Clone + /// Returns true if the element has a CSS animation. fn has_css_animations(&self, _pseudo: Option<&PseudoElement>) -> bool; - /// Returns true if the element has a CSS transition. + /// Returns true if the element has a CSS transition (including running transitions and + /// completed transitions). fn has_css_transitions(&self, _pseudo: Option<&PseudoElement>) -> bool; /// Returns true if the element has animation restyle hints. @@ -484,6 +489,44 @@ pub trait TElement : Eq + PartialEq + Debug + Hash + Sized + Copy + Clone + return data.get_restyle() .map_or(false, |r| r.hint.has_animation_hint()); } + + /// Gets the current existing CSS transitions, by |property, end value| pairs in a HashMap. + #[cfg(feature = "gecko")] + fn get_css_transitions_info(&self, + pseudo: Option<&PseudoElement>) + -> HashMap>; + + /// Does a rough (and cheap) check for whether or not transitions might need to be updated that + /// will quickly return false for the common case of no transitions specified or running. If + /// this returns false, we definitely don't need to update transitions but if it returns true + /// we can perform the more thoroughgoing check, needs_transitions_update, to further + /// reduce the possibility of false positives. + #[cfg(feature = "gecko")] + fn might_need_transitions_update(&self, + old_values: &Option<&Arc>, + new_values: &Arc, + pseudo: Option<&PseudoElement>) -> bool; + + /// Returns true if one of the transitions needs to be updated on this element. We check all + /// the transition properties to make sure that updating transitions is necessary. + /// This method should only be called if might_needs_transitions_update returns true when + /// passed the same parameters. + #[cfg(feature = "gecko")] + fn needs_transitions_update(&self, + before_change_style: &Arc, + after_change_style: &Arc, + pseudo: Option<&PseudoElement>) -> bool; + + /// Returns true if we need to update transitions for the specified property on this element. + #[cfg(feature = "gecko")] + fn needs_transitions_update_per_property(&self, + property: TransitionProperty, + combined_duration: f32, + before_change_style: &Arc, + after_change_style: &Arc, + existing_transitions: &HashMap>) + -> bool; } /// Trait abstracting over different kinds of dirty-descendants bits. diff --git a/components/style/gecko/wrapper.rs b/components/style/gecko/wrapper.rs index d429c905b71..f64703dfd9c 100644 --- a/components/style/gecko/wrapper.rs +++ b/components/style/gecko/wrapper.rs @@ -57,7 +57,7 @@ use media_queries::Device; use parking_lot::RwLock; use properties::{ComputedValues, parse_style_attribute}; use properties::{Importance, PropertyDeclaration, PropertyDeclarationBlock}; -use properties::animated_properties::AnimationValueMap; +use properties::animated_properties::{AnimationValue, AnimationValueMap, TransitionProperty}; use properties::style_structs::Font; use rule_tree::CascadeLevel as ServoCascadeLevel; use selector_parser::{ElementExt, Snapshot}; @@ -67,6 +67,7 @@ use selectors::parser::{AttrSelector, NamespaceConstraint}; use shared_lock::Locked; use sink::Push; use std::cell::RefCell; +use std::collections::HashMap; use std::fmt; use std::hash::{Hash, Hasher}; use std::ptr; @@ -665,7 +666,9 @@ impl<'le> TElement for GeckoElement<'le> { (self.flags() & node_flags) == node_flags } - fn update_animations(&self, pseudo: Option<&PseudoElement>, + fn update_animations(&self, + pseudo: Option<&PseudoElement>, + before_change_style: Option>, tasks: UpdateAnimationsTasks) { // We have to update animations even if the element has no computed style // since it means the element is in a display:none subtree, we should destroy @@ -692,9 +695,10 @@ impl<'le> TElement for GeckoElement<'le> { ); let atom_ptr = PseudoElement::ns_atom_or_null_from_opt(pseudo); + let before_change_values = before_change_style.as_ref().map(|v| *HasArcFFI::arc_as_borrowed(v)); unsafe { Gecko_UpdateAnimations(self.0, atom_ptr, - None, + before_change_values, computed_values_opt, parent_values_opt, tasks.bits()); @@ -715,6 +719,162 @@ impl<'le> TElement for GeckoElement<'le> { let atom_ptr = PseudoElement::ns_atom_or_null_from_opt(pseudo); unsafe { Gecko_ElementHasCSSTransitions(self.0, atom_ptr) } } + + fn get_css_transitions_info(&self, + pseudo: Option<&PseudoElement>) + -> HashMap> { + use gecko_bindings::bindings::Gecko_ElementTransitions_EndValueAt; + use gecko_bindings::bindings::Gecko_ElementTransitions_Length; + use gecko_bindings::bindings::Gecko_ElementTransitions_PropertyAt; + + let atom_ptr = PseudoElement::ns_atom_or_null_from_opt(pseudo); + let collection_length = unsafe { Gecko_ElementTransitions_Length(self.0, atom_ptr) }; + let mut map = HashMap::with_capacity(collection_length); + for i in 0..collection_length { + let (property, raw_end_value) = unsafe { + (Gecko_ElementTransitions_PropertyAt(self.0, atom_ptr, i as usize).into(), + Gecko_ElementTransitions_EndValueAt(self.0, atom_ptr, i as usize)) + }; + let end_value = AnimationValue::arc_from_borrowed(&raw_end_value); + debug_assert!(end_value.is_some()); + map.insert(property, end_value.unwrap().clone()); + } + map + } + + fn might_need_transitions_update(&self, + old_values: &Option<&Arc>, + new_values: &Arc, + pseudo: Option<&PseudoElement>) -> bool { + use properties::longhands::display::computed_value as display; + + if old_values.is_none() { + return false; + } + + let ref new_box_style = new_values.get_box(); + let transition_not_running = !self.has_css_transitions(pseudo) && + new_box_style.transition_property_count() == 1 && + new_box_style.transition_combined_duration_at(0) <= 0.0f32; + let new_display_style = new_box_style.clone_display(); + let old_display_style = old_values.map(|ref old| old.get_box().clone_display()).unwrap(); + + new_box_style.transition_property_count() > 0 && + !transition_not_running && + (new_display_style != display::T::none && + old_display_style != display::T::none) + } + + // Detect if there are any changes that require us to update transitions. This is used as a + // more thoroughgoing check than the, cheaper might_need_transitions_update check. + // The following logic shadows the logic used on the Gecko side + // (nsTransitionManager::DoUpdateTransitions) where we actually perform the update. + // https://drafts.csswg.org/css-transitions/#starting + fn needs_transitions_update(&self, + before_change_style: &Arc, + after_change_style: &Arc, + pseudo: Option<&PseudoElement>) -> bool { + use gecko_bindings::structs::nsCSSPropertyID; + use properties::animated_properties; + use std::collections::HashSet; + + debug_assert!(self.might_need_transitions_update(&Some(before_change_style), + after_change_style, + pseudo), + "We should only call needs_transitions_update if \ + might_need_transitions_update returns true"); + + let ref after_change_box_style = after_change_style.get_box(); + let transitions_count = after_change_box_style.transition_property_count(); + let existing_transitions = self.get_css_transitions_info(pseudo); + let mut transitions_to_keep = if !existing_transitions.is_empty() && + (after_change_box_style.transition_nscsspropertyid_at(0) != + nsCSSPropertyID::eCSSPropertyExtra_all_properties) { + Some(HashSet::::with_capacity(transitions_count)) + } else { + None + }; + + // Check if this property is none, custom or unknown. + let is_none_or_custom_property = |property: nsCSSPropertyID| -> bool { + return property == nsCSSPropertyID::eCSSPropertyExtra_no_properties || + property == nsCSSPropertyID::eCSSPropertyExtra_variable || + property == nsCSSPropertyID::eCSSProperty_UNKNOWN; + }; + + for i in 0..transitions_count { + let property = after_change_box_style.transition_nscsspropertyid_at(i); + let combined_duration = after_change_box_style.transition_combined_duration_at(i); + + // We don't need to update transition for none/custom properties. + if is_none_or_custom_property(property) { + continue; + } + + let mut property_check_helper = |property: TransitionProperty| -> bool { + if self.needs_transitions_update_per_property(property, + combined_duration, + before_change_style, + after_change_style, + &existing_transitions) { + return true; + } + + if let Some(set) = transitions_to_keep.as_mut() { + set.insert(property); + } + false + }; + // FIXME: Bug 1353628: Shorthand properties are parsed failed now, so after fixing + // that, we have to handle shorthand. + if property == nsCSSPropertyID::eCSSPropertyExtra_all_properties { + if TransitionProperty::any(property_check_helper) { + return true; + } + } else { + if animated_properties::nscsspropertyid_is_animatable(property) && + property_check_helper(property.into()) { + return true; + } + } + } + + // Check if we have to cancel the running transition because this is not a matching + // transition-property value. + transitions_to_keep.map_or(false, |set| { + existing_transitions.keys().any(|property| !set.contains(property)) + }) + } + + fn needs_transitions_update_per_property(&self, + property: TransitionProperty, + combined_duration: f32, + before_change_style: &Arc, + after_change_style: &Arc, + existing_transitions: &HashMap>) + -> bool { + use properties::animated_properties::AnimatedProperty; + + // We don't allow transitions on properties that are not interpolable. + if property.is_discrete() { + return false; + } + + if existing_transitions.contains_key(&property) { + // If there is an existing transition, update only if the end value differs. + // If the end value has not changed, we should leave the currently running + // transition as-is since we don't want to interrupt its timing function. + let after_value = + Arc::new(AnimationValue::from_computed_values(&property, after_change_style)); + return existing_transitions.get(&property).unwrap() != &after_value; + } + + combined_duration > 0.0f32 && + AnimatedProperty::from_transition_property(&property, + before_change_style, + after_change_style).does_animate() + } } impl<'le> PartialEq for GeckoElement<'le> { diff --git a/components/style/gecko_bindings/bindings.rs b/components/style/gecko_bindings/bindings.rs index bd7b434c5d8..2ea1f286db5 100644 --- a/components/style/gecko_bindings/bindings.rs +++ b/components/style/gecko_bindings/bindings.rs @@ -653,6 +653,25 @@ extern "C" { aPseudoTagOrNull: *mut nsIAtom) -> bool; } +extern "C" { + pub fn Gecko_ElementTransitions_Length(aElement: RawGeckoElementBorrowed, + aPseudoTagOrNull: *mut nsIAtom) + -> usize; +} +extern "C" { + pub fn Gecko_ElementTransitions_PropertyAt(aElement: + RawGeckoElementBorrowed, + aPseudoTagOrNull: *mut nsIAtom, + aIndex: usize) + -> nsCSSPropertyID; +} +extern "C" { + pub fn Gecko_ElementTransitions_EndValueAt(aElement: + RawGeckoElementBorrowed, + aPseudoTagOrNull: *mut nsIAtom, + aIndex: usize) + -> RawServoAnimationValueBorrowedOrNull; +} extern "C" { pub fn Gecko_GetProgressFromComputedTiming(aComputedTiming: RawGeckoComputedTimingBorrowed) diff --git a/components/style/matching.rs b/components/style/matching.rs index 88daa198d57..c986b5825c7 100644 --- a/components/style/matching.rs +++ b/components/style/matching.rs @@ -477,10 +477,14 @@ trait PrivateMatchMethods: TElement { // Handle animations. if animate && !context.shared.traversal_flags.for_animation_only() { + let pseudo_style = pseudo_style.as_ref().map(|&(ref pseudo, ref computed_style)| { + (*pseudo, &**computed_style) + }); self.process_animations(context, &mut old_values, &mut new_values, - pseudo); + primary_style, + &pseudo_style); } // Accumulate restyle damage. @@ -556,20 +560,63 @@ trait PrivateMatchMethods: TElement { context: &mut StyleContext, old_values: &mut Option>, new_values: &mut Arc, - pseudo: Option<&PseudoElement>) { - use context::{CSS_ANIMATIONS, EFFECT_PROPERTIES}; + primary_style: &ComputedStyle, + pseudo_style: &Option<(&PseudoElement, &ComputedStyle)>) { + use context::{CSS_ANIMATIONS, CSS_TRANSITIONS, EFFECT_PROPERTIES}; use context::UpdateAnimationsTasks; + let pseudo = pseudo_style.map(|(p, _)| p); let mut tasks = UpdateAnimationsTasks::empty(); if self.needs_update_animations(old_values, new_values, pseudo) { tasks.insert(CSS_ANIMATIONS); } + + let before_change_style = if self.might_need_transitions_update(&old_values.as_ref(), + new_values, + pseudo) { + let after_change_style = if self.has_css_transitions(pseudo) { + self.get_after_change_style(context, primary_style, pseudo_style) + } else { + None + }; + + // In order to avoid creating a SequentialTask for transitions which may not be updated, + // we check it per property to make sure Gecko side will really update transition. + let needs_transitions_update = { + // We borrow new_values here, so need to add a scope to make sure we release it + // before assigning a new value to it. + let after_change_style_ref = match after_change_style { + Some(ref value) => value, + None => &new_values + }; + self.needs_transitions_update(old_values.as_ref().unwrap(), + after_change_style_ref, + pseudo) + }; + + if needs_transitions_update { + if let Some(values_without_transitions) = after_change_style { + *new_values = values_without_transitions; + } + tasks.insert(CSS_TRANSITIONS); + + // We need to clone old_values into SequentialTask, so we can use it later. + old_values.clone() + } else { + None + } + } else { + None + }; + if self.has_animations(pseudo) { tasks.insert(EFFECT_PROPERTIES); } + if !tasks.is_empty() { let task = ::context::SequentialTask::update_animations(*self, pseudo.cloned(), + before_change_style, tasks); context.thread_local.tasks.push(task); } @@ -580,7 +627,8 @@ trait PrivateMatchMethods: TElement { context: &mut StyleContext, old_values: &mut Option>, new_values: &mut Arc, - _pseudo: Option<&PseudoElement>) { + _primary_style: &ComputedStyle, + _pseudo_style: &Option<(&PseudoElement, &ComputedStyle)>) { let possibly_expired_animations = &mut context.thread_local.current_element_info.as_mut().unwrap() .possibly_expired_animations; diff --git a/components/style/properties/gecko.mako.rs b/components/style/properties/gecko.mako.rs index 02e3ace6f44..bbf0dec5682 100644 --- a/components/style/properties/gecko.mako.rs +++ b/components/style/properties/gecko.mako.rs @@ -44,6 +44,7 @@ use gecko_bindings::bindings::ServoComputedValuesBorrowedOrNull; use gecko_bindings::bindings::{Gecko_ResetFilters, Gecko_CopyFiltersFrom}; use gecko_bindings::bindings::RawGeckoPresContextBorrowed; use gecko_bindings::structs::{self, StyleComplexColor}; +use gecko_bindings::structs::nsCSSPropertyID; use gecko_bindings::structs::nsStyleVariables; use gecko_bindings::sugar::ns_style_coord::{CoordDataValue, CoordData, CoordDataMut}; use gecko_bindings::sugar::ownership::HasArcFFI; @@ -1985,6 +1986,11 @@ fn static_assert() { ${impl_transition_time_value('duration', 'Duration')} ${impl_transition_timing_function()} + pub fn transition_combined_duration_at(&self, index: usize) -> f32 { + // https://drafts.csswg.org/css-transitions/#transition-combined-duration + self.gecko.mTransitions[index].mDuration.max(0.0) + self.gecko.mTransitions[index].mDelay + } + pub fn set_transition_property(&mut self, v: longhands::transition_property::computed_value::T) { use gecko_bindings::structs::nsCSSPropertyID::eCSSPropertyExtra_no_properties; @@ -2018,6 +2024,10 @@ fn static_assert() { self.gecko.mTransitions[index].mProperty.into() } + pub fn transition_nscsspropertyid_at(&self, index: usize) -> nsCSSPropertyID { + self.gecko.mTransitions[index].mProperty + } + pub fn copy_transition_property_from(&mut self, other: &Self) { unsafe { self.gecko.mTransitions.ensure_len(other.gecko.mTransitions.len()) };