Media controls

&

Shadow DOM

Whistler 2019

fernando@mozilla.com

<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