diff --git a/components/embedder_traits/resources.rs b/components/embedder_traits/resources.rs index a29208fc67c..95799aef0d5 100644 --- a/components/embedder_traits/resources.rs +++ b/components/embedder_traits/resources.rs @@ -60,6 +60,7 @@ pub enum Resource { PresentationalHintsCSS, QuirksModeCSS, RippyPNG, + MediaControls, } pub trait ResourceReaderMethods { @@ -94,6 +95,7 @@ fn resources_for_tests() -> Box { Resource::PresentationalHintsCSS => "presentational-hints.css", Resource::QuirksModeCSS => "quirks-mode.css", Resource::RippyPNG => "rippy.png", + Resource::MediaControls => "media_controls.js", }; let mut path = env::current_exe().unwrap(); path = path.canonicalize().unwrap(); diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs index 1122fa52552..7e1314f400a 100644 --- a/components/script/dom/document.rs +++ b/components/script/dom/document.rs @@ -163,6 +163,7 @@ use style::stylesheet_set::DocumentStylesheetSet; use style::stylesheets::{Origin, OriginSet, Stylesheet}; use url::percent_encoding::percent_decode; use url::Host; +use uuid::Uuid; /// The number of times we are allowed to see spurious `requestAnimationFrame()` calls before /// falling back to fake ones. @@ -385,6 +386,12 @@ pub struct Document { shadow_roots: DomRefCell>>, /// Whether any of the shadow roots need the stylesheets flushed. shadow_roots_styles_changed: Cell, + /// List of registered media controls. + /// We need to keep this list to allow the media controls to + /// access the "privileged" document.servoGetMediaControls(id) API, + /// where `id` needs to match any of the registered ShadowRoots + /// hosting the media controls UI. + media_controls: DomRefCell>>, } #[derive(JSTraceable, MallocSizeOf)] @@ -2457,6 +2464,18 @@ impl Document { self.responsive_images.borrow_mut().remove(i); } } + + pub fn register_media_controls(&self, controls: &ShadowRoot) -> String { + let id = Uuid::new_v4().to_string(); + self.media_controls + .borrow_mut() + .insert(id.clone(), Dom::from_ref(controls)); + id + } + + pub fn unregister_media_controls(&self, id: &str) { + self.media_controls.borrow_mut().remove(id); + } } #[derive(MallocSizeOf, PartialEq)] @@ -2750,6 +2769,7 @@ impl Document { delayed_tasks: Default::default(), shadow_roots: DomRefCell::new(HashSet::new()), shadow_roots_styles_changed: Cell::new(false), + media_controls: DomRefCell::new(HashMap::new()), } } @@ -4551,6 +4571,15 @@ impl DocumentMethods for Document { fn ExitFullscreen(&self) -> Rc { self.exit_fullscreen() } + + // Servo only API to get an instance of the controls of a specific + // media element matching the given id. + fn ServoGetMediaControls(&self, id: DOMString) -> Fallible> { + match self.media_controls.borrow().get(&*id) { + Some(m) => Ok(DomRoot::from_ref(&*m)), + None => Err(Error::InvalidAccess), + } + } } fn update_with_current_time_ms(marker: &Cell) { diff --git a/components/script/dom/element.rs b/components/script/dom/element.rs index 1145cb0fd7e..b09aaf0dce7 100644 --- a/components/script/dom/element.rs +++ b/components/script/dom/element.rs @@ -78,7 +78,7 @@ use crate::dom::nodelist::NodeList; use crate::dom::promise::Promise; use crate::dom::raredata::ElementRareData; use crate::dom::servoparser::ServoParser; -use crate::dom::shadowroot::ShadowRoot; +use crate::dom::shadowroot::{IsUserAgentWidget, ShadowRoot}; use crate::dom::text::Text; use crate::dom::validation::Validatable; use crate::dom::virtualmethods::{vtable_for, VirtualMethods}; @@ -231,13 +231,6 @@ impl FromStr for AdjacentPosition { } } -/// Whether a shadow root hosts an User Agent widget. -#[derive(PartialEq)] -pub enum IsUserAgentWidget { - No, - Yes, -} - // // Element methods // @@ -494,7 +487,7 @@ impl Element { } // Steps 4, 5 and 6. - let shadow_root = ShadowRoot::new(self, &*self.node.owner_doc()); + let shadow_root = ShadowRoot::new(self, &*self.node.owner_doc(), is_ua_widget); self.ensure_rare_data().shadow_root = Some(Dom::from_ref(&*shadow_root)); shadow_root .upcast::() @@ -504,6 +497,8 @@ impl Element { self.node.owner_doc().register_shadow_root(&*shadow_root); } + self.upcast::().dirty(NodeDamage::OtherNodeDamage); + Ok(shadow_root) } } diff --git a/components/script/dom/htmlmediaelement.rs b/components/script/dom/htmlmediaelement.rs index 9ba07fe0c09..7be61332189 100644 --- a/components/script/dom/htmlmediaelement.rs +++ b/components/script/dom/htmlmediaelement.rs @@ -15,6 +15,7 @@ use crate::dom::bindings::codegen::Bindings::HTMLMediaElementBinding::HTMLMediaE use crate::dom::bindings::codegen::Bindings::HTMLSourceElementBinding::HTMLSourceElementMethods; use crate::dom::bindings::codegen::Bindings::MediaErrorBinding::MediaErrorConstants::*; use crate::dom::bindings::codegen::Bindings::MediaErrorBinding::MediaErrorMethods; +use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeBinding::NodeMethods; use crate::dom::bindings::codegen::Bindings::TextTrackBinding::{TextTrackKind, TextTrackMode}; use crate::dom::bindings::codegen::InheritTypes::{ElementTypeId, HTMLElementTypeId}; use crate::dom::bindings::codegen::InheritTypes::{HTMLMediaElementTypeId, NodeTypeId}; @@ -33,11 +34,12 @@ use crate::dom::document::Document; use crate::dom::element::{ cors_setting_for_element, reflect_cross_origin_attribute, set_cross_origin_attribute, }; -use crate::dom::element::{AttributeMutation, Element}; +use crate::dom::element::{AttributeMutation, Element, ElementCreator}; use crate::dom::event::Event; use crate::dom::eventtarget::EventTarget; use crate::dom::globalscope::GlobalScope; use crate::dom::htmlelement::HTMLElement; +use crate::dom::htmlscriptelement::HTMLScriptElement; use crate::dom::htmlsourceelement::HTMLSourceElement; use crate::dom::htmlvideoelement::HTMLVideoElement; use crate::dom::mediaerror::MediaError; @@ -45,6 +47,7 @@ use crate::dom::mediastream::MediaStream; use crate::dom::node::{document_from_node, window_from_node, Node, NodeDamage, UnbindContext}; use crate::dom::performanceresourcetiming::InitiatorType; use crate::dom::promise::Promise; +use crate::dom::shadowroot::IsUserAgentWidget; use crate::dom::texttrack::TextTrack; use crate::dom::texttracklist::TextTrackList; use crate::dom::timeranges::{TimeRanges, TimeRangesContainer}; @@ -59,6 +62,7 @@ use crate::network_listener::{self, NetworkListener, PreInvoke, ResourceTimingLi use crate::script_thread::ScriptThread; use crate::task_source::TaskSource; use dom_struct::dom_struct; +use embedder_traits::resources::{self, Resource as EmbedderResource}; use euclid::Size2D; use headers::{ContentLength, ContentRange, HeaderMapExt}; use html5ever::{LocalName, Prefix}; @@ -335,6 +339,11 @@ pub struct HTMLMediaElement { current_fetch_context: DomRefCell>, /// Player Id reported the player thread id: Cell, + /// Media controls id. + /// In order to workaround the lack of privileged JS context, we secure the + /// the access to the "privileged" document.servoGetMediaControls(id) API by + /// keeping a whitelist of media controls identifiers. + media_controls_id: DomRefCell>, } /// @@ -397,6 +406,7 @@ impl HTMLMediaElement { next_timeupdate_event: Cell::new(time::get_time() + Duration::milliseconds(250)), current_fetch_context: DomRefCell::new(None), id: Cell::new(0), + media_controls_id: DomRefCell::new(None), } } @@ -1712,6 +1722,47 @@ impl HTMLMediaElement { .start(0) .unwrap_or_else(|_| self.playback_position.get()) } + + fn render_controls(&self) { + println!("render_controls"); + // XXX cannot render controls while parsing. + // XXX render controls as a top layer. + // XXX check that controls are not already rendered. + let element = self.htmlelement.upcast::(); + if let Ok(shadow_root) = element.attach_shadow(IsUserAgentWidget::Yes) { + let document = document_from_node(self); + let script = HTMLScriptElement::new( + local_name!("script"), + None, + &document, + ElementCreator::ScriptCreated, + ); + let mut media_controls = resources::read_string(EmbedderResource::MediaControls); + // This is our hacky way to temporarily workaround the lack of a privileged + // JS context. + // The media controls UI accesses the document.servoGetMediaControls(id) API + // to get an instance to the media controls ShadowRoot. + // `id` needs to match the internally generated UUID assigned to a media element. + let id = document.register_media_controls(&shadow_root); + let media_controls = media_controls.as_mut_str().replace("@@@id@@@", &id); + *self.media_controls_id.borrow_mut() = Some(id); + script + .upcast::() + .SetTextContent(Some(DOMString::from(media_controls))); + if let Err(e) = shadow_root + .upcast::() + .AppendChild(&*script.upcast::()) + { + warn!("Could not render media controls {:?}", e); + } + + self.upcast::().dirty(NodeDamage::OtherNodeDamage); + } + } + + fn hide_controls(&self) { + println!("hide_controls"); + } } // XXX Placeholder for [https://github.com/servo/servo/issues/22293] @@ -1754,6 +1805,9 @@ impl Drop for HTMLMediaElement { .unwrap() .shutdown_player(&client_context_id, player.clone()); } + if let Some(ref id) = *self.media_controls_id.borrow() { + document_from_node(self).unregister_media_controls(id); + } } } @@ -2177,19 +2231,23 @@ impl VirtualMethods for HTMLMediaElement { fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation) { self.super_type().unwrap().attribute_mutated(attr, mutation); - if &local_name!("muted") == attr.local_name() { - self.SetMuted(mutation.new_value(attr).is_some()); - return; - } - - if mutation.new_value(attr).is_none() { - return; - } - match attr.local_name() { + &local_name!("muted") => { + self.SetMuted(mutation.new_value(attr).is_some()); + }, &local_name!("src") => { + if mutation.new_value(attr).is_none() { + return; + } self.media_element_load_algorithm(); }, + &local_name!("controls") => { + if mutation.new_value(attr).is_some() { + self.render_controls(); + } else { + self.hide_controls(); + } + }, _ => (), }; } diff --git a/components/script/dom/htmlstyleelement.rs b/components/script/dom/htmlstyleelement.rs index e483453252a..8c8864c4626 100644 --- a/components/script/dom/htmlstyleelement.rs +++ b/components/script/dom/htmlstyleelement.rs @@ -24,6 +24,7 @@ use dom_struct::dom_struct; use html5ever::{LocalName, Prefix}; use net_traits::ReferrerPolicy; use servo_arc::Arc; +use servo_url::ServoUrl; use std::cell::Cell; use style::media_queries::MediaList; use style::parser::ParserContext as CssParserContext; @@ -111,10 +112,22 @@ impl HTMLStyleElement { let mq = Arc::new(shared_lock.wrap(MediaList::parse(&context, &mut CssParser::new(&mut input)))); let loader = StylesheetLoader::for_element(self.upcast()); + let (url, origin) = if let Some(shadow_root) = self + .upcast::() + .containing_shadow_root() { + if shadow_root.is_user_agent_widget() { + (ServoUrl::parse(&format!("chrome://{:?}", window.get_url().to_string())).unwrap(), Origin::UserAgent) + } else { + (window.get_url(), Origin::Author) + } + } else { + (window.get_url(), Origin::Author) + }; + let sheet = Stylesheet::from_str( &data, - window.get_url(), - Origin::Author, + url, + origin, mq, shared_lock, Some(&loader), diff --git a/components/script/dom/shadowroot.rs b/components/script/dom/shadowroot.rs index ae108f8109c..d655b8468f5 100644 --- a/components/script/dom/shadowroot.rs +++ b/components/script/dom/shadowroot.rs @@ -28,6 +28,13 @@ use style::media_queries::Device; use style::shared_lock::SharedRwLockReadGuard; use style::stylesheets::Stylesheet; +/// Whether a shadow root hosts an User Agent widget. +#[derive(JSTraceable, MallocSizeOf, PartialEq)] +pub enum IsUserAgentWidget { + No, + Yes, +} + // https://dom.spec.whatwg.org/#interface-shadowroot #[dom_struct] pub struct ShadowRoot { @@ -39,11 +46,13 @@ pub struct ShadowRoot { author_styles: DomRefCell>, stylesheet_list: MutNullableDom, window: Dom, + /// Whether this ShadowRoot hosts a User Agent widget. + is_widget: IsUserAgentWidget, } impl ShadowRoot { #[allow(unrooted_must_root)] - fn new_inherited(host: &Element, document: &Document) -> ShadowRoot { + fn new_inherited(host: &Element, document: &Document, is_widget: IsUserAgentWidget) -> ShadowRoot { let document_fragment = DocumentFragment::new_inherited(document); let node = document_fragment.upcast::(); node.set_flag(NodeFlags::IS_IN_SHADOW_TREE, true); @@ -59,12 +68,13 @@ impl ShadowRoot { author_styles: DomRefCell::new(AuthorStyles::new()), stylesheet_list: MutNullableDom::new(None), window: Dom::from_ref(document.window()), + is_widget, } } - pub fn new(host: &Element, document: &Document) -> DomRoot { + pub fn new(host: &Element, document: &Document, is_widget: IsUserAgentWidget) -> DomRoot { reflect_dom_object( - Box::new(ShadowRoot::new_inherited(host, document)), + Box::new(ShadowRoot::new_inherited(host, document, is_widget)), document.window(), ShadowRootBinding::Wrap, ) @@ -152,6 +162,10 @@ impl ShadowRoot { root, ); } + + pub fn is_user_agent_widget(&self) -> bool { + self.is_widget == IsUserAgentWidget::Yes + } } impl ShadowRootMethods for ShadowRoot { diff --git a/components/script/dom/webidls/Document.webidl b/components/script/dom/webidls/Document.webidl index 0127f8ecf98..ebcdea32166 100644 --- a/components/script/dom/webidls/Document.webidl +++ b/components/script/dom/webidls/Document.webidl @@ -211,3 +211,9 @@ partial interface Document { }; Document implements DocumentOrShadowRoot; + +// Servo internal API. +partial interface Document { + [Throws] + ShadowRoot servoGetMediaControls(DOMString id); +}; diff --git a/components/script/stylesheet_loader.rs b/components/script/stylesheet_loader.rs index 69975c7803b..c2716264bb7 100644 --- a/components/script/stylesheet_loader.rs +++ b/components/script/stylesheet_loader.rs @@ -141,7 +141,15 @@ impl FetchResponseListener for StylesheetContext { // TODO: Get the actual value. http://dev.w3.org/csswg/css-syntax/#environment-encoding let environment_encoding = UTF_8; let protocol_encoding_label = metadata.charset.as_ref().map(|s| &**s); - let final_url = metadata.final_url; + let final_url = if let Some(ref shadow_root) = self.shadow_root { + if shadow_root.root().is_user_agent_widget() { + ServoUrl::parse(&format!("chrome://{:?}", metadata.final_url.to_string())).unwrap() + } else { + metadata.final_url + } + } else { + metadata.final_url + }; let win = window_from_node(&*elem); diff --git a/ports/glutin/resources.rs b/ports/glutin/resources.rs index 5a48037f524..839ebdab1d4 100644 --- a/ports/glutin/resources.rs +++ b/ports/glutin/resources.rs @@ -29,6 +29,7 @@ fn filename(file: Resource) -> &'static str { Resource::PresentationalHintsCSS => "presentational-hints.css", Resource::QuirksModeCSS => "quirks-mode.css", Resource::RippyPNG => "rippy.png", + Resource::MediaControls => "media_controls.js", } } diff --git a/ports/libsimpleservo/api/src/lib.rs b/ports/libsimpleservo/api/src/lib.rs index 9dac35f1aa4..82e81f1f673 100644 --- a/ports/libsimpleservo/api/src/lib.rs +++ b/ports/libsimpleservo/api/src/lib.rs @@ -702,6 +702,9 @@ impl ResourceReaderMethods for ResourceReaderInstance { Resource::BluetoothBlocklist => { &include_bytes!("../../../../resources/gatt_blocklist.txt")[..] }, + Resource::MediaControls => { + &include_bytes!("../../../../resources/media_controls.js")[..] + }, }) } diff --git a/resources/media_controls.js b/resources/media_controls.js new file mode 100644 index 00000000000..216fad4d603 --- /dev/null +++ b/resources/media_controls.js @@ -0,0 +1,23 @@ +console.log('YO'); +let controls = document.servoGetMediaControls("@@@id@@@"); + +let style = document.createElement("style"); +style.textContent = `#controls { + -servo-top-layer: top; + display: block; + position: fixed; + left: 0px; + bottom: 0px; + height: 30px; + width: 100%; + background: blue; +}`; +controls.appendChild(style); + +let div = document.createElement("div"); +div.setAttribute("id", "controls"); +let button = document.createElement("button"); +button.textContent = "Click me"; +div.appendChild(button); +controls.appendChild(div); +console.log('INNER', div.innerHTML);