diff --git a/components/style/Cargo.toml b/components/style/Cargo.toml index 96438546fc58..0022d09714c3 100644 --- a/components/style/Cargo.toml +++ b/components/style/Cargo.toml @@ -38,3 +38,4 @@ url = "0.2.16" mod_path = "0.1" bitflags = "*" cssparser = "0.3.1" +num = "0.1.24" diff --git a/components/style/lib.rs b/components/style/lib.rs index f0c3e17c5505..59a936dd737d 100644 --- a/components/style/lib.rs +++ b/components/style/lib.rs @@ -34,6 +34,7 @@ extern crate selectors; #[macro_use] extern crate lazy_static; +extern crate num; extern crate util; diff --git a/components/style/viewport.rs b/components/style/viewport.rs index e9588b27bc63..9115b9a8b26e 100644 --- a/components/style/viewport.rs +++ b/components/style/viewport.rs @@ -3,8 +3,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use cssparser::{Parser, DeclarationListParser, AtRuleParser, DeclarationParser, ToCss, parse_important}; +use geom::size::{Size2D, TypedSize2D}; +use geom::scale_factor::ScaleFactor; use parser::{ParserContext, log_css_error}; +use properties::longhands; use stylesheets::Origin; +use util::geometry::{Au, PagePx, ViewportPx}; use values::specified::{AllowedNumericType, Length, LengthOrPercentageOrAuto}; use std::ascii::AsciiExt; @@ -300,3 +304,206 @@ impl<'a, I> ViewportDescriptorDeclarationCascade for I cascade(self) } } + +#[derive(Debug, PartialEq)] +pub struct ViewportConstraints { + pub size: TypedSize2D, + + pub initial_zoom: ScaleFactor, + pub min_zoom: Option>, + pub max_zoom: Option>, + + pub user_zoom: UserZoom, + pub orientation: Orientation +} + +impl ToCss for ViewportConstraints { + fn to_css(&self, dest: &mut W) -> fmt::Result + where W: fmt::Write + { + try!(write!(dest, "@viewport {{")); + try!(write!(dest, " width: {}px;", self.size.width.get())); + try!(write!(dest, " height: {}px;", self.size.height.get())); + try!(write!(dest, " zoom: {};", self.initial_zoom.get())); + if let Some(min_zoom) = self.min_zoom { + try!(write!(dest, " min-zoom: {};", min_zoom.get())); + } + if let Some(max_zoom) = self.max_zoom { + try!(write!(dest, " max-zoom: {};", max_zoom.get())); + } + try!(write!(dest, " user-zoom: ")); try!(self.user_zoom.to_css(dest)); + try!(write!(dest, "; orientation: ")); try!(self.orientation.to_css(dest)); + write!(dest, "; }}") + } +} + +impl ViewportConstraints { + pub fn maybe_new(initial_viewport: TypedSize2D, + rule: &ViewportRule) + -> Option + { + use std::cmp; + use num::{Float, ToPrimitive}; + + if rule.declarations.is_empty() { + return None + } + + let mut min_width = None; + let mut max_width = None; + + let mut min_height = None; + let mut max_height = None; + + let mut initial_zoom = None; + let mut min_zoom = None; + let mut max_zoom = None; + + let mut user_zoom = UserZoom::Zoom; + let mut orientation = Orientation::Auto; + + // collapse the list of declarations into descriptor values + for declaration in rule.declarations.iter() { + match declaration.descriptor { + ViewportDescriptor::MinWidth(value) => min_width = Some(value), + ViewportDescriptor::MaxWidth(value) => max_width = Some(value), + + ViewportDescriptor::MinHeight(value) => min_height = Some(value), + ViewportDescriptor::MaxHeight(value) => max_height = Some(value), + + ViewportDescriptor::Zoom(value) => initial_zoom = value.to_f32(), + ViewportDescriptor::MinZoom(value) => min_zoom = value.to_f32(), + ViewportDescriptor::MaxZoom(value) => max_zoom = value.to_f32(), + + ViewportDescriptor::UserZoom(value) => user_zoom = value, + ViewportDescriptor::Orientation(value) => orientation = value + } + } + + // TODO: return `None` if all descriptors are either absent or initial value + + macro_rules! choose { + ($op:ident, $opta:expr, $optb:expr) => { + match ($opta, $optb) { + (None, None) => None, + (a, None) => a.clone(), + (None, b) => b.clone(), + (a, b) => Some(a.clone().unwrap().$op(b.clone().unwrap())), + } + } + } + macro_rules! min { + ($opta:expr, $optb:expr) => { + choose!(min, $opta, $optb) + } + } + macro_rules! max { + ($opta:expr, $optb:expr) => { + choose!(max, $opta, $optb) + } + } + + // DEVICE-ADAPT § 6.2.1 Resolve min-zoom and max-zoom values + if min_zoom.is_some() && max_zoom.is_some() { + max_zoom = Some(min_zoom.clone().unwrap().max(max_zoom.unwrap())) + } + + // DEVICE-ADAPT § 6.2.2 Constrain zoom value to the [min-zoom, max-zoom] range + if initial_zoom.is_some() { + initial_zoom = max!(min_zoom, min!(max_zoom, initial_zoom)); + } + + // DEVICE-ADAPT § 6.2.3 Resolve non-auto lengths to pixel lengths + // + // Note: DEVICE-ADAPT § 5. states that relative length values are + // resolved against initial values + let initial_viewport = Size2D(Au::from_f32_px(initial_viewport.width.get()), + Au::from_f32_px(initial_viewport.height.get())); + + macro_rules! to_pixel_length { + ($value:ident, $dimension:ident) => { + if let Some($value) = $value { + match $value { + LengthOrPercentageOrAuto::Length(ref value) => Some(match value { + &Length::Absolute(length) => length, + &Length::FontRelative(length) => { + let initial_font_size = longhands::font_size::get_initial_value(); + length.to_computed_value(initial_font_size, initial_font_size) + } + &Length::ViewportPercentage(length) => + length.to_computed_value(initial_viewport), + _ => unreachable!() + }), + LengthOrPercentageOrAuto::Percentage(value) => Some(initial_viewport.$dimension.scale_by(value)), + LengthOrPercentageOrAuto::Auto => None, + } + } else { + None + } + } + } + + let min_width = to_pixel_length!(min_width, width); + let max_width = to_pixel_length!(max_width, width); + let min_height = to_pixel_length!(min_height, height); + let max_height = to_pixel_length!(max_height, height); + + // DEVICE-ADAPT § 6.2.4 Resolve initial width and height from min/max descriptors + macro_rules! resolve { + ($min:ident, $max:ident, $initial:expr) => { + if $min.is_some() || $max.is_some() { + let max = match $max { + Some(max) => cmp::min(max, $initial), + None => $initial + }; + + Some(match $min { + Some(min) => cmp::max(min, max), + None => max + }) + } else { + None + }; + } + } + + let width = resolve!(min_width, max_width, initial_viewport.width); + let height = resolve!(min_height, max_height, initial_viewport.height); + + // DEVICE-ADAPT § 6.2.5 Resolve width value + let width = if width.is_none() && height.is_none() { + Some(initial_viewport.width) + } else { + width + }; + + let width = width.unwrap_or_else(|| match initial_viewport.height { + Au(0) => initial_viewport.width, + initial_height => { + let ratio = initial_viewport.width.to_f32_px() / initial_height.to_f32_px(); + Au::from_f32_px(height.clone().unwrap().to_f32_px() * ratio) + } + }); + + // DEVICE-ADAPT § 6.2.6 Resolve height value + let height = height.unwrap_or_else(|| match initial_viewport.width { + Au(0) => initial_viewport.height, + initial_width => { + let ratio = initial_viewport.height.to_f32_px() / initial_width.to_f32_px(); + Au::from_f32_px(width.to_f32_px() * ratio) + } + }); + + Some(ViewportConstraints { + size: TypedSize2D(width.to_f32_px(), height.to_f32_px()), + + // TODO: compute a zoom factor for 'auto' as suggested by DEVICE-ADAPT § 10. + initial_zoom: ScaleFactor::new(initial_zoom.unwrap_or(1.)), + min_zoom: min_zoom.map(ScaleFactor::new), + max_zoom: max_zoom.map(ScaleFactor::new), + + user_zoom: user_zoom, + orientation: orientation + }) + } +} diff --git a/tests/unit/style/viewport.rs b/tests/unit/style/viewport.rs index ebd2360b6aab..fe72f27dc3d2 100644 --- a/tests/unit/style/viewport.rs +++ b/tests/unit/style/viewport.rs @@ -200,3 +200,74 @@ fn multiple_stylesheets_cascading() { assert_declaration_eq!(&declarations[1], User, MinHeight: LengthOrPercentageOrAuto::Length(Length::from_px(200.)), !important); assert_declaration_eq!(&declarations[2], Author, Zoom: Zoom::Number(3.), !important); } + +#[test] +fn constrain_viewport() { + let url = Url::parse("http://localhost").unwrap(); + let context = ParserContext::new(Origin::Author, &url); + + macro_rules! from_css { + ($css:expr) => { + &ViewportRule::parse(&mut Parser::new($css), &context).unwrap() + } + } + + let initial_viewport = TypedSize2D(800., 600.); + assert_eq!(ViewportConstraints::maybe_new(initial_viewport, from_css!("")), + None); + + let initial_viewport = TypedSize2D(800., 600.); + assert_eq!(ViewportConstraints::maybe_new(initial_viewport, from_css!("width: 320px auto")), + Some(ViewportConstraints { + size: initial_viewport, + + initial_zoom: ScaleFactor::new(1.), + min_zoom: None, + max_zoom: None, + + user_zoom: UserZoom::Zoom, + orientation: Orientation::Auto + })); + + let initial_viewport = TypedSize2D(200., 150.); + assert_eq!(ViewportConstraints::maybe_new(initial_viewport, from_css!("width: 320px auto")), + Some(ViewportConstraints { + size: TypedSize2D(320., 240.), + + initial_zoom: ScaleFactor::new(1.), + min_zoom: None, + max_zoom: None, + + user_zoom: UserZoom::Zoom, + orientation: Orientation::Auto + })); + + let initial_viewport = TypedSize2D(800., 600.); + assert_eq!(ViewportConstraints::maybe_new(initial_viewport, from_css!("width: 320px auto")), + Some(ViewportConstraints { + size: initial_viewport, + + initial_zoom: ScaleFactor::new(1.), + min_zoom: None, + max_zoom: None, + + user_zoom: UserZoom::Zoom, + orientation: Orientation::Auto + })); + + let initial_viewport = TypedSize2D(800., 600.); + assert_eq!(ViewportConstraints::maybe_new(initial_viewport, from_css!("width: 800px; height: 600px;\ + zoom: 1;\ + user-zoom: zoom;\ + orientation: auto;")), + Some(ViewportConstraints { + size: initial_viewport, + + initial_zoom: ScaleFactor::new(1.), + min_zoom: None, + max_zoom: None, + + user_zoom: UserZoom::Zoom, + orientation: Orientation::Auto + })); +}