diff --git a/components/compositing/Cargo.toml b/components/compositing/Cargo.toml index f035bc4ef17..a16df45f0ed 100644 --- a/components/compositing/Cargo.toml +++ b/components/compositing/Cargo.toml @@ -28,6 +28,9 @@ path = "../profile_traits" [dependencies.net_traits] path = "../net_traits" +[dependencies.style] +path = "../style" + [dependencies.util] path = "../util" @@ -62,6 +65,7 @@ git = "https://github.com/servo/gleam" git = "https://github.com/servo/rust-x11-clipboard" [dependencies] +num = "0.1.24" url = "0.2.16" time = "0.1.17" libc = "*" diff --git a/components/compositing/compositor.rs b/components/compositing/compositor.rs index 221dcf23f7f..7774673b5ba 100644 --- a/components/compositing/compositor.rs +++ b/components/compositing/compositor.rs @@ -43,6 +43,7 @@ use std::mem as std_mem; use std::rc::Rc; use std::slice::bytes::copy_memory; use std::sync::mpsc::Sender; +use style::viewport::ViewportConstraints; use time::{precise_time_ns, precise_time_s}; use url::Url; use util::geometry::{PagePx, ScreenPx, ViewportPx}; @@ -75,6 +76,10 @@ pub struct IOCompositor { /// "Mobile-style" zoom that does not reflow the page. viewport_zoom: ScaleFactor, + /// Viewport zoom constraints provided by @viewport. + min_viewport_zoom: Option>, + max_viewport_zoom: Option>, + /// "Desktop-style" zoom that resizes the viewport to fit the window. /// See `ViewportPx` docs in util/geom.rs for details. page_zoom: ScaleFactor, @@ -219,6 +224,8 @@ impl IOCompositor { shutdown_state: ShutdownState::NotShuttingDown, page_zoom: ScaleFactor::new(1.0), viewport_zoom: ScaleFactor::new(1.0), + min_viewport_zoom: None, + max_viewport_zoom: None, zoom_action: false, zoom_time: 0f64, got_load_complete_message: false, @@ -389,6 +396,10 @@ impl IOCompositor { } } + (Msg::ViewportConstrained(pipeline_id, constraints), ShutdownState::NotShuttingDown) => { + self.constrain_viewport(pipeline_id, constraints); + } + // When we are shutting_down, we need to avoid performing operations // such as Paint that may crash because we have begun tearing down // the rest of our resources. @@ -945,6 +956,21 @@ impl IOCompositor { } } + fn constrain_viewport(&mut self, pipeline_id: PipelineId, constraints: ViewportConstraints) { + let is_root = self.root_pipeline.as_ref().map_or(false, |root_pipeline| { + root_pipeline.id == pipeline_id + }); + + if is_root { + // TODO: actual viewport size + + self.viewport_zoom = constraints.initial_zoom; + self.min_viewport_zoom = constraints.min_zoom; + self.max_viewport_zoom = constraints.max_zoom; + self.update_zoom_transform(); + } + } + fn device_pixels_per_screen_px(&self) -> ScaleFactor { match opts::get().device_pixels_per_px { Some(device_pixels_per_px) => device_pixels_per_px, @@ -976,12 +1002,19 @@ impl IOCompositor { // TODO(pcwalton): I think this should go through the same queuing as scroll events do. fn on_pinch_zoom_window_event(&mut self, magnification: f32) { + use num::Float; + self.zoom_action = true; self.zoom_time = precise_time_s(); let old_viewport_zoom = self.viewport_zoom; - self.viewport_zoom = ScaleFactor::new((self.viewport_zoom.get() * magnification).max(1.0)); - let viewport_zoom = self.viewport_zoom; + let mut viewport_zoom = self.viewport_zoom.get() * magnification; + if let Some(min_zoom) = self.min_viewport_zoom.as_ref() { + viewport_zoom = min_zoom.get().max(viewport_zoom) + } + let viewport_zoom = self.max_viewport_zoom.as_ref().map_or(1., |z| z.get()).min(viewport_zoom); + let viewport_zoom = ScaleFactor::new(viewport_zoom); + self.viewport_zoom = viewport_zoom; self.update_zoom_transform(); @@ -1452,4 +1485,3 @@ pub enum CompositingReason { /// The window has been zoomed. Zoom, } - diff --git a/components/compositing/compositor_task.rs b/components/compositing/compositor_task.rs index 0f2b1249703..c64f3bfeb5e 100644 --- a/components/compositing/compositor_task.rs +++ b/components/compositing/compositor_task.rs @@ -26,6 +26,7 @@ use profile_traits::time; use std::sync::mpsc::{channel, Sender, Receiver}; use std::fmt::{Error, Formatter, Debug}; use std::rc::Rc; +use style::viewport::ViewportConstraints; use url::Url; use util::cursor::Cursor; @@ -219,6 +220,8 @@ pub enum Msg { SetCursor(Cursor), /// Informs the compositor that the paint task for the given pipeline has exited. PaintTaskExited(PipelineId), + /// Alerts the compositor that the viewport has been constrained in some manner + ViewportConstrained(PipelineId, ViewportConstraints), } impl Debug for Msg { @@ -245,6 +248,7 @@ impl Debug for Msg { Msg::KeyEvent(..) => write!(f, "KeyEvent"), Msg::SetCursor(..) => write!(f, "SetCursor"), Msg::PaintTaskExited(..) => write!(f, "PaintTaskExited"), + Msg::ViewportConstrained(..) => write!(f, "ViewportConstrained"), } } } @@ -303,4 +307,3 @@ pub trait CompositorEventListener { /// Requests that the compositor send the title for the main frame as soon as possible. fn get_title_for_main_frame(&self); } - diff --git a/components/compositing/constellation.rs b/components/compositing/constellation.rs index 5b69a678117..a0dddfcedec 100644 --- a/components/compositing/constellation.rs +++ b/components/compositing/constellation.rs @@ -34,6 +34,7 @@ use std::io::{self, Write}; use std::marker::PhantomData; use std::mem::replace; use std::sync::mpsc::{Sender, Receiver, channel}; +use style::viewport::ViewportConstraints; use url::Url; use util::cursor::Cursor; use util::geometry::PagePx; @@ -418,6 +419,10 @@ impl Constellation { self.handle_webdriver_command_msg(pipeline_id, command); } + ConstellationMsg::ViewportConstrained(pipeline_id, constraints) => { + debug!("constellation got viewport-constrained event message"); + self.handle_viewport_constrained_msg(pipeline_id, constraints); + } } true } @@ -913,6 +918,11 @@ impl Constellation { self.window_size = new_size; } + /// Handle updating actual viewport / zoom due to @viewport rules + fn handle_viewport_constrained_msg(&mut self, pipeline_id: PipelineId, constraints: ViewportConstraints) { + self.compositor_proxy.send(CompositorMsg::ViewportConstrained(pipeline_id, constraints)); + } + // Close a frame (and all children) fn close_frame(&mut self, frame_id: FrameId, exit_mode: ExitPipelineMode) { let frame = self.frames.remove(&frame_id).unwrap(); diff --git a/components/compositing/headless.rs b/components/compositing/headless.rs index e0c5c1fad1c..e61b8290aec 100644 --- a/components/compositing/headless.rs +++ b/components/compositing/headless.rs @@ -107,8 +107,9 @@ impl CompositorEventListener for NullCompositor { Msg::ChangePageTitle(..) | Msg::ChangePageUrl(..) | Msg::KeyEvent(..) | - Msg::SetCursor(..) => {} - Msg::PaintTaskExited(..) => {} + Msg::SetCursor(..) | + Msg::PaintTaskExited(..) | + Msg::ViewportConstrained(..) => {} } true } diff --git a/components/compositing/lib.rs b/components/compositing/lib.rs index 2b3a4172403..de013523827 100644 --- a/components/compositing/lib.rs +++ b/components/compositing/lib.rs @@ -19,8 +19,10 @@ extern crate png; extern crate script_traits; extern crate msg; extern crate net; +extern crate num; extern crate profile_traits; extern crate net_traits; +extern crate style; #[macro_use] extern crate util; extern crate gleam; diff --git a/components/layout/layout_task.rs b/components/layout/layout_task.rs index b98c7f99957..324c765280e 100644 --- a/components/layout/layout_task.rs +++ b/components/layout/layout_task.rs @@ -847,19 +847,32 @@ impl LayoutTask { let mut rw_data = self.lock_rw_data(possibly_locked_rw_data); - // TODO: Calculate the "actual viewport": - // http://www.w3.org/TR/css-device-adapt/#actual-viewport - let viewport_size = data.window_size.initial_viewport; + let initial_viewport = data.window_size.initial_viewport; let old_screen_size = rw_data.screen_size; - let current_screen_size = Size2D(Au::from_f32_px(viewport_size.width.get()), - Au::from_f32_px(viewport_size.height.get())); + let current_screen_size = Size2D(Au::from_f32_px(initial_viewport.width.get()), + Au::from_f32_px(initial_viewport.height.get())); rw_data.screen_size = current_screen_size; // Handle conditions where the entire flow tree is invalid. let screen_size_changed = current_screen_size != old_screen_size; if screen_size_changed { - let device = Device::new(MediaType::Screen, data.window_size.initial_viewport); + // Calculate the actual viewport as per DEVICE-ADAPT § 6 + let device = Device::new(MediaType::Screen, initial_viewport); rw_data.stylist.set_device(device); + + if let Some(constraints) = rw_data.stylist.constrain_viewport() { + debug!("Viewport constraints: {:?}", constraints); + + // other rules are evaluated against the actual viewport + rw_data.screen_size = Size2D(Au::from_f32_px(constraints.size.width.get()), + Au::from_f32_px(constraints.size.height.get())); + let device = Device::new(MediaType::Screen, constraints.size); + rw_data.stylist.set_device(device); + + // let the constellation know about the viewport constraints + let ConstellationChan(ref constellation_chan) = rw_data.constellation_chan; + constellation_chan.send(ConstellationMsg::ViewportConstrained(self.id, constraints)).unwrap(); + } } // If the entire flow tree is invalid, then it will be reflowed anyhow. diff --git a/components/msg/constellation_msg.rs b/components/msg/constellation_msg.rs index 4d9d2d19fb4..e5363037628 100644 --- a/components/msg/constellation_msg.rs +++ b/components/msg/constellation_msg.rs @@ -14,6 +14,7 @@ use layers::geometry::DevicePixel; use util::cursor::Cursor; use util::geometry::{PagePx, ViewportPx}; use std::sync::mpsc::{channel, Sender, Receiver}; +use style::viewport::ViewportConstraints; use webdriver_traits::WebDriverScriptCommand; use url::Url; @@ -233,7 +234,9 @@ pub enum Msg { /// Requests that the constellation retrieve the current contents of the clipboard GetClipboardContents(Sender), // Dispatch a webdriver command - WebDriverCommand(PipelineId, WebDriverScriptCommand) + WebDriverCommand(PipelineId, WebDriverScriptCommand), + /// Notifies the constellation that the viewport has been constrained in some manner + ViewportConstrained(PipelineId, ViewportConstraints), } #[derive(Clone, Eq, PartialEq)] diff --git a/components/msg/lib.rs b/components/msg/lib.rs index fc125583e95..558ccf55a96 100644 --- a/components/msg/lib.rs +++ b/components/msg/lib.rs @@ -9,6 +9,7 @@ extern crate hyper; extern crate layers; extern crate util; extern crate url; +extern crate style; extern crate webdriver_traits; #[cfg(target_os="macos")] diff --git a/components/servo/Cargo.lock b/components/servo/Cargo.lock index 060328b59da..2494f04f668 100644 --- a/components/servo/Cargo.lock +++ b/components/servo/Cargo.lock @@ -123,9 +123,11 @@ dependencies = [ "msg 0.0.1", "net 0.0.1", "net_traits 0.0.1", + "num 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)", "png 0.1.0 (git+https://github.com/servo/rust-png)", "profile_traits 0.0.1", "script_traits 0.0.1", + "style 0.0.1", "time 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", "url 0.2.31 (registry+https://github.com/rust-lang/crates.io-index)", "util 0.0.1", @@ -1120,6 +1122,7 @@ dependencies = [ "lazy_static 0.1.10 (git+https://github.com/Kimundi/lazy-static.rs)", "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "mod_path 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "num 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)", "plugins 0.0.1", "rustc-serialize 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)", "selectors 0.1.0 (git+https://github.com/servo/rust-selectors)", diff --git a/components/style/Cargo.toml b/components/style/Cargo.toml index 96438546fc5..0022d09714c 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 109216ecf55..59a936dd737 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; @@ -50,6 +51,7 @@ pub mod media_queries; pub mod font_face; pub mod legacy; pub mod animation; +pub mod viewport; macro_rules! reexport_computed_values { ( $( $name: ident )+ ) => { @@ -63,4 +65,3 @@ macro_rules! reexport_computed_values { } } longhand_properties_idents!(reexport_computed_values); - diff --git a/components/style/parser.rs b/components/style/parser.rs index c9d1891d708..0e3ea92db52 100644 --- a/components/style/parser.rs +++ b/components/style/parser.rs @@ -11,6 +11,7 @@ use log; use stylesheets::Origin; pub struct ParserContext<'a> { + pub stylesheet_origin: Origin, pub base_url: &'a Url, pub selector_context: SelectorParserContext, } @@ -20,6 +21,7 @@ impl<'a> ParserContext<'a> { let mut selector_context = SelectorParserContext::new(); selector_context.in_user_agent_stylesheet = stylesheet_origin == Origin::UserAgent; ParserContext { + stylesheet_origin: stylesheet_origin, base_url: base_url, selector_context: selector_context, } diff --git a/components/style/selector_matching.rs b/components/style/selector_matching.rs index 211ae3dbe56..a9c0839f5e8 100644 --- a/components/style/selector_matching.rs +++ b/components/style/selector_matching.rs @@ -17,6 +17,7 @@ use media_queries::Device; use node::TElementAttributes; use properties::{PropertyDeclaration, PropertyDeclarationBlock}; use stylesheets::{Stylesheet, CSSRuleIteratorExt, Origin}; +use viewport::{ViewportConstraints, ViewportRuleCascade}; pub type DeclarationBlock = GenericDeclarationBlock>; @@ -69,6 +70,14 @@ impl Stylist { stylist } + pub fn constrain_viewport(&self) -> Option { + let cascaded_rule = self.stylesheets.iter() + .flat_map(|s| s.effective_rules(&self.device).viewport()) + .cascade(); + + ViewportConstraints::maybe_new(self.device.viewport_size, &cascaded_rule) + } + pub fn update(&mut self) -> bool { if self.is_dirty { self.element_map = PerPseudoElementSelectorMap::new(); diff --git a/components/style/stylesheets.rs b/components/style/stylesheets.rs index 90f4a7510df..a4eac87ce84 100644 --- a/components/style/stylesheets.rs +++ b/components/style/stylesheets.rs @@ -19,6 +19,7 @@ use properties::{PropertyDeclarationBlock, parse_property_declaration_list}; use media_queries::{Device, MediaQueryList, parse_media_query_list}; use font_face::{FontFaceRule, parse_font_face_block}; use util::smallvec::SmallVec2; +use viewport::ViewportRule; /// Each style rule has an origin, which determines where it enters the cascade. @@ -53,6 +54,7 @@ pub enum CSSRule { Style(StyleRule), Media(MediaRule), FontFace(FontFaceRule), + Viewport(ViewportRule), } #[derive(Debug, PartialEq)] @@ -216,6 +218,7 @@ pub mod rule_filter { use std::marker::PhantomData; use super::{CSSRule, MediaRule, StyleRule}; use super::super::font_face::FontFaceRule; + use super::super::viewport::ViewportRule; macro_rules! rule_filter { ($variant:ident -> $value:ty) => { @@ -259,6 +262,7 @@ pub mod rule_filter { rule_filter!(FontFace -> FontFaceRule); rule_filter!(Media -> MediaRule); rule_filter!(Style -> StyleRule); + rule_filter!(Viewport -> ViewportRule); } /// Extension methods for `CSSRule` iterators. @@ -271,6 +275,9 @@ pub trait CSSRuleIteratorExt<'a>: Iterator { /// Yield only style rules. fn style(self) -> rule_filter::Style<'a, Self>; + + /// Yield only @viewport rules. + fn viewport(self) -> rule_filter::Viewport<'a, Self>; } impl<'a, I> CSSRuleIteratorExt<'a> for I where I: Iterator { @@ -288,6 +295,11 @@ impl<'a, I> CSSRuleIteratorExt<'a> for I where I: Iterator { fn style(self) -> rule_filter::Style<'a, I> { rule_filter::Style::new(self) } + + #[inline] + fn viewport(self) -> rule_filter::Viewport<'a, I> { + rule_filter::Viewport::new(self) + } } fn parse_nested_rules(context: &ParserContext, input: &mut Parser) -> Vec { @@ -324,6 +336,7 @@ enum State { enum AtRulePrelude { FontFace, Media(MediaQueryList), + Viewport, } @@ -414,6 +427,13 @@ impl<'a, 'b> AtRuleParser for NestedRuleParser<'a, 'b> { }, "font-face" => { Ok(AtRuleType::WithBlock(AtRulePrelude::FontFace)) + }, + "viewport" => { + if ::util::opts::experimental_enabled() { + Ok(AtRuleType::WithBlock(AtRulePrelude::Viewport)) + } else { + Err(()) + } } _ => Err(()) } @@ -430,6 +450,9 @@ impl<'a, 'b> AtRuleParser for NestedRuleParser<'a, 'b> { rules: parse_nested_rules(self.context, input), })) } + AtRulePrelude::Viewport => { + ViewportRule::parse(input, self.context).map(CSSRule::Viewport) + } } } } diff --git a/components/style/values.rs b/components/style/values.rs index 88ae4b0417c..5c2c29f7aef 100644 --- a/components/style/values.rs +++ b/components/style/values.rs @@ -86,6 +86,22 @@ pub mod specified { use util::geometry::Au; use super::CSSFloat; + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub enum AllowedNumericType { + All, + NonNegative + } + + impl AllowedNumericType { + #[inline] + pub fn is_ok(&self, value: f32) -> bool { + match self { + &AllowedNumericType::All => true, + &AllowedNumericType::NonNegative => value >= 0., + } + } + } + #[derive(Clone, PartialEq, Debug)] pub struct CSSColor { pub parsed: cssparser::Color, @@ -294,21 +310,21 @@ pub mod specified { const AU_PER_PC: CSSFloat = AU_PER_PT * 12.; impl Length { #[inline] - fn parse_internal(input: &mut Parser, negative_ok: bool) -> Result { + fn parse_internal(input: &mut Parser, context: &AllowedNumericType) -> Result { match try!(input.next()) { - Token::Dimension(ref value, ref unit) if negative_ok || value.value >= 0. => { - Length::parse_dimension(value.value, unit) - } - Token::Number(ref value) if value.value == 0. => Ok(Length::Absolute(Au(0))), + Token::Dimension(ref value, ref unit) if context.is_ok(value.value) => + Length::parse_dimension(value.value, unit), + Token::Number(ref value) if value.value == 0. => + Ok(Length::Absolute(Au(0))), _ => Err(()) } } #[allow(dead_code)] pub fn parse(input: &mut Parser) -> Result { - Length::parse_internal(input, /* negative_ok = */ true) + Length::parse_internal(input, &AllowedNumericType::All) } pub fn parse_non_negative(input: &mut Parser) -> Result { - Length::parse_internal(input, /* negative_ok = */ false) + Length::parse_internal(input, &AllowedNumericType::NonNegative) } pub fn parse_dimension(value: CSSFloat, unit: &str) -> Result { match_ignore_ascii_case! { unit, @@ -353,30 +369,27 @@ pub mod specified { } } impl LengthOrPercentage { - fn parse_internal(input: &mut Parser, negative_ok: bool) - -> Result { + fn parse_internal(input: &mut Parser, context: &AllowedNumericType) + -> Result + { match try!(input.next()) { - Token::Dimension(ref value, ref unit) if negative_ok || value.value >= 0. => { - Length::parse_dimension(value.value, unit) - .map(LengthOrPercentage::Length) - } - Token::Percentage(ref value) if negative_ok || value.unit_value >= 0. => { - Ok(LengthOrPercentage::Percentage(value.unit_value)) - } - Token::Number(ref value) if value.value == 0. => { - Ok(LengthOrPercentage::Length(Length::Absolute(Au(0)))) - } + Token::Dimension(ref value, ref unit) if context.is_ok(value.value) => + Length::parse_dimension(value.value, unit).map(LengthOrPercentage::Length), + Token::Percentage(ref value) if context.is_ok(value.unit_value) => + Ok(LengthOrPercentage::Percentage(value.unit_value)), + Token::Number(ref value) if value.value == 0. => + Ok(LengthOrPercentage::Length(Length::Absolute(Au(0)))), _ => Err(()) } } #[allow(dead_code)] #[inline] pub fn parse(input: &mut Parser) -> Result { - LengthOrPercentage::parse_internal(input, /* negative_ok = */ true) + LengthOrPercentage::parse_internal(input, &AllowedNumericType::All) } #[inline] pub fn parse_non_negative(input: &mut Parser) -> Result { - LengthOrPercentage::parse_internal(input, /* negative_ok = */ false) + LengthOrPercentage::parse_internal(input, &AllowedNumericType::NonNegative) } } @@ -397,33 +410,30 @@ pub mod specified { } } } + impl LengthOrPercentageOrAuto { - fn parse_internal(input: &mut Parser, negative_ok: bool) - -> Result { + fn parse_internal(input: &mut Parser, context: &AllowedNumericType) + -> Result + { match try!(input.next()) { - Token::Dimension(ref value, ref unit) if negative_ok || value.value >= 0. => { - Length::parse_dimension(value.value, unit) - .map(LengthOrPercentageOrAuto::Length) - } - Token::Percentage(ref value) if negative_ok || value.unit_value >= 0. => { - Ok(LengthOrPercentageOrAuto::Percentage(value.unit_value)) - } - Token::Number(ref value) if value.value == 0. => { - Ok(LengthOrPercentageOrAuto::Length(Length::Absolute(Au(0)))) - } - Token::Ident(ref value) if value.eq_ignore_ascii_case("auto") => { - Ok(LengthOrPercentageOrAuto::Auto) - } + Token::Dimension(ref value, ref unit) if context.is_ok(value.value) => + Length::parse_dimension(value.value, unit).map(LengthOrPercentageOrAuto::Length), + Token::Percentage(ref value) if context.is_ok(value.unit_value) => + Ok(LengthOrPercentageOrAuto::Percentage(value.unit_value)), + Token::Number(ref value) if value.value == 0. => + Ok(LengthOrPercentageOrAuto::Length(Length::Absolute(Au(0)))), + Token::Ident(ref value) if value.eq_ignore_ascii_case("auto") => + Ok(LengthOrPercentageOrAuto::Auto), _ => Err(()) } } #[inline] pub fn parse(input: &mut Parser) -> Result { - LengthOrPercentageOrAuto::parse_internal(input, /* negative_ok = */ true) + LengthOrPercentageOrAuto::parse_internal(input, &AllowedNumericType::All) } #[inline] pub fn parse_non_negative(input: &mut Parser) -> Result { - LengthOrPercentageOrAuto::parse_internal(input, /* negative_ok = */ false) + LengthOrPercentageOrAuto::parse_internal(input, &AllowedNumericType::NonNegative) } } @@ -438,40 +448,36 @@ pub mod specified { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { match self { &LengthOrPercentageOrNone::Length(length) => length.to_css(dest), - &LengthOrPercentageOrNone::Percentage(percentage) - => write!(dest, "{}%", percentage * 100.), + &LengthOrPercentageOrNone::Percentage(percentage) => + write!(dest, "{}%", percentage * 100.), &LengthOrPercentageOrNone::None => dest.write_str("none"), } } } impl LengthOrPercentageOrNone { - fn parse_internal(input: &mut Parser, negative_ok: bool) - -> Result { + fn parse_internal(input: &mut Parser, context: &AllowedNumericType) + -> Result + { match try!(input.next()) { - Token::Dimension(ref value, ref unit) if negative_ok || value.value >= 0. => { - Length::parse_dimension(value.value, unit) - .map(LengthOrPercentageOrNone::Length) - } - Token::Percentage(ref value) if negative_ok || value.unit_value >= 0. => { - Ok(LengthOrPercentageOrNone::Percentage(value.unit_value)) - } - Token::Number(ref value) if value.value == 0. => { - Ok(LengthOrPercentageOrNone::Length(Length::Absolute(Au(0)))) - } - Token::Ident(ref value) if value.eq_ignore_ascii_case("none") => { - Ok(LengthOrPercentageOrNone::None) - } + Token::Dimension(ref value, ref unit) if context.is_ok(value.value) => + Length::parse_dimension(value.value, unit).map(LengthOrPercentageOrNone::Length), + Token::Percentage(ref value) if context.is_ok(value.unit_value) => + Ok(LengthOrPercentageOrNone::Percentage(value.unit_value)), + Token::Number(ref value) if value.value == 0. => + Ok(LengthOrPercentageOrNone::Length(Length::Absolute(Au(0)))), + Token::Ident(ref value) if value.eq_ignore_ascii_case("none") => + Ok(LengthOrPercentageOrNone::None), _ => Err(()) } } #[allow(dead_code)] #[inline] pub fn parse(input: &mut Parser) -> Result { - LengthOrPercentageOrNone::parse_internal(input, /* negative_ok = */ true) + LengthOrPercentageOrNone::parse_internal(input, &AllowedNumericType::All) } #[inline] pub fn parse_non_negative(input: &mut Parser) -> Result { - LengthOrPercentageOrNone::parse_internal(input, /* negative_ok = */ false) + LengthOrPercentageOrNone::parse_internal(input, &AllowedNumericType::NonNegative) } } diff --git a/components/style/viewport.rs b/components/style/viewport.rs new file mode 100644 index 00000000000..9115b9a8b26 --- /dev/null +++ b/components/style/viewport.rs @@ -0,0 +1,509 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * 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; +use std::collections::hash_map::{Entry, HashMap}; +use std::fmt; +use std::intrinsics; + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum ViewportDescriptor { + MinWidth(LengthOrPercentageOrAuto), + MaxWidth(LengthOrPercentageOrAuto), + + MinHeight(LengthOrPercentageOrAuto), + MaxHeight(LengthOrPercentageOrAuto), + + Zoom(Zoom), + MinZoom(Zoom), + MaxZoom(Zoom), + + UserZoom(UserZoom), + Orientation(Orientation) +} + +/// Zoom is a number | percentage | auto +/// See http://dev.w3.org/csswg/css-device-adapt/#descdef-viewport-zoom +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Zoom { + Number(f32), + Percentage(f32), + Auto, +} + +impl ToCss for Zoom { + fn to_css(&self, dest: &mut W) -> fmt::Result + where W: fmt::Write + { + match self { + &Zoom::Number(number) => write!(dest, "{}", number), + &Zoom::Percentage(percentage) => write!(dest, "{}%", percentage * 100.), + &Zoom::Auto => write!(dest, "auto") + } + } +} + +impl Zoom { + pub fn parse(input: &mut Parser) -> Result { + use cssparser::Token; + + match try!(input.next()) { + Token::Percentage(ref value) if AllowedNumericType::NonNegative.is_ok(value.unit_value) => + Ok(Zoom::Percentage(value.unit_value)), + Token::Number(ref value) if AllowedNumericType::NonNegative.is_ok(value.value) => + Ok(Zoom::Number(value.value)), + Token::Ident(ref value) if value.eq_ignore_ascii_case("auto") => + Ok(Zoom::Auto), + _ => Err(()) + } + } + + #[inline] + pub fn to_f32(&self) -> Option { + match self { + &Zoom::Number(number) => Some(number as f32), + &Zoom::Percentage(percentage) => Some(percentage as f32), + &Zoom::Auto => None + } + } +} + +define_css_keyword_enum!(UserZoom: + "zoom" => Zoom, + "fixed" => Fixed); + +define_css_keyword_enum!(Orientation: + "auto" => Auto, + "portrait" => Portrait, + "landscape" => Landscape); + +struct ViewportRuleParser<'a, 'b: 'a> { + context: &'a ParserContext<'b> +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct ViewportDescriptorDeclaration { + pub origin: Origin, + pub descriptor: ViewportDescriptor, + pub important: bool +} + +impl ViewportDescriptorDeclaration { + pub fn new(origin: Origin, + descriptor: ViewportDescriptor, + important: bool) -> ViewportDescriptorDeclaration + { + ViewportDescriptorDeclaration { + origin: origin, + descriptor: descriptor, + important: important + } + } +} + +fn parse_shorthand(input: &mut Parser) -> Result<[LengthOrPercentageOrAuto; 2], ()> { + let min = try!(LengthOrPercentageOrAuto::parse_non_negative(input)); + match input.try(|input| LengthOrPercentageOrAuto::parse_non_negative(input)) { + Err(()) => Ok([min.clone(), min]), + Ok(max) => Ok([min, max]) + } +} + +impl<'a, 'b> AtRuleParser for ViewportRuleParser<'a, 'b> { + type Prelude = (); + type AtRule = Vec; +} + +impl<'a, 'b> DeclarationParser for ViewportRuleParser<'a, 'b> { + type Declaration = Vec; + + fn parse_value(&self, name: &str, input: &mut Parser) -> Result, ()> { + macro_rules! declaration { + ($declaration:ident($parse:path)) => { + declaration!($declaration(value: try!($parse(input)), + important: input.try(parse_important).is_ok())) + }; + ($declaration:ident(value: $value:expr, important: $important:expr)) => { + ViewportDescriptorDeclaration::new( + self.context.stylesheet_origin, + ViewportDescriptor::$declaration($value), + $important) + } + } + + macro_rules! ok { + ($declaration:ident($parse:path)) => { + Ok(vec![declaration!($declaration($parse))]) + }; + (shorthand -> [$min:ident, $max:ident]) => {{ + let shorthand = try!(parse_shorthand(input)); + let important = input.try(parse_important).is_ok(); + + Ok(vec![declaration!($min(value: shorthand[0], important: important)), + declaration!($max(value: shorthand[1], important: important))]) + }} + } + + match name { + n if n.eq_ignore_ascii_case("min-width") => + ok!(MinWidth(LengthOrPercentageOrAuto::parse_non_negative)), + n if n.eq_ignore_ascii_case("max-width") => + ok!(MaxWidth(LengthOrPercentageOrAuto::parse_non_negative)), + n if n.eq_ignore_ascii_case("width") => + ok!(shorthand -> [MinWidth, MaxWidth]), + + n if n.eq_ignore_ascii_case("min-height") => + ok!(MinHeight(LengthOrPercentageOrAuto::parse_non_negative)), + n if n.eq_ignore_ascii_case("max-height") => + ok!(MaxHeight(LengthOrPercentageOrAuto::parse_non_negative)), + n if n.eq_ignore_ascii_case("height") => + ok!(shorthand -> [MinHeight, MaxHeight]), + + n if n.eq_ignore_ascii_case("zoom") => + ok!(Zoom(Zoom::parse)), + n if n.eq_ignore_ascii_case("min-zoom") => + ok!(MinZoom(Zoom::parse)), + n if n.eq_ignore_ascii_case("max-zoom") => + ok!(MaxZoom(Zoom::parse)), + + n if n.eq_ignore_ascii_case("user-zoom") => + ok!(UserZoom(UserZoom::parse)), + n if n.eq_ignore_ascii_case("orientation") => + ok!(Orientation(Orientation::parse)), + + _ => Err(()), + } + } +} + +#[derive(Debug, PartialEq)] +pub struct ViewportRule { + pub declarations: Vec +} + +impl ViewportRule { + pub fn parse<'a>(input: &mut Parser, context: &'a ParserContext) + -> Result + { + let parser = ViewportRuleParser { context: context }; + + let mut errors = vec![]; + let valid_declarations = DeclarationListParser::new(input, parser) + .filter_map(|result| { + match result { + Ok(declarations) => Some(declarations), + Err(range) => { + errors.push(range); + None + } + } + }) + .flat_map(|declarations| declarations.into_iter()) + .collect::>(); + + for range in errors { + let pos = range.start; + let message = format!("Unsupported @viewport descriptor declaration: '{}'", + input.slice(range)); + log_css_error(input, pos, &*message); + } + + Ok(ViewportRule { declarations: valid_declarations.iter().cascade() }) + } +} + +pub trait ViewportRuleCascade: Iterator + Sized { + fn cascade(self) -> ViewportRule; +} + +impl<'a, I> ViewportRuleCascade for I + where I: Iterator +{ + #[inline] + fn cascade(self) -> ViewportRule { + ViewportRule { + declarations: self.flat_map(|r| r.declarations.iter()).cascade() + } + } +} + +trait ViewportDescriptorDeclarationCascade: Iterator + Sized { + fn cascade(self) -> Vec; +} + +/// Computes the cascade precedence as according to +/// http://dev.w3.org/csswg/css-cascade/#cascade-origin +fn cascade_precendence(origin: Origin, important: bool) -> u8 { + match (origin, important) { + (Origin::UserAgent, true) => 1, + (Origin::User, true) => 2, + (Origin::Author, true) => 3, + (Origin::Author, false) => 4, + (Origin::User, false) => 5, + (Origin::UserAgent, false) => 6, + } +} + +impl ViewportDescriptorDeclaration { + fn higher_or_equal_precendence(&self, other: &ViewportDescriptorDeclaration) -> bool { + let self_precedence = cascade_precendence(self.origin, self.important); + let other_precedence = cascade_precendence(other.origin, other.important); + + self_precedence <= other_precedence + } +} + +fn cascade<'a, I>(iter: I) -> Vec + where I: Iterator +{ + let mut declarations: HashMap = HashMap::new(); + + // index is used to reconstruct order of appearance after all declarations + // have been added to the map + let mut index = 0; + for declaration in iter { + let descriptor = unsafe { + intrinsics::discriminant_value(&declaration.descriptor) + }; + + match declarations.entry(descriptor) { + Entry::Occupied(mut entry) => { + if declaration.higher_or_equal_precendence(entry.get().1) { + entry.insert((index, declaration)); + index += 1; + } + } + Entry::Vacant(entry) => { + entry.insert((index, declaration)); + index += 1; + } + } + } + + // convert to a list and sort the descriptors by order of appearance + let mut declarations: Vec<_> = declarations.into_iter().map(|kv| kv.1).collect(); + declarations.sort_by(|a, b| a.0.cmp(&b.0)); + declarations.into_iter().map(|id| *id.1).collect::>() +} + +impl<'a, I> ViewportDescriptorDeclarationCascade for I + where I: Iterator +{ + #[inline] + fn cascade(self) -> Vec { + 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/ports/cef/Cargo.lock b/ports/cef/Cargo.lock index 5ccf477f827..bbd087757a2 100644 --- a/ports/cef/Cargo.lock +++ b/ports/cef/Cargo.lock @@ -131,9 +131,11 @@ dependencies = [ "msg 0.0.1", "net 0.0.1", "net_traits 0.0.1", + "num 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)", "png 0.1.0 (git+https://github.com/servo/rust-png)", "profile_traits 0.0.1", "script_traits 0.0.1", + "style 0.0.1", "time 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", "url 0.2.31 (registry+https://github.com/rust-lang/crates.io-index)", "util 0.0.1", @@ -1127,6 +1129,7 @@ dependencies = [ "lazy_static 0.1.10 (git+https://github.com/Kimundi/lazy-static.rs)", "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "mod_path 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "num 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)", "plugins 0.0.1", "rustc-serialize 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)", "selectors 0.1.0 (git+https://github.com/servo/rust-selectors)", diff --git a/ports/gonk/Cargo.lock b/ports/gonk/Cargo.lock index 5b249c86bfc..ad15c9a1005 100644 --- a/ports/gonk/Cargo.lock +++ b/ports/gonk/Cargo.lock @@ -114,9 +114,11 @@ dependencies = [ "msg 0.0.1", "net 0.0.1", "net_traits 0.0.1", + "num 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)", "png 0.1.0 (git+https://github.com/servo/rust-png)", "profile_traits 0.0.1", "script_traits 0.0.1", + "style 0.0.1", "time 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", "url 0.2.31 (registry+https://github.com/rust-lang/crates.io-index)", "util 0.0.1", @@ -1099,6 +1101,7 @@ dependencies = [ "lazy_static 0.1.10 (git+https://github.com/Kimundi/lazy-static.rs)", "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "mod_path 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "num 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)", "plugins 0.0.1", "rustc-serialize 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)", "selectors 0.1.0 (git+https://github.com/servo/rust-selectors)", diff --git a/tests/ref/basic.list b/tests/ref/basic.list index e864d1fa024..fd5197b13a0 100644 --- a/tests/ref/basic.list +++ b/tests/ref/basic.list @@ -311,6 +311,7 @@ resolution=800x600 == viewport_percentage_vmin_vmax.html viewport_percentage_vmi resolution=600x800 == viewport_percentage_vmin_vmax.html viewport_percentage_vmin_vmax_b.html resolution=800x600 == viewport_percentage_vw_vh.html viewport_percentage_vw_vh_a.html resolution=600x800 == viewport_percentage_vw_vh.html viewport_percentage_vw_vh_b.html +experimental == viewport_rule.html viewport_rule_ref.html == visibility_hidden.html visibility_hidden_ref.html flaky_cpu == webgl-context/clearcolor.html webgl-context/clearcolor_ref.html diff --git a/tests/ref/viewport_rule.html b/tests/ref/viewport_rule.html new file mode 100644 index 00000000000..bccd08bb79d --- /dev/null +++ b/tests/ref/viewport_rule.html @@ -0,0 +1,29 @@ + + + + + + +
+
+
+
+ + diff --git a/tests/ref/viewport_rule_ref.html b/tests/ref/viewport_rule_ref.html new file mode 100644 index 00000000000..2cd2b347d3f --- /dev/null +++ b/tests/ref/viewport_rule_ref.html @@ -0,0 +1,24 @@ + + + + + + +
+
+
+
+ + diff --git a/tests/unit/style/lib.rs b/tests/unit/style/lib.rs index 9e379d1336b..a90707b522c 100644 --- a/tests/unit/style/lib.rs +++ b/tests/unit/style/lib.rs @@ -16,6 +16,7 @@ extern crate util; #[cfg(test)] mod stylesheets; #[cfg(test)] mod media_queries; +#[cfg(test)] mod viewport; #[cfg(test)] mod writing_modes { use util::logical_geometry::WritingMode; diff --git a/tests/unit/style/viewport.rs b/tests/unit/style/viewport.rs new file mode 100644 index 00000000000..6be92ea24fd --- /dev/null +++ b/tests/unit/style/viewport.rs @@ -0,0 +1,275 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use cssparser::Parser; +use geom::size::TypedSize2D; +use geom::scale_factor::ScaleFactor; +use style::media_queries::{Device, MediaType}; +use style::parser::ParserContext; +use style::stylesheets::{Origin, Stylesheet, CSSRuleIteratorExt}; +use style::values::specified::{Length, LengthOrPercentageOrAuto}; +use style::viewport::*; +use url::Url; + +macro_rules! stylesheet { + ($css:expr, $origin:ident) => { + Stylesheet::from_str($css, + Url::parse("http://localhost").unwrap(), + Origin::$origin); + } +} + +fn test_viewport_rule(css: &str, + device: &Device, + callback: F) + where F: Fn(&Vec, &str) +{ + ::util::opts::set_experimental_enabled(true); + + let stylesheet = stylesheet!(css, Author); + let mut rule_count = 0; + for rule in stylesheet.effective_rules(&device).viewport() { + rule_count += 1; + callback(&rule.declarations, css); + } + assert!(rule_count > 0); +} + +macro_rules! assert_declarations_len { + ($declarations:ident == 1) => { + assert!($declarations.len() == 1, + "expected 1 declaration; have {}: {:?})", + $declarations.len(), $declarations) + }; + ($declarations:ident == $len:expr) => { + assert!($declarations.len() == $len, + "expected {} declarations; have {}: {:?})", + $len, $declarations.len(), $declarations) + } +} + +#[test] +fn empty_viewport_rule() { + let device = Device::new(MediaType::Screen, TypedSize2D(800., 600.)); + + test_viewport_rule("@viewport {}", &device, |declarations, css| { + println!("{}", css); + assert_declarations_len!(declarations == 0); + }); +} + +macro_rules! assert_declaration_eq { + ($d:expr, $origin:ident, $expected:ident: $value:expr) => {{ + assert_eq!($d.origin, Origin::$origin); + assert_eq!($d.descriptor, ViewportDescriptor::$expected($value)); + assert!($d.important == false, "descriptor should not be !important"); + }}; + ($d:expr, $origin:ident, $expected:ident: $value:expr, !important) => {{ + assert_eq!($d.origin, Origin::$origin); + assert_eq!($d.descriptor, ViewportDescriptor::$expected($value)); + assert!($d.important == true, "descriptor should be !important"); + }}; +} + +#[test] +fn simple_viewport_rules() { + let device = Device::new(MediaType::Screen, TypedSize2D(800., 600.)); + + test_viewport_rule("@viewport { width: auto; height: auto;\ + zoom: auto; min-zoom: 0; max-zoom: 200%;\ + user-zoom: zoom; orientation: auto; }", + &device, |declarations, css| { + println!("{}", css); + assert_declarations_len!(declarations == 9); + assert_declaration_eq!(&declarations[0], Author, MinWidth: LengthOrPercentageOrAuto::Auto); + assert_declaration_eq!(&declarations[1], Author, MaxWidth: LengthOrPercentageOrAuto::Auto); + assert_declaration_eq!(&declarations[2], Author, MinHeight: LengthOrPercentageOrAuto::Auto); + assert_declaration_eq!(&declarations[3], Author, MaxHeight: LengthOrPercentageOrAuto::Auto); + assert_declaration_eq!(&declarations[4], Author, Zoom: Zoom::Auto); + assert_declaration_eq!(&declarations[5], Author, MinZoom: Zoom::Number(0.)); + assert_declaration_eq!(&declarations[6], Author, MaxZoom: Zoom::Percentage(2.)); + assert_declaration_eq!(&declarations[7], Author, UserZoom: UserZoom::Zoom); + assert_declaration_eq!(&declarations[8], Author, Orientation: Orientation::Auto); + }); + + test_viewport_rule("@viewport { min-width: 200px; max-width: auto;\ + min-height: 200px; max-height: auto; }", + &device, |declarations, css| { + println!("{}", css); + assert_declarations_len!(declarations == 4); + assert_declaration_eq!(&declarations[0], Author, MinWidth: LengthOrPercentageOrAuto::Length(Length::from_px(200.))); + assert_declaration_eq!(&declarations[1], Author, MaxWidth: LengthOrPercentageOrAuto::Auto); + assert_declaration_eq!(&declarations[2], Author, MinHeight: LengthOrPercentageOrAuto::Length(Length::from_px(200.))); + assert_declaration_eq!(&declarations[3], Author, MaxHeight: LengthOrPercentageOrAuto::Auto); + }); +} + +#[test] +fn cascading_within_viewport_rule() { + let device = Device::new(MediaType::Screen, TypedSize2D(800., 600.)); + + // normal order of appearance + test_viewport_rule("@viewport { min-width: 200px; min-width: auto; }", + &device, |declarations, css| { + println!("{}", css); + assert_declarations_len!(declarations == 1); + assert_declaration_eq!(&declarations[0], Author, MinWidth: LengthOrPercentageOrAuto::Auto); + }); + + // !important order of appearance + test_viewport_rule("@viewport { min-width: 200px !important; min-width: auto !important; }", + &device, |declarations, css| { + println!("{}", css); + assert_declarations_len!(declarations == 1); + assert_declaration_eq!(&declarations[0], Author, MinWidth: LengthOrPercentageOrAuto::Auto, !important); + }); + + // !important vs normal + test_viewport_rule("@viewport { min-width: auto !important; min-width: 200px; }", + &device, |declarations, css| { + println!("{}", css); + assert_declarations_len!(declarations == 1); + assert_declaration_eq!(&declarations[0], Author, MinWidth: LengthOrPercentageOrAuto::Auto, !important); + }); + + // normal longhands vs normal shorthand + test_viewport_rule("@viewport { min-width: 200px; max-width: 200px; width: auto; }", + &device, |declarations, css| { + println!("{}", css); + assert_declarations_len!(declarations == 2); + assert_declaration_eq!(&declarations[0], Author, MinWidth: LengthOrPercentageOrAuto::Auto); + assert_declaration_eq!(&declarations[1], Author, MaxWidth: LengthOrPercentageOrAuto::Auto); + }); + + // normal shorthand vs normal longhands + test_viewport_rule("@viewport { width: 200px; min-width: auto; max-width: auto; }", + &device, |declarations, css| { + println!("{}", css); + assert_declarations_len!(declarations == 2); + assert_declaration_eq!(&declarations[0], Author, MinWidth: LengthOrPercentageOrAuto::Auto); + assert_declaration_eq!(&declarations[1], Author, MaxWidth: LengthOrPercentageOrAuto::Auto); + }); + + // one !important longhand vs normal shorthand + test_viewport_rule("@viewport { min-width: auto !important; width: 200px; }", + &device, |declarations, css| { + println!("{}", css); + assert_declarations_len!(declarations == 2); + assert_declaration_eq!(&declarations[0], Author, MinWidth: LengthOrPercentageOrAuto::Auto, !important); + assert_declaration_eq!(&declarations[1], Author, MaxWidth: LengthOrPercentageOrAuto::Length(Length::from_px(200.))); + }); + + // both !important longhands vs normal shorthand + test_viewport_rule("@viewport { min-width: auto !important; max-width: auto !important; width: 200px; }", + &device, |declarations, css| { + println!("{}", css); + assert_declarations_len!(declarations == 2); + assert_declaration_eq!(&declarations[0], Author, MinWidth: LengthOrPercentageOrAuto::Auto, !important); + assert_declaration_eq!(&declarations[1], Author, MaxWidth: LengthOrPercentageOrAuto::Auto, !important); + }); +} + +#[test] +fn multiple_stylesheets_cascading() { + let device = Device::new(MediaType::Screen, TypedSize2D(800., 600.)); + + let stylesheets = vec![ + stylesheet!("@viewport { min-width: 100px; min-height: 100px; zoom: 1; }", UserAgent), + stylesheet!("@viewport { min-width: 200px; min-height: 200px; }", User), + stylesheet!("@viewport { min-width: 300px; }", Author)]; + + let declarations = stylesheets.iter() + .flat_map(|s| s.effective_rules(&device).viewport()) + .cascade() + .declarations; + assert_declarations_len!(declarations == 3); + assert_declaration_eq!(&declarations[0], UserAgent, Zoom: Zoom::Number(1.)); + assert_declaration_eq!(&declarations[1], User, MinHeight: LengthOrPercentageOrAuto::Length(Length::from_px(200.))); + assert_declaration_eq!(&declarations[2], Author, MinWidth: LengthOrPercentageOrAuto::Length(Length::from_px(300.))); + + let stylesheets = vec![ + stylesheet!("@viewport { min-width: 100px !important; }", UserAgent), + stylesheet!("@viewport { min-width: 200px !important; min-height: 200px !important; }", User), + stylesheet!("@viewport { min-width: 300px !important; min-height: 300px !important; zoom: 3 !important; }", Author)]; + + let declarations = stylesheets.iter() + .flat_map(|s| s.effective_rules(&device).viewport()) + .cascade() + .declarations; + assert_declarations_len!(declarations == 3); + assert_declaration_eq!(&declarations[0], UserAgent, MinWidth: LengthOrPercentageOrAuto::Length(Length::from_px(100.)), !important); + 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 + })); +}