style: Share custom property declarations if they don't contain variable references.

This commit is contained in:
Emilio Cobos Álvarez 2017-10-08 23:31:46 +02:00
parent 7e143372bd
commit a6eaa0812a
No known key found for this signature in database
GPG key ID: 056B727BB9C1027C
8 changed files with 180 additions and 135 deletions

1
Cargo.lock generated
View file

@ -3234,6 +3234,7 @@ dependencies = [
"malloc_size_of_derive 0.0.1", "malloc_size_of_derive 0.0.1",
"selectors 0.19.0", "selectors 0.19.0",
"serde 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)",
"servo_arc 0.0.1",
"servo_atoms 0.0.1", "servo_atoms 0.0.1",
"webrender_api 0.52.0 (git+https://github.com/servo/webrender)", "webrender_api 0.52.0 (git+https://github.com/servo/webrender)",
] ]

View file

@ -10,7 +10,7 @@ use Atom;
use cssparser::{Delimiter, Parser, ParserInput, SourcePosition, Token, TokenSerializationType}; use cssparser::{Delimiter, Parser, ParserInput, SourcePosition, Token, TokenSerializationType};
use precomputed_hash::PrecomputedHash; use precomputed_hash::PrecomputedHash;
use properties::{CSSWideKeyword, DeclaredValue}; use properties::{CSSWideKeyword, DeclaredValue};
use selector_map::{PrecomputedHashSet, PrecomputedDiagnosticHashMap}; use selector_map::{PrecomputedHashSet, PrecomputedHashMap, PrecomputedDiagnosticHashMap};
use selectors::parser::SelectorParseError; use selectors::parser::SelectorParseError;
use servo_arc::Arc; use servo_arc::Arc;
use std::ascii::AsciiExt; use std::ascii::AsciiExt;
@ -35,14 +35,14 @@ pub fn parse_name(s: &str) -> Result<&str, ()> {
} }
} }
/// A specified value for a custom property is just a set of tokens. /// A value for a custom property is just a set of tokens.
/// ///
/// We preserve the original CSS for serialization, and also the variable /// We preserve the original CSS for serialization, and also the variable
/// references to other custom property names. /// references to other custom property names.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "gecko", derive(MallocSizeOf))] #[cfg_attr(feature = "gecko", derive(MallocSizeOf))]
#[cfg_attr(feature = "servo", derive(HeapSizeOf))] #[cfg_attr(feature = "servo", derive(HeapSizeOf))]
pub struct SpecifiedValue { pub struct VariableValue {
css: String, css: String,
first_token_type: TokenSerializationType, first_token_type: TokenSerializationType,
@ -52,24 +52,6 @@ pub struct SpecifiedValue {
references: PrecomputedHashSet<Name>, references: PrecomputedHashSet<Name>,
} }
/// This struct is a cheap borrowed version of a `SpecifiedValue`.
pub struct BorrowedSpecifiedValue<'a> {
css: &'a str,
first_token_type: TokenSerializationType,
last_token_type: TokenSerializationType,
references: &'a PrecomputedHashSet<Name>,
}
/// A computed value is just a set of tokens as well, until we resolve variables
/// properly.
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "servo", derive(HeapSizeOf))]
pub struct ComputedValue {
css: String,
first_token_type: TokenSerializationType,
last_token_type: TokenSerializationType,
}
impl ToCss for SpecifiedValue { impl ToCss for SpecifiedValue {
fn to_css<W>(&self, dest: &mut W) -> fmt::Result fn to_css<W>(&self, dest: &mut W) -> fmt::Result
where W: fmt::Write, where W: fmt::Write,
@ -78,14 +60,6 @@ impl ToCss for SpecifiedValue {
} }
} }
impl ToCss for ComputedValue {
fn to_css<W>(&self, dest: &mut W) -> fmt::Result
where W: fmt::Write,
{
dest.write_str(&self.css)
}
}
/// A map from CSS variable names to CSS variable computed values, used for /// A map from CSS variable names to CSS variable computed values, used for
/// resolving. /// resolving.
/// ///
@ -93,7 +67,17 @@ impl ToCss for ComputedValue {
/// DOM. CSSDeclarations expose property names as indexed properties, which /// DOM. CSSDeclarations expose property names as indexed properties, which
/// need to be stable. So we keep an array of property names which order is /// need to be stable. So we keep an array of property names which order is
/// determined on the order that they are added to the name-value map. /// determined on the order that they are added to the name-value map.
pub type CustomPropertiesMap = OrderedMap<Name, ComputedValue>; ///
/// The variable values are guaranteed to not have references to other
/// properties.
pub type CustomPropertiesMap = OrderedMap<Name, Arc<VariableValue>>;
/// Both specified and computed values are VariableValues, the difference is
/// whether var() functions are expanded.
pub type SpecifiedValue = VariableValue;
/// Both specified and computed values are VariableValues, the difference is
/// whether var() functions are expanded.
pub type ComputedValue = VariableValue;
/// A map that preserves order for the keys, and that is easily indexable. /// A map that preserves order for the keys, and that is easily indexable.
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@ -136,6 +120,11 @@ where
value value
} }
/// Get whether there's a value on the map for `key`.
pub fn contains_key(&self, key: &K) -> bool {
self.values.contains_key(key)
}
/// Get the key located at the given index. /// Get the key located at the given index.
pub fn get_key_at(&self, index: u32) -> Option<&K> { pub fn get_key_at(&self, index: u32) -> Option<&K> {
self.index.get(index as usize) self.index.get(index as usize)
@ -211,22 +200,30 @@ where
} }
} }
impl ComputedValue { impl VariableValue {
fn empty() -> ComputedValue { fn empty() -> Self {
ComputedValue { Self {
css: String::new(), css: String::new(),
last_token_type: TokenSerializationType::nothing(), last_token_type: TokenSerializationType::nothing(),
first_token_type: TokenSerializationType::nothing(), first_token_type: TokenSerializationType::nothing(),
references: PrecomputedHashSet::default(),
} }
} }
fn push(&mut self, css: &str, css_first_token_type: TokenSerializationType, fn push(
css_last_token_type: TokenSerializationType) { &mut self,
// This happens e.g. between to subsequent var() functions: `var(--a)var(--b)`. css: &str,
// In that case, css_*_token_type is non-sensical. css_first_token_type: TokenSerializationType,
css_last_token_type: TokenSerializationType
) {
// This happens e.g. between two subsequent var() functions:
// `var(--a)var(--b)`.
//
// In that case, css_*_token_type is nonsensical.
if css.is_empty() { if css.is_empty() {
return return
} }
self.first_token_type.set_if_nothing(css_first_token_type); self.first_token_type.set_if_nothing(css_first_token_type);
// If self.first_token_type was nothing, // If self.first_token_type was nothing,
// self.last_token_type is also nothing and this will be false: // self.last_token_type is also nothing and this will be false:
@ -237,27 +234,32 @@ impl ComputedValue {
self.last_token_type = css_last_token_type self.last_token_type = css_last_token_type
} }
fn push_from(&mut self, position: (SourcePosition, TokenSerializationType), fn push_from(
input: &Parser, last_token_type: TokenSerializationType) { &mut self,
position: (SourcePosition, TokenSerializationType),
input: &Parser,
last_token_type: TokenSerializationType
) {
self.push(input.slice_from(position.0), position.1, last_token_type) self.push(input.slice_from(position.0), position.1, last_token_type)
} }
fn push_variable(&mut self, variable: &ComputedValue) { fn push_variable(&mut self, variable: &ComputedValue) {
debug_assert!(variable.references.is_empty());
self.push(&variable.css, variable.first_token_type, variable.last_token_type) self.push(&variable.css, variable.first_token_type, variable.last_token_type)
} }
} }
impl SpecifiedValue { impl VariableValue {
/// Parse a custom property SpecifiedValue. /// Parse a custom property value.
pub fn parse<'i, 't>( pub fn parse<'i, 't>(
input: &mut Parser<'i, 't>, input: &mut Parser<'i, 't>,
) -> Result<Box<Self>, ParseError<'i>> { ) -> Result<Arc<Self>, ParseError<'i>> {
let mut references = PrecomputedHashSet::default(); let mut references = PrecomputedHashSet::default();
let (first_token_type, css, last_token_type) = let (first_token_type, css, last_token_type) =
parse_self_contained_declaration_value(input, Some(&mut references))?; parse_self_contained_declaration_value(input, Some(&mut references))?;
Ok(Box::new(SpecifiedValue { Ok(Arc::new(VariableValue {
css: css.into_owned(), css: css.into_owned(),
first_token_type, first_token_type,
last_token_type, last_token_type,
@ -467,7 +469,7 @@ fn parse_var_function<'i, 't>(
pub struct CustomPropertiesBuilder<'a> { pub struct CustomPropertiesBuilder<'a> {
seen: PrecomputedHashSet<&'a Name>, seen: PrecomputedHashSet<&'a Name>,
may_have_cycles: bool, may_have_cycles: bool,
specified_custom_properties: OrderedMap<&'a Name, Option<BorrowedSpecifiedValue<'a>>>, custom_properties: Option<CustomPropertiesMap>,
inherited: Option<&'a Arc<CustomPropertiesMap>>, inherited: Option<&'a Arc<CustomPropertiesMap>>,
} }
@ -477,7 +479,7 @@ impl<'a> CustomPropertiesBuilder<'a> {
Self { Self {
seen: PrecomputedHashSet::default(), seen: PrecomputedHashSet::default(),
may_have_cycles: false, may_have_cycles: false,
specified_custom_properties: OrderedMap::new(), custom_properties: None,
inherited, inherited,
} }
} }
@ -486,27 +488,30 @@ impl<'a> CustomPropertiesBuilder<'a> {
pub fn cascade( pub fn cascade(
&mut self, &mut self,
name: &'a Name, name: &'a Name,
specified_value: DeclaredValue<'a, Box<SpecifiedValue>>, specified_value: DeclaredValue<'a, Arc<SpecifiedValue>>,
) { ) {
let was_already_present = !self.seen.insert(name); let was_already_present = !self.seen.insert(name);
if was_already_present { if was_already_present {
return; return;
} }
if self.custom_properties.is_none() {
self.custom_properties = Some(match self.inherited {
Some(inherited) => (**inherited).clone(),
None => CustomPropertiesMap::new(),
})
}
let map = self.custom_properties.as_mut().unwrap();
match specified_value { match specified_value {
DeclaredValue::Value(ref specified_value) => { DeclaredValue::Value(ref specified_value) => {
self.may_have_cycles |= !specified_value.references.is_empty(); self.may_have_cycles |= !specified_value.references.is_empty();
self.specified_custom_properties.insert(name, Some(BorrowedSpecifiedValue { map.insert(name.clone(), (*specified_value).clone());
css: &specified_value.css,
first_token_type: specified_value.first_token_type,
last_token_type: specified_value.last_token_type,
references: &specified_value.references,
}));
}, },
DeclaredValue::WithVariables(_) => unreachable!(), DeclaredValue::WithVariables(_) => unreachable!(),
DeclaredValue::CSSWideKeyword(keyword) => match keyword { DeclaredValue::CSSWideKeyword(keyword) => match keyword {
CSSWideKeyword::Initial => { CSSWideKeyword::Initial => {
self.specified_custom_properties.insert(name, None); map.remove(name);
} }
CSSWideKeyword::Unset | // Custom properties are inherited by default. CSSWideKeyword::Unset | // Custom properties are inherited by default.
CSSWideKeyword::Inherit => {} // The inherited value is what we already have. CSSWideKeyword::Inherit => {} // The inherited value is what we already have.
@ -521,14 +526,16 @@ impl<'a> CustomPropertiesBuilder<'a> {
/// ///
/// Otherwise, just use the inherited custom properties map. /// Otherwise, just use the inherited custom properties map.
pub fn build(mut self) -> Option<Arc<CustomPropertiesMap>> { pub fn build(mut self) -> Option<Arc<CustomPropertiesMap>> {
if self.specified_custom_properties.is_empty() { let mut map = match self.custom_properties.take() {
return self.inherited.cloned(); Some(m) => m,
} None => return self.inherited.cloned(),
};
if self.may_have_cycles { if self.may_have_cycles {
remove_cycles(&mut self.specified_custom_properties); remove_cycles(&mut map);
substitute_all(&mut map);
} }
Some(Arc::new(substitute_all(self.specified_custom_properties, self.inherited))) Some(Arc::new(map))
} }
} }
@ -536,7 +543,7 @@ impl<'a> CustomPropertiesBuilder<'a> {
/// ///
/// The initial value of a custom property is represented by this property not /// The initial value of a custom property is represented by this property not
/// being in the map. /// being in the map.
fn remove_cycles(map: &mut OrderedMap<&Name, Option<BorrowedSpecifiedValue>>) { fn remove_cycles(map: &mut CustomPropertiesMap) {
let mut to_remove = PrecomputedHashSet::default(); let mut to_remove = PrecomputedHashSet::default();
{ {
let mut visited = PrecomputedHashSet::default(); let mut visited = PrecomputedHashSet::default();
@ -545,7 +552,7 @@ fn remove_cycles(map: &mut OrderedMap<&Name, Option<BorrowedSpecifiedValue>>) {
walk(map, name, &mut stack, &mut visited, &mut to_remove); walk(map, name, &mut stack, &mut visited, &mut to_remove);
fn walk<'a>( fn walk<'a>(
map: &OrderedMap<&'a Name, Option<BorrowedSpecifiedValue<'a>>>, map: &'a CustomPropertiesMap,
name: &'a Name, name: &'a Name,
stack: &mut Vec<&'a Name>, stack: &mut Vec<&'a Name>,
visited: &mut PrecomputedHashSet<&'a Name>, visited: &mut PrecomputedHashSet<&'a Name>,
@ -555,11 +562,11 @@ fn remove_cycles(map: &mut OrderedMap<&Name, Option<BorrowedSpecifiedValue>>) {
if already_visited_before { if already_visited_before {
return return
} }
if let Some(&Some(ref value)) = map.get(&name) { if let Some(ref value) = map.get(name) {
if !value.references.is_empty() { if !value.references.is_empty() {
stack.push(name); stack.push(name);
for next in value.references { for next in value.references.iter() {
if let Some(position) = stack.iter().position(|&x| x == next) { if let Some(position) = stack.iter().position(|x| *x == next) {
// Found a cycle // Found a cycle
for &in_cycle in &stack[position..] { for &in_cycle in &stack[position..] {
to_remove.insert(in_cycle.clone()); to_remove.insert(in_cycle.clone());
@ -580,97 +587,116 @@ fn remove_cycles(map: &mut OrderedMap<&Name, Option<BorrowedSpecifiedValue>>) {
} }
/// Replace `var()` functions for all custom properties. /// Replace `var()` functions for all custom properties.
fn substitute_all( fn substitute_all(custom_properties_map: &mut CustomPropertiesMap) {
specified_values_map: OrderedMap<&Name, Option<BorrowedSpecifiedValue>>, // FIXME(emilio): This stash is needed because we can't prove statically to
inherited: Option<&Arc<CustomPropertiesMap>>, // rustc that we don't try to mutate the same variable from two recursive
) -> CustomPropertiesMap { // `substitute_one` calls.
let mut custom_properties_map = match inherited { //
None => CustomPropertiesMap::new(), // If this is really really hot, we may be able to cheat using `unsafe`, I
Some(inherited) => (**inherited).clone(), // guess...
}; let mut stash = PrecomputedHashMap::default();
let mut invalid = PrecomputedHashSet::default(); let mut invalid = PrecomputedHashSet::default();
for (name, value) in specified_values_map.iter() { for (name, value) in custom_properties_map.iter() {
let value = match *value { if !value.references.is_empty() && !stash.contains_key(name) {
Some(ref v) => v, let _ = substitute_one(
None => { name,
custom_properties_map.remove(*name); value,
continue; custom_properties_map,
} None,
}; &mut stash,
&mut invalid,
let _ = substitute_one( );
name, }
value,
&specified_values_map,
None,
&mut custom_properties_map,
&mut invalid,
);
} }
custom_properties_map for (name, value) in stash.drain() {
custom_properties_map.insert(name, value);
}
for name in invalid.drain() {
custom_properties_map.remove(&name);
}
debug_assert!(custom_properties_map.iter().all(|(_, v)| v.references.is_empty()));
} }
/// Replace `var()` functions for one custom property. /// Replace `var()` functions for one custom property, leaving the result in
/// Also recursively record results for other custom properties referenced by `var()` functions. /// `stash`.
/// Return `Err(())` for invalid at computed time. ///
/// or `Ok(last_token_type that was pushed to partial_computed_value)` otherwise. /// Also recursively record results for other custom properties referenced by
/// `var()` functions.
///
/// Return `Err(())` for invalid at computed time. or `Ok(last_token_type that
/// was pushed to partial_computed_value)` otherwise.
fn substitute_one( fn substitute_one(
name: &Name, name: &Name,
specified_value: &BorrowedSpecifiedValue, specified_value: &Arc<VariableValue>,
specified_values_map: &OrderedMap<&Name, Option<BorrowedSpecifiedValue>>, custom_properties: &CustomPropertiesMap,
partial_computed_value: Option<&mut ComputedValue>, partial_computed_value: Option<&mut VariableValue>,
custom_properties_map: &mut CustomPropertiesMap, stash: &mut PrecomputedHashMap<Name, Arc<VariableValue>>,
invalid: &mut PrecomputedHashSet<Name>, invalid: &mut PrecomputedHashSet<Name>,
) -> Result<TokenSerializationType, ()> { ) -> Result<TokenSerializationType, ()> {
debug_assert!(!specified_value.references.is_empty());
debug_assert!(!stash.contains_key(name));
if invalid.contains(name) { if invalid.contains(name) {
return Err(()); return Err(());
} }
let computed_value = if specified_value.references.is_empty() { let mut computed_value = ComputedValue::empty();
// The specified value contains no var() reference let mut input = ParserInput::new(&specified_value.css);
ComputedValue { let mut input = Parser::new(&mut input);
css: specified_value.css.to_owned(), let mut position = (input.position(), specified_value.first_token_type);
first_token_type: specified_value.first_token_type,
last_token_type: specified_value.last_token_type, let result = substitute_block(
} &mut input,
} else { &mut position,
let mut partial_computed_value = ComputedValue::empty(); &mut computed_value,
let mut input = ParserInput::new(&specified_value.css); &mut |name, partial_computed_value| {
let mut input = Parser::new(&mut input); if let Some(already_computed) = stash.get(name) {
let mut position = (input.position(), specified_value.first_token_type); partial_computed_value.push_variable(already_computed);
let result = substitute_block( return Ok(already_computed.last_token_type);
&mut input, &mut position, &mut partial_computed_value,
&mut |name, partial_computed_value| {
if let Some(other_specified_value) = specified_values_map.get(&name).and_then(|v| v.as_ref()) {
substitute_one(
name,
other_specified_value,
specified_values_map,
Some(partial_computed_value),
custom_properties_map,
invalid,
)
} else {
Err(())
}
} }
);
if let Ok(last_token_type) = result { let other_specified_value = match custom_properties.get(name) {
partial_computed_value.push_from(position, &input, last_token_type); Some(v) => v,
partial_computed_value None => return Err(()),
} else { };
if other_specified_value.references.is_empty() {
partial_computed_value.push_variable(other_specified_value);
return Ok(other_specified_value.last_token_type);
}
substitute_one(
name,
other_specified_value,
custom_properties,
Some(partial_computed_value),
stash,
invalid
)
}
);
match result {
Ok(last_token_type) => {
computed_value.push_from(position, &input, last_token_type);
}
Err(..) => {
invalid.insert(name.clone()); invalid.insert(name.clone());
return Err(()) return Err(())
} }
}; }
if let Some(partial_computed_value) = partial_computed_value { if let Some(partial_computed_value) = partial_computed_value {
partial_computed_value.push_variable(&computed_value) partial_computed_value.push_variable(&computed_value)
} }
let last_token_type = computed_value.last_token_type; let last_token_type = computed_value.last_token_type;
custom_properties_map.insert(name.clone(), computed_value); stash.insert(name.clone(), Arc::new(computed_value));
Ok(last_token_type) Ok(last_token_type)
} }

View file

@ -1398,7 +1398,12 @@ pub enum PropertyDeclaration {
), ),
/// A custom property declaration, with the property name and the declared /// A custom property declaration, with the property name and the declared
/// value. /// value.
Custom(::custom_properties::Name, DeclaredValueOwned<Box<::custom_properties::SpecifiedValue>>), #[cfg_attr(feature = "gecko", ignore_malloc_size_of = "XXX: how to handle this?")]
Custom(
::custom_properties::Name,
#[cfg_attr(feature = "gecko", ignore_malloc_size_of = "XXX: how to handle this?")]
DeclaredValueOwned<Arc<::custom_properties::SpecifiedValue>>
),
} }
impl fmt::Debug for PropertyDeclaration { impl fmt::Debug for PropertyDeclaration {

View file

@ -9,6 +9,7 @@
use Atom; use Atom;
use cssparser::serialize_identifier; use cssparser::serialize_identifier;
use custom_properties; use custom_properties;
use servo_arc::Arc;
use std::fmt; use std::fmt;
use style_traits::ToCss; use style_traits::ToCss;
@ -153,7 +154,8 @@ pub struct PaintWorklet {
pub name: Atom, pub name: Atom,
/// The arguments for the worklet. /// The arguments for the worklet.
/// TODO: store a parsed representation of the arguments. /// TODO: store a parsed representation of the arguments.
pub arguments: Vec<custom_properties::SpecifiedValue>, #[cfg_attr(feature = "servo", ignore_heap_size_of = "Arc")]
pub arguments: Vec<Arc<custom_properties::SpecifiedValue>>,
} }
trivial_to_computed_value!(PaintWorklet); trivial_to_computed_value!(PaintWorklet);

View file

@ -917,7 +917,7 @@ impl Parse for PaintWorklet {
let name = Atom::from(&**input.expect_ident()?); let name = Atom::from(&**input.expect_ident()?);
let arguments = input.try(|input| { let arguments = input.try(|input| {
input.expect_comma()?; input.expect_comma()?;
input.parse_comma_separated(|input| Ok(*SpecifiedValue::parse(input)?)) input.parse_comma_separated(|input| SpecifiedValue::parse(input))
}).unwrap_or(vec![]); }).unwrap_or(vec![]);
Ok(PaintWorklet { name, arguments }) Ok(PaintWorklet { name, arguments })
}) })

View file

@ -26,3 +26,4 @@ selectors = { path = "../selectors" }
serde = {version = "1.0", optional = true} serde = {version = "1.0", optional = true}
webrender_api = {git = "https://github.com/servo/webrender", optional = true} webrender_api = {git = "https://github.com/servo/webrender", optional = true}
servo_atoms = {path = "../atoms", optional = true} servo_atoms = {path = "../atoms", optional = true}
servo_arc = {path = "../servo_arc" }

View file

@ -24,6 +24,7 @@ extern crate euclid;
extern crate selectors; extern crate selectors;
#[cfg(feature = "servo")] #[macro_use] extern crate serde; #[cfg(feature = "servo")] #[macro_use] extern crate serde;
#[cfg(feature = "servo")] extern crate webrender_api; #[cfg(feature = "servo")] extern crate webrender_api;
extern crate servo_arc;
#[cfg(feature = "servo")] extern crate servo_atoms; #[cfg(feature = "servo")] extern crate servo_atoms;
#[cfg(feature = "servo")] pub use webrender_api::DevicePixel; #[cfg(feature = "servo")] pub use webrender_api::DevicePixel;

View file

@ -7,6 +7,7 @@
use app_units::Au; use app_units::Au;
use cssparser::{BasicParseError, ParseError, Parser, Token, UnicodeRange, serialize_string}; use cssparser::{BasicParseError, ParseError, Parser, Token, UnicodeRange, serialize_string};
use cssparser::ToCss as CssparserToCss; use cssparser::ToCss as CssparserToCss;
use servo_arc::Arc;
use std::fmt::{self, Write}; use std::fmt::{self, Write};
/// Serialises a value according to its CSS representation. /// Serialises a value according to its CSS representation.
@ -343,6 +344,14 @@ impl<T> ToCss for Box<T> where T: ?Sized + ToCss {
} }
} }
impl<T> ToCss for Arc<T> where T: ?Sized + ToCss {
fn to_css<W>(&self, dest: &mut W) -> fmt::Result
where W: Write,
{
(**self).to_css(dest)
}
}
impl ToCss for Au { impl ToCss for Au {
fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: Write { fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: Write {
self.to_f64_px().to_css(dest)?; self.to_f64_px().to_css(dest)?;