Bug 1341372 - Part 6: Trigger transitions.

1. We need to call get_after_change_style, which is the computed styles
   without transition rules, while process_animations.
2. If we have after-change style, we may replace the new computed values with
   after-change style, according to whether we really need to update
   transitions.
3. There are some cases we don't update transitions, so we need to early
   return. might_needs_transitions_update() will check it first and it
   will filter out most common cases.
4. needs_transitions_update() will check each property and existing running
   transitions to make sure we really don't need to update transitions.
   The logic of this function is similar with that of
   nsTransitionManager::DoUpdateTransitions().

MozReview-Commit-ID: 2ccdPjgrxKz
This commit is contained in:
Boris Chiou 2017-04-17 14:32:41 +08:00
parent 2399cde504
commit eb8db7b892
6 changed files with 315 additions and 16 deletions

View file

@ -19,6 +19,7 @@ use font_metrics::FontMetricsProvider;
#[cfg(feature = "gecko")] use gecko_bindings::structs; #[cfg(feature = "gecko")] use gecko_bindings::structs;
use matching::StyleSharingCandidateCache; use matching::StyleSharingCandidateCache;
use parking_lot::RwLock; use parking_lot::RwLock;
#[cfg(feature = "gecko")] use properties::ComputedValues;
#[cfg(feature = "gecko")] use selector_parser::PseudoElement; #[cfg(feature = "gecko")] use selector_parser::PseudoElement;
use selectors::matching::ElementSelectorFlags; use selectors::matching::ElementSelectorFlags;
#[cfg(feature = "servo")] use servo_config::opts; #[cfg(feature = "servo")] use servo_config::opts;
@ -285,10 +286,22 @@ bitflags! {
pub enum SequentialTask<E: TElement> { pub enum SequentialTask<E: TElement> {
/// Entry to avoid an unused type parameter error on servo. /// Entry to avoid an unused type parameter error on servo.
Unused(SendElement<E>), Unused(SendElement<E>),
/// 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")] #[cfg(feature = "gecko")]
/// Marks that we need to update CSS animations, update effect properties of UpdateAnimations {
/// any type of animations after the normal traversal. /// The target element.
UpdateAnimations(SendElement<E>, Option<PseudoElement>, UpdateAnimationsTasks), el: SendElement<E>,
/// The target pseudo element.
pseudo: Option<PseudoElement>,
/// 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<Arc<ComputedValues>>,
/// The tasks which are performed in this SequentialTask.
tasks: UpdateAnimationsTasks
},
} }
impl<E: TElement> SequentialTask<E> { impl<E: TElement> SequentialTask<E> {
@ -299,18 +312,24 @@ impl<E: TElement> SequentialTask<E> {
match self { match self {
Unused(_) => unreachable!(), Unused(_) => unreachable!(),
#[cfg(feature = "gecko")] #[cfg(feature = "gecko")]
UpdateAnimations(el, pseudo, tasks) => { UpdateAnimations { el, pseudo, before_change_style, tasks } => {
unsafe { el.update_animations(pseudo.as_ref(), 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")] #[cfg(feature = "gecko")]
/// Creates a task to update various animation state on a given (pseudo-)element. pub fn update_animations(el: E,
pub fn update_animations(el: E, pseudo: Option<PseudoElement>, pseudo: Option<PseudoElement>,
before_change_style: Option<Arc<ComputedValues>>,
tasks: UpdateAnimationsTasks) -> Self { tasks: UpdateAnimationsTasks) -> Self {
use self::SequentialTask::*; 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 }
} }
} }

View file

@ -14,12 +14,15 @@ use data::ElementData;
use element_state::ElementState; use element_state::ElementState;
use font_metrics::FontMetricsProvider; use font_metrics::FontMetricsProvider;
use properties::{ComputedValues, PropertyDeclarationBlock}; 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 rule_tree::CascadeLevel;
use selector_parser::{ElementExt, PreExistingComputedValues, PseudoElement}; use selector_parser::{ElementExt, PreExistingComputedValues, PseudoElement};
use selectors::matching::ElementSelectorFlags; use selectors::matching::ElementSelectorFlags;
use shared_lock::Locked; use shared_lock::Locked;
use sink::Push; use sink::Push;
use std::fmt; use std::fmt;
#[cfg(feature = "gecko")] use std::collections::HashMap;
use std::fmt::Debug; use std::fmt::Debug;
use std::hash::Hash; use std::hash::Hash;
use std::ops::Deref; 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. /// Creates a task to update various animation state on a given (pseudo-)element.
#[cfg(feature = "gecko")] #[cfg(feature = "gecko")]
fn update_animations(&self, _pseudo: Option<&PseudoElement>, fn update_animations(&self, _pseudo: Option<&PseudoElement>,
before_change_style: Option<Arc<ComputedValues>>,
tasks: UpdateAnimationsTasks); tasks: UpdateAnimationsTasks);
/// Returns true if the element has relevant animations. Relevant /// 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. /// Returns true if the element has a CSS animation.
fn has_css_animations(&self, _pseudo: Option<&PseudoElement>) -> bool; 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; fn has_css_transitions(&self, _pseudo: Option<&PseudoElement>) -> bool;
/// Returns true if the element has animation restyle hints. /// 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() return data.get_restyle()
.map_or(false, |r| r.hint.has_animation_hint()); .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<TransitionProperty, Arc<AnimationValue>>;
/// 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<ComputedValues>>,
new_values: &Arc<ComputedValues>,
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<ComputedValues>,
after_change_style: &Arc<ComputedValues>,
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<ComputedValues>,
after_change_style: &Arc<ComputedValues>,
existing_transitions: &HashMap<TransitionProperty,
Arc<AnimationValue>>)
-> bool;
} }
/// Trait abstracting over different kinds of dirty-descendants bits. /// Trait abstracting over different kinds of dirty-descendants bits.

View file

@ -57,7 +57,7 @@ use media_queries::Device;
use parking_lot::RwLock; use parking_lot::RwLock;
use properties::{ComputedValues, parse_style_attribute}; use properties::{ComputedValues, parse_style_attribute};
use properties::{Importance, PropertyDeclaration, PropertyDeclarationBlock}; use properties::{Importance, PropertyDeclaration, PropertyDeclarationBlock};
use properties::animated_properties::AnimationValueMap; use properties::animated_properties::{AnimationValue, AnimationValueMap, TransitionProperty};
use properties::style_structs::Font; use properties::style_structs::Font;
use rule_tree::CascadeLevel as ServoCascadeLevel; use rule_tree::CascadeLevel as ServoCascadeLevel;
use selector_parser::{ElementExt, Snapshot}; use selector_parser::{ElementExt, Snapshot};
@ -67,6 +67,7 @@ use selectors::parser::{AttrSelector, NamespaceConstraint};
use shared_lock::Locked; use shared_lock::Locked;
use sink::Push; use sink::Push;
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt; use std::fmt;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::ptr; use std::ptr;
@ -665,7 +666,9 @@ impl<'le> TElement for GeckoElement<'le> {
(self.flags() & node_flags) == node_flags (self.flags() & node_flags) == node_flags
} }
fn update_animations(&self, pseudo: Option<&PseudoElement>, fn update_animations(&self,
pseudo: Option<&PseudoElement>,
before_change_style: Option<Arc<ComputedValues>>,
tasks: UpdateAnimationsTasks) { tasks: UpdateAnimationsTasks) {
// We have to update animations even if the element has no computed style // 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 // 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 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 { unsafe {
Gecko_UpdateAnimations(self.0, atom_ptr, Gecko_UpdateAnimations(self.0, atom_ptr,
None, before_change_values,
computed_values_opt, computed_values_opt,
parent_values_opt, parent_values_opt,
tasks.bits()); tasks.bits());
@ -715,6 +719,162 @@ impl<'le> TElement for GeckoElement<'le> {
let atom_ptr = PseudoElement::ns_atom_or_null_from_opt(pseudo); let atom_ptr = PseudoElement::ns_atom_or_null_from_opt(pseudo);
unsafe { Gecko_ElementHasCSSTransitions(self.0, atom_ptr) } unsafe { Gecko_ElementHasCSSTransitions(self.0, atom_ptr) }
} }
fn get_css_transitions_info(&self,
pseudo: Option<&PseudoElement>)
-> HashMap<TransitionProperty, Arc<AnimationValue>> {
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<ComputedValues>>,
new_values: &Arc<ComputedValues>,
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<ComputedValues>,
after_change_style: &Arc<ComputedValues>,
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::<TransitionProperty>::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<ComputedValues>,
after_change_style: &Arc<ComputedValues>,
existing_transitions: &HashMap<TransitionProperty,
Arc<AnimationValue>>)
-> 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> { impl<'le> PartialEq for GeckoElement<'le> {

View file

@ -653,6 +653,25 @@ extern "C" {
aPseudoTagOrNull: *mut nsIAtom) aPseudoTagOrNull: *mut nsIAtom)
-> bool; -> 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" { extern "C" {
pub fn Gecko_GetProgressFromComputedTiming(aComputedTiming: pub fn Gecko_GetProgressFromComputedTiming(aComputedTiming:
RawGeckoComputedTimingBorrowed) RawGeckoComputedTimingBorrowed)

View file

@ -477,10 +477,14 @@ trait PrivateMatchMethods: TElement {
// Handle animations. // Handle animations.
if animate && !context.shared.traversal_flags.for_animation_only() { 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, self.process_animations(context,
&mut old_values, &mut old_values,
&mut new_values, &mut new_values,
pseudo); primary_style,
&pseudo_style);
} }
// Accumulate restyle damage. // Accumulate restyle damage.
@ -556,20 +560,63 @@ trait PrivateMatchMethods: TElement {
context: &mut StyleContext<Self>, context: &mut StyleContext<Self>,
old_values: &mut Option<Arc<ComputedValues>>, old_values: &mut Option<Arc<ComputedValues>>,
new_values: &mut Arc<ComputedValues>, new_values: &mut Arc<ComputedValues>,
pseudo: Option<&PseudoElement>) { primary_style: &ComputedStyle,
use context::{CSS_ANIMATIONS, EFFECT_PROPERTIES}; pseudo_style: &Option<(&PseudoElement, &ComputedStyle)>) {
use context::{CSS_ANIMATIONS, CSS_TRANSITIONS, EFFECT_PROPERTIES};
use context::UpdateAnimationsTasks; use context::UpdateAnimationsTasks;
let pseudo = pseudo_style.map(|(p, _)| p);
let mut tasks = UpdateAnimationsTasks::empty(); let mut tasks = UpdateAnimationsTasks::empty();
if self.needs_update_animations(old_values, new_values, pseudo) { if self.needs_update_animations(old_values, new_values, pseudo) {
tasks.insert(CSS_ANIMATIONS); 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) { if self.has_animations(pseudo) {
tasks.insert(EFFECT_PROPERTIES); tasks.insert(EFFECT_PROPERTIES);
} }
if !tasks.is_empty() { if !tasks.is_empty() {
let task = ::context::SequentialTask::update_animations(*self, let task = ::context::SequentialTask::update_animations(*self,
pseudo.cloned(), pseudo.cloned(),
before_change_style,
tasks); tasks);
context.thread_local.tasks.push(task); context.thread_local.tasks.push(task);
} }
@ -580,7 +627,8 @@ trait PrivateMatchMethods: TElement {
context: &mut StyleContext<Self>, context: &mut StyleContext<Self>,
old_values: &mut Option<Arc<ComputedValues>>, old_values: &mut Option<Arc<ComputedValues>>,
new_values: &mut Arc<ComputedValues>, new_values: &mut Arc<ComputedValues>,
_pseudo: Option<&PseudoElement>) { _primary_style: &ComputedStyle,
_pseudo_style: &Option<(&PseudoElement, &ComputedStyle)>) {
let possibly_expired_animations = let possibly_expired_animations =
&mut context.thread_local.current_element_info.as_mut().unwrap() &mut context.thread_local.current_element_info.as_mut().unwrap()
.possibly_expired_animations; .possibly_expired_animations;

View file

@ -44,6 +44,7 @@ use gecko_bindings::bindings::ServoComputedValuesBorrowedOrNull;
use gecko_bindings::bindings::{Gecko_ResetFilters, Gecko_CopyFiltersFrom}; use gecko_bindings::bindings::{Gecko_ResetFilters, Gecko_CopyFiltersFrom};
use gecko_bindings::bindings::RawGeckoPresContextBorrowed; use gecko_bindings::bindings::RawGeckoPresContextBorrowed;
use gecko_bindings::structs::{self, StyleComplexColor}; use gecko_bindings::structs::{self, StyleComplexColor};
use gecko_bindings::structs::nsCSSPropertyID;
use gecko_bindings::structs::nsStyleVariables; use gecko_bindings::structs::nsStyleVariables;
use gecko_bindings::sugar::ns_style_coord::{CoordDataValue, CoordData, CoordDataMut}; use gecko_bindings::sugar::ns_style_coord::{CoordDataValue, CoordData, CoordDataMut};
use gecko_bindings::sugar::ownership::HasArcFFI; use gecko_bindings::sugar::ownership::HasArcFFI;
@ -1985,6 +1986,11 @@ fn static_assert() {
${impl_transition_time_value('duration', 'Duration')} ${impl_transition_time_value('duration', 'Duration')}
${impl_transition_timing_function()} ${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) { pub fn set_transition_property(&mut self, v: longhands::transition_property::computed_value::T) {
use gecko_bindings::structs::nsCSSPropertyID::eCSSPropertyExtra_no_properties; use gecko_bindings::structs::nsCSSPropertyID::eCSSPropertyExtra_no_properties;
@ -2018,6 +2024,10 @@ fn static_assert() {
self.gecko.mTransitions[index].mProperty.into() 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) { pub fn copy_transition_property_from(&mut self, other: &Self) {
unsafe { self.gecko.mTransitions.ensure_len(other.gecko.mTransitions.len()) }; unsafe { self.gecko.mTransitions.ensure_len(other.gecko.mTransitions.len()) };