servo: Merge #13453 - Implement matchMedia and MediaQueryList (from metajack:media-query-list); r=jdm
authorJack Moffitt <jack@metajack.im>
Wed, 02 Nov 2016 14:51:12 -0500
changeset 340043 52f8204eba56d1d131a63b43799880890406d878
parent 340042 95dbf80ffecd90f5b67575d0d028376722e0c3c1
child 340044 b19b2b0f0253f7d7aef5a3616681baed4e7bda9f
push id31307
push usergszorc@mozilla.com
push dateSat, 04 Feb 2017 00:59:06 +0000
treeherdermozilla-central@94079d43835f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjdm
servo: Merge #13453 - Implement matchMedia and MediaQueryList (from metajack:media-query-list); r=jdm <!-- Please describe your changes on the following line: --> --- <!-- Thank you for contributing to Servo! Please replace each `[ ]` by `[X]` when the step is complete, and replace `__` with appropriate data: --> - [x] `./mach build -d` does not report any errors - [x] `./mach test-tidy` does not report any errors - [ ] These changes fix #13376 (github issue number if applicable). <!-- Either: --> - [x] There are tests for these changes OR - [ ] These changes do not require tests because _____ <!-- Pull requests that do not address these steps are welcome, but they will require additional verification as part of the review process. --> Fixes #13376. Source-Repo: https://github.com/servo/servo Source-Revision: 6ef46ab9e4ec08d5f5226d41f0cac77c3584bae9
servo/components/script/Cargo.toml
servo/components/script/dom/bindings/codegen/Bindings.conf
servo/components/script/dom/bindings/trace.rs
servo/components/script/dom/mediaquerylist.rs
servo/components/script/dom/mod.rs
servo/components/script/dom/webidls/MediaQueryList.webidl
servo/components/script/dom/webidls/Window.webidl
servo/components/script/dom/window.rs
servo/components/script/lib.rs
servo/components/script/script_thread.rs
servo/components/servo/Cargo.lock
servo/components/style/lib.rs
servo/components/style/media_queries.rs
servo/ports/cef/Cargo.lock
servo/tests/unit/style/media_queries.rs
--- a/servo/components/script/Cargo.toml
+++ b/servo/components/script/Cargo.toml
@@ -63,16 +63,17 @@ regex = "0.1.43"
 rustc-serialize = "0.3"
 script_layout_interface = {path = "../script_layout_interface"}
 script_traits = {path = "../script_traits"}
 selectors = "0.14"
 serde = "0.8"
 smallvec = "0.1"
 string_cache = {version = "0.2.26", features = ["heap_size", "unstable"]}
 style = {path = "../style"}
+style_traits = {path = "../style_traits"}
 time = "0.1.12"
 url = {version = "1.2", features = ["heap_size", "query_encoding"]}
 util = {path = "../util"}
 uuid = {version = "0.3.1", features = ["v4"]}
 websocket = "0.17"
 xml5ever = {version = "0.1.2", features = ["unstable"]}
 
 [dependencies.webrender_traits]
