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