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 Prelude = ();
245     type AtRule = Vec<ViewportDescriptorDeclaration>;
246     type Error = StyleParseErrorKind<'i>;
247 }
248 
249 impl<'a, 'b, 'i> DeclarationParser<'i> for ViewportRuleParser<'a, 'b> {
250     type Declaration = Vec<ViewportDescriptorDeclaration>;
251     type Error = StyleParseErrorKind<'i>;
252 
parse_value<'t>( &mut self, name: CowRcStr<'i>, input: &mut Parser<'i, 't>, ) -> Result<Vec<ViewportDescriptorDeclaration>, ParseError<'i>>253     fn parse_value<'t>(
254         &mut self,
255         name: CowRcStr<'i>,
256         input: &mut Parser<'i, 't>,
257     ) -> Result<Vec<ViewportDescriptorDeclaration>, ParseError<'i>> {
258         macro_rules! declaration {
259             ($declaration:ident($parse:expr)) => {
260                 declaration!($declaration {
261                     value: $parse(input)?,
262                     important: input.try_parse(parse_important).is_ok(),
263                 })
264             };
265             ($declaration:ident { value: $value:expr, important: $important:expr, }) => {
266                 ViewportDescriptorDeclaration::new(
267                     self.context.stylesheet_origin,
268                     ViewportDescriptor::$declaration($value),
269                     $important,
270                 )
271             };
272         }
273 
274         macro_rules! ok {
275             ($declaration:ident($parse:expr)) => {
276                 Ok(vec![declaration!($declaration($parse))])
277             };
278             (shorthand -> [$min:ident, $max:ident]) => {{
279                 let shorthand = parse_shorthand(self.context, input)?;
280                 let important = input.try_parse(parse_important).is_ok();
281 
282                 Ok(vec![
283                     declaration!($min {
284                         value: shorthand.0,
285                         important: important,
286                     }),
287                     declaration!($max {
288                         value: shorthand.1,
289                         important: important,
290                     }),
291                 ])
292             }};
293         }
294 
295         match_ignore_ascii_case! { &*name,
296             "min-width" => ok!(MinWidth(|i| ViewportLength::parse(self.context, i))),
297             "max-width" => ok!(MaxWidth(|i| ViewportLength::parse(self.context, i))),
298             "width" => ok!(shorthand -> [MinWidth, MaxWidth]),
299             "min-height" => ok!(MinHeight(|i| ViewportLength::parse(self.context, i))),
300             "max-height" => ok!(MaxHeight(|i| ViewportLength::parse(self.context, i))),
301             "height" => ok!(shorthand -> [MinHeight, MaxHeight]),
302             "zoom" => ok!(Zoom(Zoom::parse)),
303             "min-zoom" => ok!(MinZoom(Zoom::parse)),
304             "max-zoom" => ok!(MaxZoom(Zoom::parse)),
305             "user-zoom" => ok!(UserZoom(UserZoom::parse)),
306             "orientation" => ok!(Orientation(Orientation::parse)),
307             _ => Err(input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))),
308         }
309     }
310 }
311 
312 /// A `@viewport` rule.
313 #[derive(Clone, Debug, PartialEq, ToShmem)]
314 #[cfg_attr(feature = "servo", derive(MallocSizeOf))]
315 pub struct ViewportRule {
316     /// The declarations contained in this @viewport rule.
317     pub declarations: Vec<ViewportDescriptorDeclaration>,
318 }
319 
320 /// Whitespace as defined by DEVICE-ADAPT § 9.2
321 // TODO: should we just use whitespace as defined by HTML5?
322 const WHITESPACE: &'static [char] = &['\t', '\n', '\r', ' '];
323 
324 /// Separators as defined by DEVICE-ADAPT § 9.2
325 // need to use \x2c instead of ',' due to test-tidy
326 const SEPARATOR: &'static [char] = &['\x2c', ';'];
327 
328 #[inline]
is_whitespace_separator_or_equals(c: &char) -> bool329 fn is_whitespace_separator_or_equals(c: &char) -> bool {
330     WHITESPACE.contains(c) || SEPARATOR.contains(c) || *c == '='
331 }
332 
333 impl ViewportRule {
334     /// Parse a single @viewport rule.
335     ///
336     /// TODO(emilio): This could use the `Parse` trait now.
parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result<Self, ParseError<'i>>337     pub fn parse<'i, 't>(
338         context: &ParserContext,
339         input: &mut Parser<'i, 't>,
340     ) -> Result<Self, ParseError<'i>> {
341         let parser = ViewportRuleParser { context };
342 
343         let mut cascade = Cascade::new();
344         let mut parser = DeclarationListParser::new(input, parser);
345         while let Some(result) = parser.next() {
346             match result {
347                 Ok(declarations) => {
348                     for declarations in declarations {
349                         cascade.add(Cow::Owned(declarations))
350                     }
351                 },
352                 Err((error, slice)) => {
353                     let location = error.location;
354                     let error = ContextualParseError::UnsupportedViewportDescriptorDeclaration(
355                         slice, error,
356                     );
357                     context.log_css_error(location, error);
358                 },
359             }
360         }
361         Ok(ViewportRule {
362             declarations: cascade.finish(),
363         })
364     }
365 }
366 
367 impl ViewportRule {
368     #[allow(missing_docs)]
from_meta(content: &str) -> Option<ViewportRule>369     pub fn from_meta(content: &str) -> Option<ViewportRule> {
370         let mut declarations = vec![None; VIEWPORT_DESCRIPTOR_VARIANTS];
371         macro_rules! push_descriptor {
372             ($descriptor:ident($value:expr)) => {{
373                 let descriptor = ViewportDescriptor::$descriptor($value);
374                 let discriminant = descriptor.discriminant_value();
375                 declarations[discriminant] = Some(ViewportDescriptorDeclaration::new(
376                     Origin::Author,
377                     descriptor,
378                     false,
379                 ));
380             }};
381         }
382 
383         let mut has_width = false;
384         let mut has_height = false;
385         let mut has_zoom = false;
386 
387         let mut iter = content.chars().enumerate();
388 
389         macro_rules! start_of_name {
390             ($iter:ident) => {
391                 $iter
392                     .by_ref()
393                     .skip_while(|&(_, c)| is_whitespace_separator_or_equals(&c))
394                     .next()
395             };
396         }
397 
398         while let Some((start, _)) = start_of_name!(iter) {
399             let property = ViewportRule::parse_meta_property(content, &mut iter, start);
400 
401             if let Some((name, value)) = property {
402                 macro_rules! push {
403                     ($descriptor:ident($translate:path)) => {
404                         if let Some(value) = $translate(value) {
405                             push_descriptor!($descriptor(value));
406                         }
407                     };
408                 }
409 
410                 match name {
411                     n if n.eq_ignore_ascii_case("width") => {
412                         if let Some(value) = ViewportLength::from_meta(value) {
413                             push_descriptor!(MinWidth(ViewportLength::ExtendToZoom));
414                             push_descriptor!(MaxWidth(value));
415                             has_width = true;
416                         }
417                     },
418                     n if n.eq_ignore_ascii_case("height") => {
419                         if let Some(value) = ViewportLength::from_meta(value) {
420                             push_descriptor!(MinHeight(ViewportLength::ExtendToZoom));
421                             push_descriptor!(MaxHeight(value));
422                             has_height = true;
423                         }
424                     },
425                     n if n.eq_ignore_ascii_case("initial-scale") => {
426                         if let Some(value) = Zoom::from_meta(value) {
427                             push_descriptor!(Zoom(value));
428                             has_zoom = true;
429                         }
430                     },
431                     n if n.eq_ignore_ascii_case("minimum-scale") => push!(MinZoom(Zoom::from_meta)),
432                     n if n.eq_ignore_ascii_case("maximum-scale") => push!(MaxZoom(Zoom::from_meta)),
433                     n if n.eq_ignore_ascii_case("user-scalable") => {
434                         push!(UserZoom(UserZoom::from_meta))
435                     },
436                     _ => {},
437                 }
438             }
439         }
440 
441         // DEVICE-ADAPT § 9.4 - The 'width' and 'height' properties
442         // http://dev.w3.org/csswg/css-device-adapt/#width-and-height-properties
443         if !has_width && has_zoom {
444             if has_height {
445                 push_descriptor!(MinWidth(ViewportLength::Specified(
446                     LengthPercentageOrAuto::Auto
447                 )));
448                 push_descriptor!(MaxWidth(ViewportLength::Specified(
449                     LengthPercentageOrAuto::Auto
450                 )));
451             } else {
452                 push_descriptor!(MinWidth(ViewportLength::ExtendToZoom));
453                 push_descriptor!(MaxWidth(ViewportLength::ExtendToZoom));
454             }
455         }
456 
457         let declarations: Vec<_> = declarations.into_iter().filter_map(|entry| entry).collect();
458         if !declarations.is_empty() {
459             Some(ViewportRule {
460                 declarations: declarations,
461             })
462         } else {
463             None
464         }
465     }
466 
parse_meta_property<'a>( content: &'a str, iter: &mut Enumerate<Chars<'a>>, start: usize, ) -> Option<(&'a str, &'a str)>467     fn parse_meta_property<'a>(
468         content: &'a str,
469         iter: &mut Enumerate<Chars<'a>>,
470         start: usize,
471     ) -> Option<(&'a str, &'a str)> {
472         fn end_of_token(iter: &mut Enumerate<Chars>) -> Option<(usize, char)> {
473             iter.by_ref()
474                 .skip_while(|&(_, c)| !is_whitespace_separator_or_equals(&c))
475                 .next()
476         }
477 
478         fn skip_whitespace(iter: &mut Enumerate<Chars>) -> Option<(usize, char)> {
479             iter.by_ref()
480                 .skip_while(|&(_, c)| WHITESPACE.contains(&c))
481                 .next()
482         }
483 
484         // <name> <whitespace>* '='
485         let end = match end_of_token(iter) {
486             Some((end, c)) if WHITESPACE.contains(&c) => match skip_whitespace(iter) {
487                 Some((_, c)) if c == '=' => end,
488                 _ => return None,
489             },
490             Some((end, c)) if c == '=' => end,
491             _ => return None,
492         };
493         let name = &content[start..end];
494 
495         // <whitespace>* <value>
496         let start = match skip_whitespace(iter) {
497             Some((start, c)) if !SEPARATOR.contains(&c) => start,
498             _ => return None,
499         };
500         let value = match end_of_token(iter) {
501             Some((end, _)) => &content[start..end],
502             _ => &content[start..],
503         };
504 
505         Some((name, value))
506     }
507 }
508 
509 impl ToCssWithGuard for ViewportRule {
510     // Serialization of ViewportRule is not specced.
to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result511     fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
512         dest.write_str("@viewport { ")?;
513         let mut iter = self.declarations.iter();
514         iter.next().unwrap().to_css(&mut CssWriter::new(dest))?;
515         for declaration in iter {
516             dest.write_str(" ")?;
517             declaration.to_css(&mut CssWriter::new(dest))?;
518         }
519         dest.write_str(" }")
520     }
521 }
522 
523 #[allow(missing_docs)]
524 pub struct Cascade {
525     declarations: Vec<Option<(usize, ViewportDescriptorDeclaration)>>,
526     count_so_far: usize,
527 }
528 
529 #[allow(missing_docs)]
530 impl Cascade {
new() -> Self531     pub fn new() -> Self {
532         Cascade {
533             declarations: vec![None; VIEWPORT_DESCRIPTOR_VARIANTS],
534             count_so_far: 0,
535         }
536     }
537 
from_stylesheets<'a, I, S>( stylesheets: I, guards: &StylesheetGuards, device: &Device, ) -> Self where I: Iterator<Item = (&'a S, Origin)>, S: StylesheetInDocument + 'static,538     pub fn from_stylesheets<'a, I, S>(
539         stylesheets: I,
540         guards: &StylesheetGuards,
541         device: &Device,
542     ) -> Self
543     where
544         I: Iterator<Item = (&'a S, Origin)>,
545         S: StylesheetInDocument + 'static,
546     {
547         let mut cascade = Self::new();
548         for (stylesheet, origin) in stylesheets {
549             stylesheet.effective_viewport_rules(device, guards.for_origin(origin), |rule| {
550                 for declaration in &rule.declarations {
551                     cascade.add(Cow::Borrowed(declaration))
552                 }
553             })
554         }
555         cascade
556     }
557 
add(&mut self, declaration: Cow<ViewportDescriptorDeclaration>)558     pub fn add(&mut self, declaration: Cow<ViewportDescriptorDeclaration>) {
559         let descriptor = declaration.descriptor.discriminant_value();
560 
561         match self.declarations[descriptor] {
562             Some((ref mut order_of_appearance, ref mut entry_declaration)) => {
563                 if declaration.higher_or_equal_precendence(entry_declaration) {
564                     *entry_declaration = declaration.into_owned();
565                     *order_of_appearance = self.count_so_far;
566                 }
567             },
568             ref mut entry @ None => {
569                 *entry = Some((self.count_so_far, declaration.into_owned()));
570             },
571         }
572         self.count_so_far += 1;
573     }
574 
finish(mut self) -> Vec<ViewportDescriptorDeclaration>575     pub fn finish(mut self) -> Vec<ViewportDescriptorDeclaration> {
576         // sort the descriptors by order of appearance
577         self.declarations
578             .sort_by_key(|entry| entry.as_ref().map(|&(index, _)| index));
579         self.declarations
580             .into_iter()
581             .filter_map(|entry| entry.map(|(_, decl)| decl))
582             .collect()
583     }
584 }
585 
586 /// Just a helper trait to be able to implement methods on ViewportConstraints.
587 pub trait MaybeNew {
588     /// Create a ViewportConstraints from a viewport size and a `@viewport`
589     /// rule.
maybe_new( device: &Device, rule: &ViewportRule, quirks_mode: QuirksMode, ) -> Option<ViewportConstraints>590     fn maybe_new(
591         device: &Device,
592         rule: &ViewportRule,
593         quirks_mode: QuirksMode,
594     ) -> Option<ViewportConstraints>;
595 }
596 
597 impl MaybeNew for ViewportConstraints {
maybe_new( device: &Device, rule: &ViewportRule, quirks_mode: QuirksMode, ) -> Option<ViewportConstraints>598     fn maybe_new(
599         device: &Device,
600         rule: &ViewportRule,
601         quirks_mode: QuirksMode,
602     ) -> Option<ViewportConstraints> {
603         use std::cmp;
604 
605         if rule.declarations.is_empty() {
606             return None;
607         }
608 
609         let mut min_width = None;
610         let mut max_width = None;
611 
612         let mut min_height = None;
613         let mut max_height = None;
614 
615         let mut initial_zoom = None;
616         let mut min_zoom = None;
617         let mut max_zoom = None;
618 
619         let mut user_zoom = UserZoom::Zoom;
620         let mut orientation = Orientation::Auto;
621 
622         // collapse the list of declarations into descriptor values
623         for declaration in &rule.declarations {
624             match declaration.descriptor {
625                 ViewportDescriptor::MinWidth(ref value) => min_width = Some(value),
626                 ViewportDescriptor::MaxWidth(ref value) => max_width = Some(value),
627 
628                 ViewportDescriptor::MinHeight(ref value) => min_height = Some(value),
629                 ViewportDescriptor::MaxHeight(ref value) => max_height = Some(value),
630 
631                 ViewportDescriptor::Zoom(value) => initial_zoom = value.to_f32(),
632                 ViewportDescriptor::MinZoom(value) => min_zoom = value.to_f32(),
633                 ViewportDescriptor::MaxZoom(value) => max_zoom = value.to_f32(),
634 
635                 ViewportDescriptor::UserZoom(value) => user_zoom = value,
636                 ViewportDescriptor::Orientation(value) => orientation = value,
637             }
638         }
639 
640         // TODO: return `None` if all descriptors are either absent or initial value
641 
642         macro_rules! choose {
643             ($op:ident, $opta:expr, $optb:expr) => {
644                 match ($opta, $optb) {
645                     (None, None) => None,
646                     (a, None) => a,
647                     (None, b) => b,
648                     (Some(a), Some(b)) => Some(a.$op(b)),
649                 }
650             };
651         }
652         macro_rules! min {
653             ($opta:expr, $optb:expr) => {
654                 choose!(min, $opta, $optb)
655             };
656         }
657         macro_rules! max {
658             ($opta:expr, $optb:expr) => {
659                 choose!(max, $opta, $optb)
660             };
661         }
662 
663         // DEVICE-ADAPT § 6.2.1 Resolve min-zoom and max-zoom values
664         if min_zoom.is_some() && max_zoom.is_some() {
665             max_zoom = Some(min_zoom.unwrap().max(max_zoom.unwrap()))
666         }
667 
668         // DEVICE-ADAPT § 6.2.2 Constrain zoom value to the [min-zoom, max-zoom] range
669         if initial_zoom.is_some() {
670             initial_zoom = max!(min_zoom, min!(max_zoom, initial_zoom));
671         }
672 
673         // DEVICE-ADAPT § 6.2.3 Resolve non-auto lengths to pixel lengths
674         let initial_viewport = device.au_viewport_size();
675 
676         let provider = get_metrics_provider_for_product();
677 
678         let mut conditions = RuleCacheConditions::default();
679         let context = Context {
680             // Note: DEVICE-ADAPT § 5. states that relative length values are
681             // resolved against initial values
682             builder: StyleBuilder::for_inheritance(device, None, None),
683             font_metrics_provider: &provider,
684             cached_system_font: None,
685             in_media_query: false,
686             quirks_mode: quirks_mode,
687             for_smil_animation: false,
688             for_non_inherited_property: None,
689             rule_cache_conditions: RefCell::new(&mut conditions),
690         };
691 
692         // DEVICE-ADAPT § 9.3 Resolving 'extend-to-zoom'
693         let extend_width;
694         let extend_height;
695         if let Some(extend_zoom) = max!(initial_zoom, max_zoom) {
696             let scale_factor = 1. / extend_zoom;
697             extend_width = Some(initial_viewport.width.scale_by(scale_factor));
698             extend_height = Some(initial_viewport.height.scale_by(scale_factor));
699         } else {
700             extend_width = None;
701             extend_height = None;
702         }
703 
704         macro_rules! to_pixel_length {
705             ($value:ident, $dimension:ident, $extend_to:ident => $auto_extend_to:expr) => {
706                 if let Some($value) = $value {
707                     match *$value {
708                         ViewportLength::Specified(ref length) => match *length {
709                             LengthPercentageOrAuto::Auto => None,
710                             LengthPercentageOrAuto::LengthPercentage(ref lop) => Some(
711                                 lop.to_computed_value(&context)
712                                     .to_used_value(initial_viewport.$dimension),
713                             ),
714                         },
715                         ViewportLength::ExtendToZoom => {
716                             // $extend_to will be 'None' if 'extend-to-zoom' is 'auto'
717                             match ($extend_to, $auto_extend_to) {
718                                 (None, None) => None,
719                                 (a, None) => a,
720                                 (None, b) => b,
721                                 (a, b) => cmp::max(a, b),
722                             }
723                         },
724                     }
725                 } else {
726                     None
727                 }
728             };
729         }
730 
731         // DEVICE-ADAPT § 9.3 states that max-descriptors need to be resolved
732         // before min-descriptors.
733         // http://dev.w3.org/csswg/css-device-adapt/#resolve-extend-to-zoom
734         let max_width = to_pixel_length!(max_width, width, extend_width => None);
735         let max_height = to_pixel_length!(max_height, height, extend_height => None);
736 
737         let min_width = to_pixel_length!(min_width, width, extend_width => max_width);
738         let min_height = to_pixel_length!(min_height, height, extend_height => max_height);
739 
740         // DEVICE-ADAPT § 6.2.4 Resolve initial width and height from min/max descriptors
741         macro_rules! resolve {
742             ($min:ident, $max:ident, $initial:expr) => {
743                 if $min.is_some() || $max.is_some() {
744                     let max = match $max {
745                         Some(max) => cmp::min(max, $initial),
746                         None => $initial,
747                     };
748 
749                     Some(match $min {
750                         Some(min) => cmp::max(min, max),
751                         None => max,
752                     })
753                 } else {
754                     None
755                 }
756             };
757         }
758 
759         let width = resolve!(min_width, max_width, initial_viewport.width);
760         let height = resolve!(min_height, max_height, initial_viewport.height);
761 
762         // DEVICE-ADAPT § 6.2.5 Resolve width value
763         let width = if width.is_none() && height.is_none() {
764             Some(initial_viewport.width)
765         } else {
766             width
767         };
768 
769         let width = width.unwrap_or_else(|| match initial_viewport.height {
770             Au(0) => initial_viewport.width,
771             initial_height => {
772                 let ratio = initial_viewport.width.to_f32_px() / initial_height.to_f32_px();
773                 Au::from_f32_px(height.unwrap().to_f32_px() * ratio)
774             },
775         });
776 
777         // DEVICE-ADAPT § 6.2.6 Resolve height value
778         let height = height.unwrap_or_else(|| match initial_viewport.width {
779             Au(0) => initial_viewport.height,
780             initial_width => {
781                 let ratio = initial_viewport.height.to_f32_px() / initial_width.to_f32_px();
782                 Au::from_f32_px(width.to_f32_px() * ratio)
783             },
784         });
785 
786         Some(ViewportConstraints {
787             size: Size2D::new(width.to_f32_px(), height.to_f32_px()),
788 
789             // TODO: compute a zoom factor for 'auto' as suggested by DEVICE-ADAPT § 10.
790             initial_zoom: PinchZoomFactor::new(initial_zoom.unwrap_or(1.)),
791             min_zoom: min_zoom.map(PinchZoomFactor::new),
792             max_zoom: max_zoom.map(PinchZoomFactor::new),
793 
794             user_zoom: user_zoom,
795             orientation: orientation,
796         })
797     }
798 }
799