Auto merge of #22036 - emilio:gecko-sync, r=emilio

style: Sync changes from mozilla-central.

See each individual commit.

This syncs everything except a bindgen update which is blocked on https://github.com/servo/mozjs/pull/159.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/servo/servo/22036)
<!-- Reviewable:end -->
This commit is contained in:
bors-servo 2018-10-28 18:52:29 -04:00 committed by GitHub
commit 1628bd5c9c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 441 additions and 594 deletions

View file

@ -72,7 +72,7 @@ impl<Impl: SelectorImpl> SelectorBuilder<Impl> {
/// Pushes a simple selector onto the current compound selector.
#[inline(always)]
pub fn push_simple_selector(&mut self, ss: Component<Impl>) {
debug_assert!(!ss.is_combinator());
assert!(!ss.is_combinator());
self.simple_selectors.push(ss);
self.current_len += 1;
}
@ -105,7 +105,7 @@ impl<Impl: SelectorImpl> SelectorBuilder<Impl> {
parsed_slotted: bool,
) -> ThinArc<SpecificityAndFlags, Component<Impl>> {
// Compute the specificity and flags.
let mut spec = SpecificityAndFlags(specificity(&*self, self.simple_selectors.iter()));
let mut spec = SpecificityAndFlags(specificity(self.simple_selectors.iter()));
if parsed_pseudo {
spec.0 |= HAS_PSEUDO_BIT;
}
@ -281,33 +281,26 @@ impl From<Specificity> for u32 {
}
}
fn specificity<Impl>(builder: &SelectorBuilder<Impl>, iter: slice::Iter<Component<Impl>>) -> u32
fn specificity<Impl>(iter: slice::Iter<Component<Impl>>) -> u32
where
Impl: SelectorImpl,
{
complex_selector_specificity(builder, iter).into()
complex_selector_specificity(iter).into()
}
fn complex_selector_specificity<Impl>(
builder: &SelectorBuilder<Impl>,
mut iter: slice::Iter<Component<Impl>>,
) -> Specificity
fn complex_selector_specificity<Impl>(iter: slice::Iter<Component<Impl>>) -> Specificity
where
Impl: SelectorImpl,
{
fn simple_selector_specificity<Impl>(
builder: &SelectorBuilder<Impl>,
simple_selector: &Component<Impl>,
specificity: &mut Specificity,
) where
Impl: SelectorImpl,
{
match *simple_selector {
Component::Combinator(ref combinator) => {
unreachable!(
"Found combinator {:?} in simple selectors vector? {:?}",
combinator, builder,
);
Component::Combinator(..) => {
unreachable!("Found combinator in simple selectors vector?");
},
Component::PseudoElement(..) | Component::LocalName(..) => {
specificity.element_selectors += 1
@ -361,15 +354,15 @@ where
},
Component::Negation(ref negated) => {
for ss in negated.iter() {
simple_selector_specificity(builder, &ss, specificity);
simple_selector_specificity(&ss, specificity);
}
},
}
}
let mut specificity = Default::default();
for simple_selector in &mut iter {
simple_selector_specificity(builder, &simple_selector, &mut specificity);
for simple_selector in iter {
simple_selector_specificity(&simple_selector, &mut specificity);
}
specificity
}

View file

@ -27,10 +27,10 @@ use std::sync::mpsc::Sender;
use stylesheets::keyframes_rule::{KeyframesAnimation, KeyframesStep, KeyframesStepValue};
use timer::Timer;
use values::computed::Time;
use values::computed::TimingFunction;
use values::computed::box_::TransitionProperty;
use values::computed::transform::TimingFunction;
use values::generics::box_::AnimationIterationCount;
use values::generics::transform::{StepPosition, TimingFunction as GenericTimingFunction};
use values::generics::easing::{StepPosition, TimingFunction as GenericTimingFunction};
/// This structure represents a keyframes animation current iteration state.
@ -363,27 +363,39 @@ impl PropertyAnimation {
GenericTimingFunction::CubicBezier { x1, y1, x2, y2 } => {
Bezier::new(x1, y1, x2, y2).solve(time, epsilon)
},
GenericTimingFunction::Steps(steps, StepPosition::Start) => {
(time * (steps as f64)).ceil() / (steps as f64)
},
GenericTimingFunction::Steps(steps, StepPosition::End) => {
(time * (steps as f64)).floor() / (steps as f64)
},
GenericTimingFunction::Frames(frames) => {
// https://drafts.csswg.org/css-timing/#frames-timing-functions
let mut out = (time * (frames as f64)).floor() / ((frames - 1) as f64);
if out > 1.0 {
// FIXME: Basically, during the animation sampling process, the input progress
// should be in the range of [0, 1]. However, |time| is not accurate enough
// here, which means |time| could be larger than 1.0 in the last animation
// frame. (It should be equal to 1.0 exactly.) This makes the output of frames
// timing function jumps to the next frame/level.
// However, this solution is still not correct because |time| is possible
// outside the range of [0, 1] after introducing Web Animations. We should fix
// this problem when implementing web animations.
out = 1.0;
GenericTimingFunction::Steps(steps, pos) => {
let mut current_step = (time * (steps as f64)).floor() as i32;
if pos == StepPosition::Start ||
pos == StepPosition::JumpStart ||
pos == StepPosition::JumpBoth {
current_step = current_step + 1;
}
out
// FIXME: We should update current_step according to the "before flag".
// In order to get the before flag, we have to know the current animation phase
// and whether the iteration is reversed. For now, we skip this calculation.
// (i.e. Treat before_flag is unset,)
// https://drafts.csswg.org/css-easing/#step-timing-function-algo
if time >= 0.0 && current_step < 0 {
current_step = 0;
}
let jumps = match pos {
StepPosition::JumpBoth => steps + 1,
StepPosition::JumpNone => steps - 1,
StepPosition::JumpStart |
StepPosition::JumpEnd |
StepPosition::Start |
StepPosition::End => steps,
};
if time <= 1.0 && current_step > jumps {
current_step = jumps;
}
(current_step as f64) / (jumps as f64)
},
GenericTimingFunction::Keyword(keyword) => {
let (x1, x2, y1, y2) = keyword.to_bezier();

View file

@ -42,12 +42,14 @@ include = [
"StyleComputedFontStretchRange",
"StyleComputedFontStyleDescriptor",
"StyleComputedFontWeightRange",
"StyleComputedTimingFunction",
"StyleDisplay",
"StyleDisplayMode",
"StyleFillRule",
"StyleFontDisplay",
"StyleFontFaceSourceListComponent",
"StyleFontLanguageOverride",
"StyleTimingFunction",
"StylePathCommand",
"StyleUnicodeRange",
]

View file

@ -1032,13 +1032,13 @@ impl TrackSize<LengthOrPercentage> {
match *self {
TrackSize::FitContent(ref lop) => {
// Gecko sets min value to None and max value to the actual value in fit-content
// https://dxr.mozilla.org/mozilla-central/rev/0eef1d5/layout/style/nsRuleNode.cpp#8221
// https://searchfox.org/mozilla-central/rev/c05d9d61188d32b8/layout/style/nsRuleNode.cpp#7910
gecko_min.set_value(CoordDataValue::None);
lop.to_gecko_style_coord(gecko_max);
},
TrackSize::Breadth(ref breadth) => {
// Set the value to both fields if there's one breadth value
// https://dxr.mozilla.org/mozilla-central/rev/0eef1d5/layout/style/nsRuleNode.cpp#8230
// https://searchfox.org/mozilla-central/rev/c05d9d61188d32b8/layout/style/nsRuleNode.cpp#7919
breadth.to_gecko_style_coord(gecko_min);
breadth.to_gecko_style_coord(gecko_max);
},

View file

@ -12,7 +12,6 @@ pub mod ns_css_value;
mod ns_style_auto_array;
pub mod ns_style_coord;
mod ns_t_array;
mod ns_timing_function;
pub mod origin_flags;
pub mod ownership;
pub mod refptr;

View file

@ -1,160 +0,0 @@
/* 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 http://mozilla.org/MPL/2.0/. */
use gecko_bindings::structs::{nsTimingFunction, nsTimingFunction_Type};
use std::mem;
use values::computed::ToComputedValue;
use values::computed::transform::TimingFunction as ComputedTimingFunction;
use values::generics::transform::{StepPosition, TimingKeyword};
use values::generics::transform::TimingFunction as GenericTimingFunction;
use values::specified::transform::TimingFunction;
impl nsTimingFunction {
fn set_as_step(&mut self, function_type: nsTimingFunction_Type, steps: u32) {
debug_assert!(
function_type == nsTimingFunction_Type::StepStart ||
function_type == nsTimingFunction_Type::StepEnd,
"function_type should be step-start or step-end"
);
self.mType = function_type;
unsafe {
self.__bindgen_anon_1
.__bindgen_anon_1
.as_mut()
.mStepsOrFrames = steps;
}
}
fn set_as_frames(&mut self, frames: u32) {
self.mType = nsTimingFunction_Type::Frames;
unsafe {
self.__bindgen_anon_1
.__bindgen_anon_1
.as_mut()
.mStepsOrFrames = frames;
}
}
fn set_as_bezier(
&mut self,
function_type: nsTimingFunction_Type,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
) {
self.mType = function_type;
unsafe {
let ref mut gecko_cubic_bezier = self.__bindgen_anon_1.mFunc.as_mut();
gecko_cubic_bezier.mX1 = x1;
gecko_cubic_bezier.mY1 = y1;
gecko_cubic_bezier.mX2 = x2;
gecko_cubic_bezier.mY2 = y2;
}
}
}
impl From<ComputedTimingFunction> for nsTimingFunction {
fn from(function: ComputedTimingFunction) -> nsTimingFunction {
TimingFunction::from_computed_value(&function).into()
}
}
impl From<TimingFunction> for nsTimingFunction {
fn from(function: TimingFunction) -> nsTimingFunction {
let mut tf: nsTimingFunction = unsafe { mem::zeroed() };
match function {
GenericTimingFunction::Steps(steps, StepPosition::Start) => {
debug_assert!(steps.value() >= 0);
tf.set_as_step(nsTimingFunction_Type::StepStart, steps.value() as u32);
},
GenericTimingFunction::Steps(steps, StepPosition::End) => {
debug_assert!(steps.value() >= 0);
tf.set_as_step(nsTimingFunction_Type::StepEnd, steps.value() as u32);
},
GenericTimingFunction::Frames(frames) => {
debug_assert!(frames.value() >= 2);
tf.set_as_frames(frames.value() as u32);
},
GenericTimingFunction::CubicBezier { x1, y1, x2, y2 } => {
tf.set_as_bezier(
nsTimingFunction_Type::CubicBezier,
x1.get(),
y1.get(),
x2.get(),
y2.get(),
);
},
GenericTimingFunction::Keyword(keyword) => {
let (x1, y1, x2, y2) = keyword.to_bezier();
tf.set_as_bezier(keyword.into(), x1, y1, x2, y2);
},
}
tf
}
}
impl From<nsTimingFunction> for ComputedTimingFunction {
fn from(function: nsTimingFunction) -> ComputedTimingFunction {
match function.mType {
nsTimingFunction_Type::StepStart => GenericTimingFunction::Steps(
unsafe {
function
.__bindgen_anon_1
.__bindgen_anon_1
.as_ref()
.mStepsOrFrames
},
StepPosition::Start,
),
nsTimingFunction_Type::StepEnd => GenericTimingFunction::Steps(
unsafe {
function
.__bindgen_anon_1
.__bindgen_anon_1
.as_ref()
.mStepsOrFrames
},
StepPosition::End,
),
nsTimingFunction_Type::Frames => GenericTimingFunction::Frames(unsafe {
function
.__bindgen_anon_1
.__bindgen_anon_1
.as_ref()
.mStepsOrFrames
}),
nsTimingFunction_Type::Ease => GenericTimingFunction::Keyword(TimingKeyword::Ease),
nsTimingFunction_Type::Linear => GenericTimingFunction::Keyword(TimingKeyword::Linear),
nsTimingFunction_Type::EaseIn => GenericTimingFunction::Keyword(TimingKeyword::EaseIn),
nsTimingFunction_Type::EaseOut => {
GenericTimingFunction::Keyword(TimingKeyword::EaseOut)
},
nsTimingFunction_Type::EaseInOut => {
GenericTimingFunction::Keyword(TimingKeyword::EaseInOut)
},
nsTimingFunction_Type::CubicBezier => unsafe {
GenericTimingFunction::CubicBezier {
x1: function.__bindgen_anon_1.mFunc.as_ref().mX1,
y1: function.__bindgen_anon_1.mFunc.as_ref().mY1,
x2: function.__bindgen_anon_1.mFunc.as_ref().mX2,
y2: function.__bindgen_anon_1.mFunc.as_ref().mY2,
}
},
}
}
}
impl From<TimingKeyword> for nsTimingFunction_Type {
fn from(keyword: TimingKeyword) -> Self {
match keyword {
TimingKeyword::Linear => nsTimingFunction_Type::Linear,
TimingKeyword::Ease => nsTimingFunction_Type::Ease,
TimingKeyword::EaseIn => nsTimingFunction_Type::EaseIn,
TimingKeyword::EaseOut => nsTimingFunction_Type::EaseOut,
TimingKeyword::EaseInOut => nsTimingFunction_Type::EaseInOut,
}
}
}

View file

@ -2394,7 +2394,7 @@ fn static_assert() {
/// from the parent.
///
/// This is a port of Gecko's old ComputeScriptLevelSize function:
/// https://dxr.mozilla.org/mozilla-central/rev/35fbf14b9/layout/style/nsRuleNode.cpp#3197-3254
/// https://searchfox.org/mozilla-central/rev/c05d9d61188d32b8/layout/style/nsRuleNode.cpp#3103
///
/// scriptlevel is a property that affects how font-size is inherited. If scriptlevel is
/// +1, for example, it will inherit as the script size multiplier times
@ -2855,7 +2855,7 @@ fn static_assert() {
${impl_simple_copy('_moz_min_font_size_ratio', 'mMinFontSizeRatio')}
</%self:impl_trait>
<%def name="impl_copy_animation_or_transition_value(type, ident, gecko_ffi_name)">
<%def name="impl_copy_animation_or_transition_value(type, ident, gecko_ffi_name, member=None)">
#[allow(non_snake_case)]
pub fn copy_${type}_${ident}_from(&mut self, other: &Self) {
self.gecko.m${type.capitalize()}s.ensure_len(other.gecko.m${type.capitalize()}s.len());
@ -2868,7 +2868,11 @@ fn static_assert() {
);
for (ours, others) in iter {
% if member:
ours.m${gecko_ffi_name}.${member} = others.m${gecko_ffi_name}.${member};
% else:
ours.m${gecko_ffi_name} = others.m${gecko_ffi_name};
% endif
}
}
@ -2923,14 +2927,14 @@ fn static_assert() {
self.gecko.m${type.capitalize()}TimingFunctionCount = input_len as u32;
for (gecko, servo) in self.gecko.m${type.capitalize()}s.iter_mut().take(input_len as usize).zip(v) {
gecko.mTimingFunction = servo.into();
gecko.mTimingFunction.mTiming = servo;
}
}
${impl_animation_or_transition_count(type, 'timing_function', 'TimingFunction')}
${impl_copy_animation_or_transition_value(type, 'timing_function', 'TimingFunction')}
${impl_copy_animation_or_transition_value(type, 'timing_function', "TimingFunction", "mTiming")}
pub fn ${type}_timing_function_at(&self, index: usize)
-> longhands::${type}_timing_function::computed_value::SingleComputedValue {
self.gecko.m${type.capitalize()}s[index].mTimingFunction.into()
self.gecko.m${type.capitalize()}s[index].mTimingFunction.mTiming
}
</%def>
@ -2996,7 +3000,9 @@ fn static_assert() {
% for value in keyword.gecko_values():
structs::${keyword.gecko_constant(value)} => Keyword::${to_camel_case(value)},
% endfor
% if keyword.gecko_inexhaustive:
_ => panic!("Found unexpected value for animation-${ident}"),
% endif
}
}
${impl_animation_count(ident, gecko_ffi_name)}

View file

@ -1356,11 +1356,11 @@ fn is_matched_operation(first: &ComputedTransformOperation, second: &ComputedTra
&TransformOperation::RotateZ(..)) |
(&TransformOperation::Perspective(..),
&TransformOperation::Perspective(..)) => true,
// we animate scale and translate operations against each other
// Match functions that have the same primitive transform function
(a, b) if a.is_translate() && b.is_translate() => true,
(a, b) if a.is_scale() && b.is_scale() => true,
(a, b) if a.is_rotate() && b.is_rotate() => true,
// InterpolateMatrix and AccumulateMatrix are for mismatched transform.
// InterpolateMatrix and AccumulateMatrix are for mismatched transforms
_ => false
}
}
@ -1829,7 +1829,7 @@ impl Animate for Quaternion {
self.3 * other.3)
.min(1.0).max(-1.0);
if dot == 1.0 {
if dot.abs() == 1.0 {
return Ok(*self);
}
@ -2468,79 +2468,112 @@ impl Animate for ComputedTransform {
return Ok(Transform(result));
}
// https://drafts.csswg.org/css-transforms-1/#transform-transform-neutral-extend-animation
fn match_operations_if_possible<'a>(
this: &mut Cow<'a, Vec<ComputedTransformOperation>>,
other: &mut Cow<'a, Vec<ComputedTransformOperation>>,
) -> bool {
if !this.iter().zip(other.iter()).all(|(this, other)| is_matched_operation(this, other)) {
return false;
}
let this = Cow::Borrowed(&self.0);
let other = Cow::Borrowed(&other.0);
if this.len() == other.len() {
return true;
}
// Interpolate the common prefix
let mut result = this
.iter()
.zip(other.iter())
.take_while(|(this, other)| is_matched_operation(this, other))
.map(|(this, other)| this.animate(other, procedure))
.collect::<Result<Vec<_>, _>>()?;
let (shorter, longer) =
if this.len() < other.len() {
(this.to_mut(), other)
} else {
(other.to_mut(), this)
};
// Deal with the remainders
let this_remainder = if this.len() > result.len() {
Some(&this[result.len()..])
} else {
None
};
let other_remainder = if other.len() > result.len() {
Some(&other[result.len()..])
} else {
None
};
shorter.reserve(longer.len());
for op in longer.iter().skip(shorter.len()) {
shorter.push(op.to_animated_zero().unwrap());
}
// The resulting operations won't be matched regardless if the
// extended component is already InterpolateMatrix /
// AccumulateMatrix.
//
// Otherwise they should be matching operations all the time.
let already_mismatched = matches!(
longer[0],
TransformOperation::InterpolateMatrix { .. } |
TransformOperation::AccumulateMatrix { .. }
);
debug_assert_eq!(
!already_mismatched,
longer.iter().zip(shorter.iter()).all(|(this, other)| is_matched_operation(this, other)),
"ToAnimatedZero should generate matched operations"
);
!already_mismatched
}
let mut this = Cow::Borrowed(&self.0);
let mut other = Cow::Borrowed(&other.0);
if match_operations_if_possible(&mut this, &mut other) {
return Ok(Transform(
this.iter().zip(other.iter())
.map(|(this, other)| this.animate(other, procedure))
.collect::<Result<Vec<_>, _>>()?
));
}
match procedure {
Procedure::Add => Err(()),
Procedure::Interpolate { progress } => {
Ok(Transform(vec![TransformOperation::InterpolateMatrix {
from_list: Transform(this.into_owned()),
to_list: Transform(other.into_owned()),
progress: Percentage(progress as f32),
}]))
},
Procedure::Accumulate { count } => {
Ok(Transform(vec![TransformOperation::AccumulateMatrix {
from_list: Transform(this.into_owned()),
to_list: Transform(other.into_owned()),
count: cmp::min(count, i32::max_value() as u64) as i32,
}]))
match (this_remainder, other_remainder) {
// If there is a remainder from *both* lists we must have had mismatched functions.
// => Add the remainders to a suitable ___Matrix function.
(Some(this_remainder), Some(other_remainder)) => match procedure {
Procedure::Add => {
debug_assert!(false, "Should have already dealt with add by the point");
return Err(());
}
Procedure::Interpolate { progress } => {
result.push(TransformOperation::InterpolateMatrix {
from_list: Transform(this_remainder.to_vec()),
to_list: Transform(other_remainder.to_vec()),
progress: Percentage(progress as f32),
});
}
Procedure::Accumulate { count } => {
result.push(TransformOperation::AccumulateMatrix {
from_list: Transform(this_remainder.to_vec()),
to_list: Transform(other_remainder.to_vec()),
count: cmp::min(count, i32::max_value() as u64) as i32,
});
}
},
// If there is a remainder from just one list, then one list must be shorter but
// completely match the type of the corresponding functions in the longer list.
// => Interpolate the remainder with identity transforms.
(Some(remainder), None) | (None, Some(remainder)) => {
let fill_right = this_remainder.is_some();
result.append(
&mut remainder
.iter()
.map(|transform| {
let identity = transform.to_animated_zero().unwrap();
match transform {
// We can't interpolate/accumulate ___Matrix types directly with a
// matrix. Instead we need to wrap it in another ___Matrix type.
TransformOperation::AccumulateMatrix { .. }
| TransformOperation::InterpolateMatrix { .. } => {
let transform_list = Transform(vec![transform.clone()]);
let identity_list = Transform(vec![identity]);
let (from_list, to_list) = if fill_right {
(transform_list, identity_list)
} else {
(identity_list, transform_list)
};
match procedure {
Procedure::Add => Err(()),
Procedure::Interpolate { progress } => {
Ok(TransformOperation::InterpolateMatrix {
from_list,
to_list,
progress: Percentage(progress as f32),
})
}
Procedure::Accumulate { count } => {
Ok(TransformOperation::AccumulateMatrix {
from_list,
to_list,
count: cmp::min(count, i32::max_value() as u64)
as i32,
})
}
}
}
_ => {
let (lhs, rhs) = if fill_right {
(transform, &identity)
} else {
(&identity, transform)
};
lhs.animate(rhs, procedure)
}
}
})
.collect::<Result<Vec<_>, _>>()?,
);
}
(None, None) => {}
}
Ok(Transform(result))
}
}

View file

@ -247,6 +247,7 @@ ${helpers.single_keyword(
gecko_enum_prefix="PlaybackDirection",
custom_consts=animation_direction_custom_consts,
extra_prefixes=animation_extra_prefixes,
gecko_inexhaustive=True,
spec="https://drafts.csswg.org/css-animations/#propdef-animation-direction",
allowed_in_keyframe_block=False,
)}
@ -258,6 +259,7 @@ ${helpers.single_keyword(
animation_value_type="none",
vector=True,
extra_prefixes=animation_extra_prefixes,
gecko_enum_prefix="StyleAnimationPlayState",
spec="https://drafts.csswg.org/css-animations/#propdef-animation-play-state",
allowed_in_keyframe_block=False,
)}
@ -270,6 +272,7 @@ ${helpers.single_keyword(
vector=True,
gecko_enum_prefix="FillMode",
extra_prefixes=animation_extra_prefixes,
gecko_inexhaustive=True,
spec="https://drafts.csswg.org/css-animations/#propdef-animation-fill-mode",
allowed_in_keyframe_block=False,
)}

View file

@ -694,6 +694,34 @@ impl<'a, 'b: 'a> StyleAdjuster<'a, 'b> {
.set_computed_justify_items(parent_justify_items.computed);
}
/// If '-webkit-appearance' is 'menulist' on a <select> element then
/// the computed value of 'line-height' is 'normal'.
///
/// https://github.com/w3c/csswg-drafts/issues/3257
#[cfg(feature = "gecko")]
fn adjust_for_appearance<E>(&mut self, element: Option<E>)
where
E: TElement,
{
use properties::longhands::_moz_appearance::computed_value::T as Appearance;
use properties::longhands::line_height::computed_value::T as LineHeight;
if self.style.get_box().clone__moz_appearance() == Appearance::Menulist {
if self.style.get_inherited_text().clone_line_height() == LineHeight::normal() {
return;
}
if self.style.pseudo.is_some() {
return;
}
let is_html_select_element =
element.map_or(false, |e| e.is_html_element() && e.local_name() == &*local_name!("select"));
if !is_html_select_element {
return;
}
self.style.mutate_inherited_text().set_line_height(LineHeight::normal());
}
}
/// Adjusts the style to account for various fixups that don't fit naturally
/// into the cascade.
///
@ -755,6 +783,10 @@ impl<'a, 'b: 'a> StyleAdjuster<'a, 'b> {
{
self.adjust_for_text_decorations_in_effect();
}
#[cfg(feature = "gecko")]
{
self.adjust_for_appearance(element);
}
self.set_bits();
}
}

View file

@ -0,0 +1,14 @@
/* 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 http://mozilla.org/MPL/2.0/. */
//! Computed types for CSS Easing functions.
use values::computed::{Integer, Number};
use values::generics::easing;
/// A computed timing function.
pub type ComputedTimingFunction = easing::TimingFunction<Integer, Number>;
/// An alias of the computed timing function.
pub type TimingFunction = ComputedTimingFunction;

View file

@ -49,6 +49,7 @@ pub use self::box_::{ScrollSnapType, TouchAction, VerticalAlign, WillChange};
pub use self::color::{Color, ColorPropertyValue, RGBAColor};
pub use self::column::ColumnCount;
pub use self::counters::{Content, ContentItem, CounterIncrement, CounterReset};
pub use self::easing::TimingFunction;
pub use self::effects::{BoxShadow, Filter, SimpleShadow};
pub use self::flex::FlexBasis;
pub use self::image::{Gradient, GradientItem, Image, ImageLayer, LineDirection, MozImageRect};
@ -76,7 +77,7 @@ pub use self::table::XSpan;
pub use self::text::{InitialLetter, LetterSpacing, LineHeight, MozTabSize};
pub use self::text::{TextAlign, TextEmphasisPosition, TextEmphasisStyle, TextOverflow, WordSpacing};
pub use self::time::Time;
pub use self::transform::{Rotate, Scale, TimingFunction, Transform, TransformOperation};
pub use self::transform::{Rotate, Scale, Transform, TransformOperation};
pub use self::transform::{TransformOrigin, TransformStyle, Translate};
pub use self::ui::{ColorOrAuto, Cursor, MozForceBrokenImageIcon};
#[cfg(feature = "gecko")]
@ -93,6 +94,7 @@ pub mod box_;
pub mod color;
pub mod column;
pub mod counters;
pub mod easing;
pub mod effects;
pub mod flex;
pub mod font;

View file

@ -22,9 +22,6 @@ pub type Transform = generic::Transform<TransformOperation>;
/// The computed value of a CSS `<transform-origin>`
pub type TransformOrigin = generic::TransformOrigin<LengthOrPercentage, LengthOrPercentage, Length>;
/// A computed timing function.
pub type TimingFunction = generic::TimingFunction<u32, Number>;
/// A vector to represent the direction vector (rotate axis) for Rotate3D.
pub type DirectionVector = Vector3D<CSSFloat>;

View file

@ -0,0 +1,111 @@
/* 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 http://mozilla.org/MPL/2.0/. */
//! Generic types for CSS Easing Functions.
//! https://drafts.csswg.org/css-easing/#timing-functions
use parser::ParserContext;
use values::CSSFloat;
/// A generic easing function.
#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss)]
#[value_info(ty = "TIMING_FUNCTION")]
#[repr(u8, C)]
pub enum TimingFunction<Integer, Number> {
/// `linear | ease | ease-in | ease-out | ease-in-out`
Keyword(TimingKeyword),
/// `cubic-bezier(<number>, <number>, <number>, <number>)`
#[allow(missing_docs)]
#[css(comma, function)]
CubicBezier {
x1: Number,
y1: Number,
x2: Number,
y2: Number,
},
/// `step-start | step-end | steps(<integer>, [ <step-position> ]?)`
/// `<step-position> = jump-start | jump-end | jump-none | jump-both | start | end`
#[css(comma, function)]
#[value_info(other_values = "step-start,step-end")]
Steps(Integer, #[css(skip_if = "is_end")] StepPosition),
}
#[allow(missing_docs)]
#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
#[derive(
Clone,
Copy,
Debug,
Eq,
MallocSizeOf,
Parse,
PartialEq,
SpecifiedValueInfo,
ToComputedValue,
ToCss,
)]
#[repr(u8)]
pub enum TimingKeyword {
Linear,
Ease,
EaseIn,
EaseOut,
EaseInOut,
}
#[cfg(feature = "gecko")]
fn step_position_jump_enabled(_context: &ParserContext) -> bool {
use gecko_bindings::structs;
unsafe { structs::StaticPrefs_sVarCache_layout_css_step_position_jump_enabled }
}
#[cfg(feature = "servo")]
fn step_position_jump_enabled(_context: &ParserContext) -> bool {
false
}
#[allow(missing_docs)]
#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToComputedValue, ToCss)]
#[repr(u8)]
pub enum StepPosition {
#[parse(condition = "step_position_jump_enabled")]
JumpStart,
#[parse(condition = "step_position_jump_enabled")]
JumpEnd,
#[parse(condition = "step_position_jump_enabled")]
JumpNone,
#[parse(condition = "step_position_jump_enabled")]
JumpBoth,
Start,
End,
}
#[inline]
fn is_end(position: &StepPosition) -> bool {
*position == StepPosition::JumpEnd || *position == StepPosition::End
}
impl<Integer, Number> TimingFunction<Integer, Number> {
/// `ease`
#[inline]
pub fn ease() -> Self {
TimingFunction::Keyword(TimingKeyword::Ease)
}
}
impl TimingKeyword {
/// Returns the keyword as a quadruplet of Bezier point coordinates
/// `(x1, y1, x2, y2)`.
#[inline]
pub fn to_bezier(self) -> (CSSFloat, CSSFloat, CSSFloat, CSSFloat) {
match self {
TimingKeyword::Linear => (0., 0., 1., 1.),
TimingKeyword::Ease => (0.25, 0.1, 0.25, 1.),
TimingKeyword::EaseIn => (0.42, 0., 1., 1.),
TimingKeyword::EaseOut => (0., 0., 0.58, 1.),
TimingKeyword::EaseInOut => (0.42, 0., 0.58, 1.),
}
}
}

View file

@ -20,6 +20,7 @@ pub mod box_;
pub mod color;
pub mod column;
pub mod counters;
pub mod easing;
pub mod effects;
pub mod flex;
pub mod font;

View file

@ -90,67 +90,6 @@ pub struct TransformOrigin<H, V, Depth> {
pub depth: Depth,
}
/// A generic timing function.
///
/// <https://drafts.csswg.org/css-timing-1/#single-timing-function-production>
#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss)]
#[value_info(ty = "TIMING_FUNCTION")]
pub enum TimingFunction<Integer, Number> {
/// `linear | ease | ease-in | ease-out | ease-in-out`
Keyword(TimingKeyword),
/// `cubic-bezier(<number>, <number>, <number>, <number>)`
#[allow(missing_docs)]
#[css(comma, function)]
CubicBezier {
x1: Number,
y1: Number,
x2: Number,
y2: Number,
},
/// `step-start | step-end | steps(<integer>, [ start | end ]?)`
#[css(comma, function)]
#[value_info(other_values = "step-start,step-end")]
Steps(Integer, #[css(skip_if = "is_end")] StepPosition),
/// `frames(<integer>)`
#[css(comma, function)]
Frames(Integer),
}
#[allow(missing_docs)]
#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
#[derive(
Clone,
Copy,
Debug,
Eq,
MallocSizeOf,
Parse,
PartialEq,
SpecifiedValueInfo,
ToComputedValue,
ToCss,
)]
pub enum TimingKeyword {
Linear,
Ease,
EaseIn,
EaseOut,
EaseInOut,
}
#[allow(missing_docs)]
#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToComputedValue, ToCss)]
pub enum StepPosition {
Start,
End,
}
#[inline]
fn is_end(position: &StepPosition) -> bool {
*position == StepPosition::End
}
impl<H, V, D> TransformOrigin<H, V, D> {
/// Returns a new transform origin.
pub fn new(horizontal: H, vertical: V, depth: D) -> Self {
@ -162,29 +101,6 @@ impl<H, V, D> TransformOrigin<H, V, D> {
}
}
impl<Integer, Number> TimingFunction<Integer, Number> {
/// `ease`
#[inline]
pub fn ease() -> Self {
TimingFunction::Keyword(TimingKeyword::Ease)
}
}
impl TimingKeyword {
/// Returns the keyword as a quadruplet of Bezier point coordinates
/// `(x1, y1, x2, y2)`.
#[inline]
pub fn to_bezier(self) -> (CSSFloat, CSSFloat, CSSFloat, CSSFloat) {
match self {
TimingKeyword::Linear => (0., 0., 1., 1.),
TimingKeyword::Ease => (0.25, 0.1, 0.25, 1.),
TimingKeyword::EaseIn => (0.42, 0., 1., 1.),
TimingKeyword::EaseOut => (0., 0., 0.58, 1.),
TimingKeyword::EaseInOut => (0.42, 0., 0.58, 1.),
}
}
}
#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss)]
/// A single operation in the list of a `transform` value
pub enum TransformOperation<Angle, Number, Length, Integer, LengthOrPercentage> {

View file

@ -0,0 +1,107 @@
/* 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 http://mozilla.org/MPL/2.0/. */
//! Specified types for CSS Easing functions.
use cssparser::Parser;
use parser::{Parse, ParserContext};
use selectors::parser::SelectorParseErrorKind;
use style_traits::{ParseError, StyleParseErrorKind};
use values::computed::easing::TimingFunction as ComputedTimingFunction;
use values::generics::easing::{StepPosition, TimingKeyword};
use values::generics::easing::TimingFunction as GenericTimingFunction;
use values::specified::{Integer, Number};
/// A specified timing function.
pub type TimingFunction = GenericTimingFunction<Integer, Number>;
impl Parse for TimingFunction {
fn parse<'i, 't>(
context: &ParserContext,
input: &mut Parser<'i, 't>,
) -> Result<Self, ParseError<'i>> {
if let Ok(keyword) = input.try(TimingKeyword::parse) {
return Ok(GenericTimingFunction::Keyword(keyword));
}
if let Ok(ident) = input.try(|i| i.expect_ident_cloned()) {
let position = match_ignore_ascii_case! { &ident,
"step-start" => StepPosition::Start,
"step-end" => StepPosition::End,
_ => {
return Err(input.new_custom_error(
SelectorParseErrorKind::UnexpectedIdent(ident.clone())
));
},
};
return Ok(GenericTimingFunction::Steps(Integer::new(1), position));
}
let location = input.current_source_location();
let function = input.expect_function()?.clone();
input.parse_nested_block(move |i| {
(match_ignore_ascii_case! { &function,
"cubic-bezier" => {
let x1 = Number::parse(context, i)?;
i.expect_comma()?;
let y1 = Number::parse(context, i)?;
i.expect_comma()?;
let x2 = Number::parse(context, i)?;
i.expect_comma()?;
let y2 = Number::parse(context, i)?;
if x1.get() < 0.0 || x1.get() > 1.0 || x2.get() < 0.0 || x2.get() > 1.0 {
return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError));
}
Ok(GenericTimingFunction::CubicBezier { x1, y1, x2, y2 })
},
"steps" => {
let steps = Integer::parse_positive(context, i)?;
let position = i.try(|i| {
i.expect_comma()?;
StepPosition::parse(context, i)
}).unwrap_or(StepPosition::End);
// jump-none accepts a positive integer greater than 1.
// FIXME(emilio): The spec asks us to avoid rejecting it at parse
// time except until computed value time.
//
// It's not totally clear it's worth it though, and no other browser
// does this.
if position == StepPosition::JumpNone && 2 > steps.value() {
return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError));
}
Ok(GenericTimingFunction::Steps(steps, position))
},
_ => Err(()),
}).map_err(|()| {
location.new_custom_error(
StyleParseErrorKind::UnexpectedFunction(function.clone())
)
})
})
}
}
// We need this for converting the specified TimingFunction into computed TimingFunction without
// Context (for some FFIs in glue.rs). In fact, we don't really need Context to get the computed
// value of TimingFunction.
impl TimingFunction {
/// Generate the ComputedTimingFunction without Context.
pub fn to_computed_value_without_context(&self) -> ComputedTimingFunction {
match *self {
GenericTimingFunction::Steps(steps, pos) => {
GenericTimingFunction::Steps(steps.value(), pos)
},
GenericTimingFunction::CubicBezier { x1, y1, x2, y2 } => {
GenericTimingFunction::CubicBezier {
x1: x1.get(),
y1: y1.get(),
x2: x2.get(),
y2: y2.get(),
}
},
GenericTimingFunction::Keyword(keyword) => GenericTimingFunction::Keyword(keyword),
}
}
}

