/
githubmirror
/
servo
Обзор
Документация
Войти
/
githubmirror
/
servo
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
components/script/dom/document/documentorshadowroot.rs
468 строк
18 KB
Simon Sapin
Resolve DOM/glyph positions during hit testing (#47048)
07 авг 2026, 10:07
Не верифицирован
07 авг 2026, 10:07
bffc115
Код
Авторство
О чём код?
/* 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::context::JSContext; use js::conversions::FromJSValConvertible; use js::rust::HandleValue; use layout_api::HitTestFlags; use script_bindings::cell::DomRefCell; use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods; use script_bindings::codegen::GenericBindings::ShadowRootBinding::ShadowRootMethods; use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods; use script_bindings::error::{Error, ErrorResult}; 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 webrender_api::units::LayoutPoint; use crate::dom::Document; use crate::dom::bindings::codegen::Bindings::NodeBinding::GetRootNodeOptions; use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods; use crate::dom::bindings::conversions::ConversionResult; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::num::Finite; use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom}; use crate::dom::css::stylesheetlist::StyleSheetListOwner; use crate::dom::customelementregistry::CustomElementRegistry; use crate::dom::element::Element; use crate::dom::node::{self, Node}; use crate::dom::types::{CSSStyleSheet, EventTarget, ShadowRoot}; 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, cx: &mut JSContext) -> Option<DomRoot<CSSStyleSheet>> { match self { StylesheetSource::Element(el) => el.upcast::<Node>().get_cssom_stylesheet(cx), 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(JSTraceable, MallocSizeOf)] pub(crate) struct DocumentOrShadowRoot { window: Dom<Window>, custom_element_registry: MutNullableDom<CustomElementRegistry>, } impl DocumentOrShadowRoot { pub(crate) fn new(window: &Window) -> Self { Self { window: Dom::from_ref(window), custom_element_registry: MutNullableDom::new(None), } } /// <https://dom.spec.whatwg.org/#dom-documentorshadowroot-customelementregistry> pub(crate) fn custom_element_registry(&self) -> Option<DomRoot<CustomElementRegistry>> { self.custom_element_registry.get() } pub(crate) fn set_custom_element_registry(&self, registry: Option<&CustomElementRegistry>) { self.custom_element_registry.set(registry); } /// Retarget the result of `elementsFromPoint` or `elementFromPoint` according to the /// resolution in <https://github.com/w3c/csswg-drafts/issues/556>. pub(crate) fn retarget_hit_test_result( &self, this: &Node, node: &Node, ) -> Option<DomRoot<Element>> { let retargeted_node = DomRoot::downcast::<Node>(node.upcast::<EventTarget>().retarget(this.upcast()))?; DomRoot::downcast::<Element>(retargeted_node.clone()).or_else(|| { let parent_node = retargeted_node.GetParentNode()?; // This node has already been retargeted, but if it is the direct descendant of // a shadow root and it isn't an element, the only reasonable thing to return is // the shadow host (even though that is outside of `this`.) This is a surprising // behavior, but this is what browsers do. if let Some(shadow_root) = parent_node.downcast::<ShadowRoot>() { Some(shadow_root.Host()) } else { retargeted_node.GetParentElement() } }) } /// <https://drafts.csswg.org/cssom-view/#dom-document-elementfrompoint> #[expect(unsafe_code)] pub(crate) fn element_from_point( &self, this: &Node, 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 flags = HitTestFlags::empty(); let result = self .window .elements_from_point_query(flags, LayoutPoint::new(x, y)); let Some(result) = result.items.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) }; self.retarget_hit_test_result(this, &node) } /// <https://drafts.csswg.org/cssom-view/#dom-document-elementsfrompoint> #[expect(unsafe_code)] pub(crate) fn elements_from_point( &self, this: &Node, 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: Let sequence be a new empty sequence. // Step 3: For each box in the viewport, in paint order, starting with the topmost // box, that would be a target for hit testing at coordinates x,y even if nothing // would be overlapping it, when applying the transforms that apply to the // descendants of the viewport, append the associated element to sequence. let flags = HitTestFlags::empty(); let result = self .window .elements_from_point_query(flags, LayoutPoint::new(x, y)); let mut elements: Vec<_> = result .items .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) }; self.retarget_hit_test_result(this, &node) }) .collect(); // Now remove consecutive duplicates. This isn't in the specification, but it // follows browser behavior. See https://github.com/w3c/csswg-drafts/issues/556. let mut last_seen = None; elements.retain(|element| { if Some(element) == last_seen.as_ref() { return false; } last_seen = Some(element.clone()); true }); // Step 4: If the document has a root element, and the last item in sequence is // not the root element, append the root element to sequence. if let Some(root_element) = document_element && elements.last() != Some(&root_element) { elements.push(root_element); } // Step 5: Return sequence. 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); }, } } /// 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( cx: &mut JSContext, adopted_stylesheets: &DomRefCell<Vec<Dom<CSSStyleSheet>>>, incoming_value: HandleValue, owner: &StyleSheetListOwner, ) -> ErrorResult { let maybe_stylesheets = Vec::<DomRoot<CSSStyleSheet>>::safe_from_jsval(cx, incoming_value, ()) .map_err(|_| Error::JSFailed)?; match maybe_stylesheets { ConversionResult::Success(stylesheets) => { rooted_vec!(let stylesheets <- stylesheets.iter().map(|s| s.as_traced())); let mut sheets = adopted_stylesheets.safe_borrow_mut(cx); DocumentOrShadowRoot::set_adopted_stylesheet(sheets.as_mut(), &stylesheets, owner) }, ConversionResult::Failure(msg) => Err(Error::Type(msg.into_owned())), } } }