| /* 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 script_bindings::error::{Error, ErrorResult}; |
| use script_bindings::script_runtime::JSContext; |
| use servo_arc::Arc; |
| use servo_config::pref; |
| use style::invalidation::media_queries::{MediaListKey, ToMediaListKey}; |
| 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 super::bindings::trace::HashMapTracedValues; |
| use crate::dom::bindings::cell::DomRefCell; |
| 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::element::Element; |
| use crate::dom::html::htmlelement::HTMLElement; |
| use crate::dom::node::{self, Node, VecPreOrderInsertionHelper}; |
| use crate::dom::shadowroot::ShadowRoot; |
| use crate::dom::stylesheetlist::StyleSheetListOwner; |
| use crate::dom::types::CSSStyleSheet; |
| 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 = "Arc"] |
| #[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 ToMediaListKey for ServoStylesheetInDocument { |
| fn to_media_list_key(&self) -> MediaListKey { |
| self.sheet.contents.to_media_list_key() |
| } |
| } |
| |
| 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(&self) -> &StylesheetContents { |
| self.sheet.contents() |
| } |
| |
| 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), |
| } |
| } |
| |
| #[allow(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; |
| } |
| |
| match self |
| .window |
| .elements_from_point_query(LayoutPoint::new(x, y), ElementsFromPointFlags::empty()) |
| .first() |
| { |
| Some(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) }; |
| let parent_node = node.GetParentNode().unwrap(); |
| let shadow_host = parent_node |
| .downcast::<ShadowRoot>() |
| .map(ShadowRootMethods::Host); |
| let element_ref = node |
| .downcast::<Element>() |
| .or(shadow_host.as_deref()) |
| .unwrap_or_else(|| { |
| parent_node |
| .downcast::<Element>() |
| .expect("Hit node should have an element or shadowroot parent") |
| }); |
| |
| Some(DomRoot::from_ref(element_ref)) |
| }, |
| None => document_element, |
| } |
| } |
| |
| #[allow(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-document-activeelement |
| pub(crate) fn get_active_element( |
| &self, |
| focused_element: Option<DomRoot<Element>>, |
| body: Option<DomRoot<HTMLElement>>, |
| document_element: Option<DomRoot<Element>>, |
| ) -> Option<DomRoot<Element>> { |
| // TODO: Step 2. |
| |
| match focused_element { |
| Some(element) => Some(element), // Step 3. and 4. |
| None => match body { |
| // Step 5. |
| Some(body) => Some(DomRoot::upcast(body)), |
| None => document_element, |
| }, |
| } |
| } |
| |
| /// Remove a stylesheet owned by `owner` from the list of document sheets. |
| #[cfg_attr(crown, allow(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, allow(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>>>>, |
| 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>>>>, |
| 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); |
| } |
| } |
| |
| // 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, |
| ) -> ErrorResult { |
| let maybe_stylesheets = |
| Vec::<DomRoot<CSSStyleSheet>>::safe_from_jsval(context, incoming_value, ()); |
| |
| match maybe_stylesheets { |
| Ok(ConversionResult::Success(stylesheets)) => { |
| rooted_vec!(let stylesheets <- stylesheets.to_owned().iter().map(|s| s.as_traced())); |
| |
| DocumentOrShadowRoot::set_adopted_stylesheet( |
| adopted_stylesheets, |
| &stylesheets, |
| owner, |
| ) |
| }, |
| Ok(ConversionResult::Failure(msg)) => Err(Error::Type(msg.to_string())), |
| Err(_) => Err(Error::Type( |
| "The provided value is not a sequence of 'CSSStylesheet'.".to_owned(), |
| )), |
| } |
| } |
| } |