1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4 
5 //! The [`@viewport`][at] at-rule and [`meta`][meta] element.
6 //!
7 //! [at]: https://drafts.csswg.org/css-device-adapt/#atviewport-rule
8 //! [meta]: https://drafts.csswg.org/css-device-adapt/#viewport-meta
9 
10 use crate::context::QuirksMode;
11 use crate::error_reporting::ContextualParseError;
12 use crate::font_metrics::get_metrics_provider_for_product;
13 use crate::media_queries::Device;
14 use crate::parser::{Parse, ParserContext};
15 use crate::properties::StyleBuilder;
16 use crate::rule_cache::RuleCacheConditions;
17 use crate::shared_lock::{SharedRwLockReadGuard, StylesheetGuards, ToCssWithGuard};
18 use crate::str::CssStringWriter;
19 use crate::stylesheets::cascading_at_rule::DescriptorDeclaration;
20 use crate::stylesheets::{Origin, StylesheetInDocument};
21 use crate::values::computed::{Context, ToComputedValue};
22 use crate::values::generics::length::LengthPercentageOrAuto;
23 use crate::values::generics::NonNegative;
24 use crate::values::specified::{self, NoCalcLength};
25 use crate::values::specified::{NonNegativeLengthPercentageOrAuto, ViewportPercentageLength};
26 use app_units::Au;
27 use cssparser::CowRcStr;
28 use cssparser::{parse_important, AtRuleParser, DeclarationListParser, DeclarationParser, Parser};
29 use euclid::Size2D;
30 use selectors::parser::SelectorParseErrorKind;
31 use std::borrow::Cow;
32 use std::cell::RefCell;
33 use std::fmt::{self, Write};
34 use std::iter::Enumerate;
35 use std::str::Chars;
36 use style_traits::viewport::{Orientation, UserZoom, ViewportConstraints, Zoom};
37 use style_traits::{CssWriter, ParseError, PinchZoomFactor, StyleParseErrorKind, ToCss};
38 
39 /// Whether parsing and processing of `@viewport` rules is enabled.
40 #[cfg(feature = "servo")]
enabled() -> bool41 pub fn enabled() -> bool {
42     use servo_config::pref;
43     pref!(layout.viewport.enabled)
44 }
45 
46 /// Whether parsing and processing of `@viewport` rules is enabled.
47 #[cfg(not(feature = "servo"))]
enabled() -> bool48 pub fn enabled() -> bool {
49     false // Gecko doesn't support @viewport.
50 }
51 
52 macro_rules! declare_viewport_descriptor {
53     ( $( $variant_name: expr => $variant: ident($data: ident), )+ ) => {
54          declare_viewport_descriptor_inner!([] [ $( $variant_name => $variant($data), )+ ] 0);
55     };
56 }
57 
58 macro_rules! declare_viewport_descriptor_inner {
59     (
60         [ $( $assigned_variant_name: expr =>
61              $assigned_variant: ident($assigned_data: ident) = $assigned_discriminant: expr, )* ]
62         [
63             $next_variant_name: expr => $next_variant: ident($next_data: ident),
64             $( $variant_name: expr => $variant: ident($data: ident), )*
65         ]
66         $next_discriminant: expr
67     ) => {
68         declare_viewport_descriptor_inner! {
69             [
70                 $( $assigned_variant_name => $assigned_variant($assigned_data) = $assigned_discriminant, )*
71                 $next_variant_name => $next_variant($next_data) = $next_discriminant,
72             ]
73             [ $( $variant_name => $variant($data), )* ]
74             $next_discriminant + 1
75         }
76     };
77 
78     (
79         [ $( $assigned_variant_name: expr =>
80              $assigned_variant: ident($assigned_data: ident) = $assigned_discriminant: expr, )* ]
81         [ ]
82         $number_of_variants: expr
83     ) => {
84         #[derive(Clone, Debug, PartialEq, ToShmem)]
85         #[cfg_attr(feature = "servo", derive(MallocSizeOf))]
86         #[allow(missing_docs)]
87         pub enum ViewportDescriptor {
88             $(
89                 $assigned_variant($assigned_data),
90             )+
91         }
92 
93         const VIEWPORT_DESCRIPTOR_VARIANTS: usize = $number_of_variants;
94 
95         impl ViewportDescriptor {
96             #[allow(missing_docs)]
97             pub fn discriminant_value(&self) -> usize {
98                 match *self {
99                     $(
100                         ViewportDescriptor::$assigned_variant(..) => $assigned_discriminant,
101                     )*
102                 }
103             }
104         }
105 
106         impl ToCss for ViewportDescriptor {
107             fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
108             where
109                 W: Write,
110             {
111                 match *self {
112                     $(
113                         ViewportDescriptor::$assigned_variant(ref val) => {
114                             dest.write_str($assigned_variant_name)?;
115                             dest.write_str(": ")?;
116                             val.to_css(dest)?;
117                         },
118                     )*
119                 }
120                 dest.write_str(";")
121             }
122         }
123     };
124 }
125 
126 declare_viewport_descriptor! {
127     "min-width" => MinWidth(ViewportLength),
128     "max-width" => MaxWidth(ViewportLength),
129 
130     "min-height" => MinHeight(ViewportLength),
131     "max-height" => MaxHeight(ViewportLength),
132 
133     "zoom" => Zoom(Zoom),
134     "min-zoom" => MinZoom(Zoom),
135     "max-zoom" => MaxZoom(Zoom),
136 
137     "user-zoom" => UserZoom(UserZoom),
138     "orientation" => Orientation(Orientation),
139 }
140 
141 trait FromMeta: Sized {
from_meta(value: &str) -> Option<Self>142     fn from_meta(value: &str) -> Option<Self>;
143 }
144 
145 /// ViewportLength is a length | percentage | auto | extend-to-zoom
146 /// See:
147 /// * http://dev.w3.org/csswg/css-device-adapt/#min-max-width-desc
148 /// * http://dev.w3.org/csswg/css-device-adapt/#extend-to-zoom
149 #[allow(missing_docs)]
150 #[cfg_attr(feature = "servo", derive(MallocSizeOf))]
151 #[derive(Clone, Debug, PartialEq, ToCss, ToShmem)]
152 pub enum ViewportLength {
153     Specified(NonNegativeLengthPercentageOrAuto),
154     ExtendToZoom,
155 }
156 
157 impl FromMeta for ViewportLength {
from_meta(value: &str) -> Option<ViewportLength>158     fn from_meta(value: &str) -> Option<ViewportLength> {
159         macro_rules! specified {
160             ($value:expr) => {
161                 ViewportLength::Specified(LengthPercentageOrAuto::LengthPercentage(NonNegative(
162                     specified::LengthPercentage::Length($value),
163                 )))
164             };
165         }
166 
167         Some(match value {
168             v if v.eq_ignore_ascii_case("device-width") => specified!(
169                 NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vw(100.))
170             ),
171             v if v.eq_ignore_ascii_case("device-height") => specified!(
172                 NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vh(100.))
173             ),
174             _ => match value.parse::<f32>() {
175                 Ok(n) if n >= 0. => specified!(NoCalcLength::from_px(n.max(1.).min(10000.))),
176                 Ok(_) => return None,
177                 Err(_) => specified!(NoCalcLength::from_px(1.)),
178             },
179         })
180     }
181 }
182 
183 impl ViewportLength {
parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result<Self, ParseError<'i>>184     fn parse<'i, 't>(
185         context: &ParserContext,
186         input: &mut Parser<'i, 't>,
187     ) -> Result<Self, ParseError<'i>> {
188         // we explicitly do not accept 'extend-to-zoom', since it is a UA
189         // internal value for <META> viewport translation
190         NonNegativeLengthPercentageOrAuto::parse(context, input).map(ViewportLength::Specified)
191     }
192 }
193 
194 impl FromMeta for Zoom {
from_meta(value: &str) -> Option<Zoom>195     fn from_meta(value: &str) -> Option<Zoom> {
196         Some(match value {
197             v if v.eq_ignore_ascii_case("yes") => Zoom::Number(1.),
198             v if v.eq_ignore_ascii_case("no") => Zoom::Number(0.1),
199             v if v.eq_ignore_ascii_case("device-width") => Zoom::Number(10.),
200             v if v.eq_ignore_ascii_case("device-height") => Zoom::Number(10.),
201             _ => match value.parse::<f32>() {
202                 Ok(n) if n >= 0. => Zoom::Number(n.max(0.1).min(10.)),
203                 Ok(_) => return None,
204                 Err(_) => Zoom::Number(0.1),
205             },
206         })
207     }
208 }
209 
210 impl FromMeta for UserZoom {
from_meta(value: &str) -> Option<UserZoom>211     fn from_meta(value: &str) -> Option<UserZoom> {
212         Some(match value {
213             v if v.eq_ignore_ascii_case("yes") => UserZoom::Zoom,
214             v if v.eq_ignore_ascii_case("no") => UserZoom::Fixed,
215             v if v.eq_ignore_ascii_case("device-width") => UserZoom::Zoom,
216             v if v.eq_ignore_ascii_case("device-height") => UserZoom::Zoom,
217             _ => match value.parse::<f32>() {
218                 Ok(n) if n >= 1. || n <= -1. => UserZoom::Zoom,
219                 _ => UserZoom::Fixed,
220             },
221         })
222     }
223 }
224 
225 struct ViewportRuleParser<'a, 'b: 'a> {
226     context: &'a ParserContext<'b>,
227 }
228 
229 #[allow(missing_docs)]
230 pub type ViewportDescriptorDeclaration = DescriptorDeclaration<ViewportDescriptor>;
231 
parse_shorthand<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result<(ViewportLength, ViewportLength), ParseError<'i>>232 fn parse_shorthand<'i, 't>(
233     context: &ParserContext,
234     input: &mut Parser<'i, 't>,
235 ) -> Result<(ViewportLength, ViewportLength), ParseError<'i>> {
236     let min = ViewportLength::parse(context, input)?;
237     match input.try_parse(|i| ViewportLength::parse(context, i)) {
238         Err(_) => Ok((min.clone(), min)),
239         Ok(max) => Ok((min, max)),
240     }
241 }
242 
243 impl<'a, 'b, 'i> AtRuleParser<'i> for ViewportRuleParser<'a, 'b> {
244     type PreludeNoBlock = ();
245     type PreludeBlock = ();
246     type AtRule = Vec<ViewportDescriptorDeclaration>;
247     type Error = StyleParseErrorKind<'i>;
248 }
249 
250 impl<'a, 'b, 'i> DeclarationParser<'i> for ViewportRuleParser<'a, 'b> {
251     type Declaration = Vec<ViewportDescriptorDeclaration>;
252     type Error = StyleParseErrorKind<'i>;
253 
parse_value<'t>( &mut self, name: CowRcStr<'i>, input: &mut Parser<'i, 't>, ) -> Result<Vec<ViewportDescriptorDeclaration>, ParseError<'i>>254     fn parse_value<'t>(
255         &mut self,
256         name: CowRcStr<'i>,
257         input: &mut Parser<'i, 't>,
258     ) -> Result<Vec<ViewportDescriptorDeclaration>, ParseError<'i>> {
259         macro_rules! declaration {
260             ($declaration:ident($parse:expr)) => {
261                 declaration!($declaration {
262                     value: $parse(input)?,
263                     important: input.try_parse(parse_important).is_ok(),
264                 })
265             };
266             ($declaration:ident { value: $value:expr, important: $important:expr, }) => {
267                 ViewportDescriptorDeclaration::new(
268                     self.context.stylesheet_origin,
269                     ViewportDescriptor::$declaration($value),
270                     $important,
271                 )
272             };
273         }
274 
275         macro_rules! ok {
276             ($declaration:ident($parse:expr)) => {
277                 Ok(vec![declaration!($declaration($parse))])
278             };
279             (shorthand -> [$min:ident, $max:ident]) => {{
280                 let shorthand = parse_shorthand(self.context, input)?;
281                 let important = input.try_parse(parse_important).is_ok();
282 
283                 Ok(vec![
284                     declaration!($min {
285                         value: shorthand.0,
286                         important: important,
287                     }),
288                     declaration!($max {
289                         value: shorthand.1,
290                         important: important,
291                     }),
292                 ])
293             }};
294         }
295 
296         match_ignore_ascii_case! { &*name,
297             "min-width" => ok!(MinWidth(|i| ViewportLength::parse(self.context, i))),
298             "max-width" => ok!(MaxWidth(|i| ViewportLength::parse(self.context, i))),
299             "width" => ok!(shorthand -> [MinWidth, MaxWidth]),
300             "min-height" => ok!(MinHeight(|i| ViewportLength::parse(self.context, i))),
301             "max-height" => ok!(MaxHeight(|i| ViewportLength::parse(self.context, i))),
302             "height" => ok!(shorthand -> [MinHeight, MaxHeight]),
303             "zoom" => ok!(Zoom(Zoom::parse)),
304             "min-zoom" => ok!(MinZoom(Zoom::parse)),
305             "max-zoom" => ok!(MaxZoom(Zoom::parse)),
306             "user-zoom" => ok!(UserZoom(UserZoom::parse)),
307             "orientation" => ok!(Orientation(Orientation::parse)),
308             _ => Err(input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))),
309         }
310     }
311 }
312 
313 /// A `@viewport` rule.
314 #[derive(Clone, Debug, PartialEq, ToShmem)]
315 #[cfg_attr(feature = "servo", derive(MallocSizeOf))]
316 pub struct ViewportRule {
317     /// The declarations contained in this @viewport rule.
318     pub declarations: Vec<ViewportDescriptorDeclaration>,
319 }
320 
321 /// Whitespace as defined by DEVICE-ADAPT § 9.2
322 // TODO: should we just use whitespace as defined by HTML5?
323 const WHITESPACE: &'static [char] = &['\t', '\n', '\r', ' '];
324 
325 /// Separators as defined by DEVICE-ADAPT § 9.2
326 // need to use \x2c instead of ',' due to test-tidy
327 const SEPARATOR: &'static [char] = &['\x2c', ';'];
328 
329 #[inline]
is_whitespace_separator_or_equals(c: &char) -> bool330 fn is_whitespace_separator_or_equals(c: &char) -> bool {
331     WHITESPACE.contains(c) || SEPARATOR.contains(c) || *c == '='
332 }
333 
334 impl ViewportRule {
335     /// Parse a single @viewport rule.
336     ///
337     /// TODO(emilio): This could use the `Parse` trait now.
parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result<Self, ParseError<'i>>338     pub fn parse<'i, 't>(
339         context: &ParserContext,
340         input: &mut Parser<'i, 't>,
341     ) -> Result<Self, ParseError<'i>> {
342         let parser = ViewportRuleParser { context };
343 
344         let mut cascade = Cascade::new();
345         let mut parser = DeclarationListParser::new(input, parser);
346         while let Some(result) = parser.next() {
347             match result {
348                 Ok(declarations) => {
349                     for declarations in declarations {
350                         cascade.add(Cow::Owned(declarations))
351                     }
352                 },
353                 Err((error, slice)) => {
354                     let location = error.location;
355                     let error = ContextualParseError::UnsupportedViewportDescriptorDeclaration(
356                         slice, error,
357                     );
358                     context.log_css_error(location, error);
359                 },
360             }
361         }
362         Ok(ViewportRule {
363             declarations: cascade.finish(),
364         })
365     }
366 }
367 
368 impl ViewportRule {
369     #[allow(missing_docs)]
from_meta(content: &str) -> Option<ViewportRule>370     pub fn from_meta(content: &str) -> Option<ViewportRule> {
371         let mut declarations = vec![None; VIEWPORT_DESCRIPTOR_VARIANTS];
372         macro_rules! push_descriptor {
373             ($descriptor:ident($value:expr)) => {{
374                 let descriptor = ViewportDescriptor::$descriptor($value);
375                 let discriminant = descriptor.discriminant_value();
376                 declarations[discriminant] = Some(ViewportDescriptorDeclaration::new(
377                     Origin::Author,
378                     descriptor,
379                     false,
380                 ));
381             }};
382         }
383 
384         let mut has_width = false;
385         let mut has_height = false;
386         let mut has_zoom = false;
387 
388         let mut iter = content.chars().enumerate();
389 
390         macro_rules! start_of_name {
391             ($iter:ident) => {
392                 $iter
393                     .by_ref()
394                     .skip_while(|&(_, c)| is_whitespace_separator_or_equals(&c))
395                     .next()
396             };
397         }
398 
399         while let Some((start, _)) = start_of_name!(iter) {
400             let property = ViewportRule::parse_meta_property(content, &mut iter, start);
401 
402             if let Some((name, value)) = property {
403                 macro_rules! push {
404                     ($descriptor:ident($translate:path)) => {
405                         if let Some(value) = $translate(value) {
406                             push_descriptor!($descriptor(value));
407                         }
408                     };
409                 }
410 
411                 match name {
412                     n if n.eq_ignore_ascii_case("width") => {
413                         if let Some(value) = ViewportLength::from_meta(value) {
414                             push_descriptor!(MinWidth(ViewportLength::ExtendToZoom));
415                             push_descriptor!(MaxWidth(value));
416                             has_width = true;
417                         }
418                     },
419                     n if n.eq_ignore_ascii_case("height") => {
420                         if let Some(value) = ViewportLength::from_meta(value) {
421                             push_descriptor!(MinHeight(ViewportLength::ExtendToZoom));
422                             push_descriptor!(MaxHeight(value));
423                             has_height = true;
424                         }
425                     },
426                     n if n.eq_ignore_ascii_case("initial-scale") => {
427                         if let Some(value) = Zoom::from_meta(value) {
428                             push_descriptor!(Zoom(value));
429                             has_zoom = true;
430                         }
431                     },
432                     n if n.eq_ignore_ascii_case("minimum-scale") => push!(MinZoom(Zoom::from_meta)),
433                     n if n.eq_ignore_ascii_case("maximum-scale") => push!(MaxZoom(Zoom::from_meta)),
434                     n if n.eq_ignore_ascii_case("user-scalable") => {
435                         push!(UserZoom(UserZoom::from_meta))
436                     },
437                     _ => {},
438                 }
439             }
440         }
441 
442         // DEVICE-ADAPT § 9.4 - The 'width' and 'height' properties
443         // http://dev.w3.org/csswg/css-device-adapt/#width-and-height-properties
444         if !has_width && has_zoom {
445             if has_height {
446                 push_descriptor!(MinWidth(ViewportLength::Specified(
447                     LengthPercentageOrAuto::Auto
448                 )));
449                 push_descriptor!(MaxWidth(ViewportLength::Specified(
450                     LengthPercentageOrAuto::Auto
451                 )));
452             } else {
453                 push_descriptor!(MinWidth(ViewportLength::ExtendToZoom));
454                 push_descriptor!(MaxWidth(ViewportLength::ExtendToZoom));
455             }
456         }
457 
458         let declarations: Vec<_> = declarations.into_iter().filter_map(|entry| entry).collect();
459         if !declarations.is_empty() {
460             Some(ViewportRule {
461                 declarations: declarations,
462             })
463         } else {
464             None
465         }
466     }
467 
parse_meta_property<'a>( content: &'a str, iter: &mut Enumerate<Chars<'a>>, start: usize, ) -> Option<(&'a str, &'a str)>468     fn parse_meta_property<'a>(
469         content: &'a str,
470         iter: &mut Enumerate<Chars<'a>>,
471         start: usize,
472     ) -> Option<(&'a str, &'a str)> {
473         fn end_of_token(iter: &mut Enumerate<Chars>) -> Option<(usize, char)> {
474             iter.by_ref()
475                 .skip_while(|&(_, c)| !is_whitespace_separator_or_equals(&c))
476                 .next()
477         }
478 
479         fn skip_whitespace(iter: &mut Enumerate<Chars>) -> Option<(usize, char)> {
480             iter.by_ref()
481                 .skip_while(|&(_, c)| WHITESPACE.contains(&c))
482                 .next()
483         }
484 
485         // <name> <whitespace>* '='
486         let end = match end_of_token(iter) {
487             Some((end, c)) if WHITESPACE.contains(&c) => match skip_whitespace(iter) {
488                 Some((_, c)) if c == '=' => end,
489                 _ => return None,
490             },
491             Some((end, c)) if c == '=' => end,
492             _ => return None,
493         };
494         let name = &content[start..end];
495 
496         // <whitespace>* <value>
497         let start = match skip_whitespace(iter) {
498             Some((start, c)) if !SEPARATOR.contains(&c) => start,
499             _ => return None,
500         };
501         let value = match end_of_token(iter) {
502             Some((end, _)) => &content[start..end],
503             _ => &content[start..],
504         };
505 
506         Some((name, value))
507     }
508 }
509 
510 impl ToCssWithGuard for ViewportRule {
511     // Serialization of ViewportRule is not specced.
to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result512     fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
513         dest.write_str("@viewport { ")?;
514         let mut iter = self.declarations.iter();
515         iter.next().unwrap().to_css(&mut CssWriter::new(dest))?;
516         for declaration in iter {
517             dest.write_str(" ")?;
518             declaration.to_css(&mut CssWriter::new(dest))?;
519         }
520         dest.write_str(" }")
521     }
522 }
523 
524 #[allow(missing_docs)]
525 pub struct Cascade {
526     declarations: Vec<Option<(usize, ViewportDescriptorDeclaration)>>,
527     count_so_far: usize,
528 }
529 
530 #[allow(missing_docs)]
531 impl Cascade {
new() -> Self532     pub fn new() -> Self {
533         Cascade {
534             declarations: vec![None; VIEWPORT_DESCRIPTOR_VARIANTS],
535             count_so_far: 0,
536         }
537     }
538 
from_stylesheets<'a, I, S>( stylesheets: I, guards: &StylesheetGuards, device: &Device, ) -> Self where I: Iterator<Item = (&'a S, Origin)>, S: StylesheetInDocument + 'static,539     pub fn from_stylesheets<'a, I, S>(
540         stylesheets: I,
541         guards: &StylesheetGuards,
542         device: &Device,
543     ) -> Self
544     where
545         I: Iterator<Item = (&'a S, Origin)>,
546         S: StylesheetInDocument + 'static,
547     {
548         let mut cascade = Self::new();
549         for (stylesheet, origin) in stylesheets {
550             stylesheet.effective_viewport_rules(device, guards.for_origin(origin), |rule| {
551                 for declaration in &rule.declarations {
552                     cascade.add(Cow::Borrowed(declaration))
553                 }
554             })
555         }
556         cascade
557     }
558 
add(&mut self, declaration: Cow<ViewportDescriptorDeclaration>)559     pub fn add(&mut self, declaration: Cow<ViewportDescriptorDeclaration>) {
560         let descriptor = declaration.descriptor.discriminant_value();
561 
562         match self.declarations[descriptor] {
563             Some((ref mut order_of_appearance, ref mut entry_declaration)) => {
564                 if declaration.higher_or_equal_precendence(entry_declaration) {
565                     *entry_declaration = declaration.into_owned();
566                     *order_of_appearance = self.count_so_far;
567                 }
568             },
569             ref mut entry @ None => {
570                 *entry = Some((self.count_so_far, declaration.into_owned()));
571             },
572         }
573         self.count_so_far += 1;
574     }
575 
finish(mut self) -> Vec<ViewportDescriptorDeclaration>576     pub fn finish(mut self) -> Vec<ViewportDescriptorDeclaration> {
577         // sort the descriptors by order of appearance
578         self.declarations
579             .sort_by_key(|entry| entry.as_ref().map(|&(index, _)| index));
580         self.declarations
581             .into_iter()
582             .filter_map(|entry| entry.map(|(_, decl)| decl))
583             .collect()
584     }
585 }
586 
587 /// Just a helper trait to be able to implement methods on ViewportConstraints.
588 pub trait MaybeNew {
589     /// Create a ViewportConstraints from a viewport size and a `@viewport`
590     /// rule.
maybe_new( device: &Device, rule: &ViewportRule, quirks_mode: QuirksMode, ) -> Option<ViewportConstraints>591     fn maybe_new(
592         device: &Device,
593         rule: &ViewportRule,
594         quirks_mode: QuirksMode,
595     ) -> Option<ViewportConstraints>;
596 }
597 
598 impl MaybeNew for ViewportConstraints {
maybe_new( device: &Device, rule: &ViewportRule, quirks_mode: QuirksMode, ) -> Option<ViewportConstraints>599     fn maybe_new(
600         device: &Device,
601         rule: &ViewportRule,
602         quirks_mode: QuirksMode,
603     ) -> Option<ViewportConstraints> {
604         use std::cmp;
605 
606         if rule.declarations.is_empty() {
607             return None;
608         }
609 
610         let mut min_width = None;
611         let mut max_width = None;
612 
613         let mut min_height = None;
614         let mut max_height = None;
615 
616         let mut initial_zoom = None;
617         let mut min_zoom = None;
618         let mut max_zoom = None;
619 
620         let mut user_zoom = UserZoom::Zoom;
621         let mut orientation = Orientation::Auto;
622 
623         // collapse the list of declarations into descriptor values
624         for declaration in &rule.declarations {
625             match declaration.descriptor {
626                 ViewportDescriptor::MinWidth(ref value) => min_width = Some(value),
627                 ViewportDescriptor::MaxWidth(ref value) => max_width = Some(value),
628 
629                 ViewportDescriptor::MinHeight(ref value) => min_height = Some(value),
630                 ViewportDescriptor::MaxHeight(ref value) => max_height = Some(value),
631 
632                 ViewportDescriptor::Zoom(value) => initial_zoom = value.to_f32(),
633                 ViewportDescriptor::MinZoom(value) => min_zoom = value.to_f32(),
634                 ViewportDescriptor::MaxZoom(value) => max_zoom = value.to_f32(),
635 
636                 ViewportDescriptor::UserZoom(value) => user_zoom = value,
637                 ViewportDescriptor::Orientation(value) => orientation = value,
638             }
639         }
640 
641         // TODO: return `None` if all descriptors are either absent or initial value
642 
643         macro_rules! choose {
644             ($op:ident, $opta:expr, $optb:expr) => {
645                 match ($opta, $optb) {
646                     (None, None) => None,
647                     (a, None) => a,
648                     (None, b) => b,
649                     (Some(a), Some(b)) => Some(a.$op(b)),
650                 }
651             };
652         }
653         macro_rules! min {
654             ($opta:expr, $optb:expr) => {
655                 choose!(min, $opta, $optb)
656             };
657         }
658         macro_rules! max {
659             ($opta:expr, $optb:expr) => {
660                 choose!(max, $opta, $optb)
661             };
662         }
663 
664         // DEVICE-ADAPT § 6.2.1 Resolve min-zoom and max-zoom values
665         if min_zoom.is_some() && max_zoom.is_some() {
666             max_zoom = Some(min_zoom.unwrap().max(max_zoom.unwrap()))
667         }
668 
669         // DEVICE-ADAPT § 6.2.2 Constrain zoom value to the [min-zoom, max-zoom] range
670         if initial_zoom.is_some() {
671             initial_zoom = max!(min_zoom, min!(max_zoom, initial_zoom));
672         }
673 
674         // DEVICE-ADAPT § 6.2.3 Resolve non-auto lengths to pixel lengths
675         let initial_viewport = device.au_viewport_size();
676 
677         let provider = get_metrics_provider_for_product();
678 
679         let mut conditions = RuleCacheConditions::default();
680         let context = Context {
681             // Note: DEVICE-ADAPT § 5. states that relative length values are
682             // resolved against initial values
683             builder: StyleBuilder::for_inheritance(device, None, None),
684             font_metrics_provider: &provider,
685             cached_system_font: None,
686             in_media_query: false,
687             quirks_mode: quirks_mode,
688             for_smil_animation: false,
689             for_non_inherited_property: None,
690             rule_cache_conditions: RefCell::new(&mut conditions),
691         };
692 
693         // DEVICE-ADAPT § 9.3 Resolving 'extend-to-zoom'
694         let extend_width;
695         let extend_height;
696         if let Some(extend_zoom) = max!(initial_zoom, max_zoom) {
697             let scale_factor = 1. / extend_zoom;
698             extend_width = Some(initial_viewport.width.scale_by(scale_factor));
699             extend_height = Some(initial_viewport.height.scale_by(scale_factor));
700         } else {
701             extend_width = None;
702             extend_height = None;
703         }
704 
705         macro_rules! to_pixel_length {
706             ($value:ident, $dimension:ident, $extend_to:ident => $auto_extend_to:expr) => {
707                 if let Some($value) = $value {
708                     match *$value {
709                         ViewportLength::Specified(ref length) => match *length {
710                             LengthPercentageOrAuto::Auto => None,
711                             LengthPercentageOrAuto::LengthPercentage(ref lop) => Some(
712                                 lop.to_computed_value(&context)
713                                     .to_used_value(initial_viewport.$dimension),
714                             ),
715                         },
716                         ViewportLength::ExtendToZoom => {
717                             // $extend_to will be 'None' if 'extend-to-zoom' is 'auto'
718                             match ($extend_to, $auto_extend_to) {
719                                 (None, None) => None,
720                                 (a, None) => a,
721                                 (None, b) => b,
722                                 (a, b) => cmp::max(a, b),
723                             }
724                         },
725                     }
726                 } else {
727                     None
728                 }
729             };
730         }
731 
732         // DEVICE-ADAPT § 9.3 states that max-descriptors need to be resolved
733         // before min-descriptors.
734         // http://dev.w3.org/csswg/css-device-adapt/#resolve-extend-to-zoom
735         let max_width = to_pixel_length!(max_width, width, extend_width => None);
736         let max_height = to_pixel_length!(max_height, height, extend_height => None);
737 
738         let min_width = to_pixel_length!(min_width, width, extend_width => max_width);
739         let min_height = to_pixel_length!(min_height, height, extend_height => max_height);
740 
741         // DEVICE-ADAPT § 6.2.4 Resolve initial width and height from min/max descriptors
742         macro_rules! resolve {
743             ($min:ident, $max:ident, $initial:expr) => {
744                 if $min.is_some() || $max.is_some() {
745                     let max = match $max {
746                         Some(max) => cmp::min(max, $initial),
747                         None => $initial,
748                     };
749 
750                     Some(match $min {
751                         Some(min) => cmp::max(min, max),
752                         None => max,
753                     })
754                 } else {
755                     None
756                 };
757             };
758         }
759 
760         let width = resolve!(min_width, max_width, initial_viewport.width);
761         let height = resolve!(min_height, max_height, initial_viewport.height);
762 
763         // DEVICE-ADAPT § 6.2.5 Resolve width value
764         let width = if width.is_none() && height.is_none() {
765             Some(initial_viewport.width)
766         } else {
767             width
768         };
769 
770         let width = width.unwrap_or_else(|| match initial_viewport.height {
771             Au(0) => initial_viewport.width,
772             initial_height => {
773                 let ratio = initial_viewport.width.to_f32_px() / initial_height.to_f32_px();
774                 Au::from_f32_px(height.unwrap().to_f32_px() * ratio)
775             },
776         });
777 
778         // DEVICE-ADAPT § 6.2.6 Resolve height value
779         let height = height.unwrap_or_else(|| match initial_viewport.width {
780             Au(0) => initial_viewport.height,
781             initial_width => {
782                 let ratio = initial_viewport.height.to_f32_px() / initial_width.to_f32_px();
783                 Au::from_f32_px(width.to_f32_px() * ratio)
784             },
785         });
786 
787         Some(ViewportConstraints {
788             size: Size2D::new(width.to_f32_px(), height.to_f32_px()),
789 
790             // TODO: compute a zoom factor for 'auto' as suggested by DEVICE-ADAPT § 10.
791             initial_zoom: PinchZoomFactor::new(initial_zoom.unwrap_or(1.)),
792             min_zoom: min_zoom.map(PinchZoomFactor::new),
793             max_zoom: max_zoom.map(PinchZoomFactor::new),
794 
795             user_zoom: user_zoom,
796             orientation: orientation,
797         })
798     }
799 }
800