View file

@ -753,8 +753,8 @@ impl ToComputedValue for KeywordSize {
// The tables in this function are originally from
// nsRuleNode::CalcFontPointSize in Gecko:
//
// https://dxr.mozilla.org/mozilla-central/rev/35fbf14b9/layout/style/nsRuleNode.cpp#3262-3336
// https://searchfox.org/mozilla-central/rev/c05d9d61188d32b8/layout/style/nsRuleNode.cpp#3150
//
// Mapping from base size and HTML size to pixels
// The first index is (base_size - 9), the second is the
// HTML size. "0" is CSS keyword xx-small, not HTML size 0,

View file

@ -43,6 +43,7 @@ pub use self::box_::{OverflowClipBox, OverscrollBehavior, Perspective, Resize};
pub use self::box_::{ScrollSnapType, TouchAction, TransitionProperty, VerticalAlign, WillChange};
pub use self::color::{Color, ColorPropertyValue, RGBAColor};
pub use self::counters::{Content, ContentItem, CounterIncrement, CounterReset};
pub use self::easing::TimingFunction;
pub use self::effects::{BoxShadow, Filter, SimpleShadow};
pub use self::flex::FlexBasis;
#[cfg(feature = "gecko")]
@ -74,7 +75,7 @@ pub use self::text::{InitialLetter, LetterSpacing, LineHeight, MozTabSize, TextA
pub use self::text::{TextEmphasisPosition, TextEmphasisStyle};
pub use self::text::{TextAlignKeyword, TextDecorationLine, TextOverflow, WordSpacing};
pub use self::time::Time;
pub use self::transform::{Rotate, Scale, TimingFunction, Transform};
pub use self::transform::{Rotate, Scale, Transform};
pub use self::transform::{TransformOrigin, TransformStyle, Translate};
pub use self::ui::{ColorOrAuto, Cursor, MozForceBrokenImageIcon};
#[cfg(feature = "gecko")]
@ -93,6 +94,7 @@ pub mod calc;
pub mod color;
pub mod column;
pub mod counters;
pub mod easing;
pub mod effects;
pub mod flex;
pub mod font;

View file

@ -6,13 +6,11 @@
use cssparser::Parser;
use parser::{Parse, ParserContext};
use selectors::parser::SelectorParseErrorKind;
use style_traits::{ParseError, StyleParseErrorKind};
use values::computed::{Context, LengthOrPercentage as ComputedLengthOrPercentage};
use values::computed::{Percentage as ComputedPercentage, ToComputedValue};
use values::computed::transform::TimingFunction as ComputedTimingFunction;
use values::generics::transform as generic;
use values::generics::transform::{Matrix, Matrix3D, StepPosition, TimingKeyword};
use values::generics::transform::{Matrix, Matrix3D};
use values::specified::{self, Angle, Integer, Length, LengthOrPercentage, Number};
use values::specified::position::{Side, X, Y};
@ -243,9 +241,6 @@ pub enum OriginComponent<S> {
Side(S),
}
/// A specified timing function.
pub type TimingFunction = generic::TimingFunction<Integer, Number>;
impl Parse for TransformOrigin {
fn parse<'i, 't>(
context: &ParserContext,
@ -350,128 +345,6 @@ impl<S> OriginComponent<S> {
}
}
#[cfg(feature = "gecko")]
#[inline]
fn allow_frames_timing() -> bool {
use gecko_bindings::structs::mozilla;
unsafe { mozilla::StaticPrefs_sVarCache_layout_css_frames_timing_enabled }
}
#[cfg(feature = "servo")]
#[inline]
fn allow_frames_timing() -> bool {
true
}
impl Parse for TimingFunction {
fn parse<'i, 't>(
context: &ParserContext,
input: &mut Parser<'i, 't>,
) -> Result<Self, ParseError<'i>> {
if let Ok(keyword) = input.try(TimingKeyword::parse) {
return Ok(generic::TimingFunction::Keyword(keyword));
}
if let Ok(ident) = input.try(|i| i.expect_ident_cloned()) {
let position = match_ignore_ascii_case! { &ident,
"step-start" => StepPosition::Start,
"step-end" => StepPosition::End,
_ => return Err(input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone()))),
};
return Ok(generic::TimingFunction::Steps(Integer::new(1), position));
}
let location = input.current_source_location();
let function = input.expect_function()?.clone();
input.parse_nested_block(move |i| {
(match_ignore_ascii_case! { &function,
"cubic-bezier" => {
let x1 = Number::parse(context, i)?;
i.expect_comma()?;
let y1 = Number::parse(context, i)?;
i.expect_comma()?;
let x2 = Number::parse(context, i)?;
i.expect_comma()?;
let y2 = Number::parse(context, i)?;
if x1.get() < 0.0 || x1.get() > 1.0 || x2.get() < 0.0 || x2.get() > 1.0 {
return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError));
}
Ok(generic::TimingFunction::CubicBezier { x1, y1, x2, y2 })
},
"steps" => {
let steps = Integer::parse_positive(context, i)?;
let position = i.try(|i| {
i.expect_comma()?;
StepPosition::parse(i)
}).unwrap_or(StepPosition::End);
Ok(generic::TimingFunction::Steps(steps, position))
},
"frames" => {
if allow_frames_timing() {
let frames = Integer::parse_with_minimum(context, i, 2)?;
Ok(generic::TimingFunction::Frames(frames))
} else {
Err(())
}
},
_ => Err(()),
}).map_err(|()| {
location.new_custom_error(StyleParseErrorKind::UnexpectedFunction(function.clone()))
})
})
}
}
impl ToComputedValue for TimingFunction {
type ComputedValue = ComputedTimingFunction;
#[inline]
fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
match *self {
generic::TimingFunction::Keyword(keyword) => generic::TimingFunction::Keyword(keyword),
generic::TimingFunction::CubicBezier { x1, y1, x2, y2 } => {
generic::TimingFunction::CubicBezier {
x1: x1.to_computed_value(context),
y1: y1.to_computed_value(context),
x2: x2.to_computed_value(context),
y2: y2.to_computed_value(context),
}
},
generic::TimingFunction::Steps(steps, position) => {
generic::TimingFunction::Steps(steps.to_computed_value(context) as u32, position)
},
generic::TimingFunction::Frames(frames) => {
generic::TimingFunction::Frames(frames.to_computed_value(context) as u32)
},
}
}
#[inline]
fn from_computed_value(computed: &Self::ComputedValue) -> Self {
match *computed {
generic::TimingFunction::Keyword(keyword) => generic::TimingFunction::Keyword(keyword),
generic::TimingFunction::CubicBezier {
ref x1,
ref y1,
ref x2,
ref y2,
} => generic::TimingFunction::CubicBezier {
x1: Number::from_computed_value(x1),
y1: Number::from_computed_value(y1),
x2: Number::from_computed_value(x2),
y2: Number::from_computed_value(y2),
},
generic::TimingFunction::Steps(steps, position) => generic::TimingFunction::Steps(
Integer::from_computed_value(&(steps as i32)),
position,
),
generic::TimingFunction::Frames(frames) => {
generic::TimingFunction::Frames(Integer::from_computed_value(&(frames as i32)))
},
}
}
}
/// A specified CSS `rotate`
pub type Rotate = generic::Rotate<Number, Angle>;

