/* 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/. */ //! [Media queries][mq]. //! //! [mq]: https://drafts.csswg.org/mediaqueries/ use Atom; use app_units::Au; use cssparser::{Delimiter, Parser, Token}; use euclid::size::{Size2D, TypedSize2D}; use serialize_comma_separated_list; use std::ascii::AsciiExt; use std::fmt; use style_traits::{ToCss, ViewportPx}; use values::computed::{self, ToComputedValue}; use values::specified; #[derive(Debug, PartialEq)] #[cfg_attr(feature = "servo", derive(HeapSizeOf))] pub struct MediaList { pub media_queries: Vec } impl ToCss for MediaList { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { serialize_comma_separated_list(dest, &self.media_queries) } } impl Default for MediaList { fn default() -> MediaList { MediaList { media_queries: vec![] } } } #[derive(PartialEq, Eq, Copy, Clone, Debug)] #[cfg_attr(feature = "servo", derive(HeapSizeOf))] pub enum Range { Min(T), Max(T), Eq(T), } impl Range { fn to_computed_range(&self, viewport_size: Size2D) -> Range { // http://dev.w3.org/csswg/mediaqueries3/#units // em units are relative to the initial font-size. let context = computed::Context::initial(viewport_size, false); match *self { Range::Min(ref width) => Range::Min(width.to_computed_value(&context)), Range::Max(ref width) => Range::Max(width.to_computed_value(&context)), Range::Eq(ref width) => Range::Eq(width.to_computed_value(&context)) } } } impl Range { fn evaluate(&self, value: T) -> bool { match *self { Range::Min(ref width) => { value >= *width }, Range::Max(ref width) => { value <= *width }, Range::Eq(ref width) => { value == *width }, } } } /// http://dev.w3.org/csswg/mediaqueries-3/#media1 #[derive(PartialEq, Copy, Clone, Debug)] #[cfg_attr(feature = "servo", derive(HeapSizeOf))] pub enum Expression { /// http://dev.w3.org/csswg/mediaqueries-3/#width Width(Range), } /// http://dev.w3.org/csswg/mediaqueries-3/#media0 #[derive(PartialEq, Eq, Copy, Clone, Debug)] #[cfg_attr(feature = "servo", derive(HeapSizeOf))] pub enum Qualifier { Only, Not, } impl ToCss for Qualifier { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { match *self { Qualifier::Not => write!(dest, "not"), Qualifier::Only => write!(dest, "only"), } } } #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "servo", derive(HeapSizeOf))] pub struct MediaQuery { pub qualifier: Option, pub media_type: MediaQueryType, pub expressions: Vec, } impl MediaQuery { /// Return a media query that never matches, used for when we fail to parse /// a given media query. fn never_matching() -> Self { Self::new(Some(Qualifier::Not), MediaQueryType::All, vec![]) } pub fn new(qualifier: Option, media_type: MediaQueryType, expressions: Vec) -> MediaQuery { MediaQuery { qualifier: qualifier, media_type: media_type, expressions: expressions, } } } impl ToCss for MediaQuery { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { if let Some(qual) = self.qualifier { try!(qual.to_css(dest)); try!(write!(dest, " ")); } match self.media_type { MediaQueryType::All => { // We need to print "all" if there's a qualifier, or there's // just an empty list of expressions. // // Otherwise, we'd serialize media queries like "(min-width: // 40px)" in "all (min-width: 40px)", which is unexpected. if self.qualifier.is_some() || self.expressions.is_empty() { try!(write!(dest, "all")); } }, MediaQueryType::Known(MediaType::Screen) => try!(write!(dest, "screen")), MediaQueryType::Known(MediaType::Print) => try!(write!(dest, "print")), MediaQueryType::Unknown(ref desc) => try!(write!(dest, "{}", desc)), } if self.expressions.is_empty() { return Ok(()); } if self.media_type != MediaQueryType::All || self.qualifier.is_some() { try!(write!(dest, " and ")); } for (i, &e) in self.expressions.iter().enumerate() { try!(write!(dest, "(")); let (mm, l) = match e { Expression::Width(Range::Min(ref l)) => ("min-", l), Expression::Width(Range::Max(ref l)) => ("max-", l), Expression::Width(Range::Eq(ref l)) => ("", l), }; try!(write!(dest, "{}width: ", mm)); try!(l.to_css(dest)); try!(write!(dest, ")")); if i != self.expressions.len() - 1 { try!(write!(dest, " and ")); } } Ok(()) } } /// http://dev.w3.org/csswg/mediaqueries-3/#media0 #[derive(PartialEq, Eq, Clone, Debug)] #[cfg_attr(feature = "servo", derive(HeapSizeOf))] pub enum MediaQueryType { All, // Always true Known(MediaType), Unknown(Atom), } impl MediaQueryType { fn parse(ident: &str) -> Self { if ident.eq_ignore_ascii_case("all") { return MediaQueryType::All; } match MediaType::parse(ident) { Some(media_type) => MediaQueryType::Known(media_type), None => MediaQueryType::Unknown(Atom::from(ident)), } } fn matches(&self, other: &MediaType) -> bool { match *self { MediaQueryType::All => true, MediaQueryType::Known(ref known_type) => known_type == other, MediaQueryType::Unknown(..) => false, } } } #[derive(PartialEq, Eq, Clone, Debug)] #[cfg_attr(feature = "servo", derive(HeapSizeOf))] pub enum MediaType { Screen, Print, } impl MediaType { fn parse(name: &str) -> Option { Some(match_ignore_ascii_case! { name, "screen" => MediaType::Screen, "print" => MediaType::Print, _ => return None }) } } #[derive(Debug)] #[cfg_attr(feature = "servo", derive(HeapSizeOf))] pub struct Device { pub media_type: MediaType, pub viewport_size: TypedSize2D, } impl Device { pub fn new(media_type: MediaType, viewport_size: TypedSize2D) -> Device { Device { media_type: media_type, viewport_size: viewport_size, } } #[inline] pub fn au_viewport_size(&self) -> Size2D { Size2D::new(Au::from_f32_px(self.viewport_size.width), Au::from_f32_px(self.viewport_size.height)) } } impl Expression { fn parse(input: &mut Parser) -> Result { try!(input.expect_parenthesis_block()); input.parse_nested_block(|input| { let name = try!(input.expect_ident()); try!(input.expect_colon()); // TODO: Handle other media features match_ignore_ascii_case! { name, "min-width" => { Ok(Expression::Width(Range::Min(try!(specified::Length::parse_non_negative(input))))) }, "max-width" => { Ok(Expression::Width(Range::Max(try!(specified::Length::parse_non_negative(input))))) }, "width" => { Ok(Expression::Width(Range::Eq(try!(specified::Length::parse_non_negative(input))))) }, _ => Err(()) } }) } } impl MediaQuery { pub fn parse(input: &mut Parser) -> Result { let mut expressions = vec![]; let qualifier = if input.try(|input| input.expect_ident_matching("only")).is_ok() { Some(Qualifier::Only) } else if input.try(|input| input.expect_ident_matching("not")).is_ok() { Some(Qualifier::Not) } else { None }; let media_type = match input.try(|input| input.expect_ident()) { Ok(ident) => MediaQueryType::parse(&*ident), Err(()) => { // Media type is only optional if qualifier is not specified. if qualifier.is_some() { return Err(()) } // Without a media type, require at least one expression. expressions.push(try!(Expression::parse(input))); MediaQueryType::All } }; // Parse any subsequent expressions loop { if input.try(|input| input.expect_ident_matching("and")).is_err() { return Ok(MediaQuery::new(qualifier, media_type, expressions)) } expressions.push(try!(Expression::parse(input))) } } } pub fn parse_media_query_list(input: &mut Parser) -> MediaList { if input.is_exhausted() { return Default::default() } let mut media_queries = vec![]; loop { media_queries.push( input.parse_until_before(Delimiter::Comma, MediaQuery::parse).ok() .unwrap_or_else(MediaQuery::never_matching)); match input.next() { Ok(Token::Comma) => {}, Ok(_) => unreachable!(), Err(()) => break, } } MediaList { media_queries: media_queries, } } impl MediaList { pub fn evaluate(&self, device: &Device) -> bool { let viewport_size = device.au_viewport_size(); // Check if it is an empty media query list or any queries match (OR condition) // https://drafts.csswg.org/mediaqueries-4/#mq-list self.media_queries.is_empty() || self.media_queries.iter().any(|mq| { let media_match = mq.media_type.matches(&device.media_type); // Check if all conditions match (AND condition) let query_match = media_match && mq.expressions.iter().all(|expression| { match *expression { Expression::Width(ref value) => value.to_computed_range(viewport_size).evaluate(viewport_size.width), } }); // Apply the logical NOT qualifier to the result match mq.qualifier { Some(Qualifier::Not) => !query_match, _ => query_match, } }) } pub fn is_empty(&self) -> bool { self.media_queries.is_empty() } }