Media controls
&
Shadow DOM
Whistler 2019
<video src="https://download.blender.org/big_buck_bunny.ogg"></video>
<video controls src="https://download.blender.org/big_buck_bunny.ogg"></video>
Native or Web?
Web FTW
Isolated DOM
Scoped CSS
Privileged JS
Requirements
Shadow DOM
<head>
<style>h1 { color: red; }</style>
</head>
<body>
<h1 id="out">Not in the shadow</h1>
<div id="host">
#shadow-root (closed)
<div>
<h1 id="in">In the shadows</div>
<style>h1 { color: blue; }</style>
</div>
</div>
</body>
Implementation
huge ugly shadow
[NoInterfaceObject]
interface DocumentOrShadowRoot {
// Selection? getSelection();
Element? elementFromPoint (double x, double y);
sequence<Element> elementsFromPoint (double x, double y);
// CaretPosition? caretPositionFromPoint (double x, double y);
readonly attribute Element? activeElement;
readonly attribute StyleSheetList styleSheets;
};
DocumentOrShadowRoot mixin
Document implements DocumentOrShadowRoot;
ShadowRoot implements DocumentOrShadowRoot;
#[must_root]
#[derive(JSTraceable, MallocSizeOf)]
pub struct DocumentOrShadowRoot {
window: Dom<Window>,
}
DocumentOrShadowRoot mixin
#[dom_struct]
pub struct Document {
document_or_shadow_root: DocumentOrShadowRoot,
[...]
#[dom_struct]
pub struct ShadowRoot {
document_or_shadow_root: DocumentOrShadowRoot,
[...]
[Exposed=Window]
interface ShadowRoot : DocumentFragment {
readonly attribute ShadowRootMode mode;
readonly attribute Element host;
};
enum ShadowRootMode { /* "open", */ "closed"};
ShadowRoot implements DocumentOrShadowRoot;
ShadowRoot interface
partial interface Element {
[Throws, Pref="dom.shadowdom.enabled"] ShadowRoot attachShadow();
// readonly attribute ShadowRoot? shadowRoot;
};
Element interface
DOM Traversals - Script
// components/script/dom/node.rs
fn traverse_preorder(&self, shadow_including: ShadowIncluding) -> TreeIterator {
TreeIterator::new(self, shadow_including)
}
impl TreeIterator {
fn next_skipping_children_impl(&mut self, current: DomRoot<Node>)
-> Option<DomRoot<Node>> {
let iter = current.inclusive_ancestors(if self.shadow_including {
ShadowIncluding::Yes
} else {
ShadowIncluding::No
});
[...]
/// Whether a tree traversal should pass shadow tree boundaries.
#[derive(PartialEq)]
pub enum ShadowIncluding { No, Yes, }
DOM Traversals - Script II
impl TreeIterator {
fn next_skipping_children_impl(&mut self, current: DomRoot<Node>)
-> Option<DomRoot<Node>> {
let iter = current.inclusive_ancestors(if self.shadow_including {
ShadowIncluding::Yes
} else {
ShadowIncluding::No
});
[...]
pub fn inclusive_ancestors(&self, shadow_including: ShadowIncluding,)
-> impl Iterator<Item = DomRoot<Node>> {
SimpleNodeIterator {
current: Some(DomRoot::from_ref(self)),
next_node: move |n| {
if shadow_including == ShadowIncluding::Yes {
if let Some(shadow_root) = n.downcast::<ShadowRoot>() {
return Some(DomRoot::from_ref(shadow_root.Host().upcast::<Node>()));
}
}
n.GetParentNode()
},
}
}
DOM Traversals - Script III
DOM Traversals - Layout
fn traversal_parent(&self) -> Option<ServoLayoutElement<'ln>> {
let parent = self.parent_node()?;
if let Some(shadow) = parent.as_shadow_root() {
return Some(shadow.host());
};
parent.as_element()
}
fn traversal_children(&self)
-> LayoutIterator<Self::TraversalChildrenIterator> {
LayoutIterator(if let Some(shadow) = self.shadow_root() {
shadow.as_node().dom_children()
} else {
self.as_node().dom_children()
})
}
Style
pub trait TShadowRoot: Sized + Copy + Clone + PartialEq {
type ConcreteNode: TNode<ConcreteShadowRoot = Self>;
fn as_node(&self) -> Self::ConcreteNode;
fn host(&self) -> <Self::ConcreteNode as TNode>::ConcreteElement;
fn style_data<'a>(&self) -> Option<&'a CascadeData>
where
Self: 'a;
fn elements_with_id<'a>(
&self,
_id: &Atom,
) -> Result<&'a [<Self::ConcreteNode as TNode>::ConcreteElement], ()>
where
Self: 'a,
{
Err(())
}
}
Style II
#[dom_struct]
pub struct Document {
stylesheets: DomRefCell<DocumentStylesheetSet<StyleSheetInDocument>>,
stylesheet_list: MutNullableDom<StyleSheetList>,
}
#[dom_struct]
pub struct ShadowRoot {
author_styles: DomRefCell<AuthorStyles<StyleSheetInDocument>>,
stylesheet_list: MutNullableDom<StyleSheetList>,
}
Style III
#[must_root]
#[derive(JSTraceable, MallocSizeOf)]
pub enum StyleSheetListOwner {
Document(Dom<Document>),
ShadowRoot(Dom<ShadowRoot>),
}
impl StyleSheetListOwner {
pub fn stylesheet_count(&self) -> usize {
match *self {
StyleSheetListOwner::Document(ref doc) => doc.stylesheet_count(),
StyleSheetListOwner::ShadowRoot(ref shadow_root) => shadow_root.stylesheet_count(),
}
}
[..]
#[dom_struct]
pub struct StyleSheetList {
reflector_: Reflector,
document_or_shadow_root: StyleSheetListOwner,
}
Style limitations
<style> h1 { color: red} </style>
<link rel="stylesheet" href="styles.css">
Node flags
IS_IN_SHADOW_TREE
IS_CONNECTED
RareData
#[derive(Default, JSTraceable, MallocSizeOf)]
#[must_root]
pub struct NodeRareData {
pub containing_shadow_root: Option<Dom<ShadowRoot>>,
pub mutation_observers: Vec<RegisteredObserver>,
}
#[derive(Default, JSTraceable, MallocSizeOf)]
#[must_root]
pub struct ElementRareData {
pub shadow_root: Option<Dom<ShadowRoot>>,
pub custom_element_reaction_queue: Vec<CustomElementReaction>,
pub custom_element_definition: Option<Rc<CustomElementDefinition>>,
pub custom_element_state: CustomElementState,
}
Media Controls
attachShadow exception
pub fn attach_shadow(&self, is_ua_widget: IsUserAgentWidget)
-> Fallible<DomRoot<ShadowRoot>> {
[...]
// Step 2.
match self.local_name() {
[...]
&local_name!("span") => {},
&local_name!("video") | &local_name!("audio")
if is_ua_widget == IsUserAgentWidget::Yes => {},
_ => return Err(Error::NotSupported),
};
[...]
Media controls construction
fn render_controls(&self) {
[...]
let shadow_root = element.attach_shadow(IsUserAgentWidget::Yes).unwrap();
[...]
let script = HTMLScriptElement::new(
local_name!("script"),
None,
&document,
ElementCreator::ScriptCreated,
);
let mut media_controls_script =
resources::read_string(EmbedderResource::MediaControlsJS);
[...]
script
.upcast::<Node>()
.SetTextContent(Some(DOMString::from(media_controls_script)));
if let Err(e) = shadow_root
.upcast::<Node>()
.AppendChild(&*script.upcast::<Node>())
{
warn!("Could not render media controls {:?}", e);
return;
}
Media controls construction II
let media_controls_style =
resources::read_string(EmbedderResource::MediaControlsCSS);
let style = HTMLStyleElement::new(
local_name!("style"),
None,
&document,
ElementCreator::ScriptCreated,
);
style
.upcast::<Node>()
.SetTextContent(Some(DOMString::from(media_controls_style)));
if let Err(e) = shadow_root
.upcast::<Node>()
.AppendChild(&*style.upcast::<Node>())
{
warn!("Could not render media controls {:?}", e);
}
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
}
Media controls construction III
// resources/media-controls.js
class MediaControls {
constructor() {
// Get the instance of the shadow root where these controls live.
this.controls = document.servoGetMediaControls("@@@id@@@");
// Get the instance of the host of these controls.
this.media = this.controls.host;
// Create root element and load markup.
this.root = document.createElement("div");
this.root.classList.add("root");
this.root.innerHTML = MARKUP;
this.controls.appendChild(this.root);
[...]
document.servoGetMediaControls(id)
let id = document.register_media_controls(&shadow_root);
let media_controls_script =
media_controls_script.as_mut_str().replace("@@@id@@@", &id);
*self.media_controls_id.borrow_mut() = Some(id);
script
.upcast::<Node>()
.SetTextContent(Some(DOMString::from(media_controls_script)));
fn ServoGetMediaControls(&self, id: DOMString)
-> Fallible<DomRoot<ShadowRoot>> {
match self.media_controls.borrow().get(&*id) {
Some(m) => Ok(DomRoot::from_ref(&*m)),
None => Err(Error::InvalidAccess),
}
}
Layout
fn build_flow_for_block_starting_with_fragments(
&mut self,
mut flow: FlowRef,
node: &ConcreteThreadSafeLayoutNode,
initial_fragments: IntermediateInlineFragments,
) -> ConstructionResult {
[...]
let is_media_element_with_widget = node.type_id() ==
Some(LayoutNodeType::Element(LayoutElementType::HTMLMediaElement)) &&
node.as_element().unwrap().is_shadow_host();
if !node.is_replaced_content() || is_media_element_with_widget {
for kid in node.children() {
[...]
self.build_block_flow_using_construction_result_of_child(
&mut flow,
node,
kid,
&mut inline_fragment_accumulator,
&mut abs_descendants,
&mut legalizer,
);
}
}
[...]
Limitations
Inline layout
<input type="range">
Media controls & Shadow DOM
By Fernando Jiménez Moreno
Media controls & Shadow DOM
- 796