--- a/servo/components/script/dom/bindings/codegen/Bindings.conf
+++ b/servo/components/script/dom/bindings/codegen/Bindings.conf
@@ -9,22 +9,26 @@
 # The configuration table maps each interface name to a |descriptor|.
 #
 # Valid fields for all descriptors:
 #   * outerObjectHook: string to use in place of default value for outerObject and thisObject
 #                      JS class hooks
 
 DOMInterfaces = {
 
+'MediaQueryList': {
+    'weakReferenceable': True,
+},
+
 'Promise': {
     'spiderMonkeyInterface': True,
 },
 
 'Range': {
-	'weakReferenceable': True,
+    'weakReferenceable': True,
 },
 
 #FIXME(jdm): This should be 'register': False, but then we don't generate enum types
 'TestBinding': {},
 
 'URL': {
     'weakReferenceable': True,
 },
--- a/servo/components/script/dom/bindings/trace.rs
+++ b/servo/components/script/dom/bindings/trace.rs
@@ -86,16 +86,17 @@ use std::path::PathBuf;
 use std::rc::Rc;
 use std::sync::Arc;
 use std::sync::atomic::{AtomicBool, AtomicUsize};
 use std::sync::mpsc::{Receiver, Sender};
 use std::time::{SystemTime, Instant};
 use string_cache::{Atom, Namespace, QualName};
 use style::attr::{AttrIdentifier, AttrValue, LengthOrPercentageOrAuto};
 use style::element_state::*;
+use style::media_queries::MediaQueryList;
 use style::properties::PropertyDeclarationBlock;
 use style::selector_impl::{ElementSnapshot, PseudoElement};
 use style::values::specified::Length;
 use time::Duration;
 use url::Origin as UrlOrigin;
 use url::Url;
 use uuid::Uuid;
 use webrender_traits::{WebGLBufferId, WebGLError, WebGLFramebufferId, WebGLProgramId};
@@ -362,17 +363,17 @@ no_jsmanaged_fields!(OpaqueStyleAndLayou
 no_jsmanaged_fields!(PathBuf);
 no_jsmanaged_fields!(CSSErrorReporter);
 no_jsmanaged_fields!(WebGLBufferId);
 no_jsmanaged_fields!(WebGLFramebufferId);
 no_jsmanaged_fields!(WebGLProgramId);
 no_jsmanaged_fields!(WebGLRenderbufferId);
 no_jsmanaged_fields!(WebGLShaderId);
 no_jsmanaged_fields!(WebGLTextureId);
-
+no_jsmanaged_fields!(MediaQueryList);
 
 impl JSTraceable for Box<ScriptChan + Send> {
     #[inline]
     fn trace(&self, _trc: *mut JSTracer) {
         // Do nothing
     }
 }
 
new file mode 100644
--- /dev/null
+++ b/servo/components/script/dom/mediaquerylist.rs
@@ -0,0 +1,156 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use cssparser::ToCss;
+use dom::bindings::cell::DOMRefCell;
+use dom::bindings::codegen::Bindings::EventHandlerBinding::EventHandlerNonNull;
+use dom::bindings::codegen::Bindings::EventListenerBinding::EventListener;
+use dom::bindings::codegen::Bindings::EventTargetBinding::EventTargetMethods;
+use dom::bindings::codegen::Bindings::MediaQueryListBinding::{self, MediaQueryListMethods};
+use dom::bindings::inheritance::Castable;
+use dom::bindings::js::{JS, Root};
+use dom::bindings::reflector::reflect_dom_object;
+use dom::bindings::str::DOMString;
+use dom::bindings::trace::JSTraceable;
+use dom::bindings::weakref::{WeakRef, WeakRefVec};
+use dom::document::Document;
+use dom::eventtarget::EventTarget;
+use euclid::scale_factor::ScaleFactor;
+use js::jsapi::JSTracer;
+use std::cell::Cell;
+use std::rc::Rc;
+use style;
+use style::media_queries::{Device, MediaType};
+use style_traits::{PagePx, ViewportPx};
+
+pub enum MediaQueryListMatchState {
+    Same(bool),
+    Changed(bool),
+}
+
+#[dom_struct]
+pub struct MediaQueryList {
+    eventtarget: EventTarget,
+    document: JS<Document>,
+    media_query_list: style::media_queries::MediaQueryList,
+    last_match_state: Cell<Option<bool>>
+}
+
+impl MediaQueryList {
+    fn new_inherited(document: &Document,
+                     media_query_list: style::media_queries::MediaQueryList) -> MediaQueryList {
+        MediaQueryList {
+            eventtarget: EventTarget::new_inherited(),
+            document: JS::from_ref(document),
+            media_query_list: media_query_list,
+            last_match_state: Cell::new(None),
+        }
+    }
+
+    pub fn new(document: &Document,
+               media_query_list: style::media_queries::MediaQueryList) -> Root<MediaQueryList> {
+        reflect_dom_object(box MediaQueryList::new_inherited(document, media_query_list),
+                           document.window(),
+                           MediaQueryListBinding::Wrap)
+    }
+}
+
+impl MediaQueryList {
+    fn evaluate_changes(&self) -> MediaQueryListMatchState {
+        let matches = self.evaluate();
+
+        let result = if let Some(old_matches) = self.last_match_state.get() {
+            if old_matches == matches {
+                MediaQueryListMatchState::Same(matches)
+            } else {
+                MediaQueryListMatchState::Changed(matches)
+            }
+        } else {
+            MediaQueryListMatchState::Changed(matches)
+        };
+
+        self.last_match_state.set(Some(matches));
+        result
+    }
+
+    pub fn evaluate(&self) -> bool {
+        if let Some(window_size) = self.document.window().window_size() {
+            let viewport_size = window_size.visible_viewport;
+            // TODO: support real ViewportPx, including zoom level
+            // This information seems not to be tracked currently, so we assume
+            // ViewportPx == PagePx
+            let page_to_viewport: ScaleFactor<f32, PagePx, ViewportPx> = ScaleFactor::new(1.0);
+            let device = Device::new(MediaType::Screen, viewport_size * page_to_viewport);
+            self.media_query_list.evaluate(&device)
+        } else {
+            false
+        }
+    }
+}
+
+impl MediaQueryListMethods for MediaQueryList {
+    // https://drafts.csswg.org/cssom-view/#dom-mediaquerylist-media
+    fn Media(&self) -> DOMString {
+        let mut s = String::new();
+        self.media_query_list.to_css(&mut s).unwrap();
+        DOMString::from_string(s)
+    }
+
+    // https://drafts.csswg.org/cssom-view/#dom-mediaquerylist-matches
+    fn Matches(&self) -> bool {
+        match self.last_match_state.get() {
+            None => self.evaluate(),
+            Some(state) => state,
+        }
+    }
+
+    // https://drafts.csswg.org/cssom-view/#dom-mediaquerylist-addlistener
+    fn AddListener(&self, listener: Option<Rc<EventListener>>) {
+        self.upcast::<EventTarget>().AddEventListener(DOMString::from_string("change".to_owned()),
+                                                      listener, false);
+    }
+
+    // https://drafts.csswg.org/cssom-view/#dom-mediaquerylist-removelistener
+    fn RemoveListener(&self, listener: Option<Rc<EventListener>>) {
+        self.upcast::<EventTarget>().RemoveEventListener(DOMString::from_string("change".to_owned()),
+                                                         listener, false);
+    }
+
+    // https://drafts.csswg.org/cssom-view/#dom-mediaquerylist-onchange
+    event_handler!(change, GetOnchange, SetOnchange);
+}
+
+#[derive(HeapSizeOf)]
+pub struct WeakMediaQueryListVec {
+    cell: DOMRefCell<WeakRefVec<MediaQueryList>>,
+}
+
+#[allow(unsafe_code)]
+impl WeakMediaQueryListVec {
+    /// Create a new vector of weak references to MediaQueryList
+    pub fn new() -> Self {
+        WeakMediaQueryListVec { cell: DOMRefCell::new(WeakRefVec::new()) }
+    }
+
+    pub fn push(&self, mql: &MediaQueryList) {
+        self.cell.borrow_mut().push(WeakRef::new(mql));
+    }
+
+    /// Evaluate media query lists and report changes
+    /// https://drafts.csswg.org/cssom-view/#evaluate-media-queries-and-report-changes
+    pub fn evaluate_and_report_changes(&self) {
+        for mql in self.cell.borrow().iter() {
+            if let MediaQueryListMatchState::Changed(_) = mql.root().unwrap().evaluate_changes() {
+                mql.root().unwrap().upcast::<EventTarget>().fire_simple_event("change");
+            }
+        }
+    }
+}
+
+#[allow(unsafe_code)]
+impl JSTraceable for WeakMediaQueryListVec {
+    fn trace(&self, _: *mut JSTracer) {
+        self.cell.borrow_mut().retain_alive()
+    }
+}
--- a/servo/components/script/dom/mod.rs
+++ b/servo/components/script/dom/mod.rs
@@ -352,16 +352,17 @@ pub mod htmltitleelement;
 pub mod htmltrackelement;
 pub mod htmlulistelement;
 pub mod htmlunknownelement;
 pub mod htmlvideoelement;
 pub mod imagedata;
 pub mod keyboardevent;
 pub mod location;
 pub mod mediaerror;
+pub mod mediaquerylist;
 pub mod messageevent;
 pub mod mimetype;
 pub mod mimetypearray;
 pub mod mouseevent;
 pub mod namednodemap;
 pub mod navigator;
 pub mod navigatorinfo;
 pub mod node;
new file mode 100644
--- /dev/null
+++ b/servo/components/script/dom/webidls/MediaQueryList.webidl
@@ -0,0 +1,14 @@
+/* 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/. */
+
+// https://drafts.csswg.org/cssom-view/#mediaquerylist
+
+[Exposed=(Window)]
+interface MediaQueryList : EventTarget {
+  readonly attribute DOMString media;
+  readonly attribute boolean matches;
+  void addListener(EventListener? listener);
+  void removeListener(EventListener? listener);
+           attribute EventHandler onchange;
+};
--- a/servo/components/script/dom/webidls/Window.webidl
+++ b/servo/components/script/dom/webidls/Window.webidl
@@ -115,17 +115,17 @@ dictionary ScrollOptions {
 // http://dev.w3.org/csswg/cssom-view/#extensions-to-the-window-interface
 dictionary ScrollToOptions : ScrollOptions {
     unrestricted double left;
     unrestricted double top;
 };
 
 // http://dev.w3.org/csswg/cssom-view/#extensions-to-the-window-interface
 partial interface Window {
-  //MediaQueryList matchMedia(DOMString query);
+  [Exposed=(Window), NewObject] MediaQueryList matchMedia(DOMString query);
   [SameObject] readonly attribute Screen screen;
 
   // browsing context
   void moveTo(long x, long y);
   void moveBy(long x, long y);
   void resizeTo(long x, long y);
   void resizeBy(long x, long y);
 
--- a/servo/components/script/dom/window.rs
+++ b/servo/components/script/dom/window.rs
@@ -1,13 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use app_units::Au;
+use cssparser::Parser;
 use devtools_traits::{ScriptToDevtoolsControlMsg, TimelineMarker, TimelineMarkerType};
 use dom::bindings::callback::ExceptionHandling;
 use dom::bindings::cell::DOMRefCell;
 use dom::bindings::codegen::Bindings::DocumentBinding::{DocumentMethods, DocumentReadyState};
 use dom::bindings::codegen::Bindings::EventHandlerBinding::EventHandlerNonNull;
 use dom::bindings::codegen::Bindings::EventHandlerBinding::OnBeforeUnloadEventHandlerNonNull;
 use dom::bindings::codegen::Bindings::EventHandlerBinding::OnErrorEventHandlerNonNull;
 use dom::bindings::codegen::Bindings::FunctionBinding::Function;
@@ -30,16 +31,17 @@ use dom::crypto::Crypto;
 use dom::cssstyledeclaration::{CSSModificationAccess, CSSStyleDeclaration};
 use dom::document::Document;
 use dom::element::Element;
 use dom::event::Event;
 use dom::globalscope::GlobalScope;
 use dom::history::History;
 use dom::htmliframeelement::build_mozbrowser_custom_event;
 use dom::location::Location;
+use dom::mediaquerylist::{MediaQueryList, WeakMediaQueryListVec};
 use dom::messageevent::MessageEvent;
 use dom::navigator::Navigator;
 use dom::node::{Node, from_untrusted_node_address, window_from_node};
 use dom::performance::Performance;
 use dom::promise::Promise;
 use dom::screen::Screen;
 use dom::storage::Storage;
 use euclid::{Point2D, Rect, Size2D};
@@ -81,16 +83,17 @@ use std::io::{Write, stderr, stdout};
 use std::rc::Rc;
 use std::sync::{Arc, Mutex};
 use std::sync::atomic::{AtomicBool, Ordering};
 use std::sync::mpsc::{Sender, channel};
 use std::sync::mpsc::TryRecvError::{Disconnected, Empty};
 use string_cache::Atom;
 use style::context::ReflowGoal;
 use style::error_reporting::ParseErrorReporter;
+use style::media_queries;
 use style::properties::longhands::overflow_x;
 use style::selector_impl::PseudoElement;
 use style::str::HTML_SPACE_CHARACTERS;
 use task_source::dom_manipulation::DOMManipulationTaskSource;
 use task_source::file_reading::FileReadingTaskSource;
 use task_source::history_traversal::HistoryTraversalTaskSource;
 use task_source::networking::NetworkingTaskSource;
 use task_source::user_interaction::UserInteractionTaskSource;
@@ -228,16 +231,19 @@ pub struct Window {
     /// A flag to prevent async events from attempting to interact with this window.
     #[ignore_heap_size_of = "defined in std"]
     ignore_further_async_events: Arc<AtomicBool>,
 
     error_reporter: CSSErrorReporter,
 
     /// A list of scroll offsets for each scrollable element.
     scroll_offsets: DOMRefCell<HashMap<UntrustedNodeAddress, Point2D<f32>>>,
+
+    /// All the MediaQueryLists we need to update
+    media_query_lists: WeakMediaQueryListVec,
 }
 
 impl Window {
     #[allow(unsafe_code)]
     pub fn clear_js_runtime_for_script_deallocation(&self) {
         unsafe {
             *self.js_runtime.borrow_for_script_deallocation() = None;
             self.browsing_context.set(None);
@@ -304,16 +310,20 @@ impl Window {
     }
 
     /// Sets a new list of scroll offsets.
     ///
     /// This is called when layout gives us new ones and WebRender is in use.
     pub fn set_scroll_offsets(&self, offsets: HashMap<UntrustedNodeAddress, Point2D<f32>>) {
         *self.scroll_offsets.borrow_mut() = offsets
     }
+
+    pub fn current_viewport(&self) -> Rect<Au> {
+        self.current_viewport.clone().get()
+    }
 }
 
 #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
 fn display_alert_dialog(message: &str) {
     tinyfiledialogs::message_box_ok("Alert!", message, MessageBoxIcon::Warning);
 }
 
 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
@@ -851,16 +861,26 @@ impl WindowMethods for Window {
             Error::Type(format!("Couldn't parse URL: {}", e))
         }));
         match open::that(url.as_str()) {
             Ok(_) => Ok(()),
             Err(e) => Err(Error::Type(format!("Couldn't open URL: {}", e))),
         }
     }
 
+    // https://drafts.csswg.org/cssom-view/#dom-window-matchmedia
+    fn MatchMedia(&self, query: DOMString) -> Root<MediaQueryList> {
+        let mut parser = Parser::new(&query);
+        let media_query_list = media_queries::parse_media_query_list(&mut parser);
+        let document = self.Document();
+        let mql = MediaQueryList::new(&document, media_query_list);
+        self.media_query_lists.push(&*mql);
+        mql
+    }
+
     #[allow(unrooted_must_root)]
     // https://fetch.spec.whatwg.org/#fetch-method
     fn Fetch(&self, input: RequestOrUSVString, init: &RequestInit) -> Rc<Promise> {
         fetch::Fetch(&self.upcast(), input, init)
     }
 }
 
 impl Window {
@@ -1472,16 +1492,20 @@ impl Window {
     }
 
     #[allow(unsafe_code)]
     pub fn dispatch_mozbrowser_event(&self, event: MozBrowserEvent) {
         assert!(PREFS.is_mozbrowser_enabled());
         let custom_event = build_mozbrowser_custom_event(&self, event);
         custom_event.upcast::<Event>().fire(self.upcast());
     }
+
+    pub fn evaluate_media_queries_and_report_changes(&self) {
+        self.media_query_lists.evaluate_and_report_changes();
+    }
 }
 
 impl Window {
     pub fn new(runtime: Rc<Runtime>,
                script_chan: MainThreadScriptChan,
                dom_task_source: DOMManipulationTaskSource,
                user_task_source: UserInteractionTaskSource,
                network_task_source: NetworkingTaskSource,
@@ -1558,16 +1582,17 @@ impl Window {
             current_state: Cell::new(WindowState::Alive),
 
             devtools_marker_sender: DOMRefCell::new(None),
             devtools_markers: DOMRefCell::new(HashSet::new()),
             webdriver_script_chan: DOMRefCell::new(None),
             ignore_further_async_events: Arc::new(AtomicBool::new(false)),
             error_reporter: error_reporter,
             scroll_offsets: DOMRefCell::new(HashMap::new()),
+            media_query_lists: WeakMediaQueryListVec::new(),
         };
 
         WindowBinding::Wrap(runtime.cx(), win)
     }
 }
 
 fn should_move_clip_rect(clip_rect: Rect<Au>, new_viewport: Rect<f32>) -> bool {
     let clip_rect = Rect::new(Point2D::new(clip_rect.origin.x.to_f32_px(),
--- a/servo/components/script/lib.rs
+++ b/servo/components/script/lib.rs
@@ -76,16 +76,17 @@ extern crate rustc_serialize;
 extern crate script_layout_interface;
 extern crate script_traits;
 extern crate selectors;
 extern crate serde;
 extern crate smallvec;
 #[macro_use(atom, ns)] extern crate string_cache;
 #[macro_use]
 extern crate style;
+extern crate style_traits;
 extern crate time;
 #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
 extern crate tinyfiledialogs;
 extern crate url;
 #[macro_use]
 extern crate util;
 extern crate uuid;
 extern crate webrender_traits;
--- a/servo/components/script/script_thread.rs
+++ b/servo/components/script/script_thread.rs
@@ -2094,16 +2094,21 @@ impl ScriptThread {
         // http://dev.w3.org/csswg/cssom-view/#resizing-viewports
         if size_type == WindowSizeType::Resize {
             let uievent = UIEvent::new(&window,
                                        DOMString::from("resize"), EventBubbles::DoesNotBubble,
                                        EventCancelable::NotCancelable, Some(&window),
                                        0i32);
             uievent.upcast::<Event>().fire(window.upcast());
         }
+
+        // https://html.spec.whatwg.org/multipage/#event-loop-processing-model
+        // Step 7.7 - evaluate media queries and report changes
+        // Since we have resized, we need to re-evaluate MQLs
+        window.evaluate_media_queries_and_report_changes();
     }
 
     /// Initiate a non-blocking fetch for a specified resource. Stores the InProgressLoad
     /// argument until a notification is received that the fetch is complete.
     fn start_page_load(&self, incomplete: InProgressLoad, mut load_data: LoadData) {
         let id = incomplete.pipeline_id.clone();
 
         let context = Arc::new(Mutex::new(ParserContext::new(id, load_data.url.clone())));
--- a/servo/components/servo/Cargo.lock
+++ b/servo/components/servo/Cargo.lock
@@ -1971,16 +1971,17 @@ dependencies = [
  "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
  "script_layout_interface 0.0.1",
  "script_traits 0.0.1",
  "selectors 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 0.8.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "smallvec 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "string_cache 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
  "style 0.0.1",
+ "style_traits 0.0.1",
  "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
  "tinyfiledialogs 0.1.0 (git+https://github.com/jdm/tinyfiledialogs)",
  "url 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "util 0.0.1",
  "uuid 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "webrender_traits 0.8.0 (git+https://github.com/servo/webrender)",
  "websocket 0.17.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "xml5ever 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
--- a/servo/components/style/lib.rs
+++ b/servo/components/style/lib.rs
@@ -132,16 +132,18 @@ mod tid;
 pub mod timer;
 pub mod traversal;
 #[macro_use]
 #[allow(non_camel_case_types)]
 pub mod values;
 pub mod viewport;
 pub mod workqueue;
 
+use cssparser::ToCss;
+use std::fmt;
 use std::sync::Arc;
 
 /// The CSS properties supported by the style system.
 // Generated from the properties.mako.rs template by build.rs
 #[macro_use]
 #[allow(unsafe_code)]
 pub mod properties {
     include!(concat!(env!("OUT_DIR"), "/properties.rs"));
@@ -171,8 +173,21 @@ longhand_properties_idents!(reexport_com
 
 /// Returns whether the two arguments point to the same value.
 #[inline]
 pub fn arc_ptr_eq<T: 'static>(a: &Arc<T>, b: &Arc<T>) -> bool {
     let a: &T = &**a;
     let b: &T = &**b;
     (a as *const T) == (b as *const T)
 }
+
+pub fn serialize_comma_separated_list<W, T>(dest: &mut W, list: &[T])
+                                            -> fmt::Result where W: fmt::Write, T: ToCss {
+    if list.len() > 0 {
+        for item in &list[..list.len()-1] {
+            try!(item.to_css(dest));
+            try!(write!(dest, ", "));
+        }
+        list[list.len()-1].to_css(dest)
+    } else {
+        Ok(())
+    }
+}
--- a/servo/components/style/media_queries.rs
+++ b/servo/components/style/media_queries.rs
@@ -2,29 +2,40 @@
  * 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 app_units::Au;
-use cssparser::{Delimiter, Parser, Token};
+use cssparser::{Delimiter, Parser, ToCss, Token};
 use euclid::size::{Size2D, TypedSize2D};
 use properties::longhands;
+use serialize_comma_separated_list;
+use std::fmt::{self, Write};
+use string_cache::Atom;
 use style_traits::ViewportPx;
 use values::specified;
 
 
 #[derive(Debug, PartialEq)]
 #[cfg_attr(feature = "servo", derive(HeapSizeOf))]
 pub struct MediaQueryList {
     pub media_queries: Vec<MediaQuery>
 }
 
+impl ToCss for MediaQueryList {
+    fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+        where W: fmt::Write
+    {
+        serialize_comma_separated_list(dest, &self.media_queries)
+    }
+}
+
 #[derive(PartialEq, Eq, Copy, Clone, Debug)]
 #[cfg_attr(feature = "servo", derive(HeapSizeOf))]
 pub enum Range<T> {
     Min(T),
     Max(T),
     //Eq(T),    // FIXME: Implement parsing support for equality then re-enable this.
 }
 
@@ -99,30 +110,68 @@ impl MediaQuery {
         MediaQuery {
             qualifier: qualifier,
             media_type: media_type,
             expressions: expressions,
         }
     }
 }
 
+impl ToCss for MediaQuery {
+    fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+        where W: fmt::Write
+    {
+        if self.qualifier == Some(Qualifier::Not) {
+            try!(write!(dest, "not "));
+        }
+
+        let mut type_ = String::new();
+        match self.media_type {
+            MediaQueryType::All => try!(write!(type_, "all")),
+            MediaQueryType::MediaType(MediaType::Screen) => try!(write!(type_, "screen")),
+            MediaQueryType::MediaType(MediaType::Print) => try!(write!(type_, "print")),
+            MediaQueryType::MediaType(MediaType::Unknown(ref desc)) => try!(write!(type_, "{}", desc)),
+        };
+        if self.expressions.is_empty() {
+            return write!(dest, "{}", type_)
+        } else if type_ != "all" || self.qualifier == Some(Qualifier::Not) {
+            try!(write!(dest, "{} and ", type_));
+        }
+        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),
+            };
+            try!(write!(dest, "{}-width: ", mm));
+            try!(l.to_css(dest));
+            if i == self.expressions.len() - 1 {
+                try!(write!(dest, ")"));
+            } else {
+                try!(write!(dest, ") and "));
+            }
+        }
+        Ok(())
+    }
+}
+
 /// http://dev.w3.org/csswg/mediaqueries-3/#media0
-#[derive(PartialEq, Eq, Copy, Clone, Debug)]
+#[derive(PartialEq, Eq, Clone, Debug)]
 #[cfg_attr(feature = "servo", derive(HeapSizeOf))]
 pub enum MediaQueryType {
     All,  // Always true
     MediaType(MediaType),
 }
 
-#[derive(PartialEq, Eq, Copy, Clone, Debug)]
+#[derive(PartialEq, Eq, Clone, Debug)]
 #[cfg_attr(feature = "servo", derive(HeapSizeOf))]
 pub enum MediaType {
     Screen,
     Print,
-    Unknown,
+    Unknown(Atom),
 }
 
 #[derive(Debug)]
 #[cfg_attr(feature = "servo", derive(HeapSizeOf))]
 pub struct Device {
     pub media_type: MediaType,
     pub viewport_size: TypedSize2D<f32, ViewportPx>,
 }
@@ -176,17 +225,17 @@ impl MediaQuery {
         };
 
         let media_type;
         if let Ok(ident) = input.try(|input| input.expect_ident()) {
             media_type = match_ignore_ascii_case! { ident,
                 "screen" => MediaQueryType::MediaType(MediaType::Screen),
                 "print" => MediaQueryType::MediaType(MediaType::Print),
                 "all" => MediaQueryType::All,
-                _ => MediaQueryType::MediaType(MediaType::Unknown)
+                _ => MediaQueryType::MediaType(MediaType::Unknown(Atom::from(&*ident)))
             }
         } else {
             // Media type is only optional if qualifier is not specified.
             if qualifier.is_some() {
                 return Err(())
             }
             media_type = MediaQueryType::All;
             // Without a media type, require at least one expression
@@ -228,18 +277,18 @@ pub fn parse_media_query_list(input: &mu
 impl MediaQueryList {
     pub fn evaluate(&self, device: &Device) -> bool {
         let viewport_size = device.au_viewport_size();
 
         // Check if any queries match (OR condition)
         self.media_queries.iter().any(|mq| {
             // Check if media matches. Unknown media never matches.
             let media_match = match mq.media_type {
-                MediaQueryType::MediaType(MediaType::Unknown) => false,
-                MediaQueryType::MediaType(media_type) => media_type == device.media_type,
+                MediaQueryType::MediaType(MediaType::Unknown(_)) => false,
+                MediaQueryType::MediaType(ref media_type) => *media_type == device.media_type,
                 MediaQueryType::All => true,
             };
 
             // 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),
--- a/servo/ports/cef/Cargo.lock
+++ b/servo/ports/cef/Cargo.lock
@@ -1822,16 +1822,17 @@ dependencies = [
  "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
  "script_layout_interface 0.0.1",
  "script_traits 0.0.1",
  "selectors 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 0.8.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "smallvec 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "string_cache 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
  "style 0.0.1",
+ "style_traits 0.0.1",
  "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
  "tinyfiledialogs 0.1.0 (git+https://github.com/jdm/tinyfiledialogs)",
  "url 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "util 0.0.1",
  "uuid 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "webrender_traits 0.8.0 (git+https://github.com/servo/webrender)",
  "websocket 0.17.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "xml5ever 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
--- a/servo/tests/unit/style/media_queries.rs
+++ b/servo/tests/unit/style/media_queries.rs
@@ -1,16 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use app_units::Au;
 use cssparser::{Parser, SourcePosition};
 use euclid::size::TypedSize2D;
 use std::borrow::ToOwned;
+use string_cache::Atom;
 use style::error_reporting::ParseErrorReporter;
 use style::media_queries::*;
 use style::parser::ParserContextExtraData;
 use style::stylesheets::{Stylesheet, Origin, CSSRule};
 use style::values::specified;
 use url::Url;
 
 pub struct CSSErrorReporterTest;
@@ -121,33 +122,33 @@ fn test_mq_print() {
 }
 
 #[test]
 fn test_mq_unknown() {
     test_media_rule("@media fridge { }", |list, css| {
         assert!(list.media_queries.len() == 1, css.to_owned());
         let q = &list.media_queries[0];
         assert!(q.qualifier == None, css.to_owned());
-        assert!(q.media_type == MediaQueryType::MediaType(MediaType::Unknown), css.to_owned());
+        assert!(q.media_type == MediaQueryType::MediaType(MediaType::Unknown(Atom::from("fridge"))), css.to_owned());
         assert!(q.expressions.len() == 0, css.to_owned());
     });
 
     test_media_rule("@media only glass { }", |list, css| {
         assert!(list.media_queries.len() == 1, css.to_owned());
         let q = &list.media_queries[0];
         assert!(q.qualifier == Some(Qualifier::Only), css.to_owned());
-        assert!(q.media_type == MediaQueryType::MediaType(MediaType::Unknown), css.to_owned());
+        assert!(q.media_type == MediaQueryType::MediaType(MediaType::Unknown(Atom::from("glass"))), css.to_owned());
         assert!(q.expressions.len() == 0, css.to_owned());
     });
 
     test_media_rule("@media not wood { }", |list, css| {
         assert!(list.media_queries.len() == 1, css.to_owned());
         let q = &list.media_queries[0];
         assert!(q.qualifier == Some(Qualifier::Not), css.to_owned());
-        assert!(q.media_type == MediaQueryType::MediaType(MediaType::Unknown), css.to_owned());
+        assert!(q.media_type == MediaQueryType::MediaType(MediaType::Unknown(Atom::from("wood"))), css.to_owned());
         assert!(q.expressions.len() == 0, css.to_owned());
     });
 }
 
 #[test]
 fn test_mq_all() {
     test_media_rule("@media all { }", |list, css| {
         assert!(list.media_queries.len() == 1, css.to_owned());
@@ -242,17 +243,17 @@ fn test_mq_expressions() {
             _ => panic!("wrong expression type"),
         }
     });
 
     test_media_rule("@media fridge and (max-width: 52px) { }", |list, css| {
         assert!(list.media_queries.len() == 1, css.to_owned());
         let q = &list.media_queries[0];
         assert!(q.qualifier == None, css.to_owned());
-        assert!(q.media_type == MediaQueryType::MediaType(MediaType::Unknown), css.to_owned());
+        assert!(q.media_type == MediaQueryType::MediaType(MediaType::Unknown(Atom::from("fridge"))), css.to_owned());
         assert!(q.expressions.len() == 1, css.to_owned());
         match q.expressions[0] {
             Expression::Width(Range::Max(w)) => assert!(w == specified::Length::Absolute(Au::from_px(52))),
             _ => panic!("wrong expression type"),
         }
     });
 }