mirror of
https://github.com/servo/servo
synced 2026-04-26 01:25:32 +02:00
`DocumentOrShadowRoot#activeElement` should return retargeted results. What that means is that if the DOM anchor of the `Document`'s focused focusable area is within a shadow root, `Document#activeElement` should return the shadow host. This change implements that behavior, properly returning the `activeElement` from both `Document` and `ShadowRoot`. Testing: This causes a decent number of WPT tests and subtests to start passing. One subtest starts to fail, because it uses the `autofocus` attribute which we do not yet support. Signed-off-by: Martin Robinson <mrobinson@igalia.com>
476 lines
18 KiB
Rust
476 lines
18 KiB
Rust
/* 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 https://mozilla.org/MPL/2.0/. */
|
||
|
||
use std::collections::HashSet;
|
||
use std::ffi::c_void;
|
||
use std::fmt;
|
||
|
||
use embedder_traits::UntrustedNodeAddress;
|
||
use js::rust::HandleValue;
|
||
use layout_api::ElementsFromPointFlags;
|
||
use rustc_hash::FxBuildHasher;
|
||
use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods;
|
||
use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods;
|
||
use script_bindings::error::{Error, ErrorResult};
|
||
use script_bindings::script_runtime::{CanGc, JSContext};
|
||
use servo_arc::Arc;
|
||
use servo_config::pref;
|
||
use style::media_queries::MediaList;
|
||
use style::shared_lock::{SharedRwLock as StyleSharedRwLock, SharedRwLockReadGuard};
|
||
use style::stylesheets::scope_rule::ImplicitScopeRoot;
|
||
use style::stylesheets::{Stylesheet, StylesheetContents};
|
||
use stylo_atoms::Atom;
|
||
use webrender_api::units::LayoutPoint;
|
||
|
||
use crate::dom::Document;
|
||
use crate::dom::bindings::cell::DomRefCell;
|
||
use crate::dom::bindings::codegen::Bindings::NodeBinding::GetRootNodeOptions;
|
||
use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods;
|
||
use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::ShadowRootMethods;
|
||
use crate::dom::bindings::conversions::{ConversionResult, SafeFromJSValConvertible};
|
||
use crate::dom::bindings::inheritance::Castable;
|
||
use crate::dom::bindings::num::Finite;
|
||
use crate::dom::bindings::root::{Dom, DomRoot};
|
||
use crate::dom::bindings::trace::HashMapTracedValues;
|
||
use crate::dom::css::stylesheetlist::StyleSheetListOwner;
|
||
use crate::dom::element::Element;
|
||
use crate::dom::node::{self, Node, VecPreOrderInsertionHelper};
|
||
use crate::dom::shadowroot::ShadowRoot;
|
||
use crate::dom::types::{CSSStyleSheet, EventTarget};
|
||
use crate::dom::window::Window;
|
||
use crate::stylesheet_set::StylesheetSetRef;
|
||
|
||
/// Stylesheet could be constructed by a CSSOM object CSSStylesheet or parsed
|
||
/// from HTML element such as `<style>` or `<link>`.
|
||
#[derive(Clone, JSTraceable, MallocSizeOf)]
|
||
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
|
||
pub(crate) enum StylesheetSource {
|
||
Element(Dom<Element>),
|
||
Constructed(Dom<CSSStyleSheet>),
|
||
}
|
||
|
||
impl StylesheetSource {
|
||
pub(crate) fn get_cssom_object(&self) -> Option<DomRoot<CSSStyleSheet>> {
|
||
match self {
|
||
StylesheetSource::Element(el) => el.upcast::<Node>().get_cssom_stylesheet(),
|
||
StylesheetSource::Constructed(ss) => Some(ss.as_rooted()),
|
||
}
|
||
}
|
||
|
||
pub(crate) fn is_a_valid_owner(&self) -> bool {
|
||
match self {
|
||
StylesheetSource::Element(el) => el.as_stylesheet_owner().is_some(),
|
||
StylesheetSource::Constructed(ss) => ss.is_constructed(),
|
||
}
|
||
}
|
||
|
||
pub(crate) fn is_constructed(&self) -> bool {
|
||
matches!(self, StylesheetSource::Constructed(_))
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, JSTraceable, MallocSizeOf)]
|
||
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
|
||
pub(crate) struct ServoStylesheetInDocument {
|
||
#[ignore_malloc_size_of = "Stylo"]
|
||
#[no_trace]
|
||
pub(crate) sheet: Arc<Stylesheet>,
|
||
/// The object that owns this stylesheet. For constructed stylesheet, it would be the
|
||
/// CSSOM object itself, and for stylesheet generated by an element, it would be the
|
||
/// html element. This is used to get the CSSOM Stylesheet within a DocumentOrShadowDOM.
|
||
pub(crate) owner: StylesheetSource,
|
||
}
|
||
|
||
// This is necessary because this type is contained within a Stylo type which needs
|
||
// Stylo's version of MallocSizeOf.
|
||
impl stylo_malloc_size_of::MallocSizeOf for ServoStylesheetInDocument {
|
||
fn size_of(&self, ops: &mut stylo_malloc_size_of::MallocSizeOfOps) -> usize {
|
||
<ServoStylesheetInDocument as malloc_size_of::MallocSizeOf>::size_of(self, ops)
|
||
}
|
||
}
|
||
|
||
impl fmt::Debug for ServoStylesheetInDocument {
|
||
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||
self.sheet.fmt(formatter)
|
||
}
|
||
}
|
||
|
||
impl PartialEq for ServoStylesheetInDocument {
|
||
fn eq(&self, other: &Self) -> bool {
|
||
Arc::ptr_eq(&self.sheet, &other.sheet)
|
||
}
|
||
}
|
||
|
||
impl ::style::stylesheets::StylesheetInDocument for ServoStylesheetInDocument {
|
||
fn enabled(&self) -> bool {
|
||
self.sheet.enabled()
|
||
}
|
||
|
||
fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList> {
|
||
self.sheet.media(guard)
|
||
}
|
||
|
||
fn contents<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> &'a StylesheetContents {
|
||
self.sheet.contents(guard)
|
||
}
|
||
|
||
fn implicit_scope_root(&self) -> Option<ImplicitScopeRoot> {
|
||
None
|
||
}
|
||
}
|
||
|
||
// https://w3c.github.io/webcomponents/spec/shadow/#extensions-to-the-documentorshadowroot-mixin
|
||
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
|
||
#[derive(Clone, JSTraceable, MallocSizeOf)]
|
||
pub(crate) struct DocumentOrShadowRoot {
|
||
window: Dom<Window>,
|
||
}
|
||
|
||
impl DocumentOrShadowRoot {
|
||
pub(crate) fn new(window: &Window) -> Self {
|
||
Self {
|
||
window: Dom::from_ref(window),
|
||
}
|
||
}
|
||
|
||
#[expect(unsafe_code)]
|
||
// https://drafts.csswg.org/cssom-view/#dom-document-elementfrompoint
|
||
pub(crate) fn element_from_point(
|
||
&self,
|
||
x: Finite<f64>,
|
||
y: Finite<f64>,
|
||
document_element: Option<DomRoot<Element>>,
|
||
has_browsing_context: bool,
|
||
) -> Option<DomRoot<Element>> {
|
||
let x = *x as f32;
|
||
let y = *y as f32;
|
||
let viewport = self.window.viewport_details().size;
|
||
|
||
if !has_browsing_context {
|
||
return None;
|
||
}
|
||
|
||
if x < 0.0 || y < 0.0 || x > viewport.width || y > viewport.height {
|
||
return None;
|
||
}
|
||
|
||
let results = self
|
||
.window
|
||
.elements_from_point_query(LayoutPoint::new(x, y), ElementsFromPointFlags::empty());
|
||
let Some(result) = results.first() else {
|
||
return document_element;
|
||
};
|
||
|
||
// SAFETY: This is safe because `Self::query_elements_from_point` has ensured that
|
||
// layout has run and any OpaqueNodes that no longer refer to real nodes are gone.
|
||
let address = UntrustedNodeAddress(result.node.0 as *const c_void);
|
||
let node = unsafe { node::from_untrusted_node_address(address) };
|
||
DomRoot::downcast::<Element>(node.clone()).or_else(|| {
|
||
let parent_node = node.GetParentNode()?;
|
||
if let Some(shadow_root) = parent_node.downcast::<ShadowRoot>() {
|
||
Some(shadow_root.Host())
|
||
} else {
|
||
node.GetParentElement()
|
||
}
|
||
})
|
||
}
|
||
|
||
#[expect(unsafe_code)]
|
||
// https://drafts.csswg.org/cssom-view/#dom-document-elementsfrompoint
|
||
pub(crate) fn elements_from_point(
|
||
&self,
|
||
x: Finite<f64>,
|
||
y: Finite<f64>,
|
||
document_element: Option<DomRoot<Element>>,
|
||
has_browsing_context: bool,
|
||
) -> Vec<DomRoot<Element>> {
|
||
let x = *x as f32;
|
||
let y = *y as f32;
|
||
let viewport = self.window.viewport_details().size;
|
||
|
||
if !has_browsing_context {
|
||
return vec![];
|
||
}
|
||
|
||
// Step 2
|
||
if x < 0.0 || y < 0.0 || x > viewport.width || y > viewport.height {
|
||
return vec![];
|
||
}
|
||
|
||
// Step 1 and Step 3
|
||
let nodes = self
|
||
.window
|
||
.elements_from_point_query(LayoutPoint::new(x, y), ElementsFromPointFlags::FindAll);
|
||
let mut elements: Vec<DomRoot<Element>> = nodes
|
||
.iter()
|
||
.flat_map(|result| {
|
||
// SAFETY: This is safe because `Self::query_elements_from_point` has ensured that
|
||
// layout has run and any OpaqueNodes that no longer refer to real nodes are gone.
|
||
let address = UntrustedNodeAddress(result.node.0 as *const c_void);
|
||
let node = unsafe { node::from_untrusted_node_address(address) };
|
||
DomRoot::downcast::<Element>(node)
|
||
})
|
||
.collect();
|
||
|
||
// Step 4
|
||
if let Some(root_element) = document_element {
|
||
if elements.last() != Some(&root_element) {
|
||
elements.push(root_element);
|
||
}
|
||
}
|
||
|
||
// Step 5
|
||
elements
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#dom-documentorshadowroot-activeelement-dev>
|
||
pub(crate) fn active_element(&self, this: &Node) -> Option<DomRoot<Element>> {
|
||
// Step 1. Let candidate be this's node document's focused area's DOM anchor.
|
||
//
|
||
// Note: When `Document::focused_element` returns `None`, that means that the
|
||
// `Document` / viewport itself is focused.
|
||
let document = self.window.Document();
|
||
let candidate = match document.focused_element() {
|
||
Some(candidate) => DomRoot::upcast::<Node>(candidate),
|
||
None => DomRoot::upcast::<Node>(document.clone()),
|
||
};
|
||
|
||
// Step 2. Set candidate to the result of retargeting candidate against this.
|
||
//
|
||
// Note: `retarget()` operates on `EventTarget`, but we can be assured that we are
|
||
// only dealing with various kinds of `Node`s here.
|
||
let candidate =
|
||
DomRoot::downcast::<Node>(candidate.upcast::<EventTarget>().retarget(this.upcast()))?;
|
||
|
||
// Step 3. If candidate's root is not this, then return null.
|
||
if this != &*candidate.GetRootNode(&GetRootNodeOptions::empty()) {
|
||
return None;
|
||
}
|
||
|
||
// Step 4. If candidate is not a Document object, then return candidate.
|
||
if let Some(candidate) = DomRoot::downcast::<Element>(candidate.clone()) {
|
||
return Some(candidate);
|
||
}
|
||
assert!(candidate.is::<Document>());
|
||
|
||
// Step 5. If candidate has a body element, then return that body element.
|
||
if let Some(body) = document.GetBody() {
|
||
return Some(DomRoot::upcast(body));
|
||
}
|
||
|
||
// Step 6. If candidate's document element is non-null, then return that document element.
|
||
if let Some(document_element) = document.GetDocumentElement() {
|
||
return Some(document_element);
|
||
}
|
||
|
||
// Step 7. Return null.
|
||
None
|
||
}
|
||
|
||
/// Remove a stylesheet owned by `owner` from the list of document sheets.
|
||
#[cfg_attr(crown, expect(crown::unrooted_must_root))] // Owner needs to be rooted already necessarily.
|
||
pub(crate) fn remove_stylesheet(
|
||
owner: StylesheetSource,
|
||
s: &Arc<Stylesheet>,
|
||
mut stylesheets: StylesheetSetRef<ServoStylesheetInDocument>,
|
||
) {
|
||
let guard = s.shared_lock.read();
|
||
|
||
// FIXME(emilio): Would be nice to remove the clone, etc.
|
||
stylesheets.remove_stylesheet(
|
||
None,
|
||
ServoStylesheetInDocument {
|
||
sheet: s.clone(),
|
||
owner,
|
||
},
|
||
&guard,
|
||
);
|
||
}
|
||
|
||
/// Add a stylesheet owned by `owner` to the list of document sheets, in the
|
||
/// correct tree position.
|
||
#[cfg_attr(crown, expect(crown::unrooted_must_root))] // Owner needs to be rooted already necessarily.
|
||
pub(crate) fn add_stylesheet(
|
||
owner: StylesheetSource,
|
||
mut stylesheets: StylesheetSetRef<ServoStylesheetInDocument>,
|
||
sheet: Arc<Stylesheet>,
|
||
insertion_point: Option<ServoStylesheetInDocument>,
|
||
style_shared_lock: &StyleSharedRwLock,
|
||
) {
|
||
debug_assert!(owner.is_a_valid_owner(), "Wat");
|
||
|
||
if owner.is_constructed() && !pref!(dom_adoptedstylesheet_enabled) {
|
||
return;
|
||
}
|
||
|
||
let sheet = ServoStylesheetInDocument { sheet, owner };
|
||
|
||
let guard = style_shared_lock.read();
|
||
|
||
match insertion_point {
|
||
Some(ip) => {
|
||
stylesheets.insert_stylesheet_before(None, sheet, ip, &guard);
|
||
},
|
||
None => {
|
||
stylesheets.append_stylesheet(None, sheet, &guard);
|
||
},
|
||
}
|
||
}
|
||
|
||
/// Remove any existing association between the provided id/name and any elements in this document.
|
||
pub(crate) fn unregister_named_element(
|
||
&self,
|
||
id_map: &DomRefCell<HashMapTracedValues<Atom, Vec<Dom<Element>>, FxBuildHasher>>,
|
||
to_unregister: &Element,
|
||
id: &Atom,
|
||
) {
|
||
debug!(
|
||
"Removing named element {:p}: {:p} id={}",
|
||
self, to_unregister, id
|
||
);
|
||
let mut id_map = id_map.borrow_mut();
|
||
let is_empty = match id_map.get_mut(id) {
|
||
None => false,
|
||
Some(elements) => {
|
||
let position = elements
|
||
.iter()
|
||
.position(|element| &**element == to_unregister)
|
||
.expect("This element should be in registered.");
|
||
elements.remove(position);
|
||
elements.is_empty()
|
||
},
|
||
};
|
||
if is_empty {
|
||
id_map.remove(id);
|
||
}
|
||
}
|
||
|
||
/// Associate an element present in this document with the provided id/name.
|
||
pub(crate) fn register_named_element(
|
||
&self,
|
||
id_map: &DomRefCell<HashMapTracedValues<Atom, Vec<Dom<Element>>, FxBuildHasher>>,
|
||
element: &Element,
|
||
id: &Atom,
|
||
root: DomRoot<Node>,
|
||
) {
|
||
debug!("Adding named element {:p}: {:p} id={}", self, element, id);
|
||
assert!(
|
||
element.upcast::<Node>().is_in_a_document_tree() ||
|
||
element.upcast::<Node>().is_in_a_shadow_tree()
|
||
);
|
||
assert!(!id.is_empty());
|
||
let mut id_map = id_map.borrow_mut();
|
||
let elements = id_map.entry(id.clone()).or_default();
|
||
elements.insert_pre_order(element, &root);
|
||
}
|
||
|
||
/// Inner part of adopted stylesheet. We are setting it by, assuming it is a FrozenArray
|
||
/// instead of an ObservableArray. Thus, it would have a completely different workflow
|
||
/// compared to the spec. The workflow here is actually following Gecko's implementation
|
||
/// of AdoptedStylesheet before the implementation of ObservableArray.
|
||
///
|
||
/// The main purpose from this function is to set the `&mut adopted_stylesheet` to match
|
||
/// `incoming_stylesheet` and update the corresponding Styleset in a Document or a ShadowRoot.
|
||
/// In case of duplicates, the setter will respect the last duplicates.
|
||
///
|
||
/// <https://drafts.csswg.org/cssom/#dom-documentorshadowroot-adoptedstylesheets>
|
||
// TODO: Handle duplicated adoptedstylesheet correctly, Stylo is preventing duplicates inside a
|
||
// Stylesheet Set. But this is not ideal. https://bugzilla.mozilla.org/show_bug.cgi?id=1978755
|
||
fn set_adopted_stylesheet(
|
||
adopted_stylesheets: &mut Vec<Dom<CSSStyleSheet>>,
|
||
incoming_stylesheets: &[Dom<CSSStyleSheet>],
|
||
owner: &StyleSheetListOwner,
|
||
) -> ErrorResult {
|
||
if !pref!(dom_adoptedstylesheet_enabled) {
|
||
return Ok(());
|
||
}
|
||
|
||
let owner_doc = match owner {
|
||
StyleSheetListOwner::Document(doc) => doc,
|
||
StyleSheetListOwner::ShadowRoot(root) => root.owner_doc(),
|
||
};
|
||
|
||
for sheet in incoming_stylesheets.iter() {
|
||
// > If value’s constructed flag is not set, or its constructor document is not equal
|
||
// > to this DocumentOrShadowRoot’s node document, throw a "NotAllowedError" DOMException.
|
||
if !sheet.constructor_document_matches(owner_doc) {
|
||
return Err(Error::NotAllowed(None));
|
||
}
|
||
}
|
||
|
||
// The set to check for the duplicates when removing the old stylesheets.
|
||
let mut stylesheet_remove_set = HashSet::with_capacity(adopted_stylesheets.len());
|
||
|
||
// Remove the old stylesheets from the StyleSet. This workflow is limited by utilities
|
||
// Stylo StyleSet given to us.
|
||
// TODO(stevennovaryo): we could optimize this by maintaining the longest common prefix
|
||
// but we should consider the implementation of ObservableArray as well.
|
||
for sheet_to_remove in adopted_stylesheets.iter() {
|
||
// Check for duplicates, only proceed with the removal if the stylesheet is not removed yet.
|
||
if stylesheet_remove_set.insert(sheet_to_remove) {
|
||
owner.remove_stylesheet(
|
||
StylesheetSource::Constructed(sheet_to_remove.clone()),
|
||
&sheet_to_remove.style_stylesheet(),
|
||
);
|
||
sheet_to_remove.remove_adopter(owner);
|
||
}
|
||
}
|
||
|
||
// The set to check for the duplicates when adding a new stylesheet.
|
||
let mut stylesheet_add_set = HashSet::with_capacity(incoming_stylesheets.len());
|
||
|
||
// Readd all stylesheet to the StyleSet. This workflow is limited by the utilities
|
||
// Stylo StyleSet given to us.
|
||
for sheet in incoming_stylesheets.iter() {
|
||
// Check for duplicates.
|
||
if !stylesheet_add_set.insert(sheet) {
|
||
// The idea is that this case is rare, so we pay the price of removing the
|
||
// old sheet from the styles and append it later rather than the other way
|
||
// around.
|
||
owner.remove_stylesheet(
|
||
StylesheetSource::Constructed(sheet.clone()),
|
||
&sheet.style_stylesheet(),
|
||
);
|
||
} else {
|
||
sheet.add_adopter(owner.clone());
|
||
}
|
||
|
||
owner.append_constructed_stylesheet(sheet);
|
||
}
|
||
|
||
*adopted_stylesheets = incoming_stylesheets.to_vec();
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Set adoptedStylesheet given a js value by converting and passing the converted
|
||
/// values to the inner [DocumentOrShadowRoot::set_adopted_stylesheet].
|
||
pub(crate) fn set_adopted_stylesheet_from_jsval(
|
||
context: JSContext,
|
||
adopted_stylesheets: &mut Vec<Dom<CSSStyleSheet>>,
|
||
incoming_value: HandleValue,
|
||
owner: &StyleSheetListOwner,
|
||
can_gc: CanGc,
|
||
) -> ErrorResult {
|
||
let maybe_stylesheets =
|
||
Vec::<DomRoot<CSSStyleSheet>>::safe_from_jsval(context, incoming_value, (), can_gc);
|
||
|
||
match maybe_stylesheets {
|
||
Ok(ConversionResult::Success(stylesheets)) => {
|
||
rooted_vec!(let stylesheets <- stylesheets.iter().map(|s| s.as_traced()));
|
||
|
||
DocumentOrShadowRoot::set_adopted_stylesheet(
|
||
adopted_stylesheets,
|
||
&stylesheets,
|
||
owner,
|
||
)
|
||
},
|
||
Ok(ConversionResult::Failure(msg)) => Err(Error::Type(msg.into_owned())),
|
||
Err(_) => Err(Error::Type(
|
||
c"The provided value is not a sequence of 'CSSStylesheet'.".to_owned(),
|
||
)),
|
||
}
|
||
}
|
||
}
|