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