diff --git a/components/style/Cargo.toml b/components/style/Cargo.toml index 40fcc390228..d673dc5c610 100644 --- a/components/style/Cargo.toml +++ b/components/style/Cargo.toml @@ -49,6 +49,7 @@ matches = "0.1" num_cpus = {version = "1.1.0", optional = true} num-integer = "0.1.32" num-traits = "0.2" +num-derive = "0.2" new-ordered-float = "1.0" owning_ref = "0.3.3" parking_lot = "0.6" diff --git a/components/style/cbindgen.toml b/components/style/cbindgen.toml index 86c3dddec4c..5a0e5e19ea7 100644 --- a/components/style/cbindgen.toml +++ b/components/style/cbindgen.toml @@ -22,5 +22,5 @@ derive_helper_methods = true [export] prefix = "Style" -include = ["StyleDisplay", "StyleAppearance"] +include = ["StyleDisplay", "StyleAppearance", "StyleDisplayMode"] item_types = ["enums"] diff --git a/components/style/gecko/media_features.rs b/components/style/gecko/media_features.rs new file mode 100644 index 00000000000..822a64d738b --- /dev/null +++ b/components/style/gecko/media_features.rs @@ -0,0 +1,610 @@ +/* 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 feature list and evaluator. + +use Atom; +use app_units::Au; +use euclid::Size2D; +use gecko_bindings::bindings; +use values::computed::CSSPixelLength; +use values::computed::Resolution; + +use media_queries::Device; +use media_queries::media_feature::{MediaFeatureDescription, Evaluator}; +use media_queries::media_feature::{AllowsRanges, ParsingRequirements}; +use media_queries::media_feature_expression::{AspectRatio, RangeOrOperator}; + +macro_rules! feature { + ($name:expr, $allows_ranges:expr, $evaluator:expr, $reqs:expr,) => { + MediaFeatureDescription { + name: $name, + allows_ranges: $allows_ranges, + evaluator: $evaluator, + requirements: $reqs, + } + } +} + +fn viewport_size(device: &Device) -> Size2D { + let pc = device.pres_context(); + if pc.mIsRootPaginatedDocument() != 0 { + // We want the page size, including unprintable areas and margins. + // FIXME(emilio, bug 1414600): Not quite! + let area = &pc.mPageSize; + return Size2D::new(Au(area.width), Au(area.height)) + } + device.au_viewport_size() +} + +fn device_size(device: &Device) -> Size2D { + let mut width = 0; + let mut height = 0; + unsafe { + bindings::Gecko_MediaFeatures_GetDeviceSize( + device.document(), + &mut width, + &mut height, + ); + } + Size2D::new(Au(width), Au(height)) +} + +fn eval_width( + device: &Device, + value: Option, + range_or_operator: Option, +) -> bool { + RangeOrOperator::evaluate( + range_or_operator, + value.map(Au::from), + viewport_size(device).width, + ) +} + +fn eval_device_width( + device: &Device, + value: Option, + range_or_operator: Option, +) -> bool { + RangeOrOperator::evaluate( + range_or_operator, + value.map(Au::from), + device_size(device).width, + ) +} + +fn eval_height( + device: &Device, + value: Option, + range_or_operator: Option, +) -> bool { + RangeOrOperator::evaluate( + range_or_operator, + value.map(Au::from), + viewport_size(device).height, + ) +} + +fn eval_device_height( + device: &Device, + value: Option, + range_or_operator: Option, +) -> bool { + RangeOrOperator::evaluate( + range_or_operator, + value.map(Au::from), + device_size(device).height, + ) +} + +fn eval_aspect_ratio_for( + device: &Device, + query_value: Option, + range_or_operator: Option, + get_size: F, +) -> bool +where + F: FnOnce(&Device) -> Size2D, +{ + let query_value = match query_value { + Some(v) => v, + None => return true, + }; + + let size = get_size(device); + RangeOrOperator::evaluate( + range_or_operator, + Some(size.height.0 as u64 * query_value.0 as u64), + size.width.0 as u64 * query_value.1 as u64, + ) +} + +fn eval_aspect_ratio( + device: &Device, + query_value: Option, + range_or_operator: Option, +) -> bool { + eval_aspect_ratio_for(device, query_value, range_or_operator, viewport_size) +} + +fn eval_device_aspect_ratio( + device: &Device, + query_value: Option, + range_or_operator: Option, +) -> bool { + eval_aspect_ratio_for(device, query_value, range_or_operator, device_size) +} + +fn eval_device_pixel_ratio( + device: &Device, + query_value: Option, + range_or_operator: Option, +) -> bool { + let ratio = unsafe { + bindings::Gecko_MediaFeatures_GetDevicePixelRatio(device.document()) + }; + + RangeOrOperator::evaluate( + range_or_operator, + query_value, + ratio, + ) +} + +#[derive(Debug, Copy, Clone, FromPrimitive, ToCss, Parse)] +#[repr(u8)] +enum Orientation { + Landscape, + Portrait, +} + +fn eval_orientation_for( + device: &Device, + value: Option, + get_size: F, +) -> bool +where + F: FnOnce(&Device) -> Size2D, +{ + let query_orientation = match value { + Some(v) => v, + None => return true, + }; + + let size = get_size(device); + + // Per spec, square viewports should be 'portrait' + let is_landscape = size.width > size.height; + match query_orientation { + Orientation::Landscape => is_landscape, + Orientation::Portrait => !is_landscape, + } +} + +fn eval_orientation( + device: &Device, + value: Option, +) -> bool { + eval_orientation_for(device, value, viewport_size) +} + +fn eval_device_orientation( + device: &Device, + value: Option, +) -> bool { + eval_orientation_for(device, value, device_size) +} + +/// Values for the display-mode media feature. +#[derive(Debug, Copy, Clone, FromPrimitive, ToCss, Parse)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum DisplayMode { + Browser = 0, + MinimalUi, + Standalone, + Fullscreen, +} + +fn eval_display_mode( + device: &Device, + query_value: Option, +) -> bool { + let query_value = match query_value { + Some(v) => v, + None => return true, + }; + + let gecko_display_mode = unsafe { + bindings::Gecko_MediaFeatures_GetDisplayMode(device.document()) + }; + + // NOTE: cbindgen guarantees the same representation. + gecko_display_mode as u8 == query_value as u8 +} + +fn eval_grid(_: &Device, query_value: Option, _: Option) -> bool { + // Gecko doesn't support grid devices (e.g., ttys), so the 'grid' feature + // is always 0. + let supports_grid = false; + query_value.map_or(supports_grid, |v| v == supports_grid) +} + +fn eval_transform_3d( + _: &Device, + query_value: Option, + _: Option, +) -> bool { + let supports_transforms = true; + query_value.map_or(supports_transforms, |v| v == supports_transforms) +} + +#[derive(Debug, Copy, Clone, FromPrimitive, ToCss, Parse)] +#[repr(u8)] +enum Scan { + Progressive, + Interlace, +} + +fn eval_scan(_: &Device, _: Option) -> bool { + // Since Gecko doesn't support the 'tv' media type, the 'scan' feature never + // matches. + false +} + +fn eval_color( + device: &Device, + query_value: Option, + range_or_operator: Option, +) -> bool { + let color_bits_per_channel = + unsafe { bindings::Gecko_MediaFeatures_GetColorDepth(device.document()) }; + RangeOrOperator::evaluate( + range_or_operator, + query_value, + color_bits_per_channel, + ) +} + +fn eval_color_index( + _: &Device, + query_value: Option, + range_or_operator: Option, +) -> bool { + // We should return zero if the device does not use a color lookup + // table. + let index = 0; + RangeOrOperator::evaluate( + range_or_operator, + query_value, + index, + ) +} + +fn eval_monochrome( + _: &Device, + query_value: Option, + range_or_operator: Option, +) -> bool { + // For color devices we should return 0. + // FIXME: On a monochrome device, return the actual color depth, not 0! + let depth = 0; + RangeOrOperator::evaluate( + range_or_operator, + query_value, + depth, + ) +} + +fn eval_resolution( + device: &Device, + query_value: Option, + range_or_operator: Option, +) -> bool { + let resolution_dppx = + unsafe { bindings::Gecko_MediaFeatures_GetResolution(device.document()) }; + RangeOrOperator::evaluate( + range_or_operator, + query_value.map(|r| r.dppx()), + resolution_dppx, + ) +} + +#[derive(Debug, Copy, Clone, FromPrimitive, ToCss, Parse)] +#[repr(u8)] +enum PrefersReducedMotion { + NoPreference, + Reduce, +} + +fn eval_prefers_reduced_motion( + device: &Device, + query_value: Option, +) -> bool { + let prefers_reduced = + unsafe { bindings::Gecko_MediaFeatures_PrefersReducedMotion(device.document()) }; + let query_value = match query_value { + Some(v) => v, + None => return prefers_reduced, + }; + + match query_value { + PrefersReducedMotion::NoPreference => !prefers_reduced, + PrefersReducedMotion::Reduce => prefers_reduced, + } +} + +fn eval_moz_is_glyph( + device: &Device, + query_value: Option, + _: Option, +) -> bool { + let is_glyph = unsafe { (*device.document()).mIsSVGGlyphsDocument() }; + query_value.map_or(is_glyph, |v| v == is_glyph) +} + +fn eval_moz_is_resource_document( + device: &Device, + query_value: Option, + _: Option, +) -> bool { + let is_resource_doc = unsafe { + bindings::Gecko_MediaFeatures_IsResourceDocument(device.document()) + }; + query_value.map_or(is_resource_doc, |v| v == is_resource_doc) +} + +fn eval_system_metric( + device: &Device, + query_value: Option, + metric: Atom, + accessible_from_content: bool, +) -> bool { + let supports_metric = unsafe { + bindings::Gecko_MediaFeatures_HasSystemMetric( + device.document(), + metric.as_ptr(), + accessible_from_content, + ) + }; + query_value.map_or(supports_metric, |v| v == supports_metric) +} + +fn eval_moz_touch_enabled( + device: &Device, + query_value: Option, + _: Option, +) -> bool { + eval_system_metric( + device, + query_value, + atom!("touch-enabled"), + /* accessible_from_content = */ true, + ) +} + +fn eval_moz_os_version( + device: &Device, + query_value: Option, + _: Option, +) -> bool { + let query_value = match query_value { + Some(v) => v, + None => return false, + }; + + let os_version = unsafe { + bindings::Gecko_MediaFeatures_GetOperatingSystemVersion(device.document()) + }; + + query_value.as_ptr() == os_version +} + +macro_rules! system_metric_feature { + ($feature_name:expr, $metric_name:expr) => { + { + fn __eval( + device: &Device, + query_value: Option, + _: Option, + ) -> bool { + eval_system_metric( + device, + query_value, + $metric_name, + /* accessible_from_content = */ false, + ) + } + + feature!( + $feature_name, + AllowsRanges::No, + Evaluator::BoolInteger(__eval), + ParsingRequirements::CHROME_AND_UA_ONLY, + ) + } + } +} + +lazy_static! { + /// Adding new media features requires (1) adding the new feature to this + /// array, with appropriate entries (and potentially any new code needed + /// to support new types in these entries and (2) ensuring that either + /// nsPresContext::MediaFeatureValuesChanged is called when the value that + /// would be returned by the evaluator function could change. + pub static ref MEDIA_FEATURES: [MediaFeatureDescription; 43] = [ + feature!( + atom!("width"), + AllowsRanges::Yes, + Evaluator::Length(eval_width), + ParsingRequirements::empty(), + ), + feature!( + atom!("height"), + AllowsRanges::Yes, + Evaluator::Length(eval_height), + ParsingRequirements::empty(), + ), + feature!( + atom!("aspect-ratio"), + AllowsRanges::Yes, + Evaluator::IntRatio(eval_aspect_ratio), + ParsingRequirements::empty(), + ), + feature!( + atom!("orientation"), + AllowsRanges::No, + keyword_evaluator!(eval_orientation, Orientation), + ParsingRequirements::empty(), + ), + feature!( + atom!("device-width"), + AllowsRanges::Yes, + Evaluator::Length(eval_device_width), + ParsingRequirements::empty(), + ), + feature!( + atom!("device-height"), + AllowsRanges::Yes, + Evaluator::Length(eval_device_height), + ParsingRequirements::empty(), + ), + feature!( + atom!("device-aspect-ratio"), + AllowsRanges::Yes, + Evaluator::IntRatio(eval_device_aspect_ratio), + ParsingRequirements::empty(), + ), + feature!( + atom!("-moz-device-orientation"), + AllowsRanges::No, + keyword_evaluator!(eval_device_orientation, Orientation), + ParsingRequirements::empty(), + ), + // Webkit extensions that we support for de-facto web compatibility. + // -webkit-{min|max}-device-pixel-ratio (controlled with its own pref): + feature!( + atom!("device-pixel-ratio"), + AllowsRanges::Yes, + Evaluator::Float(eval_device_pixel_ratio), + ParsingRequirements::WEBKIT_PREFIX | + ParsingRequirements::WEBKIT_DEVICE_PIXEL_RATIO_PREF_ENABLED, + ), + // -webkit-transform-3d. + feature!( + atom!("transform-3d"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_transform_3d), + ParsingRequirements::WEBKIT_PREFIX, + ), + feature!( + atom!("-moz-device-pixel-ratio"), + AllowsRanges::Yes, + Evaluator::Float(eval_device_pixel_ratio), + ParsingRequirements::empty(), + ), + feature!( + atom!("resolution"), + AllowsRanges::Yes, + Evaluator::Resolution(eval_resolution), + ParsingRequirements::empty(), + ), + feature!( + atom!("display-mode"), + AllowsRanges::No, + keyword_evaluator!(eval_display_mode, DisplayMode), + ParsingRequirements::empty(), + ), + feature!( + atom!("grid"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_grid), + ParsingRequirements::empty(), + ), + feature!( + atom!("scan"), + AllowsRanges::No, + keyword_evaluator!(eval_scan, Scan), + ParsingRequirements::empty(), + ), + feature!( + atom!("color"), + AllowsRanges::Yes, + Evaluator::Integer(eval_color), + ParsingRequirements::empty(), + ), + feature!( + atom!("color-index"), + AllowsRanges::Yes, + Evaluator::Integer(eval_color_index), + ParsingRequirements::empty(), + ), + feature!( + atom!("monochrome"), + AllowsRanges::Yes, + Evaluator::Integer(eval_monochrome), + ParsingRequirements::empty(), + ), + feature!( + atom!("prefers-reduced-motion"), + AllowsRanges::No, + keyword_evaluator!(eval_prefers_reduced_motion, PrefersReducedMotion), + ParsingRequirements::empty(), + ), + + // Internal -moz-is-glyph media feature: applies only inside SVG glyphs. + // Internal because it is really only useful in the user agent anyway + // and therefore not worth standardizing. + feature!( + atom!("-moz-is-glyph"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_moz_is_glyph), + ParsingRequirements::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("-moz-is-resource-document"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_moz_is_resource_document), + ParsingRequirements::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("-moz-os-version"), + AllowsRanges::No, + Evaluator::Ident(eval_moz_os_version), + ParsingRequirements::CHROME_AND_UA_ONLY, + ), + // FIXME(emilio): make system metrics store the -moz- atom, and remove + // some duplication here. + system_metric_feature!(atom!("-moz-scrollbar-start-backward"), atom!("scrollbar-start-backward")), + system_metric_feature!(atom!("-moz-scrollbar-start-forward"), atom!("scrollbar-start-forward")), + system_metric_feature!(atom!("-moz-scrollbar-end-backward"), atom!("scrollbar-end-backward")), + system_metric_feature!(atom!("-moz-scrollbar-end-forward"), atom!("scrollbar-end-forward")), + system_metric_feature!(atom!("-moz-scrollbar-thumb-proportional"), atom!("scrollbar-thumb-proportional")), + system_metric_feature!(atom!("-moz-overlay-scrollbars"), atom!("overlay-scrollbars")), + system_metric_feature!(atom!("-moz-windows-default-theme"), atom!("windows-default-theme")), + system_metric_feature!(atom!("-moz-mac-graphite-theme"), atom!("mac-graphite-theme")), + system_metric_feature!(atom!("-moz-mac-yosemite-theme"), atom!("mac-yosemite-theme")), + system_metric_feature!(atom!("-moz-windows-accent-color-in-titlebar"), atom!("windows-accent-color-in-titlebar")), + system_metric_feature!(atom!("-moz-windows-compositor"), atom!("windows-compositor")), + system_metric_feature!(atom!("-moz-windows-classic"), atom!("windows-classic")), + system_metric_feature!(atom!("-moz-windows-glass"), atom!("windows-glass")), + system_metric_feature!(atom!("-moz-menubar-drag"), atom!("menubar-drag")), + system_metric_feature!(atom!("-moz-swipe-animation-enabled"), atom!("swipe-animation-enabled")), + system_metric_feature!(atom!("-moz-gtk-csd-available"), atom!("gtk-csd-available")), + system_metric_feature!(atom!("-moz-gtk-csd-minimize-button"), atom!("gtk-csd-minimize-button")), + system_metric_feature!(atom!("-moz-gtk-csd-maximize-button"), atom!("gtk-csd-maximize-button")), + system_metric_feature!(atom!("-moz-gtk-csd-close-button"), atom!("gtk-csd-close-button")), + system_metric_feature!(atom!("-moz-system-dark-theme"), atom!("system-dark-theme")), + // This is the only system-metric media feature that's accessible to + // content as of today. + // FIXME(emilio): Restrict (or remove?) when bug 1035774 lands. + feature!( + atom!("-moz-touch-enabled"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_moz_touch_enabled), + ParsingRequirements::empty(), + ), + ]; +} diff --git a/components/style/gecko/media_queries.rs b/components/style/gecko/media_queries.rs index ff4ca2b47cd..c41bc4ffd73 100644 --- a/components/style/gecko/media_queries.rs +++ b/components/style/gecko/media_queries.rs @@ -6,34 +6,23 @@ use app_units::AU_PER_PX; use app_units::Au; -use context::QuirksMode; -use cssparser::{Parser, RGBA, Token}; +use cssparser::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 gecko_bindings::structs::nsCSSKeywordAndBoolTableEntry; +use gecko_bindings::structs::{nsPresContext, RawGeckoPresContextOwned}; use media_queries::MediaType; -use parser::{Parse, ParserContext}; use properties::ComputedValues; use servo_arc::Arc; -use std::fmt::{self, Write}; +use std::fmt; use std::sync::atomic::{AtomicBool, AtomicIsize, AtomicUsize, Ordering}; -use str::{starts_with_ignore_ascii_case, string_as_ascii_lowercase}; use string_cache::Atom; -use style_traits::{CSSPixel, CssWriter, DevicePixel}; -use style_traits::{ParseError, StyleParseErrorKind, ToCss}; +use style_traits::{CSSPixel, DevicePixel}; use style_traits::viewport::ViewportConstraints; -use stylesheets::Origin; -use values::{serialize_atom_identifier, CSSFloat, CustomIdent, KeyframesName}; -use values::computed::{self, ToComputedValue}; +use values::{CustomIdent, KeyframesName}; 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. @@ -71,11 +60,8 @@ impl fmt::Debug for Device { let mut doc_uri = nsCString::new(); unsafe { - let doc = - &*self.pres_context().mDocument.raw::(); - bindings::Gecko_nsIURI_Debug( - doc.mDocumentURI.raw::(), + (*self.document()).mDocumentURI.raw::(), &mut doc_uri, ) }; @@ -157,10 +143,17 @@ impl Device { } /// Gets the pres context associated with this document. + #[inline] pub fn pres_context(&self) -> &nsPresContext { unsafe { &*self.pres_context } } + /// Gets the document pointer. + #[inline] + pub fn document(&self) -> *mut structs::nsIDocument { + self.pres_context().mDocument.raw::() + } + /// Recreates the default computed values. pub fn reset_computed_values(&mut self) { self.default_values = ComputedValues::default_values(self.pres_context()); @@ -248,758 +241,3 @@ impl Device { 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, MallocSizeOf, PartialEq)] -pub enum Range { - /// At least the specified value. - Min, - /// At most the specified value. - Max, -} - -/// The operator that was specified in this media feature. -#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq)] -enum Operator { - Equal, - GreaterThan, - GreaterThanEqual, - LessThan, - LessThanEqual, -} - -impl ToCss for Operator { - fn to_css(&self, dest: &mut CssWriter) -> fmt::Result - where - W: fmt::Write, - { - dest.write_str(match *self { - Operator::Equal => "=", - Operator::LessThan => "<", - Operator::LessThanEqual => "<=", - Operator::GreaterThan => ">", - Operator::GreaterThanEqual => ">=", - }) - } -} - -/// Either a `Range` or an `Operator`. -/// -/// Ranged media features are not allowed with operations (that'd make no -/// sense). -#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq)] -enum RangeOrOperator { - Range(Range), - Operator(Operator), -} - -/// A feature expression for gecko contains a reference to the media feature, -/// the value the media query contained, and the range to evaluate. -#[derive(Clone, Debug, MallocSizeOf)] -pub struct MediaFeatureExpression { - feature: &'static nsMediaFeature, - value: Option, - range_or_operator: Option, -} - -impl ToCss for MediaFeatureExpression { - 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-")?; - } - - if let Some(RangeOrOperator::Range(range)) = self.range_or_operator { - match range { - Range::Min => dest.write_str("min-")?, - Range::Max => dest.write_str("max-")?, - } - } - - // NB: CssStringWriter not needed, feature names are under control. - write!(dest, "{}", unsafe { - Atom::from_static(*self.feature.mName) - })?; - - if let Some(RangeOrOperator::Operator(op)) = self.range_or_operator { - dest.write_char(' ')?; - op.to_css(dest)?; - dest.write_char(' ')?; - } else if self.value.is_some() { - dest.write_str(": ")?; - } - - if let Some(ref val) = self.value { - val.to_css(dest, self)?; - } - - dest.write_str(")") - } -} - -impl PartialEq for MediaFeatureExpression { - fn eq(&self, other: &Self) -> bool { - self.feature.mName == other.feature.mName && self.value == other.value && - self.range_or_operator == other.range_or_operator - } -} - -/// 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, MallocSizeOf, 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), - /// Similar to the above Enumerated but the variant keyword table has an - /// additional field what the keyword value means in the Boolean Context. - BoolEnumerated(i16), - /// An identifier. - Ident(Atom), -} - -impl MediaExpressionValue { - fn from_css_value( - for_expr: &MediaFeatureExpression, - 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::eBoolEnumerated => { - let value = css_value.integer_unchecked() as i16; - Some(MediaExpressionValue::BoolEnumerated(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: &MediaFeatureExpression) -> 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 { - let keyword = find_in_table( - *for_expr.feature.mData.mKeywordTable.as_ref(), - |_kw, val| val == value, - |e| e.keyword(), - ).expect("Value not found in the keyword table?"); - - MediaExpressionValue::keyword_to_css(keyword, dest) - }, - MediaExpressionValue::BoolEnumerated(value) => unsafe { - let keyword = find_in_table( - *for_expr.feature.mData.mKeywordAndBoolTable.as_ref(), - |_kw, val| val == value, - |e| e.keyword(), - ).expect("Value not found in the keyword table?"); - - MediaExpressionValue::keyword_to_css(keyword, dest) - } - } - } - - fn keyword_to_css(keyword: nsCSSKeyword, dest: &mut CssWriter) -> fmt::Result - where - W: fmt::Write, - { - 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; - unsafe { - 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 -} - -trait TableEntry { - fn value(&self) -> i16; - fn keyword(&self) -> nsCSSKeyword; -} - -impl TableEntry for nsCSSKTableEntry { - fn value(&self) -> i16 { - self.mValue - } - fn keyword(&self) -> nsCSSKeyword { - self.mKeyword - } -} - -impl TableEntry for nsCSSKeywordAndBoolTableEntry { - fn value(&self) -> i16 { - self._base.mValue - } - fn keyword(&self) -> nsCSSKeyword { - self._base.mKeyword - } -} - -unsafe fn find_in_table( - current_entry: *const T, - find: FindFunc, - result_func: ResultFunc, -) -> Option -where - T: TableEntry, - FindFunc: Fn(nsCSSKeyword, i16) -> bool, - ResultFunc: Fn(&T) -> R, -{ - let mut current_entry = current_entry; - - loop { - let value = (*current_entry).value(); - let keyword = (*current_entry).keyword(); - - if value == -1 { - return None; // End of the table. - } - - if find(keyword, value) { - return Some(result_func(&*current_entry)); - } - - 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 first_table_entry: *const nsCSSKTableEntry = - unsafe { *feature.mData.mKeywordTable.as_ref() }; - - let value = parse_keyword(input, first_table_entry)?; - - MediaExpressionValue::Enumerated(value) - }, - nsMediaFeature_ValueType::eBoolEnumerated => { - let first_table_entry: *const nsCSSKeywordAndBoolTableEntry = - unsafe { *feature.mData.mKeywordAndBoolTable.as_ref() }; - - let value = parse_keyword(input, first_table_entry)?; - - MediaExpressionValue::BoolEnumerated(value) - }, - nsMediaFeature_ValueType::eIdent => { - MediaExpressionValue::Ident(Atom::from(input.expect_ident()?.as_ref())) - }, - }; - - Ok(value) -} - -/// Parse a keyword and returns the corresponding i16 value. -fn parse_keyword<'i, 't, T>( - input: &mut Parser<'i, 't>, - first_table_entry: *const T, -) -> Result> -where - T: TableEntry, -{ - 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 value = unsafe { - find_in_table(first_table_entry, |kw, _| kw == keyword, |e| e.value()) - }; - - match value { - Some(value) => Ok(value), - None => Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)), - } -} - -/// Consumes an operation or a colon, or returns an error. -fn consume_operation_or_colon( - input: &mut Parser, -) -> Result, ()> { - let first_delim = { - let next_token = match input.next() { - Ok(t) => t, - Err(..) => return Err(()), - }; - - match *next_token { - Token::Colon => return Ok(None), - Token::Delim(oper) => oper, - _ => return Err(()), - } - }; - Ok(Some(match first_delim { - '=' => Operator::Equal, - '>' => { - if input.try(|i| i.expect_delim('=')).is_ok() { - Operator::GreaterThanEqual - } else { - Operator::GreaterThan - } - } - '<' => { - if input.try(|i| i.expect_delim('=')).is_ok() { - Operator::LessThanEqual - } else { - Operator::LessThan - } - } - _ => return Err(()), - })) -} - -impl MediaFeatureExpression { - /// Trivially construct a new expression. - fn new( - feature: &'static nsMediaFeature, - value: Option, - range_or_operator: Option, - ) -> Self { - Self { - feature, - value, - range_or_operator, - } - } - - /// 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()?; - input.parse_nested_block(|input| { - Self::parse_in_parenthesis_block(context, input) - }) - } - - /// Parse a media feature expression where we've already consumed the - /// parenthesis. - pub fn parse_in_parenthesis_block<'i, 't>( - context: &ParserContext, - input: &mut Parser<'i, 't>, - ) -> Result> { - // 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()?; - - 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..]; - Some(Range::Min) - } else if starts_with_ignore_ascii_case(feature_name, "max-") { - feature_name = &feature_name[4..]; - Some(Range::Max) - } else { - None - }; - - let atom = Atom::from(string_as_ascii_lowercase(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.is_some() && - feature.mRangeType != nsMediaFeature_RangeType::eMinMaxAllowed - { - return Err(location.new_custom_error( - StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()), - )); - } - } - - let feature_allows_ranges = - feature.mRangeType == nsMediaFeature_RangeType::eMinMaxAllowed; - - let operator = input.try(consume_operation_or_colon); - let operator = match operator { - Err(..) => { - // 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 range.is_some() { - return Err(input.new_custom_error( - StyleParseErrorKind::RangedExpressionWithNoValue - )); - } - - return Ok(Self::new(feature, None, None)); - } - Ok(operator) => operator, - }; - - let range_or_operator = match range { - Some(range) => { - if operator.is_some() { - return Err(input.new_custom_error( - StyleParseErrorKind::MediaQueryUnexpectedOperator - )); - } - Some(RangeOrOperator::Range(range)) - } - None => { - match operator { - Some(operator) => { - if !feature_allows_ranges { - return Err(input.new_custom_error( - StyleParseErrorKind::MediaQueryUnexpectedOperator - )); - } - Some(RangeOrOperator::Operator(operator)) - } - None => None, - } - } - }; - - let value = - parse_feature_value(feature, feature.mValueType, context, input).map_err(|err| { - err.location - .new_custom_error(StyleParseErrorKind::MediaQueryExpectedFeatureValue) - })?; - - Ok(Self::new(feature, Some(value), range_or_operator)) - } - - /// 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.feature.mRangeType == nsMediaFeature_RangeType::eMinMaxAllowed || - self.range_or_operator.is_none(), - "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., - ), - BoolEnumerated(value) => { - let value = unsafe { - find_in_table( - *self.feature.mData.mKeywordAndBoolTable.as_ref(), - |_kw, val| val == value, - |e| e.mValueInBooleanContext, - ) - }; - value.expect("Value not found in the keyword table?") - }, - _ => true, - }; - }, - }; - - // FIXME(emilio): Handle the possible floating point errors? - let cmp = match (actual_value, required_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 { - one.to_dpi() - } - }; - - actual_dpi.partial_cmp(&other.to_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; - }, - (&BoolEnumerated(one), &BoolEnumerated(other)) => { - debug_assert_ne!( - self.feature.mRangeType, - nsMediaFeature_RangeType::eMinMaxAllowed - ); - return one == other; - }, - _ => unreachable!(), - }; - - let range_or_op = match self.range_or_operator { - Some(r) => r, - None => return cmp == Ordering::Equal, - }; - - match range_or_op { - RangeOrOperator::Range(range) => { - cmp == Ordering::Equal || match range { - Range::Min => cmp == Ordering::Greater, - Range::Max => cmp == Ordering::Less, - } - } - RangeOrOperator::Operator(op) => { - match op { - Operator::Equal => cmp == Ordering::Equal, - Operator::GreaterThan => cmp == Ordering::Greater, - Operator::GreaterThanEqual => { - cmp == Ordering::Equal || cmp == Ordering::Greater - } - Operator::LessThan => cmp == Ordering::Less, - Operator::LessThanEqual => { - cmp == Ordering::Equal || cmp == Ordering::Less - } - } - } - } - } -} diff --git a/components/style/gecko/mod.rs b/components/style/gecko/mod.rs index cfdf45d43b8..b9b6886518c 100644 --- a/components/style/gecko/mod.rs +++ b/components/style/gecko/mod.rs @@ -11,6 +11,7 @@ pub mod arc_types; pub mod conversions; pub mod data; pub mod global_style_data; +pub mod media_features; pub mod media_queries; pub mod pseudo_element; pub mod restyle_damage; diff --git a/components/style/lib.rs b/components/style/lib.rs index a43f3f87c45..1c3630b0986 100644 --- a/components/style/lib.rs +++ b/components/style/lib.rs @@ -67,6 +67,8 @@ extern crate matches; pub extern crate nsstring; #[cfg(feature = "gecko")] extern crate num_cpus; +#[macro_use] +extern crate num_derive; extern crate num_integer; extern crate num_traits; extern crate ordered_float; @@ -128,15 +130,13 @@ pub mod font_face; pub mod font_metrics; #[cfg(feature = "gecko")] #[allow(unsafe_code)] -pub mod gecko; -#[cfg(feature = "gecko")] -#[allow(unsafe_code)] pub mod gecko_bindings; pub mod hash; pub mod invalidation; #[allow(missing_docs)] // TODO. pub mod logical_geometry; pub mod matching; +#[macro_use] pub mod media_queries; pub mod parallel; pub mod parser; @@ -190,11 +190,16 @@ pub mod properties { include!(concat!(env!("OUT_DIR"), "/properties.rs")); } +#[cfg(feature = "gecko")] +#[allow(unsafe_code)] +pub mod gecko; + // uses a macro from properties #[cfg(feature = "servo")] #[allow(unsafe_code)] pub mod servo; + #[cfg(feature = "gecko")] #[allow(unsafe_code, missing_docs)] pub mod gecko_properties { diff --git a/components/style/media_queries/media_condition.rs b/components/style/media_queries/media_condition.rs index 4b80794af39..dbd677d0aee 100644 --- a/components/style/media_queries/media_condition.rs +++ b/components/style/media_queries/media_condition.rs @@ -13,7 +13,6 @@ use std::fmt::{self, Write}; use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; use super::{Device, MediaFeatureExpression}; - /// A binary `and` or `or` operator. #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss)] #[allow(missing_docs)] diff --git a/components/style/media_queries/media_feature.rs b/components/style/media_queries/media_feature.rs new file mode 100644 index 00000000000..7c0bfc12bef --- /dev/null +++ b/components/style/media_queries/media_feature.rs @@ -0,0 +1,172 @@ +/* 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 features. + +use super::Device; +use super::media_feature_expression::{AspectRatio, RangeOrOperator}; + +use Atom; +use cssparser::Parser; +use parser::ParserContext; +use std::fmt; +use style_traits::ParseError; +use values::computed::{CSSPixelLength, Resolution}; + +/// A generic discriminant for an enum value. +pub type KeywordDiscriminant = u8; + +type MediaFeatureEvaluator = fn( + device: &Device, + // null == no value was given in the query. + value: Option, + range_or_operator: Option, +) -> bool; + +/// Serializes a given discriminant. +/// +/// FIXME(emilio): we could prevent this allocation if the ToCss code would +/// generate a method for keywords to get the static string or something. +pub type KeywordSerializer = fn(KeywordDiscriminant) -> String; + +/// Parses a given identifier. +pub type KeywordParser = for <'a, 'i, 't> fn( + context: &'a ParserContext, + input: &'a mut Parser<'i, 't>, +) -> Result>; + +/// An evaluator for a given media feature. +/// +/// This determines the kind of values that get parsed, too. +#[allow(missing_docs)] +pub enum Evaluator { + Length(MediaFeatureEvaluator), + Integer(MediaFeatureEvaluator), + Float(MediaFeatureEvaluator), + BoolInteger(MediaFeatureEvaluator), + /// An integer ratio, such as the one from device-pixel-ratio. + IntRatio(MediaFeatureEvaluator), + /// A resolution. + Resolution(MediaFeatureEvaluator), + /// A keyword value. + Enumerated { + /// The parser to get a discriminant given a string. + parser: KeywordParser, + /// The serializer to get a string from a discriminant. + /// + /// This is guaranteed to be called with a keyword that `parser` has + /// produced. + serializer: KeywordSerializer, + /// The evaluator itself. This is guaranteed to be called with a + /// keyword that `parser` has produced. + evaluator: MediaFeatureEvaluator, + }, + Ident(MediaFeatureEvaluator), +} + +/// A simple helper macro to create a keyword evaluator. +/// +/// This assumes that keyword feature expressions don't accept ranges, and +/// asserts if that's not true. As of today there's nothing like that (does that +/// even make sense?). +macro_rules! keyword_evaluator { + ($actual_evaluator:ident, $keyword_type:ty) => { + { + fn __parse<'i, 't>( + context: &$crate::parser::ParserContext, + input: &mut $crate::cssparser::Parser<'i, 't>, + ) -> Result< + $crate::media_queries::media_feature::KeywordDiscriminant, + ::style_traits::ParseError<'i>, + > { + let kw = <$keyword_type as $crate::parser::Parse>::parse(context, input)?; + Ok(kw as $crate::media_queries::media_feature::KeywordDiscriminant) + } + + fn __serialize(kw: $crate::media_queries::media_feature::KeywordDiscriminant) -> String { + // This unwrap is ok because the only discriminants that get + // back to us is the ones that `parse` produces. + let value: $keyword_type = + ::num_traits::cast::FromPrimitive::from_u8(kw).unwrap(); + <$keyword_type as ::style_traits::ToCss>::to_css_string(&value) + } + + fn __evaluate( + device: &$crate::media_queries::Device, + value: Option<$crate::media_queries::media_feature::KeywordDiscriminant>, + range_or_operator: Option<$crate::media_queries::media_feature_expression::RangeOrOperator>, + ) -> bool { + debug_assert!( + range_or_operator.is_none(), + "Since when do keywords accept ranges?" + ); + // This unwrap is ok because the only discriminants that get + // back to us is the ones that `parse` produces. + let value: Option<$keyword_type> = + value.map(|kw| ::num_traits::cast::FromPrimitive::from_u8(kw).unwrap()); + $actual_evaluator(device, value) + } + + $crate::media_queries::media_feature::Evaluator::Enumerated { + parser: __parse, + serializer: __serialize, + evaluator: __evaluate, + } + } + } +} + +bitflags! { + /// Different requirements or toggles that change how a expression is + /// parsed. + pub struct ParsingRequirements: u8 { + /// The feature should only be parsed in chrome and ua sheets. + const CHROME_AND_UA_ONLY = 1 << 0; + /// The feature requires a -webkit- prefix. + const WEBKIT_PREFIX = 1 << 1; + /// The feature requires the webkit-device-pixel-ratio preference to be + /// enabled. + const WEBKIT_DEVICE_PIXEL_RATIO_PREF_ENABLED = 1 << 2; + } +} + +/// Whether a media feature allows ranges or not. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[allow(missing_docs)] +pub enum AllowsRanges { + Yes, + No, +} + +/// A description of a media feature. +pub struct MediaFeatureDescription { + /// The media feature name, in ascii lowercase. + pub name: Atom, + /// Whether min- / max- prefixes are allowed or not. + pub allows_ranges: AllowsRanges, + /// The evaluator, which we also use to determine which kind of value to + /// parse. + pub evaluator: Evaluator, + /// Different requirements that need to hold for the feature to be + /// successfully parsed. + pub requirements: ParsingRequirements, +} + +impl MediaFeatureDescription { + /// Whether this media feature allows ranges. + #[inline] + pub fn allows_ranges(&self) -> bool { + self.allows_ranges == AllowsRanges::Yes + } +} + +impl fmt::Debug for MediaFeatureDescription { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("MediaFeatureExpression") + .field("name", &self.name) + .field("allows_ranges", &self.allows_ranges) + .field("requirements", &self.requirements) + .finish() + } +} diff --git a/components/style/media_queries/media_feature_expression.rs b/components/style/media_queries/media_feature_expression.rs new file mode 100644 index 00000000000..60fb2b2a087 --- /dev/null +++ b/components/style/media_queries/media_feature_expression.rs @@ -0,0 +1,562 @@ +/* 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/. */ + +//! Parsing for media feature expressions, like `(foo: bar)` or +//! `(width >= 400px)`. + +use super::Device; +use super::media_feature::{Evaluator, MediaFeatureDescription}; +use super::media_feature::{ParsingRequirements, KeywordDiscriminant}; + +use Atom; +use cssparser::{Parser, Token}; +use context::QuirksMode; +use num_traits::Zero; +use parser::{Parse, ParserContext}; +use std::fmt::{self, Write}; +use str::{starts_with_ignore_ascii_case, string_as_ascii_lowercase}; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; +use stylesheets::Origin; +use values::{serialize_atom_identifier, CSSFloat}; +use values::specified::{Integer, Length, Number, Resolution}; +use values::computed::{self, ToComputedValue}; + +#[cfg(feature = "gecko")] +use gecko_bindings::structs; + +#[cfg(feature = "gecko")] +use gecko::media_features::MEDIA_FEATURES; +#[cfg(feature = "servo")] +use servo::media_queries::MEDIA_FEATURES; + +/// An aspect ratio, with a numerator and denominator. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq)] +pub struct AspectRatio(pub u32, pub u32); + +impl ToCss for AspectRatio { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + self.0.to_css(dest)?; + dest.write_char('/')?; + self.1.to_css(dest) + } +} + +/// The kind of matching that should be performed on a media feature value. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq)] +pub enum Range { + /// At least the specified value. + Min, + /// At most the specified value. + Max, +} + +/// The operator that was specified in this media feature. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq)] +pub enum Operator { + /// = + Equal, + /// > + GreaterThan, + /// >= + GreaterThanEqual, + /// < + LessThan, + /// <= + LessThanEqual, +} + +impl ToCss for Operator { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + dest.write_str(match *self { + Operator::Equal => "=", + Operator::LessThan => "<", + Operator::LessThanEqual => "<=", + Operator::GreaterThan => ">", + Operator::GreaterThanEqual => ">=", + }) + } +} + +/// Either a `Range` or an `Operator`. +/// +/// Ranged media features are not allowed with operations (that'd make no +/// sense). +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq)] +pub enum RangeOrOperator { + /// A `Range`. + Range(Range), + /// An `Operator`. + Operator(Operator), +} + +impl RangeOrOperator { + /// Evaluate a given range given a query value and a value from the browser. + pub fn evaluate( + range_or_op: Option, + query_value: Option, + value: T, + ) -> bool + where + T: PartialOrd + Zero + { + use std::cmp::Ordering; + + let query_value = match query_value { + Some(v) => v, + None => return value != Zero::zero(), + }; + + let cmp = match value.partial_cmp(&query_value) { + Some(c) => c, + None => return false, + }; + + let range_or_op = match range_or_op { + Some(r) => r, + None => return cmp == Ordering::Equal, + }; + + match range_or_op { + RangeOrOperator::Range(range) => { + cmp == Ordering::Equal || match range { + Range::Min => cmp == Ordering::Greater, + Range::Max => cmp == Ordering::Less, + } + } + RangeOrOperator::Operator(op) => { + match op { + Operator::Equal => cmp == Ordering::Equal, + Operator::GreaterThan => cmp == Ordering::Greater, + Operator::GreaterThanEqual => { + cmp == Ordering::Equal || cmp == Ordering::Greater + } + Operator::LessThan => cmp == Ordering::Less, + Operator::LessThanEqual => { + cmp == Ordering::Equal || cmp == Ordering::Less + } + } + } + } + } +} + +/// A feature expression contains a reference to the media feature, the value +/// the media query contained, and the range to evaluate. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct MediaFeatureExpression { + feature: &'static MediaFeatureDescription, + value: Option, + range_or_operator: Option, +} + +impl PartialEq for MediaFeatureExpression { + fn eq(&self, other: &Self) -> bool { + self.feature as *const _ == other.feature as *const _ && + self.value == other.value && + self.range_or_operator == other.range_or_operator + } +} + +impl ToCss for MediaFeatureExpression { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + dest.write_str("(")?; + + if self.feature.requirements.contains(ParsingRequirements::WEBKIT_PREFIX) { + dest.write_str("-webkit-")?; + } + + if let Some(RangeOrOperator::Range(range)) = self.range_or_operator { + match range { + Range::Min => dest.write_str("min-")?, + Range::Max => dest.write_str("max-")?, + } + } + + // NB: CssStringWriter not needed, feature names are under control. + write!(dest, "{}", self.feature.name)?; + + if let Some(RangeOrOperator::Operator(op)) = self.range_or_operator { + dest.write_char(' ')?; + op.to_css(dest)?; + dest.write_char(' ')?; + } else if self.value.is_some() { + dest.write_str(": ")?; + } + + if let Some(ref val) = self.value { + val.to_css(dest, self)?; + } + + dest.write_str(")") + } +} + +/// Consumes an operation or a colon, or returns an error. +fn consume_operation_or_colon( + input: &mut Parser, +) -> Result, ()> { + let first_delim = { + let next_token = match input.next() { + Ok(t) => t, + Err(..) => return Err(()), + }; + + match *next_token { + Token::Colon => return Ok(None), + Token::Delim(oper) => oper, + _ => return Err(()), + } + }; + Ok(Some(match first_delim { + '=' => Operator::Equal, + '>' => { + if input.try(|i| i.expect_delim('=')).is_ok() { + Operator::GreaterThanEqual + } else { + Operator::GreaterThan + } + } + '<' => { + if input.try(|i| i.expect_delim('=')).is_ok() { + Operator::LessThanEqual + } else { + Operator::LessThan + } + } + _ => return Err(()), + })) +} + +impl MediaFeatureExpression { + fn new( + feature: &'static MediaFeatureDescription, + value: Option, + range_or_operator: Option, + ) -> Self { + Self { feature, value, range_or_operator } + } + + /// 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()?; + input.parse_nested_block(|input| { + Self::parse_in_parenthesis_block(context, input) + }) + } + + /// Parse a media feature expression where we've already consumed the + /// parenthesis. + pub fn parse_in_parenthesis_block<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + // 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()?; + + let mut requirements = ParsingRequirements::empty(); + + if context.chrome_rules_enabled() || + context.stylesheet_origin == Origin::UserAgent + { + requirements.insert(ParsingRequirements::CHROME_AND_UA_ONLY); + } + + let result = { + let mut feature_name = &**ident; + + #[cfg(feature = "gecko")] + { + if unsafe { structs::StaticPrefs_sVarCache_layout_css_prefixes_webkit } && + starts_with_ignore_ascii_case(feature_name, "-webkit-") + { + feature_name = &feature_name[8..]; + requirements.insert(ParsingRequirements::WEBKIT_PREFIX); + if unsafe { + structs::StaticPrefs_sVarCache_layout_css_prefixes_device_pixel_ratio_webkit + } { + requirements.insert(ParsingRequirements::WEBKIT_DEVICE_PIXEL_RATIO_PREF_ENABLED); + } + } + } + + let range = if starts_with_ignore_ascii_case(feature_name, "min-") { + feature_name = &feature_name[4..]; + Some(Range::Min) + } else if starts_with_ignore_ascii_case(feature_name, "max-") { + feature_name = &feature_name[4..]; + Some(Range::Max) + } else { + None + }; + + let atom = Atom::from(string_as_ascii_lowercase(feature_name)); + match MEDIA_FEATURES.iter().find(|f| f.name == atom) { + 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.requirements & !requirements).is_empty() { + return Err(location.new_custom_error( + StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()), + )); + } + + if range.is_some() && !feature.allows_ranges() { + return Err(location.new_custom_error( + StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()), + )); + } + } + + let operator = input.try(consume_operation_or_colon); + let operator = match operator { + Err(..) => { + // 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 range.is_some() { + return Err(input.new_custom_error( + StyleParseErrorKind::RangedExpressionWithNoValue + )); + } + + return Ok(Self::new(feature, None, None)); + } + Ok(operator) => operator, + }; + + let range_or_operator = match range { + Some(range) => { + if operator.is_some() { + return Err(input.new_custom_error( + StyleParseErrorKind::MediaQueryUnexpectedOperator + )); + } + Some(RangeOrOperator::Range(range)) + } + None => { + match operator { + Some(operator) => { + if !feature.allows_ranges() { + return Err(input.new_custom_error( + StyleParseErrorKind::MediaQueryUnexpectedOperator + )); + } + Some(RangeOrOperator::Operator(operator)) + } + None => None, + } + } + }; + + let value = + MediaExpressionValue::parse(feature, context, input).map_err(|err| { + err.location + .new_custom_error(StyleParseErrorKind::MediaQueryExpectedFeatureValue) + })?; + + Ok(Self::new(feature, Some(value), range_or_operator)) + } + + /// Returns whether this media query evaluates to true for the given device. + pub fn matches(&self, device: &Device, quirks_mode: QuirksMode) -> bool { + let value = self.value.as_ref(); + + macro_rules! expect { + ($variant:ident) => { + value.map(|value| { + match *value { + MediaExpressionValue::$variant(ref v) => v, + _ => unreachable!("Unexpected MediaExpressionValue"), + } + }) + } + } + + match self.feature.evaluator { + Evaluator::Length(eval) => { + let computed = expect!(Length).map(|specified| { + computed::Context::for_media_query_evaluation(device, quirks_mode, |context| { + specified.to_computed_value(context) + }) + }); + eval(device, computed, self.range_or_operator) + } + Evaluator::Integer(eval) => { + eval(device, expect!(Integer).cloned(), self.range_or_operator) + } + Evaluator::Float(eval) => { + eval(device, expect!(Float).cloned(), self.range_or_operator) + } + Evaluator::IntRatio(eval) => { + eval(device, expect!(IntRatio).cloned(), self.range_or_operator) + }, + Evaluator::Resolution(eval) => { + let computed = expect!(Resolution).map(|specified| { + computed::Context::for_media_query_evaluation(device, quirks_mode, |context| { + specified.to_computed_value(context) + }) + }); + eval(device, computed, self.range_or_operator) + } + Evaluator::Enumerated { evaluator, .. } => { + evaluator( + device, + expect!(Enumerated).cloned(), + self.range_or_operator, + ) + } + Evaluator::Ident(eval) => { + eval(device, expect!(Ident).cloned(), self.range_or_operator) + } + Evaluator::BoolInteger(eval) => { + eval(device, expect!(BoolInteger).cloned(), self.range_or_operator) + } + } + } +} + +/// 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, MallocSizeOf, 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(AspectRatio), + /// A resolution. + Resolution(Resolution), + /// An enumerated value, defined by the variant keyword table in the + /// feature's `mData` member. + Enumerated(KeywordDiscriminant), + /// An identifier. + Ident(Atom), +} + +impl MediaExpressionValue { + fn to_css( + &self, + dest: &mut CssWriter, + for_expr: &MediaFeatureExpression, + ) -> 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(ratio) => { + ratio.to_css(dest) + }, + MediaExpressionValue::Resolution(ref r) => r.to_css(dest), + MediaExpressionValue::Ident(ref ident) => serialize_atom_identifier(ident, dest), + MediaExpressionValue::Enumerated(value) => { + match for_expr.feature.evaluator { + Evaluator::Enumerated { serializer, .. } => { + dest.write_str(&*serializer(value)) + } + _ => unreachable!(), + } + }, + } + } + + fn parse<'i, 't>( + for_feature: &MediaFeatureDescription, + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + Ok(match for_feature.evaluator { + Evaluator::Length(..) => { + let length = Length::parse_non_negative(context, input)?; + MediaExpressionValue::Length(length) + } + Evaluator::Integer(..) => { + let integer = Integer::parse_non_negative(context, input)?; + MediaExpressionValue::Integer(integer.value() as u32) + } + Evaluator::BoolInteger(..) => { + 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) + } + Evaluator::Float(..) => { + let number = Number::parse(context, input)?; + MediaExpressionValue::Float(number.get()) + } + Evaluator::IntRatio(..) => { + let a = Integer::parse_positive(context, input)?; + input.expect_delim('/')?; + let b = Integer::parse_positive(context, input)?; + MediaExpressionValue::IntRatio(AspectRatio( + a.value() as u32, + b.value() as u32 + )) + } + Evaluator::Resolution(..) => { + MediaExpressionValue::Resolution(Resolution::parse(context, input)?) + } + Evaluator::Enumerated { parser, .. } => { + MediaExpressionValue::Enumerated(parser(context, input)?) + } + Evaluator::Ident(..) => { + MediaExpressionValue::Ident(Atom::from(input.expect_ident()?.as_ref())) + } + }) + } +} diff --git a/components/style/media_queries/mod.rs b/components/style/media_queries/mod.rs index d27e33cc64c..b59ec32443d 100644 --- a/components/style/media_queries/mod.rs +++ b/components/style/media_queries/mod.rs @@ -9,12 +9,16 @@ mod media_condition; mod media_list; mod media_query; +#[macro_use] +pub mod media_feature; +pub mod media_feature_expression; pub use self::media_condition::MediaCondition; pub use self::media_list::MediaList; pub use self::media_query::{MediaQuery, MediaQueryType, MediaType, Qualifier}; +pub use self::media_feature_expression::MediaFeatureExpression; #[cfg(feature = "servo")] -pub use servo::media_queries::{Device, MediaFeatureExpression}; +pub use servo::media_queries::Device; #[cfg(feature = "gecko")] -pub use gecko::media_queries::{Device, MediaFeatureExpression}; +pub use gecko::media_queries::Device;