View file

@ -35,19 +35,3 @@ fn test_steps() {
assert!(parse(transition_timing_function::parse, "steps(-1)").is_err());
assert!(parse(transition_timing_function::parse, "steps(1, middle)").is_err());
}
#[test]
fn test_frames() {
assert_roundtrip_with_context!(transition_timing_function::parse, "frames( 2 )", "frames(2)");
assert_roundtrip_with_context!(transition_timing_function::parse, "frames(10000)");
// Frames number must be an integer greater than 1
assert!(parse(transition_timing_function::parse, "frames(1)").is_err());
assert!(parse(transition_timing_function::parse, "frames(-2)").is_err());
assert!(parse(transition_timing_function::parse, "frames()").is_err());
assert!(parse(transition_timing_function::parse, "frames(,)").is_err());
assert!(parse(transition_timing_function::parse, "frames(a)").is_err());
assert!(parse(transition_timing_function::parse, "frames(2.0)").is_err());
assert!(parse(transition_timing_function::parse, "frames(2.5)").is_err());
assert!(parse(transition_timing_function::parse, "frames(2 3)").is_err());
}

View file

@ -707,86 +707,6 @@ mod shorthand_serialization {
}
}
mod transition {
pub use super::*;
#[test]
fn transition_should_serialize_all_available_properties() {
let block_text = "transition-property: margin-left; \
transition-duration: 3s; \
transition-delay: 4s; \
transition-timing-function: cubic-bezier(0.2, 5, 0.5, 2);";
let block = parse(|c, i| Ok(parse_property_declaration_list(c, i)), block_text).unwrap();
let serialization = block.to_css_string();
assert_eq!(serialization, "transition: margin-left 3s cubic-bezier(0.2, 5, 0.5, 2) 4s;");
}
#[test]
fn serialize_multiple_transitions() {
let block_text = "transition-property: margin-left, width; \
transition-duration: 3s, 2s; \
transition-delay: 4s, 5s; \
transition-timing-function: cubic-bezier(0.2, 5, 0.5, 2), ease;";
let block = parse(|c, i| Ok(parse_property_declaration_list(c, i)), block_text).unwrap();
let serialization = block.to_css_string();
assert_eq!(serialization, "transition: \
margin-left 3s cubic-bezier(0.2, 5, 0.5, 2) 4s, \
width 2s ease 5s;");
}
#[test]
fn serialize_multiple_transitions_unequal_property_lists() {
// When the lengths of property values are different, the shorthand serialization
// should not be used. Previously the implementation cycled values if the lists were
// uneven. This is incorrect, in that we should serialize to a shorthand only when the
// lists have the same length (this affects background, transition and animation).
// https://github.com/servo/servo/issues/15398 )
// The duration below has 1 extra value.
let block_text = "transition-property: margin-left, width; \
transition-duration: 3s, 2s, 4s; \
transition-delay: 4s, 5s; \
transition-timing-function: cubic-bezier(0.2, 5, 0.5, 2), ease;";
let block = parse(|c, i| Ok(parse_property_declaration_list(c, i)), block_text).unwrap();
let serialization = block.to_css_string();
assert_eq!(serialization, block_text);
}
#[test]
fn transition_should_serialize_acceptable_step_timing_function() {
let block_text = "transition-property: margin-left; \
transition-duration: 3s; \
transition-delay: 4s; \
transition-timing-function: steps(2, start);";
let block = parse(|c, i| Ok(parse_property_declaration_list(c, i)), block_text).unwrap();
let serialization = block.to_css_string();
assert_eq!(serialization, "transition: margin-left 3s steps(2, start) 4s;");
}
#[test]
fn transition_should_serialize_acceptable_frames_timing_function() {
let block_text = "transition-property: margin-left; \
transition-duration: 3s; \
transition-delay: 4s; \
transition-timing-function: frames(2);";
let block = parse(|c, i| Ok(parse_property_declaration_list(c, i)), block_text).unwrap();
let serialization = block.to_css_string();
assert_eq!(serialization, "transition: margin-left 3s frames(2) 4s;");
}
}
mod keywords {
pub use super::*;
#[test]

View file

@ -28,7 +28,7 @@ use style::stylesheets::keyframes_rule::{Keyframe, KeyframeSelector, KeyframePer
use style::values::{KeyframesName, CustomIdent};
use style::values::computed::Percentage;
use style::values::specified::{LengthOrPercentageOrAuto, PositionComponent};
use style::values::specified::transform::TimingFunction;
use style::values::specified::TimingFunction;
pub fn block_from<I>(iterable: I) -> PropertyDeclarationBlock
where I: IntoIterator<Item=(PropertyDeclaration, Importance)> {