mirror of
https://github.com/servo/servo
synced 2026-05-09 08:32:31 +02:00
Make Servo match the focus bits of the HTML specification a bit more by storing a `FocusableArea` as the currently focused thing in a `Document` instead of an optional `Element`. There is always a focused area of a `Document`, defaulting to the viewport. This is important to support the case of focused navigables and image maps parts in the future. Some focus chain debugging code has been removed as the focus chain concept needs to be reworked to match the specification more closely. Testing: This should not change behavior so existing tests should suffice. Signed-off-by: Martin Robinson <mrobinson@igalia.com>
473 lines
18 KiB
Rust
473 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.
|
||
let document = self.window.Document();
|
||
let candidate = document
|
||
.focus_handler()
|
||
.focused_area()
|
||
.dom_anchor(&document);
|
||
|
||
// 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(),
|
||
)),
|
||
}
|
||
}
|
||
}
|