/* 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/. */ //! Gecko's media-query device and expression representation. use app_units::AU_PER_PX; use app_units::Au; use context::QuirksMode; use cssparser::{BasicParseErrorKind, Parser, RGBA}; use euclid::Size2D; use euclid::TypedScale; use gecko::values::{convert_nscolor_to_rgba, convert_rgba_to_nscolor}; use gecko_bindings::bindings; use gecko_bindings::structs; use gecko_bindings::structs::{nsCSSKTableEntry, nsCSSKeyword, nsCSSUnit, nsCSSValue}; use gecko_bindings::structs::{nsMediaFeature, nsMediaFeature_RangeType}; use gecko_bindings::structs::{nsMediaFeature_ValueType, nsPresContext}; use gecko_bindings::structs::RawGeckoPresContextOwned; use media_queries::MediaType; use parser::{Parse, ParserContext}; use properties::ComputedValues; use servo_arc::Arc; use std::fmt::{self, Write}; use std::sync::atomic::{AtomicBool, AtomicIsize, AtomicUsize, Ordering}; use str::starts_with_ignore_ascii_case; use string_cache::Atom; use style_traits::{CSSPixel, CssWriter, DevicePixel}; use style_traits::{ParseError, StyleParseErrorKind, ToCss}; use style_traits::viewport::ViewportConstraints; use stylesheets::Origin; use values::{serialize_atom_identifier, CSSFloat, CustomIdent, KeyframesName}; use values::computed::{self, ToComputedValue}; use values::computed::font::FontSize; use values::specified::{Integer, Length, Number, Resolution}; /// The `Device` in Gecko wraps a pres context, has a default values computed, /// and contains all the viewport rule state. pub struct Device { /// NB: The pres context lifetime is tied to the styleset, who owns the /// stylist, and thus the `Device`, so having a raw pres context pointer /// here is fine. pres_context: RawGeckoPresContextOwned, default_values: Arc, /// The font size of the root element /// This is set when computing the style of the root /// element, and used for rem units in other elements. /// /// When computing the style of the root element, there can't be any /// other style being computed at the same time, given we need the style of /// the parent to compute everything else. So it is correct to just use /// a relaxed atomic here. root_font_size: AtomicIsize, /// The body text color, stored as an `nscolor`, used for the "tables /// inherit from body" quirk. /// /// body_text_color: AtomicUsize, /// Whether any styles computed in the document relied on the root font-size /// by using rem units. used_root_font_size: AtomicBool, /// Whether any styles computed in the document relied on the viewport size /// by using vw/vh/vmin/vmax units. used_viewport_size: AtomicBool, } unsafe impl Sync for Device {} unsafe impl Send for Device {} impl Device { /// Trivially constructs a new `Device`. pub fn new(pres_context: RawGeckoPresContextOwned) -> Self { assert!(!pres_context.is_null()); Device { pres_context, default_values: ComputedValues::default_values(unsafe { &*pres_context }), // FIXME(bz): Seems dubious? root_font_size: AtomicIsize::new(FontSize::medium().size().0 as isize), body_text_color: AtomicUsize::new(unsafe { &*pres_context }.mDefaultColor as usize), used_root_font_size: AtomicBool::new(false), used_viewport_size: AtomicBool::new(false), } } /// Tells the device that a new viewport rule has been found, and stores the /// relevant viewport constraints. pub fn account_for_viewport_rule(&mut self, _constraints: &ViewportConstraints) { unreachable!("Gecko doesn't support @viewport"); } /// Whether any animation name may be referenced from the style of any /// element. pub fn animation_name_may_be_referenced(&self, name: &KeyframesName) -> bool { unsafe { bindings::Gecko_AnimationNameMayBeReferencedFromStyle( self.pres_context(), name.as_atom().as_ptr(), ) } } /// Returns the default computed values as a reference, in order to match /// Servo. pub fn default_computed_values(&self) -> &ComputedValues { &self.default_values } /// Returns the default computed values as an `Arc`. pub fn default_computed_values_arc(&self) -> &Arc { &self.default_values } /// Get the font size of the root element (for rem) pub fn root_font_size(&self) -> Au { self.used_root_font_size.store(true, Ordering::Relaxed); Au::new(self.root_font_size.load(Ordering::Relaxed) as i32) } /// Set the font size of the root element (for rem) pub fn set_root_font_size(&self, size: Au) { self.root_font_size .store(size.0 as isize, Ordering::Relaxed) } /// Sets the body text color for the "inherit color from body" quirk. /// /// pub fn set_body_text_color(&self, color: RGBA) { self.body_text_color .store(convert_rgba_to_nscolor(&color) as usize, Ordering::Relaxed) } /// Returns the body text color. pub fn body_text_color(&self) -> RGBA { convert_nscolor_to_rgba(self.body_text_color.load(Ordering::Relaxed) as u32) } /// Gets the pres context associated with this document. pub fn pres_context(&self) -> &nsPresContext { unsafe { &*self.pres_context } } /// Recreates the default computed values. pub fn reset_computed_values(&mut self) { self.default_values = ComputedValues::default_values(self.pres_context()); } /// Rebuild all the cached data. pub fn rebuild_cached_data(&mut self) { self.reset_computed_values(); self.used_root_font_size.store(false, Ordering::Relaxed); self.used_viewport_size.store(false, Ordering::Relaxed); } /// Returns whether we ever looked up the root font size of the Device. pub fn used_root_font_size(&self) -> bool { self.used_root_font_size.load(Ordering::Relaxed) } /// Recreates all the temporary state that the `Device` stores. /// /// This includes the viewport override from `@viewport` rules, and also the /// default computed values. pub fn reset(&mut self) { self.reset_computed_values(); } /// Returns the current media type of the device. pub fn media_type(&self) -> MediaType { // Gecko allows emulating random media with mIsEmulatingMedia and // mMediaEmulated. let context = self.pres_context(); let medium_to_use = if context.mIsEmulatingMedia() != 0 { context.mMediaEmulated.mRawPtr } else { context.mMedium }; MediaType(CustomIdent(unsafe { Atom::from_raw(medium_to_use) })) } /// Returns the current viewport size in app units. pub fn au_viewport_size(&self) -> Size2D { let area = &self.pres_context().mVisibleArea; Size2D::new(Au(area.width), Au(area.height)) } /// Returns the current viewport size in app units, recording that it's been /// used for viewport unit resolution. pub fn au_viewport_size_for_viewport_unit_resolution(&self) -> Size2D { self.used_viewport_size.store(true, Ordering::Relaxed); self.au_viewport_size() } /// Returns whether we ever looked up the viewport size of the Device. pub fn used_viewport_size(&self) -> bool { self.used_viewport_size.load(Ordering::Relaxed) } /// Returns the device pixel ratio. pub fn device_pixel_ratio(&self) -> TypedScale { let override_dppx = self.pres_context().mOverrideDPPX; if override_dppx > 0.0 { return TypedScale::new(override_dppx); } let au_per_dpx = self.pres_context().mCurAppUnitsPerDevPixel as f32; let au_per_px = AU_PER_PX as f32; TypedScale::new(au_per_px / au_per_dpx) } /// Returns whether document colors are enabled. pub fn use_document_colors(&self) -> bool { self.pres_context().mUseDocumentColors() != 0 } /// Returns the default background color. pub fn default_background_color(&self) -> RGBA { convert_nscolor_to_rgba(self.pres_context().mBackgroundColor) } /// Applies text zoom to a font-size or line-height value (see nsStyleFont::ZoomText). pub fn zoom_text(&self, size: Au) -> Au { size.scale_by(self.pres_context().mEffectiveTextZoom) } /// Un-apply text zoom (see nsStyleFont::UnzoomText). pub fn unzoom_text(&self, size: Au) -> Au { size.scale_by(1. / self.pres_context().mEffectiveTextZoom) } } /// The kind of matching that should be performed on a media feature value. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Range { /// At least the specified value. Min, /// At most the specified value. Max, /// Exactly the specified value. Equal, } /// A expression for gecko contains a reference to the media feature, the value /// the media query contained, and the range to evaluate. #[derive(Clone, Debug)] pub struct Expression { feature: &'static nsMediaFeature, value: Option, range: Range, } impl ToCss for Expression { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: fmt::Write, { dest.write_str("(")?; if (self.feature.mReqFlags & structs::nsMediaFeature_RequirementFlags_eHasWebkitPrefix) != 0 { dest.write_str("-webkit-")?; } match self.range { Range::Min => dest.write_str("min-")?, Range::Max => dest.write_str("max-")?, Range::Equal => {}, } // NB: CssStringWriter not needed, feature names are under control. write!(dest, "{}", unsafe { Atom::from_static(*self.feature.mName) })?; if let Some(ref val) = self.value { dest.write_str(": ")?; val.to_css(dest, self)?; } dest.write_str(")") } } impl PartialEq for Expression { fn eq(&self, other: &Expression) -> bool { self.feature.mName == other.feature.mName && self.value == other.value && self.range == other.range } } /// A value found or expected in a media expression. /// /// FIXME(emilio): How should calc() serialize in the Number / Integer / /// BoolInteger / IntRatio case, as computed or as specified value? /// /// If the first, this would need to store the relevant values. /// /// See: https://github.com/w3c/csswg-drafts/issues/1968 #[derive(Clone, Debug, PartialEq)] pub enum MediaExpressionValue { /// A length. Length(Length), /// A (non-negative) integer. Integer(u32), /// A floating point value. Float(CSSFloat), /// A boolean value, specified as an integer (i.e., either 0 or 1). BoolInteger(bool), /// Two integers separated by '/', with optional whitespace on either side /// of the '/'. IntRatio(u32, u32), /// A resolution. Resolution(Resolution), /// An enumerated value, defined by the variant keyword table in the /// feature's `mData` member. Enumerated(i16), /// An identifier. Ident(Atom), } impl MediaExpressionValue { fn from_css_value(for_expr: &Expression, css_value: &nsCSSValue) -> Option { // NB: If there's a null value, that means that we don't support the // feature. if css_value.mUnit == nsCSSUnit::eCSSUnit_Null { return None; } match for_expr.feature.mValueType { nsMediaFeature_ValueType::eLength => { debug_assert_eq!(css_value.mUnit, nsCSSUnit::eCSSUnit_Pixel); let pixels = css_value.float_unchecked(); Some(MediaExpressionValue::Length(Length::from_px(pixels))) }, nsMediaFeature_ValueType::eInteger => { let i = css_value.integer_unchecked(); debug_assert!(i >= 0); Some(MediaExpressionValue::Integer(i as u32)) }, nsMediaFeature_ValueType::eFloat => { debug_assert_eq!(css_value.mUnit, nsCSSUnit::eCSSUnit_Number); Some(MediaExpressionValue::Float(css_value.float_unchecked())) }, nsMediaFeature_ValueType::eBoolInteger => { debug_assert_eq!(css_value.mUnit, nsCSSUnit::eCSSUnit_Integer); let i = css_value.integer_unchecked(); debug_assert!(i == 0 || i == 1); Some(MediaExpressionValue::BoolInteger(i == 1)) }, nsMediaFeature_ValueType::eResolution => { debug_assert_eq!(css_value.mUnit, nsCSSUnit::eCSSUnit_Pixel); Some(MediaExpressionValue::Resolution(Resolution::Dppx( css_value.float_unchecked(), ))) }, nsMediaFeature_ValueType::eEnumerated => { let value = css_value.integer_unchecked() as i16; Some(MediaExpressionValue::Enumerated(value)) }, nsMediaFeature_ValueType::eIdent => { debug_assert_eq!(css_value.mUnit, nsCSSUnit::eCSSUnit_AtomIdent); Some(MediaExpressionValue::Ident(unsafe { Atom::from_raw(*css_value.mValue.mAtom.as_ref()) })) }, nsMediaFeature_ValueType::eIntRatio => { let array = unsafe { css_value.array_unchecked() }; debug_assert_eq!(array.len(), 2); let first = array[0].integer_unchecked(); let second = array[1].integer_unchecked(); debug_assert!(first >= 0 && second >= 0); Some(MediaExpressionValue::IntRatio(first as u32, second as u32)) }, } } } impl MediaExpressionValue { fn to_css(&self, dest: &mut CssWriter, for_expr: &Expression) -> fmt::Result where W: fmt::Write, { match *self { MediaExpressionValue::Length(ref l) => l.to_css(dest), MediaExpressionValue::Integer(v) => v.to_css(dest), MediaExpressionValue::Float(v) => v.to_css(dest), MediaExpressionValue::BoolInteger(v) => dest.write_str(if v { "1" } else { "0" }), MediaExpressionValue::IntRatio(a, b) => { a.to_css(dest)?; dest.write_char('/')?; b.to_css(dest) }, MediaExpressionValue::Resolution(ref r) => r.to_css(dest), MediaExpressionValue::Ident(ref ident) => serialize_atom_identifier(ident, dest), MediaExpressionValue::Enumerated(value) => unsafe { use std::{slice, str}; use std::os::raw::c_char; // NB: All the keywords on nsMediaFeatures are static, // well-formed utf-8. let mut length = 0; let (keyword, _value) = find_in_table( *for_expr.feature.mData.mKeywordTable.as_ref(), |_kw, val| val == value, ).expect("Value not found in the keyword table?"); let buffer: *const c_char = bindings::Gecko_CSSKeywordString(keyword, &mut length); let buffer = slice::from_raw_parts(buffer as *const u8, length as usize); let string = str::from_utf8_unchecked(buffer); dest.write_str(string) }, } } } fn find_feature(mut f: F) -> Option<&'static nsMediaFeature> where F: FnMut(&'static nsMediaFeature) -> bool, { unsafe { let mut features = structs::nsMediaFeatures_features.as_ptr(); while !(*features).mName.is_null() { if f(&*features) { return Some(&*features); } features = features.offset(1); } } None } unsafe fn find_in_table( mut current_entry: *const nsCSSKTableEntry, mut f: F, ) -> Option<(nsCSSKeyword, i16)> where F: FnMut(nsCSSKeyword, i16) -> bool, { loop { let value = (*current_entry).mValue; let keyword = (*current_entry).mKeyword; if value == -1 { return None; // End of the table. } if f(keyword, value) { return Some((keyword, value)); } current_entry = current_entry.offset(1); } } fn parse_feature_value<'i, 't>( feature: &nsMediaFeature, feature_value_type: nsMediaFeature_ValueType, context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let value = match feature_value_type { nsMediaFeature_ValueType::eLength => { let length = Length::parse_non_negative(context, input)?; MediaExpressionValue::Length(length) }, nsMediaFeature_ValueType::eInteger => { let integer = Integer::parse_non_negative(context, input)?; MediaExpressionValue::Integer(integer.value() as u32) }, nsMediaFeature_ValueType::eBoolInteger => { let integer = Integer::parse_non_negative(context, input)?; let value = integer.value(); if value > 1 { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } MediaExpressionValue::BoolInteger(value == 1) }, nsMediaFeature_ValueType::eFloat => { let number = Number::parse(context, input)?; MediaExpressionValue::Float(number.get()) }, nsMediaFeature_ValueType::eIntRatio => { let a = Integer::parse_positive(context, input)?; input.expect_delim('/')?; let b = Integer::parse_positive(context, input)?; MediaExpressionValue::IntRatio(a.value() as u32, b.value() as u32) }, nsMediaFeature_ValueType::eResolution => { MediaExpressionValue::Resolution(Resolution::parse(context, input)?) }, nsMediaFeature_ValueType::eEnumerated => { let location = input.current_source_location(); let keyword = input.expect_ident()?; let keyword = unsafe { bindings::Gecko_LookupCSSKeyword(keyword.as_bytes().as_ptr(), keyword.len() as u32) }; let first_table_entry: *const nsCSSKTableEntry = unsafe { *feature.mData.mKeywordTable.as_ref() }; let value = match unsafe { find_in_table(first_table_entry, |kw, _| kw == keyword) } { Some((_kw, value)) => value, None => { return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)) }, }; MediaExpressionValue::Enumerated(value) }, nsMediaFeature_ValueType::eIdent => { MediaExpressionValue::Ident(Atom::from(input.expect_ident()?.as_ref())) }, }; Ok(value) } impl Expression { /// Trivially construct a new expression. fn new( feature: &'static nsMediaFeature, value: Option, range: Range, ) -> Self { Self { feature, value, range, } } /// Parse a media expression of the form: /// /// ``` /// (media-feature: media-value) /// ``` pub fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { input.expect_parenthesis_block().map_err(|err| { err.location.new_custom_error(match err.kind { BasicParseErrorKind::UnexpectedToken(t) => { StyleParseErrorKind::ExpectedIdentifier(t) }, _ => StyleParseErrorKind::UnspecifiedError, }) })?; input.parse_nested_block(|input| { // FIXME: remove extra indented block when lifetimes are non-lexical let feature; let range; { let location = input.current_source_location(); let ident = input.expect_ident().map_err(|err| { err.location.new_custom_error(match err.kind { BasicParseErrorKind::UnexpectedToken(t) => { StyleParseErrorKind::ExpectedIdentifier(t) }, _ => StyleParseErrorKind::UnspecifiedError, }) })?; let mut flags = 0; if context.chrome_rules_enabled() || context.stylesheet_origin == Origin::UserAgent { flags |= structs::nsMediaFeature_RequirementFlags_eUserAgentAndChromeOnly; } let result = { let mut feature_name = &**ident; if unsafe { structs::StaticPrefs_sVarCache_layout_css_prefixes_webkit } && starts_with_ignore_ascii_case(feature_name, "-webkit-") { feature_name = &feature_name[8..]; flags |= structs::nsMediaFeature_RequirementFlags_eHasWebkitPrefix; if unsafe { structs::StaticPrefs_sVarCache_layout_css_prefixes_device_pixel_ratio_webkit } { flags |= structs::nsMediaFeature_RequirementFlags_eWebkitDevicePixelRatioPrefEnabled; } } let range = if starts_with_ignore_ascii_case(feature_name, "min-") { feature_name = &feature_name[4..]; Range::Min } else if starts_with_ignore_ascii_case(feature_name, "max-") { feature_name = &feature_name[4..]; Range::Max } else { Range::Equal }; let atom = Atom::from(feature_name); match find_feature(|f| atom.as_ptr() == unsafe { *f.mName as *mut _ }) { Some(f) => Ok((f, range)), None => Err(()), } }; match result { Ok((f, r)) => { feature = f; range = r; }, Err(()) => { return Err(location.new_custom_error( StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()), )) }, } if (feature.mReqFlags & !flags) != 0 { return Err(location.new_custom_error( StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()), )); } if range != Range::Equal && feature.mRangeType != nsMediaFeature_RangeType::eMinMaxAllowed { return Err(location.new_custom_error( StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()), )); } } // If there's no colon, this is a media query of the form // '()', that is, there's no value specified. // // Gecko doesn't allow ranged expressions without a value, so just // reject them here too. if input.try(|i| i.expect_colon()).is_err() { if range != Range::Equal { return Err(input.new_custom_error(StyleParseErrorKind::RangedExpressionWithNoValue)); } return Ok(Expression::new(feature, None, range)); } let value = parse_feature_value(feature, feature.mValueType, context, input).map_err(|err| { err.location .new_custom_error(StyleParseErrorKind::MediaQueryExpectedFeatureValue) })?; Ok(Expression::new(feature, Some(value), range)) }) } /// Returns whether this media query evaluates to true for the given device. pub fn matches(&self, device: &Device, quirks_mode: QuirksMode) -> bool { let mut css_value = nsCSSValue::null(); unsafe { (self.feature.mGetter.unwrap())( device .pres_context() .mDocument .raw::(), self.feature, &mut css_value, ) }; let value = match MediaExpressionValue::from_css_value(self, &css_value) { Some(v) => v, None => return false, }; self.evaluate_against(device, &value, quirks_mode) } fn evaluate_against( &self, device: &Device, actual_value: &MediaExpressionValue, quirks_mode: QuirksMode, ) -> bool { use self::MediaExpressionValue::*; use std::cmp::Ordering; debug_assert!( self.range == Range::Equal || self.feature.mRangeType == nsMediaFeature_RangeType::eMinMaxAllowed, "Whoops, wrong range" ); // http://dev.w3.org/csswg/mediaqueries3/#units // em units are relative to the initial font-size. let required_value = match self.value { Some(ref v) => v, None => { // If there's no value, always match unless it's a zero length // or a zero integer or boolean. return match *actual_value { BoolInteger(v) => v, Integer(v) => v != 0, Length(ref l) => computed::Context::for_media_query_evaluation( device, quirks_mode, |context| l.to_computed_value(&context).px() != 0., ), _ => true, }; }, }; // FIXME(emilio): Handle the possible floating point errors? let cmp = match (required_value, actual_value) { (&Length(ref one), &Length(ref other)) => { computed::Context::for_media_query_evaluation(device, quirks_mode, |context| { one.to_computed_value(&context) .to_i32_au() .cmp(&other.to_computed_value(&context).to_i32_au()) }) }, (&Integer(one), &Integer(ref other)) => one.cmp(other), (&BoolInteger(one), &BoolInteger(ref other)) => one.cmp(other), (&Float(one), &Float(ref other)) => one.partial_cmp(other).unwrap(), (&IntRatio(one_num, one_den), &IntRatio(other_num, other_den)) => { // Extend to avoid overflow. (one_num as u64 * other_den as u64).cmp(&(other_num as u64 * one_den as u64)) }, (&Resolution(ref one), &Resolution(ref other)) => { let actual_dpi = unsafe { if (*device.pres_context).mOverrideDPPX > 0.0 { self::Resolution::Dppx((*device.pres_context).mOverrideDPPX).to_dpi() } else { other.to_dpi() } }; one.to_dpi().partial_cmp(&actual_dpi).unwrap() }, (&Ident(ref one), &Ident(ref other)) => { debug_assert_ne!( self.feature.mRangeType, nsMediaFeature_RangeType::eMinMaxAllowed ); return one == other; }, (&Enumerated(one), &Enumerated(other)) => { debug_assert_ne!( self.feature.mRangeType, nsMediaFeature_RangeType::eMinMaxAllowed ); return one == other; }, _ => unreachable!(), }; cmp == Ordering::Equal || match self.range { Range::Min => cmp == Ordering::Less, Range::Equal => false, Range::Max => cmp == Ordering::Greater, } } }