servo/components/script/dom/documentorshadowroot.rs
Martin Robinson 23acb623c8
script: Allow reflows that do not produce display lists (#37186)
This change has two parts which depend on each other:

1. An early exit in the layout process, which allows for skipping
   display list construction entirely when nothing would change.
2. A simplification and unification of the way that "fake" animation
   frames are triggered. Now this happens on an entire ScriptThread at
   once and is based on whether or not any Pipeline triggered a display
   list update.

   Animations are never canceled in the compositor when the Pipeline
   isn't updating, instead the fake animation frame is triggered far
   enough in the future that an unexpected compositor tick will cancel
   it. This could happen, for instance, if some other Pipeline in some
   other ScriptThread produced a new display list for a tick. This makes
   everything simpler about these ticks.

The goal is that in a future change the ScriptThread-based animation
ticks will be made more generic so that they can throttle the number of
"update the rendering" calls triggered by script.

This should make Servo do a lot less work when moving the cursor over a
page. Before it would constantly produce new display lists.

Fixes: #17029.
Testing: This should not cause any web observable changes. The fact that
all WPT tests keep passing is the test for this change.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Oriol Brufau <obrufau@igalia.com>
2025-06-12 19:25:04 +00:00

320 lines
10 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::fmt;
use embedder_traits::UntrustedNodeAddress;
use euclid::default::Point2D;
use script_layout_interface::{NodesFromPointQueryType, QueryMsg};
use servo_arc::Arc;
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 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::inheritance::Castable;
use crate::dom::bindings::num::Finite;
use crate::dom::bindings::root::{Dom, DomRoot};
use crate::dom::element::Element;
use crate::dom::htmlelement::HTMLElement;
use crate::dom::node::{self, Node, VecPreOrderInsertionHelper};
use crate::dom::shadowroot::ShadowRoot;
use crate::dom::window::Window;
use crate::script_runtime::CanGc;
use crate::stylesheet_set::StylesheetSetRef;
#[derive(Clone, JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
pub(crate) struct StyleSheetInDocument {
#[ignore_malloc_size_of = "Arc"]
#[no_trace]
pub(crate) sheet: Arc<Stylesheet>,
pub(crate) owner: Dom<Element>,
}
// 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 StyleSheetInDocument {
fn size_of(&self, ops: &mut stylo_malloc_size_of::MallocSizeOfOps) -> usize {
<StyleSheetInDocument as malloc_size_of::MallocSizeOf>::size_of(self, ops)
}
}
impl fmt::Debug for StyleSheetInDocument {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
self.sheet.fmt(formatter)
}
}
impl PartialEq for StyleSheetInDocument {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.sheet, &other.sheet)
}
}
impl ToMediaListKey for StyleSheetInDocument {
fn to_media_list_key(&self) -> MediaListKey {
self.sheet.contents.to_media_list_key()
}
}
impl ::style::stylesheets::StylesheetInDocument for StyleSheetInDocument {
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(JSTraceable, MallocSizeOf)]
pub(crate) struct DocumentOrShadowRoot {
window: Dom<Window>,
}
impl DocumentOrShadowRoot {
pub(crate) fn new(window: &Window) -> Self {
Self {
window: Dom::from_ref(window),
}
}
pub(crate) fn nodes_from_point(
&self,
client_point: &Point2D<f32>,
query_type: NodesFromPointQueryType,
can_gc: CanGc,
) -> Vec<UntrustedNodeAddress> {
self.window
.layout_reflow(QueryMsg::NodesFromPointQuery, can_gc);
self.window
.layout()
.query_nodes_from_point(*client_point, query_type)
}
#[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,
can_gc: CanGc,
) -> Option<DomRoot<Element>> {
let x = *x as f32;
let y = *y as f32;
let point = &Point2D::new(x, y);
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
.nodes_from_point(point, NodesFromPointQueryType::Topmost, can_gc)
.first()
{
Some(address) => {
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,
can_gc: CanGc,
) -> Vec<DomRoot<Element>> {
let x = *x as f32;
let y = *y as f32;
let point = &Point2D::new(x, y);
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.nodes_from_point(point, NodesFromPointQueryType::All, can_gc);
let mut elements: Vec<DomRoot<Element>> = nodes
.iter()
.flat_map(|&untrusted_node_address| {
let node = unsafe { node::from_untrusted_node_address(untrusted_node_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: &Element,
s: &Arc<Stylesheet>,
mut stylesheets: StylesheetSetRef<StyleSheetInDocument>,
) {
let guard = s.shared_lock.read();
// FIXME(emilio): Would be nice to remove the clone, etc.
stylesheets.remove_stylesheet(
None,
StyleSheetInDocument {
sheet: s.clone(),
owner: Dom::from_ref(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: &Element,
mut stylesheets: StylesheetSetRef<StyleSheetInDocument>,
sheet: Arc<Stylesheet>,
insertion_point: Option<StyleSheetInDocument>,
style_shared_lock: &StyleSharedRwLock,
) {
debug_assert!(owner.as_stylesheet_owner().is_some(), "Wat");
let sheet = StyleSheetInDocument {
sheet,
owner: Dom::from_ref(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);
}
}