Skip to main content

blitz_dom/
document.rs

1use crate::NodeTree;
2use crate::events::{DragMode, ScrollAnimationState, handle_dom_event};
3use crate::font_metrics::BlitzFontMetricsProvider;
4use crate::layout::construct::ConstructionTask;
5use crate::layout::damage::ALL_DAMAGE;
6use crate::mutator::ViewportMut;
7use crate::net::{
8    Resource, ResourceHandler, ResourceLoadResponse, StylesheetHandler, StylesheetLoader,
9};
10use crate::node::{
11    ImageData, NodeFlags, RasterImageData, SpecialElementData, Status, TextBrush, TextGranularity,
12};
13use crate::selection::TextSelection;
14use crate::stylo_to_cursor_icon::stylo_to_cursor_icon;
15use crate::traversal::TreeTraverser;
16use crate::url::DocumentUrl;
17use crate::util::ImageType;
18use crate::{
19    DEFAULT_CSS, DocumentConfig, DocumentMutator, DummyHtmlParserProvider, ElementData,
20    EventDriver, HtmlParserProvider, Node, NodeData, NoopEventHandler, StyleThreading,
21    TextNodeData,
22};
23use blitz_traits::devtools::DevtoolSettings;
24use blitz_traits::events::{BlitzScrollEvent, DomEvent, DomEventData, HitResult, UiEvent};
25use blitz_traits::navigation::{DummyNavigationProvider, NavigationProvider};
26use blitz_traits::net::{AbortSignal, DummyNetProvider, NetProvider, Request, RequestDestination};
27use blitz_traits::node_id::NodeId;
28use blitz_traits::shell::{ColorScheme, DummyShellProvider, ShellProvider, Viewport};
29use cursor_icon::CursorIcon;
30use linebender_resource_handle::Blob;
31use markup5ever::{local_name, ns};
32use parley::{FontContext, PlainEditorDriver};
33use rustc_hash::FxHashSet;
34use selectors::{Element, matching::QuirksMode};
35use smallvec::SmallVec;
36use std::any::Any;
37use std::cell::RefCell;
38use std::collections::{BTreeMap, Bound, HashMap, HashSet};
39use std::ops::{Deref, DerefMut};
40use std::rc::Rc;
41use std::str::FromStr;
42use std::sync::atomic::{AtomicUsize, Ordering};
43use std::sync::mpsc::{Receiver, Sender, channel};
44use std::sync::{Arc, Mutex, MutexGuard, OnceLock, RwLockReadGuard, RwLockWriteGuard};
45use std::task::{Context as TaskContext, Waker};
46use style::Atom;
47use style::animation::{AnimationState, DocumentAnimationSet};
48use style::attr::{AttrIdentifier, AttrValue};
49use style::data::{ElementData as StyloElementData, ElementStyles};
50use style::media_queries::MediaType;
51use style::properties::ComputedValues;
52use style::properties::style_structs::Font;
53use style::queries::values::PrefersColorScheme;
54use style::selector_parser::ServoElementSnapshot;
55use style::servo::media_features::PointerCapabilities;
56use style::servo_arc::Arc as ServoArc;
57use style::values::GenericAtomIdent;
58use style::values::computed::ui::CursorKind;
59use style::values::computed::{Overflow, UserSelect};
60use style::values::specified::box_::{DisplayInside, DisplayOutside};
61use style::{
62    device::Device,
63    dom::{TDocument, TNode},
64    media_queries::MediaList,
65    selector_parser::SnapshotMap,
66    shared_lock::{SharedRwLock, StylesheetGuards},
67    stylesheets::{AllowImportRules, DocumentStyleSheet, Origin, Stylesheet},
68    stylist::Stylist,
69};
70use thin_vec::ThinVec;
71use url::Url;
72use web_time::Instant;
73
74#[cfg(feature = "parallel-construct")]
75use thread_local::ThreadLocal;
76
77pub enum DocGuard<'a> {
78    Ref(&'a BaseDocument),
79    RefCell(std::cell::Ref<'a, BaseDocument>),
80    RwLock(RwLockReadGuard<'a, BaseDocument>),
81    Mutex(MutexGuard<'a, BaseDocument>),
82}
83
84impl Deref for DocGuard<'_> {
85    type Target = BaseDocument;
86    #[inline(always)]
87    fn deref(&self) -> &Self::Target {
88        match self {
89            Self::Ref(base_document) => base_document,
90            Self::RefCell(refcell_guard) => refcell_guard,
91            Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
92            Self::Mutex(mutex_guard) => mutex_guard,
93        }
94    }
95}
96
97pub enum DocGuardMut<'a> {
98    Ref(&'a mut BaseDocument),
99    RefCell(std::cell::RefMut<'a, BaseDocument>),
100    RwLock(RwLockWriteGuard<'a, BaseDocument>),
101    Mutex(MutexGuard<'a, BaseDocument>),
102}
103
104impl Deref for DocGuardMut<'_> {
105    type Target = BaseDocument;
106    #[inline(always)]
107    fn deref(&self) -> &Self::Target {
108        match self {
109            Self::Ref(base_document) => base_document,
110            Self::RefCell(refcell_guard) => refcell_guard,
111            Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
112            Self::Mutex(mutex_guard) => mutex_guard,
113        }
114    }
115}
116
117impl DerefMut for DocGuardMut<'_> {
118    #[inline(always)]
119    fn deref_mut(&mut self) -> &mut Self::Target {
120        match self {
121            Self::Ref(base_document) => base_document,
122            Self::RefCell(refcell_guard) => &mut *refcell_guard,
123            Self::RwLock(rw_lock_read_guard) => &mut *rw_lock_read_guard,
124            Self::Mutex(mutex_guard) => &mut *mutex_guard,
125        }
126    }
127}
128
129/// Abstraction over wrappers around [`BaseDocument`] to allow for them all to
130/// be driven by [`blitz-shell`](https://docs.rs/blitz-shell)
131pub trait Document: Any + 'static {
132    fn inner(&self) -> DocGuard<'_>;
133    fn inner_mut(&mut self) -> DocGuardMut<'_>;
134
135    /// Update the [`Document`] in response to a [`UiEvent`] (click, keypress, etc)
136    fn handle_ui_event(&mut self, event: UiEvent) {
137        let mut doc = self.inner_mut();
138        let mut driver = EventDriver::new(&mut *doc, NoopEventHandler);
139        driver.handle_ui_event(event);
140    }
141
142    /// Poll any pending async operations, and flush changes to the underlying [`BaseDocument`]
143    fn poll(&mut self, task_context: Option<TaskContext>) -> bool {
144        // Default implementation does nothing
145        let _ = task_context;
146        false
147    }
148
149    /// Drain immediate work for a host's settle loop.
150    ///
151    /// Pass `run_timers = true` only on the first pass to allow one batch of
152    /// due timer and animation callbacks. Subsequent passes must leave those
153    /// callbacks scheduled while draining scripts and microtasks.
154    ///
155    /// Documents that execute timers must override this method. Wrappers that
156    /// poll child documents must propagate the policy to their children.
157    fn poll_for_settle(&mut self, task_context: Option<TaskContext>, run_timers: bool) -> bool {
158        let _ = run_timers;
159        self.poll(task_context)
160    }
161
162    /// Get the [`Document`]'s id
163    fn id(&self) -> usize {
164        self.inner().id
165    }
166}
167
168/// What the pre-click activation steps changed, so a cancelled click can put
169/// it back. Produced by [`BaseDocument::run_pre_click_activation`].
170#[derive(Debug, Clone, PartialEq)]
171pub struct PreClickActivation {
172    /// Every node whose checkedness moved, paired with the value it held.
173    previous: Vec<(NodeId, bool)>,
174}
175
176/// Escape text for inclusion in an HTML clipboard payload.
177///
178/// The five characters that would otherwise be read as markup. A selection is
179/// document text, so a paragraph containing `a < b` must arrive as `a &lt; b`
180/// rather than opening a tag in whatever receives the paste.
181fn escape_html(text: &str) -> String {
182    let mut out = String::with_capacity(text.len());
183    for ch in text.chars() {
184        match ch {
185            '&' => out.push_str("&amp;"),
186            '<' => out.push_str("&lt;"),
187            '>' => out.push_str("&gt;"),
188            '"' => out.push_str("&quot;"),
189            _ => out.push(ch),
190        }
191    }
192    out
193}
194
195pub struct PlainDocument(pub BaseDocument);
196impl Document for PlainDocument {
197    fn inner(&self) -> DocGuard<'_> {
198        DocGuard::Ref(&self.0)
199    }
200    fn inner_mut(&mut self) -> DocGuardMut<'_> {
201        DocGuardMut::Ref(&mut self.0)
202    }
203}
204
205impl Document for BaseDocument {
206    fn inner(&self) -> DocGuard<'_> {
207        DocGuard::Ref(self)
208    }
209    fn inner_mut(&mut self) -> DocGuardMut<'_> {
210        DocGuardMut::Ref(self)
211    }
212}
213
214impl Document for Rc<RefCell<BaseDocument>> {
215    fn inner(&self) -> DocGuard<'_> {
216        DocGuard::RefCell(self.borrow())
217    }
218
219    fn inner_mut(&mut self) -> DocGuardMut<'_> {
220        DocGuardMut::RefCell(self.borrow_mut())
221    }
222}
223
224pub enum DocumentEvent {
225    ResourceLoad(ResourceLoadResponse),
226    /// A navigation originating from within an iframe's sub-document
227    /// (e.g. a link click), to be applied to the iframe identified by `node_id`.
228    NavigateIframe {
229        node_id: NodeId,
230        url: Url,
231    },
232}
233
234/// How urgently a document needs another animation frame.
235#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
236pub enum AnimationPacing {
237    Idle,
238    Caret,
239    SlowCss,
240    Interactive,
241}
242
243pub struct BaseDocument {
244    /// ID of the document
245    id: usize,
246
247    // Config
248    /// Base url for resolving linked resources (stylesheets, images, fonts, etc)
249    pub(crate) url: DocumentUrl,
250    // Devtool settings. Currently used to render debug overlays
251    pub(crate) devtool_settings: DevtoolSettings,
252    // Viewport details such as the dimensions, HiDPI scale, and zoom factor,
253    pub(crate) viewport: Viewport,
254    // Scroll within our viewport
255    pub(crate) viewport_scroll: crate::Point<f64>,
256    /// Changes recorded for a script's `MutationObserver`, or `None` while no
257    /// observer is registered. See [`crate::DomMutation`].
258    pub(crate) mutation_log: Option<Vec<crate::DomMutation>>,
259    /// Content handler changes, independent of MutationObserver recording.
260    inline_handler_changes: Vec<(NodeId, crate::QualName, Option<crate::node::AttrAtom>)>,
261    /// Weak registrations make pages without live ranges pay only a branch.
262    pub(crate) live_ranges: Vec<crate::range::WeakRange>,
263    /// Script Selection shares the same live range object as getRangeAt().
264    pub dom_selection: Option<crate::LiveRange>,
265    /// CSS media type used to evaluate `@media` rules.
266    pub(crate) media_type: MediaType,
267    /// Strategy for Stylo's style traversal during `resolve`.
268    pub(crate) style_threading: StyleThreading,
269    /// Whether incremental layout is enabled for this document.
270    pub(crate) incremental_layout: bool,
271    /// How deeply this document is nested within other documents
272    /// (0 for a root document). Used to limit `<iframe>` nesting depth.
273    pub(crate) subdocument_depth: usize,
274
275    // Events
276    pub(crate) tx: Sender<DocumentEvent>,
277    // rx will always be Some, except temporarily while processing events
278    pub(crate) rx: Option<Receiver<DocumentEvent>>,
279
280    /// A slotmap-backed tree of nodes
281    ///
282    /// We pin the tree to a guarantee to the nodes it creates that the tree is stable in memory.
283    /// There is no way to create the tree - publicly or privately - that would invalidate that invariant.
284    pub(crate) nodes: Box<NodeTree>,
285
286    /// Batch cache invalidation across recursive subtree teardown.
287    node_drop_depth: usize,
288    /// `<details>` elements whose `open` attribute appeared or disappeared since the script
289    /// runtime last took them, each with whether it was open before its first change. The
290    /// HTML toggle task coalesces per element, so an element is listed once.
291    details_toggles: Vec<(NodeId, bool)>,
292    /// Bumped whenever the stylist device changes (viewport size, scale or colour scheme),
293    /// so script can skip re-evaluating media queries while it stays the same.
294    viewport_generation: u64,
295    /// The inert document that owns every `<template>`'s contents, made on first use. It
296    /// has no browsing context, so nothing in it is upgraded as a custom element.
297    template_contents_owner: Option<NodeId>,
298    dropped_node_ids: FxHashSet<NodeId>,
299    dropped_node_parents: FxHashSet<NodeId>,
300    /// Survivors that can still point at an id freed by this batch.
301    ///
302    /// Recorded while the node is still in the slab. The purge runs after a
303    /// subtree drop has already removed those nodes, so the links have to be
304    /// captured first.
305    dropped_reference_owners: FxHashSet<NodeId>,
306
307    /// The id of the root node (a Document node)
308    pub(crate) root_node_id: NodeId,
309
310    /// For each `position: fixed` node reparented onto the root element, the
311    /// layout parent it was taken from.
312    ///
313    /// Hoisting gives a fixed node the viewport as its containing block, which
314    /// is what CSS asks for. It must not also decide which stacking context the
315    /// node paints in: that follows the box tree, and the two are independent.
316    /// Without this record the node joins the root's stacking context, so a
317    /// negative z-index fixed layer inside an `isolation: isolate` ancestor
318    /// paints beneath every background between them and disappears.
319    pub(crate) hoisted_fixed_parents: HashMap<NodeId, NodeId>,
320
321    /// Authored child slots restored temporarily for static-position layout.
322    pub(crate) hoisted_fixed_indices: HashMap<NodeId, usize>,
323
324    /// Escaped absolute boxes whose explicit axes follow a containing block.
325    pub(crate) abspos_placements: Vec<crate::layout::abspos::AbsposPlacement>,
326
327    /// Maintained during the existing fixed/sticky walk on style or tree changes.
328    pub(crate) abspos_candidates: Vec<crate::layout::abspos::AbsposCandidate>,
329    pub(crate) abspos_candidate_ids: HashSet<NodeId>,
330    pub(crate) abspos_inputs: HashMap<NodeId, crate::layout::abspos::AbsposInputs>,
331    /// Candidates whose hypothetical layouts or styles were written this pass.
332    pub(crate) abspos_written: HashSet<NodeId>,
333    pub(crate) abspos_candidates_dirty: bool,
334    pub(crate) abspos_layout_dirty: bool,
335    /// Whether the normal root layout missed its PerformLayout cache.
336    pub(crate) abspos_normal_layout: bool,
337
338    /// Every `position: fixed` node whose containing block is the viewport,
339    /// which is every one of them except those under a transformed ancestor.
340    ///
341    /// Collected by the walk that hoists them, and used by
342    /// `resolve_fixed_positions` to hold them still while the page scrolls.
343    pub(crate) fixed_nodes: Vec<NodeId>,
344
345    /// The viewport scroll currently baked into those nodes' locations.
346    ///
347    /// One value for the whole document rather than one per node: the pin is
348    /// the same displacement for every fixed box, because they all share the
349    /// viewport as their containing block. Reset by `resolve_layout`, which
350    /// rewrites the locations it was added to.
351    pub(crate) fixed_scroll_offset: crate::Point<f64>,
352
353    /// Every `position: sticky` node in the document, in tree order.
354    ///
355    /// Collected by the same walk that hoists fixed nodes, because both need
356    /// one pre-order pass over the box tree and a second one would cost the
357    /// same on every frame of every document, sticky or not. Tree order matters:
358    /// a sticky box inside another sticky box is adjusted on top of its
359    /// ancestor's adjustment, so the ancestor has to be settled first.
360    pub(crate) sticky_nodes: Vec<NodeId>,
361
362    /// For each sticky node, the offset currently baked into its
363    /// `final_layout().location`.
364    ///
365    /// The adjustment is written into the box itself so that paint, hit testing
366    /// and `absolute_position` cannot disagree about where the box is. That
367    /// makes the pass non-idempotent unless it can recover the flow position it
368    /// started from, which is what this records. Cleared by `resolve_layout`,
369    /// which rewrites every location from taffy and so discards the offsets
370    /// along with them.
371    pub(crate) sticky_offsets: HashMap<NodeId, taffy::Point<f32>>,
372
373    /// Stacking contexts holding a hoisted child that an ancestor clips.
374    ///
375    /// Collected while flushing styles so that `resolve_hoisted_clips` visits
376    /// those contexts alone, rather than scanning every node in the document
377    /// after every layout to find the handful that hoist anything at all.
378    pub(crate) hoisted_clip_hosts: HashSet<NodeId>,
379
380    /// Contexts with hoisted paint children, maintained by the style flush.
381    pub(crate) hoisted_position_hosts: HashSet<NodeId>,
382
383    // Stylo
384    /// The Stylo engine
385    pub(crate) stylist: Stylist,
386    pub(crate) animations: DocumentAnimationSet,
387    /// Monotonic animation clock used by the most recent resolve.
388    ///
389    /// Embedders may inspect or capture between window frames. A diagnostic
390    /// caller historically passed `0.0` and rewound every CSS transition in
391    /// the document; retaining the high-water mark makes that impossible at
392    /// the document boundary.
393    pub(crate) last_resolve_animation_time: f64,
394    /// Stylo shared lock
395    pub(crate) guard: SharedRwLock,
396    /// Stylo invalidation map. We insert into this map prior to mutating nodes.
397    pub(crate) snapshots: SnapshotMap,
398
399    // Parley contexts
400    /// A Parley font context
401    pub(crate) font_ctx: Arc<Mutex<parley::FontContext>>,
402    #[cfg(feature = "parallel-construct")]
403    /// Thread-and-document-local copies to the font context
404    pub(crate) thread_font_contexts: ThreadLocal<RefCell<Box<FontContext>>>,
405    /// A Parley layout context
406    pub(crate) layout_ctx: parley::LayoutContext<TextBrush>,
407
408    /// The real (non-anonymous) node which is currently hovered (if any).
409    /// This is never a layout-generated (anonymous) node, so it remains valid
410    /// across box-tree reconstruction.
411    pub(crate) hover_node_id: Option<NodeId>,
412    /// The precise (may be anonymous) layout node under the pointer (if any).
413    /// This can be invalidated by box-tree reconstruction, and is re-resolved against
414    /// fresh layout at the end of every `resolve` pass.
415    pub(crate) hover_hit_node_id: Option<NodeId>,
416    /// Whether the node which is currently hovered is a text node/span
417    pub(crate) hover_node_is_text: bool,
418    /// The last known pointer position in client coordinates (viewport-relative, unscrolled).
419    pub(crate) last_client_pointer_position: Option<taffy::Point<f32>>,
420    /// Exact DOM target selected by semantic automation.
421    ///
422    /// Window pointers leave this empty and are re-hit-tested after layout.
423    /// An inspector already resolved identity, so re-hit-testing its synthetic
424    /// centre coordinate would silently replace that target with an overlap.
425    pub(crate) semantic_hover_node_id: Option<NodeId>,
426    /// The node which is currently focussed (if any)
427    pub(crate) focus_node_id: Option<NodeId>,
428    /// The node which is currently active (if any)
429    pub(crate) active_node_id: Option<NodeId>,
430    /// The node which recieved a mousedown event (if any)
431    pub(crate) mousedown_node_id: Option<NodeId>,
432    /// The last time a mousedown was made (for double-click detection)
433    pub(crate) last_mousedown_time: Option<Instant>,
434    /// The position where mousedown occurred (for selection drags and double-click detection)
435    pub(crate) mousedown_position: taffy::Point<f32>,
436    /// How many clicks have been made in quick succession
437    pub(crate) click_count: u16,
438    /// Whether we're currently in a text selection drag (moved 2px+ from mousedown)
439    pub(crate) drag_mode: DragMode,
440    /// The scrollbar thumb currently under the pointer, if any
441    pub(crate) hovered_scrollbar: Option<crate::node::ScrollbarRef>,
442    /// When each scroll container's overlay scrollbars were last shown
443    /// (scrolled, or the pointer left the thumb); drives their fade-out
444    pub(crate) scrollbar_activity: HashMap<NodeId, Instant>,
445    /// Whether and what kind of scroll animation is currently in progress
446    pub(crate) scroll_animation: ScrollAnimationState,
447
448    /// Text selection state (for non-input text)
449    pub(crate) text_selection: TextSelection,
450
451    // TODO: collapse animating state into a bitflags
452    /// Whether there are active CSS animations/transitions (so we should re-render every frame)
453    pub(crate) has_active_animations: bool,
454    /// Whether there is a `<canvas>` element in the DOM (so we should re-render every frame)
455    pub(crate) has_canvas: bool,
456    /// The most urgent animation cadence required by any subdocument.
457    pub(crate) subdoc_animation_pacing: AnimationPacing,
458
459    /// Map of id attribute values to node IDs for fast lookups.
460    /// May contain multiple nodes for the same id: `get_element_by_id`
461    /// returns the first in tree order.
462    pub(crate) nodes_to_id: HashMap<String, SmallVec<[NodeId; 1]>>,
463    /// Map of `<style>` and `<link>` node IDs to their associated stylesheet
464    pub(crate) nodes_to_stylesheet: BTreeMap<NodeId, DocumentStyleSheet>,
465    /// `<style>`/`<link>` nodes whose sheet is in the document Stylist. Shadow
466    /// tree sheets live elsewhere, and Stylo panics on removing a sheet it
467    /// does not hold.
468    pub(crate) document_sheet_nodes: HashSet<NodeId>,
469    /// The shadow root each shadow-tree `<style>`/`<link>` sheet was installed
470    /// into. Unloading happens after the node is detached, when its root can
471    /// no longer be found by walking up, so the owner is remembered here.
472    #[cfg(feature = "shadow-dom")]
473    pub(crate) shadow_sheet_roots: HashMap<NodeId, NodeId>,
474    /// Stylesheets added by the useragent
475    /// where the key is the hashed CSS
476    pub(crate) ua_stylesheets: HashMap<String, DocumentStyleSheet>,
477    /// Map from form control node ID's to their associated forms node ID's
478    pub(crate) controls_to_form: HashMap<NodeId, NodeId>,
479    /// Nodes that contain sub documents
480    pub(crate) sub_document_nodes: HashSet<NodeId>,
481    /// Load state (abort controller and in-flight request id) for each
482    /// `<iframe>` element whose sub-document is loaded automatically
483    pub(crate) iframe_loads: HashMap<NodeId, crate::iframe::IframeLoad>,
484    /// Nodes whose layout construction is waiting for a later pass.
485    pub(crate) deferred_construction_nodes: Vec<ConstructionTask>,
486    /// Which parts of the document differ from the previously painted frame.
487    ///
488    /// Off unless a consumer asks for it, so a document that never questions
489    /// its own frames does not pay to answer. See
490    /// [`set_paint_damage_tracking`](Self::set_paint_damage_tracking).
491    pub(crate) paint_damage: crate::paint_damage::PaintDamageTracker,
492
493    /// Nodes that contain custom widgets
494    #[cfg(feature = "custom-widget")]
495    pub(crate) custom_widget_nodes: HashSet<NodeId>,
496    /// Rendering resources allocated by custom widgets that should be deallocated during the next render
497    #[cfg(feature = "custom-widget")]
498    pub(crate) pending_resource_deallocations: Vec<anyrender::ResourceId>,
499
500    /// Registry of custom element definitions keyed by tag name
501    #[cfg(feature = "shadow-dom")]
502    pub(crate) custom_element_registry: crate::node::CustomElementRegistry,
503    /// Nodes that are shadow hosts (have an attached shadow root)
504    #[cfg(feature = "shadow-dom")]
505    pub(crate) shadow_host_nodes: HashSet<NodeId>,
506    #[cfg(feature = "shadow-dom")]
507    pub(crate) dirty_shadow_hosts: HashSet<NodeId>,
508    #[cfg(feature = "shadow-dom")]
509    pub(crate) pending_slot_changes: Vec<NodeId>,
510    #[cfg(feature = "shadow-dom")]
511    pub(crate) signaled_slots: HashSet<NodeId>,
512    #[cfg(feature = "shadow-dom")]
513    pub(crate) adopted_stylesheets: HashMap<NodeId, Vec<DocumentStyleSheet>>,
514    /// Nodes that have an attached custom element controller
515    #[cfg(feature = "shadow-dom")]
516    pub(crate) custom_element_nodes: HashSet<NodeId>,
517    /// Template element -> declarative shadow root, while that template is open
518    /// on the parser stack. Stored here because each streaming sink call builds
519    /// a new parser.
520    #[cfg(feature = "shadow-dom")]
521    pub(crate) declarative_template_shadows: HashMap<NodeId, NodeId>,
522
523    /// Native candidate index and mutation reactions for JavaScript custom elements.
524    pub(crate) script_custom_elements: crate::node::script_custom_element::Registry,
525
526    /// Cache of loaded images, keyed by URL. Allows reusing images across multiple
527    /// elements without re-fetching from the network.
528    pub(crate) image_cache: HashMap<String, ImageData>,
529
530    /// Tracks in-flight image requests. When an image is being fetched, additional
531    /// requests for the same URL are queued here instead of starting new fetches.
532    /// Value is a list of (node_id, image_type) pairs waiting for the image.
533    pub(crate) pending_images: HashMap<String, Vec<(NodeId, ImageType)>>,
534
535    // Tracks in-flight "critical" resources (e.g. stylesheets linked from the `<head>`),
536    // keyed by request id
537    pub(crate) pending_critical_resources: HashSet<usize>,
538
539    pub(crate) web_fonts: Vec<Arc<crate::net::web_fonts::WebFont>>,
540    pub(crate) font_epoch: usize,
541    document_referrer: String,
542    document_content_type: String,
543    document_window_focused: bool,
544
545    // Service providers
546    /// Network provider. Can be used to fetch assets.
547    pub net_provider: Arc<dyn NetProvider>,
548    /// Navigation provider. Can be used to navigate to a new page (bubbles up the event
549    /// on e.g. clicking a Link)
550    pub navigation_provider: Arc<dyn NavigationProvider>,
551    /// Shell provider. Can be used to request a redraw or set the cursor icon
552    pub shell_provider: Arc<dyn ShellProvider>,
553    /// HTML parser provider. Used to parse HTML for setInnerHTML
554    pub html_parser_provider: Arc<dyn HtmlParserProvider>,
555    /// Carried on every sub-resource `Request` this document issues; aborting
556    /// it cancels all in-flight fetches tied to this document. Set via
557    /// [`DocumentConfig::abort_signal`].
558    pub(crate) abort_signal: Option<AbortSignal>,
559}
560
561pub(crate) fn make_device(
562    viewport: &Viewport,
563    media_type: MediaType,
564    font_ctx: Arc<Mutex<FontContext>>,
565) -> Device {
566    let width = viewport.window_size.0 as f32 / viewport.scale();
567    let height = viewport.window_size.1 as f32 / viewport.scale();
568    let viewport_size = euclid::Size2D::new(width, height);
569    let device_size = euclid::Size2D::new(width, height) * viewport.scale();
570    let device_pixel_ratio = euclid::Scale::new(viewport.scale());
571
572    Device::new(
573        media_type,
574        selectors::matching::QuirksMode::NoQuirks,
575        viewport_size,
576        device_size,
577        device_pixel_ratio,
578        Box::new(BlitzFontMetricsProvider { font_ctx }),
579        ComputedValues::initial_values_with_font_override(Font::initial_values()),
580        match viewport.color_scheme {
581            ColorScheme::Light => PrefersColorScheme::Light,
582            ColorScheme::Dark => PrefersColorScheme::Dark,
583        },
584        PointerCapabilities::default(),
585        PointerCapabilities::default(),
586    )
587}
588
589/// Whether layout reuses its caches, and how that can be overridden at runtime.
590///
591/// Incremental layout is on unless a caller or the environment turns it off.
592///
593/// The environment override exists so a single build can be measured both ways:
594/// with it off every `resolve` clears the Taffy cache and re-shapes every inline
595/// root from scratch, so comparing the two in separate binaries would also
596/// compare two different compilations. `BLITZ_INCREMENTAL=0` forces the old
597/// behaviour, `=1` forces the new one.
598///
599/// This used to fall back to `cfg!(feature = "incremental")`. That feature is
600/// gone, replaced by `DocumentConfig::incremental`, and for a while afterwards
601/// this function was never called at all: the config read
602/// `unwrap_or(true)` directly, so `BLITZ_INCREMENTAL` was accepted and ignored.
603fn incremental_layout_default() -> bool {
604    !matches!(
605        std::env::var("BLITZ_INCREMENTAL").ok().as_deref(),
606        Some("0" | "false" | "off")
607    )
608}
609
610impl BaseDocument {
611    /// Create a new (empty) [`BaseDocument`] with the specified configuration
612    pub fn new(config: DocumentConfig) -> Self {
613        static ID_GENERATOR: AtomicUsize = AtomicUsize::new(1);
614
615        let id = ID_GENERATOR.fetch_add(1, Ordering::SeqCst);
616
617        let font_ctx = config
618            .font_ctx
619            .map(|mut font_ctx| {
620                font_ctx.source_cache.make_shared();
621                // font_ctx.collection.make_shared();
622                font_ctx
623            })
624            .unwrap_or_else(|| {
625                use parley::fontique::{Collection, CollectionOptions, SourceCache};
626                let mut font_ctx = FontContext {
627                    source_cache: SourceCache::new_shared(),
628                    collection: Collection::new(CollectionOptions {
629                        shared: false,
630                        system_fonts: cfg!(all(
631                            feature = "system-fonts",
632                            not(target_arch = "wasm32")
633                        )),
634                    }),
635                };
636                font_ctx
637                    .collection
638                    .register_fonts(Blob::new(Arc::new(crate::BULLET_FONT) as _), None);
639                font_ctx
640            });
641        let font_ctx = Arc::new(Mutex::new(font_ctx));
642        if let Some(fallback) = config.family_fallback {
643            crate::font_fallback::set(fallback);
644        }
645
646        // Make sure we turn on stylo features *before* creating the Stylist
647        style_config::set_pref!("layout.grid.enabled", true);
648        style_config::set_pref!("layout.unimplemented", true);
649        style_config::set_pref!("layout.columns.enabled", true);
650        style_config::set_pref!("layout.css.basic-shape-shape.enabled", true);
651        style_config::set_pref!("layout.threads", -1);
652
653        let viewport = config.viewport.unwrap_or_default();
654        let media_type = config.media_type.unwrap_or_else(MediaType::screen);
655        let device = make_device(&viewport, media_type.clone(), font_ctx.clone());
656        let stylist = Stylist::new(device, QuirksMode::NoQuirks);
657        let snapshots = SnapshotMap::new();
658        let nodes = Box::new(NodeTree::new());
659        let guard = SharedRwLock::new();
660        let nodes_to_id = HashMap::new();
661
662        let base_url = config
663            .base_url
664            .and_then(|url| DocumentUrl::from_str(&url).ok())
665            .unwrap_or_default();
666
667        let net_provider = config
668            .net_provider
669            .unwrap_or_else(|| Arc::new(DummyNetProvider));
670        let navigation_provider = config
671            .navigation_provider
672            .unwrap_or_else(|| Arc::new(DummyNavigationProvider));
673        let shell_provider = config
674            .shell_provider
675            .unwrap_or_else(|| Arc::new(DummyShellProvider));
676        let html_parser_provider = config
677            .html_parser_provider
678            .unwrap_or_else(|| Arc::new(DummyHtmlParserProvider));
679
680        let (tx, rx) = channel();
681
682        let mut doc = Self {
683            node_drop_depth: 0,
684            details_toggles: Vec::new(),
685            viewport_generation: 0,
686            template_contents_owner: None,
687            dropped_node_ids: FxHashSet::default(),
688            dropped_node_parents: FxHashSet::default(),
689            dropped_reference_owners: FxHashSet::default(),
690            hoisted_fixed_parents: HashMap::new(),
691            hoisted_fixed_indices: HashMap::new(),
692            abspos_placements: Vec::new(),
693            abspos_candidates: Vec::new(),
694            abspos_candidate_ids: HashSet::new(),
695            abspos_inputs: HashMap::new(),
696            abspos_written: HashSet::new(),
697            abspos_candidates_dirty: true,
698            abspos_layout_dirty: true,
699            abspos_normal_layout: false,
700            fixed_nodes: Vec::new(),
701            fixed_scroll_offset: crate::Point::ZERO,
702            sticky_nodes: Vec::new(),
703            sticky_offsets: HashMap::new(),
704            hoisted_clip_hosts: HashSet::new(),
705            hoisted_position_hosts: HashSet::new(),
706            id,
707            tx,
708            rx: Some(rx),
709
710            guard,
711            nodes,
712            root_node_id: NodeId::default(),
713            stylist,
714            animations: DocumentAnimationSet::default(),
715            last_resolve_animation_time: 0.0,
716            snapshots,
717            nodes_to_id,
718            viewport,
719            media_type,
720            style_threading: config.style_threading,
721            incremental_layout: config
722                .incremental
723                .unwrap_or_else(incremental_layout_default),
724            subdocument_depth: config.subdocument_depth,
725            devtool_settings: DevtoolSettings::default(),
726            viewport_scroll: crate::Point::ZERO,
727            mutation_log: None,
728            inline_handler_changes: Vec::new(),
729            live_ranges: Vec::new(),
730            dom_selection: None,
731            url: base_url,
732            ua_stylesheets: HashMap::new(),
733            nodes_to_stylesheet: BTreeMap::new(),
734            document_sheet_nodes: HashSet::new(),
735            #[cfg(feature = "shadow-dom")]
736            shadow_sheet_roots: HashMap::new(),
737            font_ctx,
738            #[cfg(feature = "parallel-construct")]
739            thread_font_contexts: ThreadLocal::new(),
740            layout_ctx: parley::LayoutContext::new(),
741
742            hover_node_id: None,
743            hover_hit_node_id: None,
744            hover_node_is_text: false,
745            last_client_pointer_position: None,
746            semantic_hover_node_id: None,
747            focus_node_id: None,
748            active_node_id: None,
749            mousedown_node_id: None,
750            has_active_animations: false,
751            subdoc_animation_pacing: AnimationPacing::Idle,
752            has_canvas: false,
753            sub_document_nodes: HashSet::new(),
754            iframe_loads: HashMap::new(),
755
756            #[cfg(feature = "custom-widget")]
757            custom_widget_nodes: HashSet::new(),
758            #[cfg(feature = "custom-widget")]
759            pending_resource_deallocations: Vec::new(),
760
761            #[cfg(feature = "shadow-dom")]
762            custom_element_registry: crate::node::CustomElementRegistry::new(),
763            #[cfg(feature = "shadow-dom")]
764            shadow_host_nodes: HashSet::new(),
765            #[cfg(feature = "shadow-dom")]
766            dirty_shadow_hosts: HashSet::new(),
767            #[cfg(feature = "shadow-dom")]
768            pending_slot_changes: Vec::new(),
769            #[cfg(feature = "shadow-dom")]
770            signaled_slots: HashSet::new(),
771            #[cfg(feature = "shadow-dom")]
772            adopted_stylesheets: HashMap::new(),
773            #[cfg(feature = "shadow-dom")]
774            custom_element_nodes: HashSet::new(),
775            #[cfg(feature = "shadow-dom")]
776            declarative_template_shadows: HashMap::new(),
777
778            deferred_construction_nodes: Vec::new(),
779            paint_damage: Default::default(),
780            script_custom_elements: Default::default(),
781            image_cache: HashMap::new(),
782            pending_images: HashMap::new(),
783            pending_critical_resources: HashSet::new(),
784            web_fonts: Vec::new(),
785            font_epoch: 0,
786            document_referrer: String::new(),
787            document_content_type: String::from("text/html"),
788            document_window_focused: false,
789            controls_to_form: HashMap::new(),
790            net_provider,
791            navigation_provider,
792            shell_provider,
793            html_parser_provider,
794            abort_signal: config.abort_signal,
795            last_mousedown_time: None,
796            mousedown_position: taffy::Point::ZERO,
797            click_count: 0,
798            drag_mode: DragMode::None,
799            hovered_scrollbar: None,
800            scrollbar_activity: HashMap::new(),
801            scroll_animation: ScrollAnimationState::None,
802            text_selection: TextSelection::default(),
803        };
804
805        // Initialise document with root Document node
806        doc.root_node_id = doc.create_node(NodeData::Document(Box::default()));
807        doc.root_node_mut().flags.insert(NodeFlags::IS_IN_DOCUMENT);
808
809        match config.ua_stylesheets {
810            Some(stylesheets) => {
811                for ss in &stylesheets {
812                    doc.add_user_agent_stylesheet(ss);
813                }
814            }
815            None => doc.add_user_agent_stylesheet(DEFAULT_CSS),
816        }
817
818        // Stylo data on the root node container is needed to render the node
819        let stylo_element_data = StyloElementData {
820            styles: ElementStyles {
821                primary: Some(
822                    ComputedValues::initial_values_with_font_override(Font::initial_values())
823                        .to_arc(),
824                ),
825                ..Default::default()
826            },
827            ..Default::default()
828        };
829        let stylo_data = doc.root_node_mut().stylo_element_data_mut();
830        *stylo_data.ensure_init_mut() = stylo_element_data;
831
832        doc
833    }
834
835    /// Set the Document's networking provider
836    pub fn set_net_provider(&mut self, net_provider: Arc<dyn NetProvider>) {
837        self.net_provider = net_provider;
838    }
839
840    /// Set the Document's navigation provider
841    pub fn set_navigation_provider(&mut self, navigation_provider: Arc<dyn NavigationProvider>) {
842        self.navigation_provider = navigation_provider;
843    }
844
845    /// Set the Document's shell provider
846    pub fn set_shell_provider(&mut self, shell_provider: Arc<dyn ShellProvider>) {
847        self.shell_provider = shell_provider;
848    }
849
850    /// Set the Document's html parser provider
851    pub fn set_html_parser_provider(&mut self, html_parser_provider: Arc<dyn HtmlParserProvider>) {
852        self.html_parser_provider = html_parser_provider;
853    }
854
855    /// Set base url for resolving linked resources (stylesheets, images, fonts, etc)
856    pub fn set_base_url(&mut self, url: &str) {
857        self.url = DocumentUrl::from(Url::parse(url).unwrap());
858    }
859
860    pub fn guard(&self) -> &SharedRwLock {
861        &self.guard
862    }
863
864    pub fn tree(&self) -> &NodeTree {
865        &self.nodes
866    }
867
868    pub fn id(&self) -> usize {
869        self.id
870    }
871
872    /// Wrapper around [`crate::net::stamped_request`]. Use the free function
873    /// when `&self` would conflict with a held `&mut` borrow on a field.
874    pub(crate) fn build_request(&self, url: url::Url) -> Request {
875        crate::net::with_referrer(
876            crate::net::stamped_request(url, self.abort_signal.as_ref()),
877            self.url(),
878        )
879    }
880
881    pub fn favicon_url(&self) -> Option<String> {
882        self.tree().iter().find_map(|(_, node)| {
883            let data = &node.data;
884            if !data.is_element_with_tag_name(&local_name!("link")) {
885                return None;
886            }
887            let rel = data.attr(local_name!("rel"))?;
888            if !rel
889                .split_ascii_whitespace()
890                .any(|v| v.eq_ignore_ascii_case("icon"))
891            {
892                return None;
893            }
894            data.attr(local_name!("href")).map(|s| s.to_string())
895        })
896    }
897
898    pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
899        self.nodes.get(node_id)
900    }
901
902    pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
903        self.nodes.get_mut(node_id)
904    }
905
906    pub fn get_focussed_node_id(&self) -> Option<NodeId> {
907        self.focus_node_id
908            .or(self.try_root_element().map(|el| el.id))
909    }
910
911    pub fn mutate<'doc>(&'doc mut self) -> DocumentMutator<'doc> {
912        DocumentMutator::new(self)
913    }
914
915    pub fn handle_dom_event<F: FnMut(DomEvent)>(
916        &mut self,
917        event: &mut DomEvent,
918        dispatch_event: F,
919    ) {
920        handle_dom_event(self, event, dispatch_event)
921    }
922
923    pub fn as_any_mut(&mut self) -> &mut dyn Any {
924        self
925    }
926
927    /// Find the label's bound input elements:
928    /// the element id referenced by the "for" attribute of a given label element
929    /// or the first input element which is nested in the label
930    /// Note that although there should only be one bound element,
931    /// we return all possibilities instead of just the first
932    /// in order to allow the caller to decide which one is correct
933    pub fn label_bound_input_element(&self, label_node_id: NodeId) -> Option<&Node> {
934        let label_element = self.nodes[label_node_id].element_data()?;
935        if let Some(target_element_dom_id) = label_element.attr(local_name!("for")) {
936            TreeTraverser::new(self)
937                .filter_map(|id| {
938                    let node = self.get_node(id)?;
939                    let element_data = node.element_data()?;
940                    if element_data.name.local != local_name!("input") {
941                        return None;
942                    }
943                    let id = element_data.id.as_ref()?;
944                    if *id == *target_element_dom_id {
945                        Some(node)
946                    } else {
947                        None
948                    }
949                })
950                .next()
951        } else {
952            TreeTraverser::new_with_root(self, label_node_id)
953                .filter_map(|child_id| {
954                    let node = self.get_node(child_id)?;
955                    let element_data = node.element_data()?;
956                    if element_data.name.local == local_name!("input") {
957                        Some(node)
958                    } else {
959                        None
960                    }
961                })
962                .next()
963        }
964    }
965
966    /// The checkedness a click changed, kept so the click's *canceled
967    /// activation steps* can put it back when a listener calls
968    /// `preventDefault()`.
969    ///
970    /// A radio carries its whole set, because selecting one clears the others.
971    pub fn run_pre_click_activation(&mut self, target: NodeId) -> Option<PreClickActivation> {
972        let node_id = crate::events::pointer::checkable_activation_target(self, target)?;
973        let el = self.get_node(node_id)?.data.downcast_element()?;
974        let is_radio = el.attr(local_name!("type")) == Some("radio");
975
976        if !is_radio {
977            let previous = el.checkbox_input_checked()?;
978            let el = self.get_node_mut(node_id)?.data.downcast_element_mut()?;
979            Self::toggle_checkbox(el);
980            return Some(PreClickActivation {
981                previous: vec![(node_id, previous)],
982            });
983        }
984
985        let radio_set = el.attr(local_name!("name")).map(str::to_string);
986        let Some(radio_set) = radio_set else {
987            let previous = el.checkbox_input_checked()?;
988            let el = self.get_node_mut(node_id)?.data.downcast_element_mut()?;
989            *el.checkbox_input_checked_mut()? = true;
990            return Some(PreClickActivation {
991                previous: vec![(node_id, previous)],
992            });
993        };
994
995        // Recorded *while* selecting rather than before it. Selecting one radio
996        // clears every other in the set, so cancelling has to restore all of
997        // them, and reading them first meant walking the whole arena twice for
998        // one press. The membership test is `toggle_radio`'s, deliberately: two
999        // predicates that disagreed would restore a different set than the one
1000        // that changed.
1001        //
1002        // That predicate is name plus "has checkbox state", which is neither
1003        // scoped to `type=radio` nor to a form owner. Wrong per HTML, and
1004        // longstanding; matching it here keeps this change to the ordering it
1005        // is about.
1006        let mut previous: Vec<(NodeId, bool)> = Vec::new();
1007        for (id, node) in self.nodes.iter_mut() {
1008            let Some(el) = node.data.downcast_element_mut() else {
1009                continue;
1010            };
1011            if el.attr(local_name!("name")) != Some(&*radio_set) {
1012                continue;
1013            }
1014            let Some(is_checked) = el.checkbox_input_checked_mut() else {
1015                continue;
1016            };
1017            previous.push((id, *is_checked));
1018            *is_checked = id == node_id;
1019        }
1020        Some(PreClickActivation { previous })
1021    }
1022
1023    /// Undo [`Self::run_pre_click_activation`]. The click's *canceled
1024    /// activation steps*.
1025    pub fn undo_pre_click_activation(&mut self, activation: PreClickActivation) {
1026        for (node_id, was_checked) in activation.previous {
1027            let Some(node) = self.get_node_mut(node_id) else {
1028                continue;
1029            };
1030            let Some(el) = node.data.downcast_element_mut() else {
1031                continue;
1032            };
1033            if let Some(is_checked) = el.checkbox_input_checked_mut() {
1034                *is_checked = was_checked;
1035            }
1036        }
1037    }
1038
1039    pub fn toggle_checkbox(el: &mut ElementData) -> bool {
1040        let Some(is_checked) = el.checkbox_input_checked_mut() else {
1041            return false;
1042        };
1043        *is_checked = !*is_checked;
1044
1045        *is_checked
1046    }
1047
1048    pub fn toggle_radio(&mut self, radio_set_name: String, target_radio_id: NodeId) {
1049        for (i, node) in self.nodes.iter_mut() {
1050            if let Some(node_data) = node.data.downcast_element_mut() {
1051                if node_data.attr(local_name!("name")) == Some(&radio_set_name) {
1052                    let was_clicked = i == target_radio_id;
1053                    let Some(is_checked) = node_data.checkbox_input_checked_mut() else {
1054                        continue;
1055                    };
1056                    *is_checked = was_clicked;
1057                }
1058            }
1059        }
1060    }
1061
1062    /// Toggle the `open` attribute of a `<details>` element, expanding or
1063    /// collapsing it. This is the default action triggered when the element's
1064    /// first `<summary>` child is activated.
1065    pub fn toggle_details_open(&mut self, details_id: NodeId) {
1066        use crate::qual_name;
1067
1068        let node = &self.nodes[details_id];
1069        if !node.data.is_element_with_tag_name(&local_name!("details")) {
1070            return;
1071        }
1072        let is_open = node.data.has_attr(local_name!("open"));
1073
1074        // Note: HTML attributes are in the empty (null) namespace, so the
1075        // QualName must not use the html namespace here, else it won't match
1076        // an `open` attribute created by the HTML parser.
1077        let mut mutator = self.mutate();
1078        if is_open {
1079            mutator.clear_attribute(details_id, qual_name!("open"));
1080        } else {
1081            mutator.set_attribute(details_id, qual_name!("open"), "");
1082        }
1083        drop(mutator);
1084
1085        self.shell_provider.request_redraw();
1086    }
1087
1088    pub fn set_style_property(&mut self, node_id: NodeId, name: &str, value: &str) {
1089        let node = &mut self.nodes[node_id];
1090        let did_change = node.element_data_mut().unwrap().set_style_property(
1091            name,
1092            value,
1093            &self.guard,
1094            self.url.url_extra_data(),
1095        );
1096        if did_change {
1097            node.mark_style_attr_updated();
1098        }
1099    }
1100
1101    pub fn remove_style_property(&mut self, node_id: NodeId, name: &str) {
1102        let node = &mut self.nodes[node_id];
1103        let did_change = node.element_data_mut().unwrap().remove_style_property(
1104            name,
1105            &self.guard,
1106            self.url.url_extra_data(),
1107        );
1108        if did_change {
1109            node.mark_style_attr_updated();
1110        }
1111    }
1112
1113    pub fn sub_document_node_ids(&self) -> Vec<NodeId> {
1114        self.sub_document_nodes.iter().copied().collect()
1115    }
1116
1117    /// Install the effective navigation referrer and response MIME type.
1118    pub fn set_navigation_metadata(&mut self, request: &Request, content_type: Option<&str>) {
1119        self.document_referrer = request
1120            .headers
1121            .get(blitz_traits::net::http::header::REFERER)
1122            .and_then(|value| value.to_str().ok())
1123            .and_then(|value| Url::parse(value).ok())
1124            .map(|mut url| {
1125                let _ = url.set_username("");
1126                let _ = url.set_password(None);
1127                url.set_fragment(None);
1128                url.to_string()
1129            })
1130            .unwrap_or_default();
1131        self.document_content_type = content_type
1132            .and_then(|value| value.split(';').next())
1133            .map(str::trim)
1134            .filter(|value| !value.is_empty())
1135            .unwrap_or("text/html")
1136            .to_ascii_lowercase();
1137    }
1138
1139    pub fn referrer(&self) -> &str {
1140        &self.document_referrer
1141    }
1142
1143    pub fn content_type(&self) -> &str {
1144        &self.document_content_type
1145    }
1146
1147    pub fn window_focused(&self) -> bool {
1148        self.document_window_focused
1149    }
1150
1151    /// Hosts must deliver focus changes on the document's owning thread.
1152    pub fn set_window_focused(&mut self, focused: bool) {
1153        self.document_window_focused = focused;
1154        for id in self.sub_document_node_ids() {
1155            if let Some(child) = self
1156                .get_node_mut(id)
1157                .and_then(|node| node.element_data_mut())
1158                .and_then(|element| element.sub_doc_data_mut())
1159            {
1160                child.inner_mut().set_window_focused(focused);
1161            }
1162        }
1163    }
1164
1165    /// 0 is standards, 1 is limited quirks, and 2 is quirks.
1166    pub fn set_document_quirks_mode(&mut self, mode: u8) {
1167        if let NodeData::Document(data) = &mut self.root_node_mut().data {
1168            data.quirks_mode = mode;
1169        }
1170    }
1171
1172    pub fn compat_mode(&self) -> &'static str {
1173        if matches!(&self.root_node().data, NodeData::Document(data) if data.quirks_mode == 2) {
1174            "BackCompat"
1175        } else {
1176            "CSS1Compat"
1177        }
1178    }
1179
1180    pub fn begin_document_stream(&mut self) {
1181        self.reset_web_fonts();
1182        self.pending_critical_resources.clear();
1183        self.pending_images.clear();
1184        self.set_document_quirks_mode(0);
1185    }
1186
1187    pub fn set_sub_document(&mut self, node_id: NodeId, mut sub_document: Box<dyn Document>) {
1188        sub_document
1189            .inner_mut()
1190            .set_window_focused(self.document_window_focused);
1191        self.nodes[node_id]
1192            .element_data_mut()
1193            .unwrap()
1194            .set_sub_document(sub_document);
1195        self.sub_document_nodes.insert(node_id);
1196    }
1197
1198    pub fn remove_sub_document(&mut self, node_id: NodeId) {
1199        self.nodes[node_id]
1200            .element_data_mut()
1201            .unwrap()
1202            .remove_sub_document();
1203        self.sub_document_nodes.remove(&node_id);
1204        if let Some(load) = self.iframe_loads.remove(&node_id) {
1205            load.abort_controller.abort();
1206        }
1207    }
1208
1209    /// Poll all sub-documents (see [`Document::poll`]), allowing them to make progress
1210    /// on any pending async operations (e.g. JavaScript timers). Hosts which poll a
1211    /// wrapper around a [`BaseDocument`] should call this from their `poll` implementation.
1212    ///
1213    /// Returns `true` if any sub-document reported changes.
1214    pub fn poll_subdocuments(&mut self, waker: Option<&Waker>) -> bool {
1215        self.poll_subdocuments_inner(waker, None)
1216    }
1217
1218    /// Propagate a settle pass's timer policy to every child document.
1219    pub fn poll_subdocuments_for_settle(
1220        &mut self,
1221        waker: Option<&Waker>,
1222        run_timers: bool,
1223    ) -> bool {
1224        self.poll_subdocuments_inner(waker, Some(run_timers))
1225    }
1226
1227    fn poll_subdocuments_inner(
1228        &mut self,
1229        waker: Option<&Waker>,
1230        settle_timers: Option<bool>,
1231    ) -> bool {
1232        let mut has_changes = false;
1233        let node_ids: Vec<NodeId> = self.sub_document_nodes.iter().copied().collect();
1234        for node_id in node_ids {
1235            let Some(sub_doc) = self
1236                .nodes
1237                .get_mut(node_id)
1238                .and_then(|node| node.subdoc_mut())
1239            else {
1240                continue;
1241            };
1242            let task_context = waker.map(TaskContext::from_waker);
1243            has_changes |= match settle_timers {
1244                Some(run_timers) => sub_doc.poll_for_settle(task_context, run_timers),
1245                None => sub_doc.poll(task_context),
1246            };
1247        }
1248        has_changes
1249    }
1250
1251    #[cfg(feature = "custom-widget")]
1252    pub fn custom_widget_node_ids(&self) -> Vec<NodeId> {
1253        self.custom_widget_nodes.iter().copied().collect()
1254    }
1255
1256    #[cfg(feature = "custom-widget")]
1257    pub fn take_pending_resource_deallocations(&mut self) -> Vec<anyrender::ResourceId> {
1258        std::mem::take(&mut self.pending_resource_deallocations)
1259    }
1260
1261    #[cfg(feature = "custom-widget")]
1262    pub fn set_custom_widget(&mut self, node_id: NodeId, widget: Box<dyn crate::Widget>) {
1263        self.nodes[node_id]
1264            .element_data_mut()
1265            .unwrap()
1266            .set_custom_widget(widget);
1267        self.custom_widget_nodes.insert(node_id);
1268    }
1269
1270    #[cfg(feature = "custom-widget")]
1271    pub fn remove_custom_widget(&mut self, node_id: NodeId) {
1272        let resources_to_deallocate = self.nodes[node_id]
1273            .element_data_mut()
1274            .unwrap()
1275            .remove_custom_widget();
1276        self.pending_resource_deallocations
1277            .extend_from_slice(&resources_to_deallocate);
1278        self.custom_widget_nodes.remove(&node_id);
1279    }
1280
1281    /// Mutable access to the custom element registry. Use
1282    /// [`CustomElementRegistry::define`](crate::node::CustomElementRegistry::define)
1283    /// to register custom elements by tag name.
1284    #[cfg(feature = "shadow-dom")]
1285    pub fn custom_elements_mut(&mut self) -> &mut crate::node::CustomElementRegistry {
1286        &mut self.custom_element_registry
1287    }
1288
1289    /// Register a custom element definition against a tag name (analogous to
1290    /// `customElements.define`).
1291    #[cfg(feature = "shadow-dom")]
1292    pub fn define_custom_element(
1293        &mut self,
1294        name: markup5ever::LocalName,
1295        definition: crate::node::CustomElementDefinition,
1296    ) {
1297        self.custom_element_registry.define(name, definition);
1298    }
1299
1300    /// The node ids of all shadow hosts in the document.
1301    #[cfg(feature = "shadow-dom")]
1302    pub fn shadow_host_node_ids(&self) -> Vec<NodeId> {
1303        self.shadow_host_nodes.iter().copied().collect()
1304    }
1305
1306    /// If `host_id` is a shadow host, returns the node id of its shadow root.
1307    #[cfg(feature = "shadow-dom")]
1308    pub fn shadow_root_id(&self, host_id: NodeId) -> Option<NodeId> {
1309        self.get_node(host_id)
1310            .and_then(|node| node.shadow_root_id())
1311    }
1312
1313    /// Attach a shadow root to the given host element, returning the node id of
1314    /// the newly-created shadow root. If the host already has a shadow root, its
1315    /// existing shadow root id is returned unchanged.
1316    #[cfg(feature = "shadow-dom")]
1317    pub fn attach_shadow(&mut self, host_id: NodeId, mode: crate::node::ShadowRootMode) -> NodeId {
1318        if let Some(existing) = self.nodes[host_id].shadow_root_id() {
1319            return existing;
1320        }
1321
1322        let shadow_root_id = self.create_node(NodeData::ShadowRoot(
1323            crate::node::ShadowRootData::new(host_id, mode),
1324        ));
1325
1326        // The shadow root's parent is the host. It is *not* added to the host's
1327        // `children` list (which holds light-DOM children); it is referenced via
1328        // the host's `ElementData::shadow_root` field instead.
1329        self.nodes[shadow_root_id].parent = Some(host_id);
1330        self.nodes[shadow_root_id].owner_document = self.nodes[host_id].owner_document;
1331        if self.nodes[host_id].flags.is_in_document() {
1332            self.nodes[shadow_root_id]
1333                .flags
1334                .insert(NodeFlags::IS_IN_DOCUMENT);
1335        }
1336
1337        self.nodes[host_id]
1338            .element_data_mut()
1339            .expect("Shadow host must be an element")
1340            .shadow_root = Some(shadow_root_id);
1341        self.shadow_host_nodes.insert(host_id);
1342        self.dirty_shadow_hosts.insert(host_id);
1343        self.nodes[host_id].set_restyle_hint(
1344            style::invalidation::element::restyle_hints::RestyleHint::restyle_subtree(),
1345        );
1346
1347        // Host needs its box tree rebuilt to account for the shadow tree.
1348        self.nodes[host_id].insert_damage(ALL_DAMAGE);
1349        self.nodes[host_id].mark_ancestors_dirty();
1350
1351        shadow_root_id
1352    }
1353
1354    /// HTML elements that `Element.attachShadow` accepts.
1355    ///
1356    /// Hyphenated names are included, matching the existing script binding.
1357    /// `template` is not a host.
1358    #[cfg(feature = "shadow-dom")]
1359    pub fn element_can_be_shadow_host(element: &ElementData) -> bool {
1360        let tag = element.name.local.as_ref();
1361        element.name.ns == ns!(html)
1362            && (tag.contains('-')
1363                || matches!(
1364                    tag,
1365                    "article"
1366                        | "aside"
1367                        | "blockquote"
1368                        | "body"
1369                        | "div"
1370                        | "footer"
1371                        | "h1"
1372                        | "h2"
1373                        | "h3"
1374                        | "h4"
1375                        | "h5"
1376                        | "h6"
1377                        | "header"
1378                        | "main"
1379                        | "nav"
1380                        | "p"
1381                        | "section"
1382                        | "span"
1383                ))
1384    }
1385
1386    /// Attach a declarative shadow root, or return false and leave the template
1387    /// as a normal template.
1388    ///
1389    /// Failure is the host being missing, not a shadow host, or already having
1390    /// a root. Success records the template so parsed children land in the root.
1391    #[cfg(feature = "shadow-dom")]
1392    pub fn try_attach_declarative_shadow(
1393        &mut self,
1394        host_id: NodeId,
1395        template_id: NodeId,
1396        mode: crate::node::ShadowRootMode,
1397        clonable: bool,
1398        serializable: bool,
1399        delegates_focus: bool,
1400    ) -> bool {
1401        let can_attach = self.nodes.get(host_id).is_some_and(|node| {
1402            node.element_data().is_some_and(|element| {
1403                Self::element_can_be_shadow_host(element) && element.shadow_root.is_none()
1404            })
1405        });
1406        if !can_attach {
1407            return false;
1408        }
1409        let root_id = self.attach_shadow(host_id, mode);
1410        if let Some(root) = self.nodes[root_id].shadow_root_data_mut() {
1411            root.declarative = true;
1412            root.clonable = clonable;
1413            root.serializable = serializable;
1414            root.delegates_focus = delegates_focus;
1415        }
1416        self.declarative_template_shadows
1417            .insert(template_id, root_id);
1418        true
1419    }
1420
1421    /// Detach (and drop) the shadow root of the given host element, if any.
1422    #[cfg(feature = "shadow-dom")]
1423    pub fn detach_shadow(&mut self, host_id: NodeId) {
1424        let shadow_root_id = self.nodes[host_id]
1425            .element_data_mut()
1426            .and_then(|el| el.shadow_root.take());
1427        if let Some(shadow_root_id) = shadow_root_id {
1428            self.drop_node_ignoring_parent(shadow_root_id);
1429            self.shadow_host_nodes.remove(&host_id);
1430            self.dirty_shadow_hosts.remove(&host_id);
1431            self.nodes[host_id].flattened_children = None;
1432            self.nodes[host_id].set_restyle_hint(
1433                style::invalidation::element::restyle_hints::RestyleHint::restyle_subtree(),
1434            );
1435            self.nodes[host_id].insert_damage(ALL_DAMAGE);
1436            self.nodes[host_id].mark_ancestors_dirty();
1437        }
1438    }
1439
1440    /// Attach a custom element controller to the given node.
1441    #[cfg(feature = "shadow-dom")]
1442    pub fn set_custom_element(
1443        &mut self,
1444        node_id: NodeId,
1445        controller: Box<dyn crate::node::CustomElement>,
1446    ) {
1447        use crate::node::{CustomElementData, SpecialElementData};
1448        self.nodes[node_id]
1449            .element_data_mut()
1450            .expect("Custom element host must be an element")
1451            .special_data = SpecialElementData::CustomElement(CustomElementData::new(controller));
1452        self.custom_element_nodes.insert(node_id);
1453    }
1454
1455    /// Detach the custom element controller from the given node (without running
1456    /// the `disconnected` callback). Returns the controller if present.
1457    #[cfg(feature = "shadow-dom")]
1458    pub fn take_custom_element(
1459        &mut self,
1460        node_id: NodeId,
1461    ) -> Option<Box<dyn crate::node::CustomElement>> {
1462        use crate::node::SpecialElementData;
1463        self.custom_element_nodes.remove(&node_id);
1464        let element = self.nodes[node_id].element_data_mut()?;
1465        if matches!(element.special_data, SpecialElementData::CustomElement(_)) {
1466            if let SpecialElementData::CustomElement(mut data) = element.special_data.take() {
1467                return data.controller.take();
1468            }
1469        }
1470        None
1471    }
1472
1473    pub fn root_node(&self) -> &Node {
1474        &self.nodes[self.root_node_id]
1475    }
1476
1477    pub fn root_node_mut(&mut self) -> &mut Node {
1478        &mut self.nodes[self.root_node_id]
1479    }
1480
1481    /// Ask this document to work out which regions differ between frames.
1482    ///
1483    /// Off by default. A consumer that turns it on is charged one pass over the
1484    /// node list per [`resolve`](Self::resolve) - a pass `resolve` already makes
1485    /// to clear damage - plus a hash lookup and a rectangle comparison per node.
1486    /// Nothing else in the document reads the result, so leaving it off costs a
1487    /// single branch.
1488    ///
1489    /// The consumer this exists for is a `backdrop-filter` cache. Blurring what
1490    /// is behind an element costs a render pass and a filter every frame, and
1491    /// the only way that stops being permanent is to skip the elements whose
1492    /// input has not changed. Turning this on is what makes that question
1493    /// answerable.
1494    ///
1495    /// The first frame after enabling reports everything as changed, because
1496    /// there is no previous frame to compare against.
1497    pub fn set_paint_damage_tracking(&mut self, enabled: bool) {
1498        self.paint_damage.set_enabled(enabled);
1499    }
1500
1501    /// Whether [`set_paint_damage_tracking`](Self::set_paint_damage_tracking) is on.
1502    pub fn paint_damage_tracking(&self) -> bool {
1503        self.paint_damage.is_enabled()
1504    }
1505
1506    /// What changed since the previously resolved frame.
1507    ///
1508    /// Empty when tracking is off, which is indistinguishable from "nothing
1509    /// changed" and deliberately so: a consumer that has not asked for the
1510    /// question to be answered must not read the empty answer as a licence to
1511    /// reuse a cache. Check
1512    /// [`paint_damage_tracking`](Self::paint_damage_tracking) first.
1513    pub fn paint_damage(&self) -> &crate::paint_damage::PaintDamage {
1514        self.paint_damage.damage()
1515    }
1516
1517    pub fn try_root_element(&self) -> Option<&Node> {
1518        TDocument::as_node(&self.root_node()).first_element_child()
1519    }
1520
1521    pub fn root_element(&self) -> &Node {
1522        TDocument::as_node(&self.root_node())
1523            .first_element_child()
1524            .unwrap()
1525            .as_element()
1526            .unwrap()
1527    }
1528
1529    pub fn create_node(&mut self, node_data: NodeData) -> NodeId {
1530        let tree_ptr = self.nodes.as_mut() as *mut NodeTree;
1531        let guard = self.guard.clone();
1532        let is_document = matches!(node_data, NodeData::Document(_));
1533        let id = self
1534            .nodes
1535            .insert_with_key(|id| Node::new(tree_ptr, id, guard, node_data));
1536        if !is_document {
1537            self.nodes[id].owner_document = Some(self.root_node_id);
1538        }
1539        self.register_script_custom_element(id);
1540        if let Some(attributes) = self.nodes[id]
1541            .element_data()
1542            .and_then(|element| element.inline_event_attributes.as_deref())
1543        {
1544            self.inline_handler_changes.extend(
1545                attributes
1546                    .iter()
1547                    .map(|attr| (id, attr.name.clone(), Some(attr.value.clone()))),
1548            );
1549        }
1550        id
1551    }
1552
1553    pub fn add_to_document_id_map(&mut self, document_id: NodeId, id: &str, node_id: NodeId) {
1554        if document_id == self.root_node_id || id.is_empty() {
1555            return;
1556        }
1557        let Some(node) = self.get_node_mut(document_id) else {
1558            return;
1559        };
1560        let NodeData::Document(data) = &mut node.data else {
1561            return;
1562        };
1563        let candidates = data.ids.entry(id.to_owned()).or_default();
1564        if !candidates.contains(&node_id) {
1565            candidates.push(node_id);
1566        }
1567    }
1568
1569    pub fn remove_from_document_id_map(&mut self, document_id: NodeId, id: &str, node_id: NodeId) {
1570        if document_id == self.root_node_id {
1571            return;
1572        }
1573        let Some(node) = self.get_node_mut(document_id) else {
1574            return;
1575        };
1576        let NodeData::Document(data) = &mut node.data else {
1577            return;
1578        };
1579        if let Some(candidates) = data.ids.get_mut(id) {
1580            candidates.retain(|candidate| *candidate != node_id);
1581            if candidates.is_empty() {
1582                data.ids.remove(id);
1583            }
1584        }
1585    }
1586
1587    /// Indexed lookup scoped to one document. Parent child-list membership
1588    /// excludes removed nodes, template contents, and shadow trees.
1589    pub fn get_element_by_id_in(&self, document_id: NodeId, id: &str) -> Option<NodeId> {
1590        if document_id == self.root_node_id {
1591            return self.get_element_by_id(id);
1592        }
1593        if id.is_empty() {
1594            return None;
1595        }
1596        let NodeData::Document(data) = &self.get_node(document_id)?.data else {
1597            return None;
1598        };
1599        data.ids
1600            .get(id)?
1601            .iter()
1602            .filter_map(|candidate| {
1603                let mut current = *candidate;
1604                let mut path = Vec::new();
1605                while current != document_id {
1606                    let node = self.get_node(current)?;
1607                    let parent_id = node.parent?;
1608                    let parent = self.get_node(parent_id)?;
1609                    path.push(parent.index_of_child(current)?);
1610                    current = parent_id;
1611                }
1612                path.reverse();
1613                Some((path, *candidate))
1614            })
1615            .min_by(|left, right| left.0.cmp(&right.0))
1616            .map(|(_, node_id)| node_id)
1617    }
1618
1619    /// Nodes that still hold a link to `node_id` after it leaves the document.
1620    ///
1621    /// Direct parents are recorded separately and force reconstruction. This
1622    /// collects everyone else the old document-wide pass would have mutated:
1623    /// surviving layout children (their `layout_parent` still points here),
1624    /// anonymous blocks that list the id without being its DOM parent, shadow
1625    /// slots and hosts, the authored parent of a hoisted fixed box, and
1626    /// ancestors whose inline layout, list marker, table grid, or stacking
1627    /// context stores a descendant id. Ancestors inside `removed` are not
1628    /// walked; detachment does not clean lists on nodes that are themselves
1629    /// going away.
1630    fn collect_live_reference_owners(
1631        nodes: &NodeTree,
1632        hoisted_fixed_parents: &HashMap<NodeId, NodeId>,
1633        removed: &FxHashSet<NodeId>,
1634        node_id: NodeId,
1635        candidates: &mut FxHashSet<NodeId>,
1636    ) {
1637        fn push_survivor(
1638            removed: &FxHashSet<NodeId>,
1639            candidates: &mut FxHashSet<NodeId>,
1640            id: NodeId,
1641        ) {
1642            if !removed.contains(&id) {
1643                candidates.insert(id);
1644            }
1645        }
1646
1647        let (removed_is_text, dom_parent, layout_parent, seeds) = {
1648            let Some(node) = nodes.get(node_id) else {
1649                return;
1650            };
1651            let mut seeds: SmallVec<[NodeId; 8]> = SmallVec::new();
1652            {
1653                let borrowed = node.layout_children.borrow();
1654                if let Some(children) = borrowed.as_ref() {
1655                    seeds.extend(children.iter().copied());
1656                }
1657            }
1658            seeds.extend(node.children.iter().copied());
1659            seeds.extend(node.anonymous_blocks.iter().copied());
1660            seeds.extend(node.before());
1661            seeds.extend(node.after());
1662            #[cfg(feature = "shadow-dom")]
1663            {
1664                seeds.extend(node.assigned_slot);
1665                seeds.extend(
1666                    node.element_data()
1667                        .and_then(|element| element.assigned_slot),
1668                );
1669                if let Some(assigned) = node.assigned_nodes.as_ref() {
1670                    seeds.extend(assigned.iter().copied());
1671                }
1672                if let Some(root) = node.shadow_root_data() {
1673                    seeds.push(root.host);
1674                }
1675            }
1676            (
1677                node.is_text_node(),
1678                node.parent,
1679                node.layout_parent.get(),
1680                seeds,
1681            )
1682        };
1683
1684        for parent_id in [dom_parent, layout_parent].into_iter().flatten() {
1685            let Some(parent) = nodes.get(parent_id) else {
1686                continue;
1687            };
1688            let anons: SmallVec<[NodeId; 4]> = parent.anonymous_blocks.iter().copied().collect();
1689            for anon in anons {
1690                let Some(anon_node) = nodes.get(anon) else {
1691                    continue;
1692                };
1693                let listed = anon_node
1694                    .children
1695                    .iter()
1696                    .any(|child| removed.contains(child))
1697                    || anon_node
1698                        .layout_children
1699                        .borrow()
1700                        .as_ref()
1701                        .is_some_and(|children| {
1702                            children.iter().any(|child| removed.contains(child))
1703                        });
1704                if listed {
1705                    push_survivor(removed, candidates, anon);
1706                }
1707            }
1708        }
1709        for id in seeds {
1710            push_survivor(removed, candidates, id);
1711        }
1712
1713        let mut stack: SmallVec<[NodeId; 16]> = SmallVec::new();
1714        if let Some(id) = dom_parent {
1715            if !removed.contains(&id) {
1716                stack.push(id);
1717            }
1718        }
1719        if let Some(id) = layout_parent {
1720            if !removed.contains(&id) {
1721                stack.push(id);
1722            }
1723        }
1724        if let Some(origin) = hoisted_fixed_parents.get(&node_id).copied() {
1725            // Hoist drops the layout edge before paint children are rebuilt, so
1726            // the authored parent can still name the fixed box.
1727            push_survivor(removed, candidates, origin);
1728            if !removed.contains(&origin) {
1729                stack.push(origin);
1730            }
1731        }
1732
1733        let mut seen: SmallVec<[NodeId; 16]> = SmallVec::new();
1734        seen.push(node_id);
1735        while let Some(id) = stack.pop() {
1736            if seen.contains(&id) {
1737                continue;
1738            }
1739            seen.push(id);
1740            let Some(ancestor) = nodes.get(id) else {
1741                continue;
1742            };
1743            let stacking = ancestor.stacking_context.as_ref().is_some_and(|context| {
1744                context.children.iter().any(|child| {
1745                    removed.contains(&child.node_id)
1746                        || child
1747                            .clip_ancestors
1748                            .iter()
1749                            .any(|clip| removed.contains(clip))
1750                })
1751            });
1752            let inline_or_list = ancestor.element_data().is_some_and(|element| {
1753                element.inline_layout_data.is_some() || element.list_item_data.is_some()
1754            });
1755            // Table grids store cells, rows, and row groups, never text nodes.
1756            let table_root = !removed_is_text && ancestor.flags.is_table_root();
1757            #[cfg(feature = "shadow-dom")]
1758            let shadow_hit = {
1759                let mut hit = ancestor
1760                    .flattened_children
1761                    .as_ref()
1762                    .is_some_and(|children| children.iter().any(|child| removed.contains(child)))
1763                    || ancestor.assigned_nodes.as_ref().is_some_and(|assigned| {
1764                        assigned.iter().any(|child| removed.contains(child))
1765                    });
1766                if let Some(root) = ancestor.shadow_root_data() {
1767                    let host = root.host;
1768                    hit = hit
1769                        || root.slots.iter().any(|slot| removed.contains(slot))
1770                        || root
1771                            .slottables
1772                            .iter()
1773                            .any(|slottable| removed.contains(slottable))
1774                        || root
1775                            .stylesheet_nodes
1776                            .iter()
1777                            .any(|sheet| removed.contains(sheet));
1778                    stack.push(host);
1779                }
1780                hit
1781            };
1782            #[cfg(not(feature = "shadow-dom"))]
1783            let shadow_hit = false;
1784            if stacking || inline_or_list || table_root || shadow_hit {
1785                push_survivor(removed, candidates, id);
1786            }
1787            if let Some(parent) = ancestor.parent {
1788                stack.push(parent);
1789            }
1790            if let Some(parent) = ancestor.layout_parent.get() {
1791                stack.push(parent);
1792            }
1793        }
1794    }
1795
1796    /// Stacking-context hosts are few, and a fixed box can paint in one that
1797    /// is not a layout ancestor of its current parent. Visit a host only when
1798    /// its hoisted list actually names a removed id.
1799    fn collect_stacking_context_owners(
1800        nodes: &NodeTree,
1801        position_hosts: &HashSet<NodeId>,
1802        clip_hosts: &HashSet<NodeId>,
1803        removed: &FxHashSet<NodeId>,
1804        candidates: &mut FxHashSet<NodeId>,
1805    ) {
1806        for host in position_hosts.iter().chain(clip_hosts.iter()).copied() {
1807            if removed.contains(&host) {
1808                continue;
1809            }
1810            let Some(node) = nodes.get(host) else {
1811                continue;
1812            };
1813            let Some(context) = node.stacking_context.as_ref() else {
1814                continue;
1815            };
1816            let references = context.children.iter().any(|child| {
1817                removed.contains(&child.node_id)
1818                    || child
1819                        .clip_ancestors
1820                        .iter()
1821                        .any(|clip| removed.contains(clip))
1822            });
1823            if references {
1824                candidates.insert(host);
1825            }
1826        }
1827    }
1828
1829    /// Remove detached or freed ids from every persistent rendering cache.
1830    ///
1831    /// DOM ancestry alone cannot find the owners: fixed boxes can belong to
1832    /// the root's layout list and another ancestor's stacking context, while
1833    /// inline boxes and table cells have additional flattened representations.
1834    /// Invalidate those owners before this mutation returns. Hidden subtrees
1835    /// retain caches, so leaving cleanup until resolve is insufficient.
1836    ///
1837    /// Only those owners are visited. A pass over every node made each
1838    /// `textContent` write rescan the whole document.
1839    pub(crate) fn invalidate_node_references(
1840        &mut self,
1841        removed: &FxHashSet<NodeId>,
1842        mut parents: FxHashSet<NodeId>,
1843        extra_owners: FxHashSet<NodeId>,
1844    ) {
1845        if removed.is_empty() {
1846            return;
1847        }
1848
1849        fn layout_references_removed(
1850            layout: &parley::Layout<TextBrush>,
1851            removed: &FxHashSet<NodeId>,
1852        ) -> bool {
1853            layout
1854                .inline_boxes()
1855                .iter()
1856                .any(|ibox| removed.contains(&NodeId::from_u64(ibox.id)))
1857                || layout.styles().iter().any(|style| {
1858                    removed.contains(&style.brush.id)
1859                        || style
1860                            .brush
1861                            .text_node
1862                            .is_some_and(|id| removed.contains(&id))
1863                })
1864        }
1865
1866        // An anonymous origin can disappear while its hoisted fixed box
1867        // survives. Reconstruct the root edge too, so construction can give
1868        // that box a current authored origin.
1869        for (&id, &origin) in &self.hoisted_fixed_parents {
1870            if removed.contains(&id) || removed.contains(&origin) {
1871                if let Some(node) = self.nodes.get(id) {
1872                    parents.extend(node.layout_parent.get());
1873                }
1874            }
1875        }
1876
1877        let mut candidates: FxHashSet<NodeId> = FxHashSet::with_capacity_and_hasher(
1878            parents.len() + extra_owners.len() + removed.len(),
1879            Default::default(),
1880        );
1881        for &id in &parents {
1882            if !removed.contains(&id) {
1883                candidates.insert(id);
1884            }
1885        }
1886        for id in extra_owners {
1887            if !removed.contains(&id) {
1888                candidates.insert(id);
1889            }
1890        }
1891        for &id in removed {
1892            let detach = self
1893                .nodes
1894                .get(id)
1895                .and_then(|node| node.layout_parent.get())
1896                .is_some_and(|parent| !removed.contains(&parent));
1897            if detach {
1898                candidates.insert(id);
1899            }
1900            if self.nodes.contains_key(id) {
1901                Self::collect_live_reference_owners(
1902                    &self.nodes,
1903                    &self.hoisted_fixed_parents,
1904                    removed,
1905                    id,
1906                    &mut candidates,
1907                );
1908            }
1909        }
1910        Self::collect_stacking_context_owners(
1911            &self.nodes,
1912            &self.hoisted_position_hosts,
1913            &self.hoisted_clip_hosts,
1914            removed,
1915            &mut candidates,
1916        );
1917
1918        self.fixed_nodes.retain(|id| !removed.contains(id));
1919        self.sticky_nodes.retain(|id| !removed.contains(id));
1920        self.sticky_offsets.retain(|id, _| !removed.contains(id));
1921        self.hoisted_fixed_parents
1922            .retain(|id, origin| !removed.contains(id) && !removed.contains(origin));
1923        let fixed_parents = &self.hoisted_fixed_parents;
1924        self.hoisted_fixed_indices
1925            .retain(|id, _| fixed_parents.contains_key(id));
1926        self.hoisted_clip_hosts.retain(|id| !removed.contains(id));
1927        self.hoisted_position_hosts
1928            .retain(|id| !removed.contains(id));
1929
1930        // These snapshots contain both box ids and containing-block/parent
1931        // ids. Discard the complete snapshots rather than retaining inputs
1932        // computed against a box tree that has just changed.
1933        self.abspos_placements.clear();
1934        self.abspos_candidates.clear();
1935        self.abspos_candidate_ids.clear();
1936        self.abspos_inputs.clear();
1937        self.abspos_written.clear();
1938        self.abspos_candidates_dirty = true;
1939        self.abspos_layout_dirty = true;
1940
1941        let mut dirty = FxHashSet::default();
1942        for id in candidates {
1943            let Some(node) = self.nodes.get_mut(id) else {
1944                continue;
1945            };
1946            // Detached nodes still owned by script keep their internal box
1947            // tree, but must lose any edge back into the live layout tree.
1948            if removed.contains(&id) {
1949                if node
1950                    .layout_parent
1951                    .get()
1952                    .is_some_and(|parent| !removed.contains(&parent))
1953                {
1954                    node.layout_parent.set(None);
1955                }
1956                continue;
1957            }
1958
1959            let mut reconstruct = parents.contains(&id);
1960            reconstruct |= node
1961                .layout_children
1962                .get_mut()
1963                .as_ref()
1964                .is_some_and(|children| children.iter().any(|id| removed.contains(id)));
1965            let mut repaint = node
1966                .paint_children
1967                .get_mut()
1968                .as_ref()
1969                .is_some_and(|children| children.iter().any(|id| removed.contains(id)));
1970
1971            let old_anonymous_count = node.anonymous_blocks.len();
1972            node.anonymous_blocks.retain(|id| !removed.contains(id));
1973            reconstruct |= old_anonymous_count != node.anonymous_blocks.len();
1974            if node.is_anonymous() {
1975                let old_child_count = node.children.len();
1976                node.children.retain(|id| !removed.contains(id));
1977                reconstruct |= old_child_count != node.children.len();
1978            }
1979            if node
1980                .layout_parent
1981                .get()
1982                .is_some_and(|parent| removed.contains(&parent))
1983            {
1984                node.layout_parent.set(None);
1985                reconstruct = true;
1986            }
1987
1988            if let Some(context) = node.stacking_context.as_mut() {
1989                let old_count = context.children.len();
1990                context.children.retain(|child| {
1991                    !removed.contains(&child.node_id)
1992                        && !child.clip_ancestors.iter().any(|id| removed.contains(id))
1993                });
1994                if old_count != context.children.len() {
1995                    // Filtering changes the split between negative and
1996                    // non-negative children as well as the content bounds.
1997                    context.sort();
1998                    context.content_area = taffy::Rect::ZERO;
1999                    repaint = true;
2000                }
2001            }
2002
2003            #[cfg(feature = "shadow-dom")]
2004            {
2005                for children in [
2006                    node.flattened_children.as_mut(),
2007                    node.assigned_nodes.as_mut(),
2008                ]
2009                .into_iter()
2010                .flatten()
2011                {
2012                    let old_count = children.len();
2013                    children.retain(|id| !removed.contains(id));
2014                    reconstruct |= old_count != children.len();
2015                }
2016                if node.assigned_slot.is_some_and(|id| removed.contains(&id)) {
2017                    node.assigned_slot = None;
2018                    reconstruct = true;
2019                }
2020                if let Some(root) = node.shadow_root_data_mut() {
2021                    let old_count = root.slots.len() + root.slottables.len();
2022                    root.slots.retain(|id| !removed.contains(id));
2023                    root.slottables.retain(|id| !removed.contains(id));
2024                    root.stylesheet_nodes.retain(|id| !removed.contains(id));
2025                    reconstruct |= old_count != root.slots.len() + root.slottables.len();
2026                }
2027            }
2028
2029            let mut table_removed = false;
2030            if let Some(element) = node.data.downcast_element_mut() {
2031                for pseudo in [&mut element.before, &mut element.after] {
2032                    if pseudo.is_some_and(|id| removed.contains(&id)) {
2033                        *pseudo = None;
2034                        reconstruct = true;
2035                    }
2036                }
2037                if element
2038                    .inline_layout_data
2039                    .as_ref()
2040                    .is_some_and(|inline| layout_references_removed(&inline.layout, removed))
2041                {
2042                    element.inline_layout_data = None;
2043                    reconstruct = true;
2044                }
2045                if element
2046                    .list_item_data
2047                    .as_ref()
2048                    .is_some_and(|item| match &item.position {
2049                        crate::node::ListItemLayoutPosition::Outside(layout) => {
2050                            layout_references_removed(layout, removed)
2051                        }
2052                        crate::node::ListItemLayoutPosition::Inside => false,
2053                    })
2054                {
2055                    element.list_item_data = None;
2056                    reconstruct = true;
2057                }
2058                if let SpecialElementData::TableRoot(context) = &element.special_data {
2059                    table_removed = context.references_removed_nodes(removed);
2060                }
2061                if table_removed {
2062                    element.special_data = SpecialElementData::None;
2063                    reconstruct = true;
2064                }
2065                #[cfg(feature = "shadow-dom")]
2066                if element
2067                    .assigned_slot
2068                    .is_some_and(|id| removed.contains(&id))
2069                {
2070                    element.assigned_slot = None;
2071                    reconstruct = true;
2072                }
2073            }
2074            if table_removed {
2075                node.flags.remove(NodeFlags::IS_TABLE_ROOT);
2076            }
2077
2078            if reconstruct || repaint {
2079                node.layout_children.get_mut().take();
2080                node.paint_children.get_mut().take();
2081                if let Some(element) = node.data.downcast_element_mut() {
2082                    element.inline_layout_data = None;
2083                }
2084                dirty.insert(id);
2085            }
2086        }
2087
2088        // Clear ancestor layout caches immediately, including ancestors that
2089        // the next incremental traversal would otherwise skip. Follow both
2090        // trees and tolerate parents already freed by a root-first drop.
2091        let mut stack: Vec<NodeId> = dirty.iter().copied().collect();
2092        let mut visited = FxHashSet::default();
2093        while let Some(id) = stack.pop() {
2094            if !visited.insert(id) || removed.contains(&id) {
2095                continue;
2096            }
2097            let Some(node) = self.nodes.get_mut(id) else {
2098                continue;
2099            };
2100            stack.extend(node.parent);
2101            stack.extend(node.layout_parent.get());
2102            #[cfg(feature = "shadow-dom")]
2103            {
2104                stack.extend(node.assigned_slot);
2105                if let Some(root) = node.shadow_root_data() {
2106                    stack.push(root.host);
2107                }
2108            }
2109            node.set_dirty_descendants();
2110            if node.stylo_element_data_opt().is_some() {
2111                node.insert_damage(if dirty.contains(&id) {
2112                    ALL_DAMAGE
2113                } else {
2114                    crate::layout::damage::ONLY_RELAYOUT
2115                        | style::selector_parser::RestyleDamage::REPAINT
2116                });
2117                node.cache_mut().clear();
2118            }
2119            if let Some(inline) = node
2120                .data
2121                .downcast_element_mut()
2122                .and_then(|element| element.inline_layout_data.as_mut())
2123            {
2124                inline.content_widths = None;
2125            }
2126        }
2127
2128        #[cfg(feature = "shadow-dom")]
2129        {
2130            let nodes = &self.nodes;
2131            self.shadow_host_nodes.retain(|id| nodes.contains_key(*id));
2132            self.dirty_shadow_hosts.retain(|id| nodes.contains_key(*id));
2133            self.pending_slot_changes
2134                .retain(|id| nodes.contains_key(*id));
2135            self.signaled_slots.retain(|id| nodes.contains_key(*id));
2136            self.dirty_shadow_hosts
2137                .extend(self.shadow_host_nodes.iter().copied());
2138        }
2139
2140        self.controls_to_form
2141            .retain(|control, form| !removed.contains(control) && !removed.contains(form));
2142        if !dirty.is_empty() {
2143            self.shell_provider.request_redraw();
2144        }
2145
2146        // PaintDamage deliberately keeps old rectangles until resolve so
2147        // pixels formerly occupied by removed boxes are included in damage.
2148    }
2149
2150    fn purge_dropped_node_references(&mut self) {
2151        if self.node_drop_depth != 0 || self.dropped_node_ids.is_empty() {
2152            return;
2153        }
2154        let removed = std::mem::take(&mut self.dropped_node_ids);
2155        let parents = std::mem::take(&mut self.dropped_node_parents);
2156        let owners = std::mem::take(&mut self.dropped_reference_owners);
2157        self.invalidate_node_references(&removed, parents, owners);
2158    }
2159
2160    /// Remove a node from the node tree, clearing interaction state and
2161    /// invalidating rendering references before the surrounding drop returns.
2162    pub(crate) fn remove_node_from_tree(&mut self, node_id: NodeId) -> Option<Node> {
2163        self.clear_interaction_state_for_removed_node(node_id);
2164        let indexed = self.get_node(node_id).and_then(|node| {
2165            Some((
2166                node.owner_document?,
2167                node.element_data()?.id.as_ref()?.to_string(),
2168            ))
2169        });
2170        if let Some((document_id, id)) = indexed {
2171            self.remove_from_document_id_map(document_id, &id, node_id);
2172        }
2173        self.forget_script_custom_element(node_id);
2174        if self.nodes.contains_key(node_id) {
2175            self.dropped_node_ids.insert(node_id);
2176            let mut owners = FxHashSet::default();
2177            Self::collect_live_reference_owners(
2178                &self.nodes,
2179                &self.hoisted_fixed_parents,
2180                &self.dropped_node_ids,
2181                node_id,
2182                &mut owners,
2183            );
2184            self.dropped_reference_owners.extend(owners);
2185        }
2186        let node = self.nodes.remove(node_id);
2187        if let Some(node) = &node {
2188            self.dropped_node_parents.extend(
2189                [node.parent, node.layout_parent.get()]
2190                    .into_iter()
2191                    .flatten(),
2192            );
2193            self.purge_dropped_node_references();
2194        }
2195        node
2196    }
2197
2198    /// The nearest element ancestor of `node_id` that is still in the
2199    /// document. Used to retarget hover/active state when the node they
2200    /// reference is removed. Tolerates already-removed ancestors (subtree
2201    /// teardown proceeds root-first) by giving up and returning `None`.
2202    fn nearest_surviving_element_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
2203        let mut current = self.get_node(node_id)?.parent;
2204        while let Some(id) = current {
2205            let node = self.get_node(id)?;
2206            if node.is_element() && node.flags.is_in_document() {
2207                return Some(id);
2208            }
2209            current = node.parent;
2210        }
2211        None
2212    }
2213
2214    /// Clear any interaction state (hover/active/focus/mousedown/selection/
2215    /// drag/scrollbar) that references `node_id`, which is being removed from
2216    /// the document, running the usual teardown steps. `node_id` must still be
2217    /// present in the slab.
2218    ///
2219    /// This matches browser semantics (WebKit `hoveredElementDidDetach` /
2220    /// `elementInActiveChainDidDetach`, Blink `HoveredElementDetached` /
2221    /// `ActiveChainNodeDetached`):
2222    /// - Hover and active retarget to the nearest surviving element ancestor
2223    ///   as a *transient bridge*: the HOVER/ACTIVE element-state bits along
2224    ///   the surviving chain stay lit (no one-frame gap in `:hover`/`:active`
2225    ///   styling), and the subsequent hover diff can unset exactly the right
2226    ///   bits. Hover is then re-resolved against the pointer position by
2227    ///   [`Self::refresh_hover`] at the end of the next resolve pass (the
2228    ///   analogue of WebKit's "fake mouse move"), which corrects the bridge
2229    ///   value — including cases where the removed node overflowed its
2230    ///   ancestor's box, so the ancestor was never truly under the pointer.
2231    /// - Focus resets to the body (encoded as `None`), running blur
2232    ///   side-effects (clearing focus element state and disabling IME for
2233    ///   text inputs).
2234    pub(crate) fn clear_interaction_state_for_removed_node(&mut self, node_id: NodeId) {
2235        if !self.nodes.contains_key(node_id) {
2236            return;
2237        }
2238
2239        if self.hover_node_id == Some(node_id) {
2240            self.hover_node_id = self.nearest_surviving_element_ancestor(node_id);
2241            self.hover_node_is_text = false;
2242        }
2243        if self.hover_hit_node_id == Some(node_id) {
2244            self.hover_hit_node_id = None;
2245        }
2246        if self.semantic_hover_node_id == Some(node_id) {
2247            self.semantic_hover_node_id = None;
2248        }
2249        if self.active_node_id == Some(node_id) {
2250            self.active_node_id = self.nearest_surviving_element_ancestor(node_id);
2251        }
2252        if self.focus_node_id == Some(node_id) {
2253            let shell_provider = self.shell_provider.clone();
2254            self.nodes[node_id].blur(shell_provider);
2255            self.focus_node_id = None;
2256        }
2257        if self.mousedown_node_id == Some(node_id) {
2258            self.mousedown_node_id = None;
2259        }
2260        if self.text_selection.anchor.node_or_parent == Some(node_id)
2261            || self.text_selection.focus.node_or_parent == Some(node_id)
2262        {
2263            self.text_selection.clear();
2264        }
2265        if self
2266            .hovered_scrollbar
2267            .is_some_and(|scrollbar| scrollbar.node_id == node_id)
2268        {
2269            self.hovered_scrollbar = None;
2270        }
2271        let drag_references_node = match &self.drag_mode {
2272            DragMode::Panning(state) => state.target == node_id,
2273            DragMode::ScrollbarDrag(state) => state.scrollbar.node_id == node_id,
2274            DragMode::Selecting | DragMode::None => false,
2275        };
2276        if drag_references_node {
2277            self.drag_mode = DragMode::None;
2278        }
2279        if matches!(
2280            &self.scroll_animation,
2281            ScrollAnimationState::Fling(state) if state.target == node_id
2282        ) {
2283            self.scroll_animation = ScrollAnimationState::None;
2284        }
2285        self.scrollbar_activity.remove(&node_id);
2286
2287        // The form-owner map is keyed by control id and was never pruned, so a
2288        // page that re-renders its fields grew an entry per render, every one
2289        // of them a freed slot. Nothing dereferences those any more, but an
2290        // unbounded map keyed on dead ids is a leak either way.
2291        self.controls_to_form.remove(&node_id);
2292    }
2293
2294    pub(crate) fn drop_node_ignoring_parent(&mut self, node_id: NodeId) -> Option<Node> {
2295        self.drop_node_ignoring_parent_with(node_id, &mut |_| {})
2296    }
2297
2298    /// Like [`Self::drop_node_ignoring_parent`], but calls `on_drop` with the id of
2299    /// every dropped node (the node itself and all of its descendants).
2300    pub(crate) fn drop_node_ignoring_parent_with(
2301        &mut self,
2302        node_id: NodeId,
2303        on_drop: &mut dyn FnMut(NodeId),
2304    ) -> Option<Node> {
2305        self.node_drop_depth += 1;
2306        let mut node = self.remove_node_from_tree(node_id);
2307        if let Some(node) = &mut node {
2308            on_drop(node_id);
2309            if let Some(before) = node.before() {
2310                self.drop_node_ignoring_parent_with(before, on_drop);
2311            }
2312            if let Some(after) = node.after() {
2313                self.drop_node_ignoring_parent_with(after, on_drop);
2314            }
2315
2316            for &child in &node.children {
2317                self.drop_node_ignoring_parent_with(child, on_drop);
2318            }
2319            if let Some(contents) = node
2320                .element_data()
2321                .and_then(|element| element.template_contents)
2322            {
2323                self.drop_node_ignoring_parent_with(contents, on_drop);
2324            }
2325
2326            // Anonymous blocks live only in the slab, so deallocate the ones this
2327            // node owns rather than leaking them.
2328            for &anon_id in &node.anonymous_blocks {
2329                self.deallocate_anonymous_block(anon_id);
2330            }
2331
2332            // Drop any attached shadow root (its children are dropped recursively
2333            // via the recursive call below).
2334            #[cfg(feature = "shadow-dom")]
2335            if let Some(shadow_root_id) = node.shadow_root_id() {
2336                self.shadow_host_nodes.remove(&node_id);
2337                self.dirty_shadow_hosts.remove(&node_id);
2338                self.custom_element_nodes.remove(&node_id);
2339                self.drop_node_ignoring_parent_with(shadow_root_id, on_drop);
2340            }
2341        }
2342        self.node_drop_depth -= 1;
2343        self.purge_dropped_node_references();
2344        node
2345    }
2346
2347    /// Deallocate an anonymous block created in a previous construction
2348    /// round, along with any anonymous blocks nested within it.
2349    pub(crate) fn deallocate_anonymous_block(&mut self, anon_id: NodeId) {
2350        // The block may already have been removed from the slab (e.g. a
2351        // whitespace-only anonymous block dropped during construction).
2352        if !self.nodes.contains_key(anon_id) {
2353            return;
2354        }
2355
2356        // Free any anonymous blocks that this block owns before removing it.
2357        self.node_drop_depth += 1;
2358        let nested = std::mem::take(&mut self.nodes[anon_id].anonymous_blocks);
2359        for nested_id in nested {
2360            self.deallocate_anonymous_block(nested_id);
2361        }
2362
2363        self.remove_node_from_tree(anon_id);
2364        self.node_drop_depth -= 1;
2365        self.purge_dropped_node_references();
2366    }
2367
2368    pub fn create_text_node(&mut self, text: &str) -> NodeId {
2369        let content = text.to_string();
2370        let data = NodeData::Text(TextNodeData::new(content));
2371        self.create_node(data)
2372    }
2373
2374    pub fn deep_clone_node(&mut self, node_id: NodeId) -> NodeId {
2375        // Load existing node
2376        let node = &self.nodes[node_id];
2377        let mut data = node.data.clone();
2378
2379        match &mut data {
2380            NodeData::Element(elem) | NodeData::AnonymousBlock(elem) => {
2381                if let Some(arc) = elem.style_attribute.as_mut() {
2382                    let read_guard = self.guard().read();
2383                    let block = arc.read_with(&read_guard);
2384                    *arc = ServoArc::new(self.guard().wrap(block.clone()));
2385                }
2386            }
2387            _ => {}
2388        }
2389
2390        let children = node.children.clone();
2391        #[cfg(feature = "shadow-dom")]
2392        let shadow_source = node.shadow_root_id();
2393        let template_contents = node
2394            .element_data()
2395            .and_then(|element| element.template_contents);
2396        let owner_document = node.owner_document;
2397        let markup = node.markup.clone();
2398        let inert_script = node
2399            .flags
2400            .contains(crate::node::NodeFlags::IS_PARSER_INERT_SCRIPT);
2401
2402        // Create new node
2403        let new_node_id = self.create_node(data);
2404        self.nodes[new_node_id].owner_document = owner_document;
2405        self.nodes[new_node_id].markup = markup;
2406        self.nodes[new_node_id]
2407            .flags
2408            .set(crate::node::NodeFlags::IS_PARSER_INERT_SCRIPT, inert_script);
2409        if let Some(document_id) = owner_document {
2410            let id = self.nodes[new_node_id]
2411                .element_data()
2412                .and_then(|element| element.id.as_ref())
2413                .map(ToString::to_string);
2414            if let Some(id) = id {
2415                self.add_to_document_id_map(document_id, &id, new_node_id);
2416            }
2417        }
2418        if let Some(contents) = template_contents {
2419            let cloned_contents = self.deep_clone_node(contents);
2420            self.nodes[new_node_id]
2421                .element_data_mut()
2422                .expect("template clone must be an element")
2423                .template_contents = Some(cloned_contents);
2424        }
2425
2426        // Recursively clone children
2427        let new_children: ThinVec<NodeId> = children
2428            .into_iter()
2429            .map(|child_id| self.deep_clone_node(child_id))
2430            .collect();
2431        for &child_id in &new_children {
2432            self.nodes[child_id].parent = Some(new_node_id);
2433            self.attach_script_custom_element_subtree(child_id, new_node_id);
2434        }
2435        self.nodes[new_node_id].children = new_children;
2436
2437        // Only a shadow root that opted into `clonable` is copied. Imperative
2438        // roots keep the previous behavior of not cloning.
2439        #[cfg(feature = "shadow-dom")]
2440        if let Some(source_root) = shadow_source {
2441            let clonable = self
2442                .nodes
2443                .get(source_root)
2444                .and_then(|node| node.shadow_root_data())
2445                .is_some_and(|root| root.clonable);
2446            if clonable {
2447                let mode = self.nodes[source_root].shadow_root_data().unwrap().mode;
2448                let serializable = self.nodes[source_root]
2449                    .shadow_root_data()
2450                    .unwrap()
2451                    .serializable;
2452                let delegates_focus = self.nodes[source_root]
2453                    .shadow_root_data()
2454                    .unwrap()
2455                    .delegates_focus;
2456                let declarative = self.nodes[source_root]
2457                    .shadow_root_data()
2458                    .unwrap()
2459                    .declarative;
2460                let shadow_children = self.nodes[source_root].children.clone();
2461                let new_root = self.attach_shadow(new_node_id, mode);
2462                if let Some(root) = self.nodes[new_root].shadow_root_data_mut() {
2463                    root.clonable = true;
2464                    root.serializable = serializable;
2465                    root.delegates_focus = delegates_focus;
2466                    root.declarative = declarative;
2467                }
2468                let cloned_shadow_children: ThinVec<NodeId> = shadow_children
2469                    .into_iter()
2470                    .map(|child_id| self.deep_clone_node(child_id))
2471                    .collect();
2472                for &child_id in &cloned_shadow_children {
2473                    self.nodes[child_id].parent = Some(new_root);
2474                    self.attach_script_custom_element_subtree(child_id, new_root);
2475                }
2476                self.nodes[new_root].children = cloned_shadow_children;
2477            }
2478        }
2479
2480        new_node_id
2481    }
2482
2483    pub(crate) fn remove_and_drop_pe(&mut self, node_id: NodeId) -> Option<Node> {
2484        fn remove_pe_ignoring_parent(doc: &mut BaseDocument, node_id: NodeId) -> Option<Node> {
2485            let mut node = doc.remove_node_from_tree(node_id);
2486            if let Some(node) = &mut node {
2487                for &child in &node.children {
2488                    remove_pe_ignoring_parent(doc, child);
2489                }
2490                for &anon_id in &node.anonymous_blocks {
2491                    doc.deallocate_anonymous_block(anon_id);
2492                }
2493            }
2494            node
2495        }
2496
2497        self.node_drop_depth += 1;
2498        let node = remove_pe_ignoring_parent(self, node_id);
2499
2500        // Update child_idx values
2501        if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
2502            let parent = &mut self.nodes[parent_id];
2503            parent.children.retain(|id| *id != node_id);
2504        }
2505
2506        self.node_drop_depth -= 1;
2507        self.purge_dropped_node_references();
2508        node
2509    }
2510
2511    pub(crate) fn resolve_url(&self, raw: &str) -> url::Url {
2512        self.url.resolve_relative(raw).unwrap_or_else(|| {
2513            panic!(
2514                "to be able to resolve {raw} with the base_url: {:?}",
2515                *self.url
2516            )
2517        })
2518    }
2519
2520    /// Navigate to `raw`, resolved against this document's base URL.
2521    ///
2522    /// The same route a link click takes, exposed so that script can reach it:
2523    /// `location.assign`, `location.replace` and `location.reload` had nowhere
2524    /// to go, because `resolve_url` and the navigation provider are both
2525    /// internal to this crate. Returns `false` when `raw` will not resolve,
2526    /// so the caller can report that rather than navigate somewhere wrong.
2527    pub fn navigate_to_url(&self, raw: &str) -> bool {
2528        let Some(url) = self.url.resolve_relative(raw) else {
2529            return false;
2530        };
2531        self.navigation_provider
2532            .navigate_to(blitz_traits::navigation::NavigationOptions::new(
2533                url,
2534                None,
2535                self.id(),
2536            ));
2537        true
2538    }
2539
2540    /// This document's URL, as a page's `location.href` reads it.
2541    pub fn current_url(&self) -> String {
2542        self.url.to_string()
2543    }
2544
2545    pub fn print_tree(&self) {
2546        crate::util::walk_tree(0, self.root_node());
2547    }
2548
2549    pub fn print_subtree(&self, node_id: NodeId) {
2550        crate::util::walk_tree(0, &self.nodes[node_id]);
2551    }
2552
2553    pub fn reload_resource_by_href(&mut self, href_to_reload: &str) {
2554        for &node_id in self.nodes_to_stylesheet.keys() {
2555            let node = &self.nodes[node_id];
2556            let Some(element) = node.element_data() else {
2557                continue;
2558            };
2559
2560            if element.name.local == local_name!("link") {
2561                if let Some(href) = element.attr(local_name!("href")) {
2562                    // println!("Node {node_id} {href} {href_to_reload} {} {}", resolved_href.as_str(), resolved_href.as_str() == url_to_reload);
2563                    if href == href_to_reload {
2564                        let resolved_href = self.resolve_url(href);
2565                        self.net_provider.fetch(
2566                            self.id(),
2567                            self.build_request(resolved_href.clone())
2568                                .destination(RequestDestination::Style),
2569                            ResourceHandler::boxed(
2570                                self.tx.clone(),
2571                                self.id,
2572                                Some(node_id),
2573                                self.shell_provider.clone(),
2574                                StylesheetHandler {
2575                                    source_url: resolved_href,
2576                                    guard: self.guard.clone(),
2577                                    net_provider: self.net_provider.clone(),
2578                                    abort_signal: self.abort_signal.clone(),
2579                                },
2580                            ),
2581                        );
2582                    }
2583                }
2584            }
2585        }
2586    }
2587
2588    pub fn process_style_element(&mut self, target_id: NodeId) {
2589        let css = self.nodes[target_id].text_content();
2590        let css = html_escape::decode_html_entities(&css);
2591        let sheet = self.make_stylesheet(&css, Origin::Author);
2592        self.add_stylesheet_for_node(sheet, target_id);
2593    }
2594
2595    pub fn remove_user_agent_stylesheet(&mut self, contents: &str) {
2596        if let Some(sheet) = self.ua_stylesheets.remove(contents) {
2597            self.stylist.remove_stylesheet(sheet, &self.guard.read());
2598        }
2599    }
2600
2601    /// The document's base URL
2602    pub fn url(&self) -> &url::Url {
2603        &self.url
2604    }
2605
2606    /// Iterate over the author stylesheets (from `<style>` and `<link>` nodes)
2607    /// currently associated with this document
2608    pub fn author_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
2609        self.nodes_to_stylesheet.values()
2610    }
2611
2612    /// Iterate over the user-agent stylesheets currently associated with this document
2613    pub fn useragent_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
2614        self.ua_stylesheets.values()
2615    }
2616
2617    pub fn add_user_agent_stylesheet(&mut self, css: &str) {
2618        let sheet = self.make_stylesheet(css, Origin::UserAgent);
2619        self.ua_stylesheets.insert(css.to_string(), sheet.clone());
2620        self.stylist.append_stylesheet(sheet, &self.guard.read());
2621    }
2622
2623    pub fn make_stylesheet(&self, css: impl AsRef<str>, origin: Origin) -> DocumentStyleSheet {
2624        let data = Stylesheet::from_str(
2625            css.as_ref(),
2626            self.url.url_extra_data(),
2627            origin,
2628            ServoArc::new(self.guard.wrap(MediaList::empty())),
2629            self.guard.clone(),
2630            Some(&StylesheetLoader {
2631                tx: self.tx.clone(),
2632                doc_id: self.id,
2633                net_provider: self.net_provider.clone(),
2634                shell_provider: self.shell_provider.clone(),
2635                abort_signal: self.abort_signal.clone(),
2636            }),
2637            None,
2638            QuirksMode::NoQuirks,
2639            AllowImportRules::Yes,
2640        );
2641
2642        DocumentStyleSheet(ServoArc::new(data))
2643    }
2644
2645    pub fn upsert_stylesheet_for_node(&mut self, node_id: NodeId) {
2646        let raw_styles = self.nodes[node_id].text_content();
2647        let sheet = self.make_stylesheet(raw_styles, Origin::Author);
2648        self.add_stylesheet_for_node(sheet, node_id);
2649    }
2650
2651    pub fn add_stylesheet_for_node(&mut self, stylesheet: DocumentStyleSheet, node_id: NodeId) {
2652        self.platform_initial_sheet_media(&stylesheet, node_id);
2653        #[cfg(feature = "shadow-dom")]
2654        if let Some(root_id) = self.containing_shadow_root(node_id) {
2655            // A sheet that moved from the document into a shadow tree leaves
2656            // the document Stylist first.
2657            if self.document_sheet_nodes.remove(&node_id)
2658                && let Some(old) = self.nodes_to_stylesheet.get(&node_id).cloned()
2659            {
2660                self.stylist.remove_stylesheet(old, &self.guard.read());
2661            }
2662            self.install_shadow_stylesheet(root_id, node_id, stylesheet);
2663            return;
2664        }
2665
2666        let old = self.nodes_to_stylesheet.insert(node_id, stylesheet.clone());
2667
2668        if let Some(old) = old
2669            && self.document_sheet_nodes.contains(&node_id)
2670        {
2671            self.stylist.remove_stylesheet(old, &self.guard.read())
2672        }
2673        self.document_sheet_nodes.insert(node_id);
2674
2675        // Fetch @font-face fonts
2676        crate::net::fetch_font_face(
2677            self.tx.clone(),
2678            self.id,
2679            Some(node_id),
2680            &stylesheet.0,
2681            &self.net_provider,
2682            &self.shell_provider,
2683            &self.guard.read(),
2684            self.abort_signal.as_ref(),
2685            &mut self.web_fonts,
2686            self.font_epoch,
2687        );
2688
2689        // Store data on element
2690        let element = &mut self.nodes[node_id].element_data_mut().unwrap();
2691        element.special_data = SpecialElementData::Stylesheet(stylesheet.clone());
2692
2693        // TODO: Nodes could potentially get reused so ordering by node_id might be wrong.
2694        // Only sheets the document Stylist holds can be an insertion point: shadow-root sheets are
2695        // in `nodes_to_stylesheet` too, and stylo panics on a `before` sheet it does not have.
2696        let insertion_point = self
2697            .nodes_to_stylesheet
2698            .range((Bound::Excluded(node_id), Bound::Unbounded))
2699            .find(|(id, _)| self.document_sheet_nodes.contains(id))
2700            .map(|(_, sheet)| sheet);
2701
2702        if let Some(insertion_point) = insertion_point {
2703            self.stylist.insert_stylesheet_before(
2704                stylesheet,
2705                insertion_point.clone(),
2706                &self.guard.read(),
2707            )
2708        } else {
2709            self.stylist
2710                .append_stylesheet(stylesheet, &self.guard.read())
2711        }
2712    }
2713
2714    pub fn handle_messages(&mut self) {
2715        // Remove event Reciever from the Document so that we can process events
2716        // without holding a borrow to the Document
2717        let rx = self.rx.take().unwrap();
2718
2719        while let Ok(msg) = rx.try_recv() {
2720            self.handle_message(msg);
2721        }
2722
2723        // Put Reciever back
2724        self.rx = Some(rx);
2725    }
2726
2727    pub fn handle_message(&mut self, msg: DocumentEvent) {
2728        match msg {
2729            DocumentEvent::ResourceLoad(resource) => self.load_resource(resource),
2730            DocumentEvent::NavigateIframe { node_id, url } => self.navigate_iframe(node_id, url),
2731        }
2732    }
2733
2734    /// Whether the Document has pending requests for "critical" resources (that should block rendering)
2735    pub fn has_pending_critical_resources(&self) -> bool {
2736        !self.pending_critical_resources.is_empty()
2737    }
2738
2739    /// How many distinct image URLs are still being fetched.
2740    ///
2741    /// Images are deliberately not "critical" resources, so they never block
2742    /// rendering. An embedder that needs a settled page (a screenshot, a test,
2743    /// a print) has no other way to tell an image that is still in flight from
2744    /// one that will never arrive.
2745    pub fn pending_image_count(&self) -> usize {
2746        self.pending_images.len()
2747    }
2748
2749    pub fn load_resource(&mut self, res: ResourceLoadResponse) {
2750        self.pending_critical_resources.remove(&res.request_id);
2751
2752        let resource = match res.result {
2753            Ok(resource) => resource,
2754            Err(err) => {
2755                if let Some(url) = res.resolved_url.as_ref() {
2756                    let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
2757                    #[cfg(feature = "tracing")]
2758                    tracing::warn!(
2759                        url = url.as_str(),
2760                        waiting_nodes = waiting_nodes.len(),
2761                        error = err.as_str(),
2762                        "Resource load failed"
2763                    );
2764                    #[cfg(not(feature = "tracing"))]
2765                    let _ = (waiting_nodes, err);
2766                } else {
2767                    #[cfg(feature = "tracing")]
2768                    tracing::warn!(error = err.as_str(), "Resource load failed (no url)");
2769                    #[cfg(not(feature = "tracing"))]
2770                    let _ = err;
2771                }
2772                return;
2773            }
2774        };
2775
2776        let (resource, font_ticket) = match resource {
2777            Resource::TrackedFont(bytes, overrides, ticket) => {
2778                if ticket.css && ticket.epoch != self.font_epoch {
2779                    ticket.finish(false);
2780                    return;
2781                }
2782                (Resource::Font(bytes, overrides), Some(ticket))
2783            }
2784            resource => (resource, None),
2785        };
2786        if matches!(&resource, Resource::Css(_) | Resource::Font(_, _))
2787            && res.node_id.is_some_and(|id| {
2788                self.get_node(id)
2789                    .is_none_or(|node| !node.flags.is_in_document())
2790            })
2791        {
2792            if let Some(ticket) = font_ticket {
2793                ticket.finish(false);
2794            }
2795            return;
2796        }
2797
2798        match resource {
2799            Resource::Css(css) => {
2800                let node_id = res.node_id.unwrap();
2801                self.add_stylesheet_for_node(css, node_id);
2802            }
2803            Resource::ImportSheet(import_rule, sheet) => {
2804                // The write that used to happen on the network worker. Here it
2805                // is on the thread that owns styling, so it cannot collide
2806                // with a concurrent read of the same lock.
2807                //
2808                // Scoped, because the `@font-face` scan below needs a read of
2809                // the same lock and this is an `AtomicRefCell`: holding the
2810                // write across it would deadlock against itself rather than
2811                // wait.
2812                {
2813                    let mut guard = self.guard.write();
2814                    import_rule.write_with(&mut guard).stylesheet =
2815                        style::stylesheets::import_rule::ImportSheet::Sheet(sheet.clone());
2816                }
2817
2818                // The same scan `add_stylesheet_for_node` does for a top-level
2819                // sheet. An imported sheet may declare fonts too, and until now
2820                // nothing fetched them from a thread allowed to read the lock.
2821                crate::net::fetch_font_face(
2822                    self.tx.clone(),
2823                    self.id,
2824                    res.node_id,
2825                    &sheet,
2826                    &self.net_provider,
2827                    &self.shell_provider,
2828                    &self.guard.read(),
2829                    self.abort_signal.as_ref(),
2830                    &mut self.web_fonts,
2831                    self.font_epoch,
2832                );
2833            }
2834            Resource::Image(_kind, width, height, image_data) => {
2835                // Create the ImageData and cache it
2836                let image = ImageData::Raster(RasterImageData::new(width, height, image_data));
2837
2838                let Some(url) = res.resolved_url.as_ref() else {
2839                    return;
2840                };
2841
2842                self.apply_loaded_image(url, image);
2843            }
2844            #[cfg(feature = "svg")]
2845            Resource::Svg(_kind, svg) => {
2846                // Create the ImageData and cache it
2847                let image = ImageData::Svg(svg);
2848
2849                let Some(url) = res.resolved_url.as_ref() else {
2850                    return;
2851                };
2852
2853                self.apply_loaded_image(url, image);
2854            }
2855            Resource::DocumentSrc(html) => {
2856                let Some(node_id) = res.node_id else {
2857                    return;
2858                };
2859                self.apply_iframe_html(node_id, res.request_id, res.resolved_url, &html);
2860            }
2861            Resource::Font(bytes, overrides) => {
2862                let font = Blob::new(Arc::new(bytes));
2863
2864                // Build a `FontInfoOverride` from the `@font-face` descriptors
2865                // captured during stylesheet parsing. Without this, parley
2866                // reads the family name from the TTF's own metadata, which
2867                // means CSS `font-family: 'Avenir Book'` won't match a font
2868                // file that internally identifies as `Avenir 45 Book`.
2869                let weight_override = overrides.weight.map(parley::fontique::FontWeight::new);
2870                let info_override = parley::fontique::FontInfoOverride {
2871                    family_name: overrides.family_name.as_deref(),
2872                    weight: weight_override,
2873                    style: overrides.style,
2874                    ..Default::default()
2875                };
2876
2877                // TODO: Investigate eliminating double-box
2878                let mut global_font_ctx = self.font_ctx.lock().unwrap();
2879                let registered = global_font_ctx
2880                    .collection
2881                    .register_fonts(font.clone(), Some(info_override));
2882                let registered = !registered.is_empty();
2883
2884                #[cfg(feature = "parallel-construct")]
2885                {
2886                    rayon::broadcast(|_ctx| {
2887                        let mut font_ctx = self
2888                            .thread_font_contexts
2889                            .get_or(|| RefCell::new(Box::new(global_font_ctx.clone())))
2890                            .borrow_mut();
2891                        font_ctx
2892                            .collection
2893                            .register_fonts(font.clone(), Some(info_override));
2894                    });
2895                }
2896                drop(global_font_ctx);
2897
2898                // TODO: see if we can only invalidate if resolved fonts may have changed
2899                self.invalidate_inline_contexts();
2900                if let Some(ticket) = font_ticket {
2901                    ticket.finish(registered);
2902                }
2903            }
2904            Resource::TrackedFont(..) => unreachable!("tracked font was normalized"),
2905            Resource::None => {
2906                // Do nothing
2907            }
2908        }
2909    }
2910
2911    /// Cache a loaded image and apply it to all nodes waiting on it
2912    /// (`<img>` elements, `background-image` layers and `mask-image` layers).
2913    fn apply_loaded_image(&mut self, url: &str, image: ImageData) {
2914        // Get all nodes waiting for this image
2915        let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
2916
2917        #[cfg(feature = "tracing")]
2918        tracing::info!(
2919            "Image {url} loaded, applying to {} nodes",
2920            waiting_nodes.len()
2921        );
2922
2923        // Cache the image
2924        self.image_cache.insert(url.to_string(), image.clone());
2925
2926        // Apply to all waiting nodes
2927        for (node_id, image_type) in waiting_nodes {
2928            let Some(node) = self.get_node_mut(node_id) else {
2929                continue;
2930            };
2931
2932            match image_type {
2933                ImageType::Image => {
2934                    node.element_data_mut().unwrap().special_data =
2935                        SpecialElementData::Image(Box::new(image.clone()));
2936
2937                    // Clear layout cache
2938                    node.cache_mut().clear();
2939                    node.insert_damage(ALL_DAMAGE);
2940                }
2941                ImageType::Background(idx) | ImageType::Mask(idx) => {
2942                    let layer_image = node.element_data_mut().and_then(|el| {
2943                        let images = match image_type {
2944                            ImageType::Background(_) => &mut el.background_images,
2945                            ImageType::Mask(_) => &mut el.mask_images,
2946                            ImageType::Image => unreachable!(),
2947                        };
2948                        images.get_mut(idx)
2949                    });
2950                    if let Some(Some(layer_image)) = layer_image {
2951                        layer_image.status = Status::Ok;
2952                        layer_image.image = image.clone();
2953                    }
2954                }
2955            }
2956        }
2957    }
2958
2959    pub fn snapshot_node(&mut self, node_id: NodeId) {
2960        let node = &mut self.nodes[node_id];
2961
2962        // Do not snapshot nodes that have never been styled. A snapshot records an element's
2963        // pre-mutation state so a restyle can diff selector matches then-vs-now. An element
2964        // that has never been styled has no "then" to diff against. Snapshotting it anyway
2965        // makes Stylo's invalidation unwrap its (absent) primary style and panic.
2966        let has_been_styled = node.primary_styles().is_some();
2967        if !has_been_styled {
2968            return;
2969        }
2970
2971        let opaque_node_id = TNode::opaque(&&*node);
2972        node.set_has_snapshot(true);
2973        node.snapshot_handled()
2974            .store(false, std::sync::atomic::Ordering::SeqCst);
2975
2976        // TODO: handle invalidations other than hover
2977        if let Some(_existing_snapshot) = self.snapshots.get_mut(&opaque_node_id) {
2978            // Do nothing
2979            // TODO: update snapshot
2980        } else {
2981            let attrs: Option<Vec<_>> = node.attrs().map(|attrs| {
2982                attrs
2983                    .iter()
2984                    .map(|attr| {
2985                        let ident = AttrIdentifier {
2986                            local_name: GenericAtomIdent(attr.name.local.clone()),
2987                            name: GenericAtomIdent(attr.name.local.clone()),
2988                            namespace: GenericAtomIdent(attr.name.ns.clone()),
2989                            prefix: None,
2990                        };
2991
2992                        let value = if attr.name.local == local_name!("id") {
2993                            AttrValue::Atom(Atom::from(&*attr.value))
2994                        } else if attr.name.local == local_name!("class") {
2995                            let classes = attr
2996                                .value
2997                                .split_ascii_whitespace()
2998                                .map(Atom::from)
2999                                .collect();
3000                            // Stylo's `AttrValue` owns a `String`, so the atom
3001                            // is materialised here. This is the one place
3002                            // interning is paid back out, and it is bounded:
3003                            // once per snapshotted attribute, not per element
3004                            // per frame.
3005                            AttrValue::TokenList(OnceLock::from(attr.value.to_string()), classes)
3006                        } else {
3007                            AttrValue::String(attr.value.to_string())
3008                        };
3009
3010                        (ident, value)
3011                    })
3012                    .collect()
3013            });
3014
3015            let changed_attrs = attrs
3016                .as_ref()
3017                .map(|attrs| attrs.iter().map(|attr| attr.0.name.clone()).collect())
3018                .unwrap_or_default();
3019
3020            self.snapshots.insert(
3021                opaque_node_id,
3022                ServoElementSnapshot {
3023                    state: Some(*node.element_state()),
3024                    attrs,
3025                    changed_attrs,
3026                    class_changed: true,
3027                    id_changed: true,
3028                    other_attributes_changed: true,
3029                },
3030            );
3031        }
3032    }
3033
3034    /// Preserve the original attributes and accumulate names changed before traversal.
3035    pub(crate) fn snapshot_attribute(&mut self, node_id: NodeId, name: &markup5ever::LocalName) {
3036        self.snapshot_node(node_id);
3037        let opaque_node_id = TNode::opaque(&&self.nodes[node_id]);
3038        if let Some(snapshot) = self.snapshots.get_mut(&opaque_node_id) {
3039            let changed_attr = GenericAtomIdent(name.clone());
3040            if !snapshot.changed_attrs.contains(&changed_attr) {
3041                snapshot.changed_attrs.extend(std::iter::once(changed_attr));
3042            }
3043        }
3044    }
3045
3046    /// Snapshot a node and act on it, if it is still there.
3047    ///
3048    /// Tolerant of a node that has gone, because the ids reaching this are
3049    /// remembered across events — focus, hover, the last press — and the node
3050    /// they name can be removed between one event and the next. Indexing
3051    /// directly turned that ordinary case into a panic inside an event handler.
3052    pub fn snapshot_node_and(&mut self, node_id: NodeId, cb: impl FnOnce(&mut Node)) {
3053        if !self.nodes.contains_key(node_id) {
3054            return;
3055        }
3056        self.snapshot_node(node_id);
3057        cb(&mut self.nodes[node_id]);
3058    }
3059
3060    // Takes (x, y) co-ordinates (relative to the )
3061    pub fn hit(&self, x: f32, y: f32) -> Option<HitResult> {
3062        self.hit_with_scrollbar(x, y).0
3063    }
3064
3065    /// Walk up the tree to the nearest DOM node whose id is stable across
3066    /// box-tree reconstruction, so canonicalized interaction state never goes
3067    /// stale.
3068    ///
3069    /// Layout-generated nodes (anonymous blocks and `::before`/`::after`
3070    /// pseudo-elements, both stored as anonymous blocks) get new ids on every
3071    /// reconstruction, so we skip any anonymous node *and* a non-anonymous node
3072    /// whose parent is anonymous (the pseudo's text content). The first
3073    /// non-anonymous node with a non-anonymous parent is a real DOM node; the
3074    /// root element's `Document` parent guarantees termination.
3075    ///
3076    /// Returns `None` if `node_id` (or an ancestor) no longer exists.
3077    pub fn nearest_non_anonymous_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
3078        // Recurse up the tree keeping a window of the current node and its
3079        // parent, advancing one step per iteration so each node is looked up
3080        // exactly once.
3081        let mut node = self.get_node(node_id)?;
3082        loop {
3083            let parent = match node.parent {
3084                Some(parent_id) => self.get_node(parent_id)?,
3085                None => return Some(node.id),
3086            };
3087            if !node.is_anonymous() && !parent.is_anonymous() {
3088                return Some(node.id);
3089            }
3090            node = parent;
3091        }
3092    }
3093
3094    pub fn focus_next_node(&mut self) -> Option<NodeId> {
3095        let focussed_node_id = self.get_focussed_node_id()?;
3096        let id = self.next_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
3097        self.set_focus_to(id);
3098        Some(id)
3099    }
3100
3101    /// Move focus to the previous focussable node in the document
3102    pub fn focus_prev_node(&mut self) -> Option<NodeId> {
3103        let focussed_node_id = self.get_focussed_node_id()?;
3104        let id = self.prev_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
3105        self.set_focus_to(id);
3106        Some(id)
3107    }
3108
3109    /// Clear the focussed node
3110    pub fn clear_focus(&mut self) {
3111        if let Some(id) = self.focus_node_id {
3112            let shell_provider = self.shell_provider.clone();
3113            self.snapshot_node_and(id, |node| node.blur(shell_provider));
3114            self.focus_node_id = None;
3115        }
3116    }
3117
3118    pub fn set_mousedown_node_id(&mut self, node_id: Option<NodeId>) {
3119        self.mousedown_node_id = node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
3120    }
3121    pub fn set_focus_to(&mut self, focus_node_id: NodeId) -> bool {
3122        let Some(focus_node_id) = self.nearest_non_anonymous_ancestor(focus_node_id) else {
3123            return false;
3124        };
3125        if Some(focus_node_id) == self.focus_node_id {
3126            return false;
3127        }
3128
3129        #[cfg(feature = "tracing")]
3130        tracing::info!("Focussed node {focus_node_id}");
3131
3132        let shell_provider = self.shell_provider.clone();
3133
3134        // Remove focus from the old node
3135        if let Some(id) = self.focus_node_id {
3136            self.snapshot_node_and(id, |node| node.blur(shell_provider.clone()));
3137        }
3138
3139        // Focus the new node
3140        self.snapshot_node_and(focus_node_id, |node| node.focus(shell_provider));
3141
3142        self.focus_node_id = Some(focus_node_id);
3143
3144        true
3145    }
3146
3147    pub fn active_node(&mut self) -> bool {
3148        let Some(hover_node_id) = self.get_hover_node_id() else {
3149            return false;
3150        };
3151
3152        if let Some(active_node_id) = self.active_node_id {
3153            if active_node_id == hover_node_id {
3154                return true;
3155            }
3156            self.unactive_node();
3157        }
3158
3159        // hover_node_id is canonicalized when stored, so this always holds.
3160        debug_assert!(
3161            self.get_node(hover_node_id)
3162                .is_some_and(|node| !node.is_anonymous()),
3163            "interaction state must reference DOM nodes, not layout-generated nodes"
3164        );
3165        let active_node_id = Some(hover_node_id);
3166
3167        let node_path = self.maybe_node_layout_ancestors(active_node_id);
3168        for &id in node_path.iter() {
3169            self.snapshot_node_and(id, |node| node.active());
3170        }
3171
3172        self.active_node_id = active_node_id;
3173
3174        true
3175    }
3176
3177    pub fn unactive_node(&mut self) -> bool {
3178        let Some(active_node_id) = self.active_node_id.take() else {
3179            return false;
3180        };
3181
3182        let node_path = self.maybe_node_layout_ancestors(Some(active_node_id));
3183        for &id in node_path.iter() {
3184            self.snapshot_node_and(id, |node| node.unactive());
3185        }
3186
3187        true
3188    }
3189
3190    /// The scrollbar thumb currently under the pointer, if any.
3191    pub fn hovered_scrollbar(&self) -> Option<crate::node::ScrollbarRef> {
3192        self.hovered_scrollbar
3193    }
3194
3195    /// The scrollbar thumb currently being dragged, if any.
3196    pub fn scrollbar_drag_target(&self) -> Option<crate::node::ScrollbarRef> {
3197        match &self.drag_mode {
3198            DragMode::ScrollbarDrag(state) => Some(state.scrollbar),
3199            _ => None,
3200        }
3201    }
3202
3203    /// The current opacity of `node_id`'s overlay scrollbars. They show at
3204    /// full opacity on scroll and fade out after a delay (Chromium's overlay
3205    /// timings); the pointer resting on a thumb, or dragging it, holds them
3206    /// visible.
3207    pub fn scrollbar_opacity(&self, node_id: NodeId) -> f32 {
3208        let interacting = |scrollbar: &crate::node::ScrollbarRef| scrollbar.node_id == node_id;
3209        if self.hovered_scrollbar.as_ref().is_some_and(interacting)
3210            || self
3211                .scrollbar_drag_target()
3212                .as_ref()
3213                .is_some_and(interacting)
3214        {
3215            return 1.0;
3216        }
3217        self.scrollbar_activity.get(&node_id).map_or(1.0, |last| {
3218            crate::node::scrollbar::opacity_at(last.elapsed())
3219        })
3220    }
3221
3222    /// Show `node_id`'s overlay scrollbars at full opacity and restart their
3223    /// fade-out delay.
3224    pub(crate) fn show_scrollbars(&mut self, node_id: NodeId) {
3225        if cfg!(feature = "scrollbars") {
3226            self.scrollbar_activity.insert(node_id, Instant::now());
3227        }
3228    }
3229
3230    /// Whether any overlay scrollbars are awaiting or animating their
3231    /// fade-out (so frames must keep rendering until they finish).
3232    fn scrollbars_animating(&self) -> bool {
3233        use crate::node::scrollbar::{FADE_DELAY, FADE_DURATION};
3234        self.scrollbar_activity
3235            .values()
3236            .any(|last| last.elapsed() < FADE_DELAY + FADE_DURATION)
3237    }
3238
3239    /// [`hit`](Self::hit), also resolving the innermost overlay scrollbar
3240    /// thumb under the point (shares the traversal, so it costs nothing
3241    /// extra).
3242    pub(crate) fn hit_with_scrollbar(
3243        &self,
3244        x: f32,
3245        y: f32,
3246    ) -> (Option<HitResult>, Option<crate::node::ScrollbarRef>) {
3247        if TDocument::as_node(&self.root_node())
3248            .first_element_child()
3249            .is_none()
3250        {
3251            #[cfg(feature = "tracing")]
3252            tracing::warn!("No DOM - not resolving hit test");
3253            return (None, None);
3254        }
3255        let mut scrollbar = None;
3256        let hit = self
3257            .root_element()
3258            .hit_inner(x, y, self.viewport().scale_f64(), &mut scrollbar);
3259        (hit, scrollbar)
3260    }
3261
3262    pub fn set_hover_to(&mut self, x: f32, y: f32) -> bool {
3263        self.semantic_hover_node_id = None;
3264        // Record the pointer position in client (unscrolled) coordinates so
3265        // that `refresh_hover` can re-resolve hover state after layout or
3266        // scroll changes.
3267        self.last_client_pointer_position = Some(taffy::Point {
3268            x: x - self.viewport_scroll.x as f32,
3269            y: y - self.viewport_scroll.y as f32,
3270        });
3271
3272        let (hit, hovered_scrollbar) = self.hit_with_scrollbar(x, y);
3273        // A faded-out thumb is not interactive: pointer moves never fade
3274        // overlay scrollbars back in (only scrolling shows them).
3275        let hovered_scrollbar =
3276            hovered_scrollbar.filter(|scrollbar| self.scrollbar_opacity(scrollbar.node_id) > 0.0);
3277        // Scrollbar-thumb hover is part of hover state: track it here so a
3278        // pointer crossing a thumb restyles it even when the hit node (the
3279        // content under the overlay thumb) is unchanged.
3280        let scrollbar_changed = hovered_scrollbar != self.hovered_scrollbar;
3281        if scrollbar_changed {
3282            // Entering a thumb restores full opacity mid-fade; leaving one
3283            // restarts the fade-out delay.
3284            for scrollbar in [self.hovered_scrollbar, hovered_scrollbar]
3285                .into_iter()
3286                .flatten()
3287            {
3288                self.show_scrollbars(scrollbar.node_id);
3289            }
3290        }
3291        self.hovered_scrollbar = hovered_scrollbar;
3292
3293        // Store both the precise layout node that was hit (transient: used for
3294        // cursor/style queries) and its canonical DOM target (persistent: must
3295        // not reference layout-generated nodes, whose ids die on box-tree
3296        // reconstruction).
3297        let hit_node_id = hit.map(|hit| hit.node_id);
3298        let hover_node_id = hit_node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
3299        let new_is_text = hit.map(|hit| hit.is_text).unwrap_or(false);
3300
3301        self.apply_hover_target(hit_node_id, hover_node_id, new_is_text, scrollbar_changed)
3302    }
3303
3304    /// Move the authored hover state to an already-resolved DOM node.
3305    ///
3306    /// Semantic automation has selected a node by identity already. Repeating
3307    /// hit testing at its centre can choose an overlapping child or overlay,
3308    /// especially inside nested scrollers, and makes `Hover { node_id }`
3309    /// target something other than the requested node. Pointer coordinates are
3310    /// still recorded for event payloads and later layout refreshes.
3311    pub fn set_hover_to_node(&mut self, node_id: NodeId, x: f32, y: f32) -> bool {
3312        self.semantic_hover_node_id = Some(node_id);
3313        self.last_client_pointer_position = Some(taffy::Point {
3314            x: x - self.viewport_scroll.x as f32,
3315            y: y - self.viewport_scroll.y as f32,
3316        });
3317
3318        let hovered_scrollbar = self.hovered_scrollbar.take();
3319        let scrollbar_changed = hovered_scrollbar.is_some();
3320        if let Some(scrollbar) = hovered_scrollbar {
3321            self.show_scrollbars(scrollbar.node_id);
3322        }
3323        let hover_node_id = self.nearest_non_anonymous_ancestor(node_id);
3324        self.apply_hover_target(Some(node_id), hover_node_id, false, scrollbar_changed)
3325    }
3326
3327    fn apply_hover_target(
3328        &mut self,
3329        hit_node_id: Option<NodeId>,
3330        hover_node_id: Option<NodeId>,
3331        new_is_text: bool,
3332        scrollbar_changed: bool,
3333    ) -> bool {
3334        let hit_changed =
3335            hit_node_id != self.hover_hit_node_id || new_is_text != self.hover_node_is_text;
3336        self.hover_hit_node_id = hit_node_id;
3337        self.hover_node_is_text = new_is_text;
3338
3339        // Return early if the new node is the same as the already-hovered node
3340        if hover_node_id == self.hover_node_id {
3341            if hit_changed {
3342                // The canonical target is unchanged (so no restyle is needed)
3343                // but the precise hit node changed, which can change the cursor
3344                // (e.g. moving between text and non-text within one element).
3345                self.shell_provider.set_cursor(self.get_cursor());
3346            }
3347            return scrollbar_changed;
3348        }
3349
3350        let old_node_path = self.maybe_node_layout_ancestors(self.hover_node_id);
3351        let new_node_path = self.maybe_node_layout_ancestors(hover_node_id);
3352        let same_count = old_node_path
3353            .iter()
3354            .zip(&new_node_path)
3355            .take_while(|(o, n)| o == n)
3356            .count();
3357        for &id in old_node_path.iter().skip(same_count) {
3358            self.snapshot_node_and(id, |node| node.unhover());
3359        }
3360        for &id in new_node_path.iter().skip(same_count) {
3361            self.snapshot_node_and(id, |node| node.hover());
3362        }
3363
3364        self.hover_node_id = hover_node_id;
3365
3366        // Update the cursor
3367        self.shell_provider.set_cursor(self.get_cursor());
3368
3369        // Request redraw
3370        self.shell_provider.request_redraw();
3371
3372        true
3373    }
3374
3375    pub fn clear_hover(&mut self) -> bool {
3376        // The pointer is no longer over the document, so stop re-resolving
3377        // hover state against it.
3378        self.last_client_pointer_position = None;
3379        self.semantic_hover_node_id = None;
3380        self.hover_hit_node_id = None;
3381
3382        let Some(hover_node_id) = self.hover_node_id else {
3383            return false;
3384        };
3385
3386        let old_node_path = self.maybe_node_layout_ancestors(Some(hover_node_id));
3387        for &id in old_node_path.iter() {
3388            self.snapshot_node_and(id, |node| node.unhover());
3389        }
3390
3391        self.hover_node_id = None;
3392        self.hover_node_is_text = false;
3393
3394        // Update the cursor
3395        self.shell_provider.set_cursor(self.get_cursor());
3396
3397        // Request redraw
3398        self.shell_provider.request_redraw();
3399
3400        true
3401    }
3402
3403    /// Re-resolve hover state against the current layout using the last known
3404    /// pointer position.
3405    ///
3406    /// TODO: synthesizing pointerenter/pointerleave DOM events for
3407    /// hover changes caused by layout shifts.
3408    pub fn refresh_hover(&mut self) -> bool {
3409        if let Some(node_id) = self.semantic_hover_node_id {
3410            if self.get_node(node_id).is_some() {
3411                let hover_node_id = self.nearest_non_anonymous_ancestor(node_id);
3412                return self.apply_hover_target(Some(node_id), hover_node_id, false, false);
3413            }
3414            self.semantic_hover_node_id = None;
3415        }
3416        let Some(pos) = self.last_client_pointer_position else {
3417            return false;
3418        };
3419        let x = pos.x + self.viewport_scroll.x as f32;
3420        let y = pos.y + self.viewport_scroll.y as f32;
3421        self.set_hover_to(x, y)
3422    }
3423
3424    pub fn get_hover_node_id(&self) -> Option<NodeId> {
3425        self.hover_node_id
3426    }
3427
3428    pub fn get_mousedown_node_id(&self) -> Option<NodeId> {
3429        self.mousedown_node_id
3430    }
3431
3432    pub fn set_viewport(&mut self, viewport: Viewport) {
3433        let scale_has_changed = viewport.scale_f64() != self.viewport.scale_f64();
3434        self.viewport = viewport;
3435        self.set_stylist_device(make_device(
3436            &self.viewport,
3437            self.media_type.clone(),
3438            self.font_ctx.clone(),
3439        ));
3440        self.scroll_viewport_by(0.0, 0.0); // Clamp scroll offset
3441
3442        if scale_has_changed {
3443            self.invalidate_inline_contexts();
3444            self.shell_provider.request_redraw();
3445        }
3446    }
3447
3448    /// Returns the current CSS media type used to evaluate `@media` rules.
3449    pub fn media_type(&self) -> &MediaType {
3450        &self.media_type
3451    }
3452
3453    /// Sets the CSS media type used to evaluate `@media` rules (e.g. `screen` or `print`)
3454    /// and rebuilds the stylist device so updated rules apply on the next restyle.
3455    pub fn set_media_type(&mut self, media_type: MediaType) {
3456        if self.media_type == media_type {
3457            return;
3458        }
3459        self.media_type = media_type;
3460        self.set_stylist_device(make_device(
3461            &self.viewport,
3462            self.media_type.clone(),
3463            self.font_ctx.clone(),
3464        ));
3465    }
3466
3467    pub fn viewport(&self) -> &Viewport {
3468        &self.viewport
3469    }
3470
3471    pub fn viewport_mut(&mut self) -> ViewportMut<'_> {
3472        ViewportMut::new(self)
3473    }
3474
3475    pub fn zoom_by(&mut self, increment: f32) {
3476        *self.viewport.zoom_mut() += increment;
3477        self.set_viewport(self.viewport.clone());
3478    }
3479
3480    pub fn zoom_to(&mut self, zoom: f32) {
3481        *self.viewport.zoom_mut() = zoom;
3482        self.set_viewport(self.viewport.clone());
3483    }
3484
3485    pub fn get_viewport(&self) -> Viewport {
3486        self.viewport.clone()
3487    }
3488
3489    /// Returns whether incremental layout is currently enabled for this document.
3490    pub fn incremental_layout(&self) -> bool {
3491        self.incremental_layout
3492    }
3493
3494    /// Enables or disables incremental layout for this document.
3495    pub fn set_incremental_layout(&mut self, enabled: bool) {
3496        self.incremental_layout = enabled;
3497    }
3498
3499    pub fn devtools(&self) -> &DevtoolSettings {
3500        &self.devtool_settings
3501    }
3502
3503    pub fn devtools_mut(&mut self) -> &mut DevtoolSettings {
3504        &mut self.devtool_settings
3505    }
3506
3507    pub fn subdoc(&self, node_id: NodeId) -> Option<&dyn Document> {
3508        self.get_node(node_id)
3509            .and_then(|node| node.element_data())
3510            .and_then(|el| el.sub_doc_data())
3511    }
3512
3513    pub fn subdoc_mut(&mut self, node_id: NodeId) -> Option<&mut dyn Document> {
3514        self.get_node_mut(node_id)
3515            .and_then(|node| node.element_data_mut())
3516            .and_then(|el| el.sub_doc_data_mut())
3517    }
3518
3519    pub fn is_animating(&self) -> bool {
3520        #[cfg(feature = "custom-widget")]
3521        let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
3522            self.nodes[node_id]
3523                .element_data()
3524                .and_then(|el| el.custom_widget_data())
3525                .is_some_and(|data| data.widget.requires_redraw())
3526        });
3527        #[cfg(not(feature = "custom-widget"))]
3528        let custom_widget_is_animating = false;
3529
3530        let animating = self.has_canvas
3531            | self.has_active_animations
3532            | (self.subdoc_animation_pacing != AnimationPacing::Idle)
3533            | custom_widget_is_animating
3534            | (self.scroll_animation != ScrollAnimationState::None)
3535            | self.scrollbars_animating();
3536
3537        if animating && crate::debug::animation_reasons_enabled() {
3538            crate::debug::report_animation_reasons(
3539                self.id(),
3540                self.has_canvas,
3541                self.has_active_animations,
3542                self.subdoc_animation_pacing != AnimationPacing::Idle,
3543                custom_widget_is_animating,
3544                self.scroll_animation != ScrollAnimationState::None,
3545                self.scrollbars_animating(),
3546                self.animating_node_names().as_deref(),
3547            );
3548        }
3549
3550        animating
3551    }
3552
3553    /// Return the cadence class for the next animation-only frame.
3554    ///
3555    /// CSS animations are commonly decorative and can use a lower cadence.
3556    /// Canvas, scrolling and custom widgets remain at the interactive cadence.
3557    pub fn animation_pacing(&self) -> AnimationPacing {
3558        let focused_text_input = self.focus_node_id.is_some_and(|node_id| {
3559            self.nodes
3560                .get(node_id)
3561                .and_then(|node| node.element_data())
3562                .is_some_and(|element| element.text_input_data().is_some())
3563        });
3564        #[cfg(feature = "custom-widget")]
3565        let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
3566            self.nodes[node_id]
3567                .element_data()
3568                .and_then(|el| el.custom_widget_data())
3569                .is_some_and(|data| data.widget.requires_redraw())
3570        });
3571        #[cfg(not(feature = "custom-widget"))]
3572        let custom_widget_is_animating = false;
3573
3574        if self.has_canvas
3575            || custom_widget_is_animating
3576            || self.scroll_animation != ScrollAnimationState::None
3577            || self.scrollbars_animating()
3578        {
3579            AnimationPacing::Interactive
3580        } else if self.has_active_animations {
3581            const SLOW_ANIMATION_SECONDS: f64 = 2.0;
3582            let sets = self.animations.sets.read();
3583            let has_fast_animation_or_transition = sets.values().any(|set| {
3584                set.transitions.iter().any(|transition| {
3585                    matches!(
3586                        transition.state,
3587                        AnimationState::Pending | AnimationState::Running
3588                    )
3589                }) || set.animations.iter().any(|animation| {
3590                    matches!(
3591                        animation.state,
3592                        AnimationState::Pending | AnimationState::Running
3593                    ) && animation.duration < SLOW_ANIMATION_SECONDS
3594                })
3595            });
3596            if has_fast_animation_or_transition {
3597                AnimationPacing::Interactive
3598            } else {
3599                AnimationPacing::SlowCss
3600            }
3601        } else if focused_text_input {
3602            AnimationPacing::Caret
3603        } else if self.subdoc_animation_pacing != AnimationPacing::Idle {
3604            self.subdoc_animation_pacing
3605        } else {
3606            AnimationPacing::Idle
3607        }
3608    }
3609
3610    /// Which elements Stylo currently holds animations or transitions for.
3611    ///
3612    /// Only built when the diagnostic is switched on: a frame loop that will
3613    /// not settle is otherwise very hard to attribute, because
3614    /// `has_active_animations` is one bool for the whole document and says
3615    /// nothing about which element is keeping it true.
3616    fn animating_node_names(&self) -> Option<String> {
3617        if !self.has_active_animations {
3618            return None;
3619        }
3620        let sets = self.animations.sets.read();
3621        let mut described: Vec<String> = sets
3622            .iter()
3623            .filter(|(_, state)| state.needs_animation_ticks())
3624            .filter_map(|(key, state)| {
3625                let node_id = NodeId::from_u64(key.node.id() as u64);
3626                let node = self.nodes.get(node_id)?;
3627                let element = node.element_data()?;
3628                let name = element
3629                    .attr(local_name!("id"))
3630                    .map(|id| format!("#{id}"))
3631                    .or_else(|| {
3632                        element
3633                            .attr(local_name!("class"))
3634                            .and_then(|c| c.split_ascii_whitespace().next())
3635                            .map(|c| format!(".{c}"))
3636                    })
3637                    .unwrap_or_else(|| element.name.local.to_string());
3638                Some(format!(
3639                    "{name}(anim={},trans={},in_doc={})",
3640                    state.animations.len(),
3641                    state.transitions.len(),
3642                    node.flags.is_in_document(),
3643                ))
3644            })
3645            .collect();
3646        described.sort();
3647        described.truncate(12);
3648        Some(described.join(" "))
3649    }
3650
3651    /// Update the device and reset the stylist to process the new size
3652    pub fn set_stylist_device(&mut self, device: Device) {
3653        self.viewport_generation = self.viewport_generation.wrapping_add(1);
3654        // Viewport units become pixels at cascade time, from this device.
3655        // Compare before `device` is moved into the stylist.
3656        let previous_viewport = self.stylist.device().au_viewport_size();
3657        let next_viewport = device.au_viewport_size();
3658
3659        // Seed the new device with the root element's current style and font-relative
3660        // unit state (used to resolve rem/rlh/rex/rch/rcap/ric units). Stylo only
3661        // updates this state when the root element's style *changes* during a restyle,
3662        // so a freshly-built device would otherwise resolve these units against the
3663        // default font-size (16px) until the root's font-size next changes.
3664        let root_styles = self
3665            .try_root_element()
3666            .and_then(|root| root.primary_styles());
3667        if let Some(root_style) = root_styles.as_deref() {
3668            device.set_root_style(root_style);
3669
3670            let font = root_style.get_font();
3671            let font_size = font.clone_font_size().computed_size();
3672            device.set_root_font_size(root_style.effective_zoom.unzoom(font_size.px()));
3673
3674            let line_height = device
3675                .calc_line_height(font, root_style.writing_mode, None)
3676                .0;
3677            device.set_root_line_height(root_style.effective_zoom.unzoom(line_height.px()));
3678        }
3679        drop(root_styles);
3680
3681        let origins = {
3682            let guard = &self.guard;
3683            let guards = StylesheetGuards {
3684                author: &guard.read(),
3685                ua_or_user: &guard.read(),
3686            };
3687            self.stylist.set_device(device, &guards)
3688        };
3689        self.stylist.force_stylesheet_origins_dirty(origins);
3690        #[cfg(feature = "shadow-dom")]
3691        for host_id in self.shadow_host_node_ids() {
3692            if let Some(root_id) = self.shadow_root_id(host_id) {
3693                self.invalidate_shadow_styles(root_id);
3694            }
3695        }
3696
3697        // `Stylist::set_device` reports media-query changes only. A CSS-pixel
3698        // change still has to recascade `vh`/`vw` (including on `::before`)
3699        // and lay out again: the initial containing block changed, and
3700        // incremental layout otherwise keeps the boxes from the previous
3701        // viewport. Capture builds the document at 0x0 and installs the real
3702        // viewport here, on the way into the next `resolve`.
3703        if previous_viewport != next_viewport {
3704            self.abspos_layout_dirty = true;
3705            let invalidated = self
3706                .try_root_element()
3707                .is_some_and(|root| style::invalidation::viewport_units::invalidate(root));
3708            if invalidated {
3709                self.shell_provider.request_redraw();
3710            }
3711        }
3712    }
3713
3714    pub fn stylist_device(&mut self) -> &Device {
3715        self.stylist.device()
3716    }
3717
3718    /// The cursor to show, where `None` means `cursor: none` — hide it.
3719    ///
3720    /// `None` is an answer, not the absence of one. The shell hides the pointer
3721    /// when it sees `None`, so every path that means "nothing to say here" must
3722    /// return `Default` instead. Returning `None` from those made the pointer
3723    /// vanish as it crossed into page content, which is the shape this used to
3724    /// have: three `?`s that each meant "no opinion" and all read as "hide".
3725    pub fn get_cursor(&self) -> Option<CursorIcon> {
3726        // Prefer the precise hit node: `cursor` and `user-select` may be set on
3727        // a pseudo-element or resolved on an anonymous box, and text hits carry
3728        // is_text via the hit node. Fall back to the canonical hover node if
3729        // the hit node has been removed (it is transient across resolves).
3730        let node_id = self
3731            .hover_hit_node_id
3732            .filter(|&id| self.nodes.contains_key(id))
3733            .or(self.get_hover_node_id());
3734        let Some(node_id) = node_id else {
3735            return Some(CursorIcon::Default);
3736        };
3737        let node = &self.nodes[node_id];
3738
3739        if let Some(subdoc) = node.subdoc().map(|doc| doc.inner()) {
3740            // Only delegate when the sub-document has hover state of its own.
3741            // Without this check an embedded document that has not been hovered
3742            // yet answers `None` — meaning "I have no hover node" — and the
3743            // pointer disappears the moment it enters the page area, which is
3744            // every page in a browser built on sub-documents.
3745            if subdoc.hover_hit_node_id.is_some() || subdoc.get_hover_node_id().is_some() {
3746                return subdoc.get_cursor();
3747            }
3748            return Some(CursorIcon::Default);
3749        }
3750
3751        let Some(style) = node.primary_styles() else {
3752            return Some(CursorIcon::Default);
3753        };
3754        let user_select = style.clone_user_select();
3755        let keyword = style.clone_cursor().keyword;
3756
3757        // Return cursor from style if it is non-auto
3758        if keyword != CursorKind::Auto {
3759            return stylo_to_cursor_icon(keyword);
3760        }
3761
3762        // Return text cursor for text inputs
3763        if node
3764            .element_data()
3765            .is_some_and(|e| e.text_input_data().is_some())
3766        {
3767            return Some(CursorIcon::Text);
3768        }
3769
3770        // Use "pointer" cursor if any ancestor is a link
3771        let mut maybe_node = Some(node);
3772        while let Some(node) = maybe_node {
3773            if node.is_link() {
3774                return Some(CursorIcon::Pointer);
3775            }
3776
3777            maybe_node = node.layout_parent.get().map(|node_id| node.with(node_id));
3778        }
3779
3780        // Return text cursor for text nodes
3781        if self.hover_node_is_text {
3782            return Some(match user_select {
3783                UserSelect::Text | UserSelect::All | UserSelect::Auto => CursorIcon::Text,
3784                UserSelect::None => CursorIcon::Default,
3785            });
3786        }
3787
3788        // Else fallback to default cursor
3789        Some(CursorIcon::Default)
3790    }
3791
3792    pub fn scroll_node_by<F: FnMut(DomEvent)>(
3793        &mut self,
3794        node_id: NodeId,
3795        x: f64,
3796        y: f64,
3797        dispatch_event: F,
3798    ) {
3799        self.scroll_node_by_has_changed(node_id, x, y, dispatch_event);
3800    }
3801
3802    /// Scroll a node by given x and y
3803    /// Will bubble scrolling up to parent node once it can no longer scroll further
3804    /// If we're already at the root node, bubbles scrolling up to the viewport
3805    ///
3806    /// A `position: sticky` box is held against the edge of the scrollport it
3807    /// lives in, so the boxes have to be re-adjusted here rather than only in
3808    /// `resolve`: a wheel event does not necessarily produce a style and layout
3809    /// pass, and a header that only unstuck on the next restyle is a header
3810    /// that visibly lags the scroll.
3811    pub fn scroll_node_by_has_changed<F: FnMut(DomEvent)>(
3812        &mut self,
3813        node_id: NodeId,
3814        x: f64,
3815        y: f64,
3816        dispatch_event: F,
3817    ) -> bool {
3818        let has_changed = self.scroll_node_by_inner(node_id, x, y, dispatch_event);
3819        if has_changed {
3820            self.resolve_positioned_scroll();
3821        }
3822        has_changed
3823    }
3824
3825    fn scroll_node_by_inner<F: FnMut(DomEvent)>(
3826        &mut self,
3827        node_id: NodeId,
3828        x: f64,
3829        y: f64,
3830        mut dispatch_event: F,
3831    ) -> bool {
3832        // Per the CSS overflow propagation rules, the root element's overflow (and usually
3833        // the <body>'s) is applied to the viewport, and the element itself must not have
3834        // a scrolling mechanism of its own. So scrolls that reach the root element are
3835        // forwarded to the viewport rather than scrolling the root element itself.
3836        if self.try_root_element().is_some_and(|el| el.id == node_id) {
3837            let has_changed = self.scroll_viewport_by_has_changed(x, y);
3838            if has_changed {
3839                let layout = *self.root_element().final_layout();
3840                let scale = self.viewport.scale() as f64;
3841                let event = BlitzScrollEvent {
3842                    scroll_top: self.viewport_scroll.y,
3843                    scroll_left: self.viewport_scroll.x,
3844                    scroll_width: layout.size.width.max(layout.content_size.width) as i32,
3845                    scroll_height: layout.size.height.max(layout.content_size.height) as i32,
3846                    client_width: (self.viewport.window_size.0 as f64 / scale) as i32,
3847                    client_height: (self.viewport.window_size.1 as f64 / scale) as i32,
3848                };
3849                dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
3850            }
3851            return has_changed;
3852        }
3853
3854        let Some(node) = self.nodes.get_mut(node_id) else {
3855            return false;
3856        };
3857
3858        // Text inputs scroll their own internal text content rather than using the generic
3859        // overflow mechanism: single-line inputs scroll horizontally, multi-line inputs scroll
3860        // vertically. Any delta the input cannot consume is bubbled up to an ancestor scroller.
3861        if node
3862            .element_data()
3863            .is_some_and(|el| el.text_input_data().is_some())
3864        {
3865            let parent = node.parent;
3866            let content_box_width = node.final_layout().content_box_width();
3867            let content_box_height = node.final_layout().content_box_height();
3868            let input = node
3869                .element_data_mut()
3870                .and_then(|el| el.text_input_data_mut())
3871                .unwrap();
3872
3873            let (bubble_x, bubble_y) = if input.is_multiline {
3874                (
3875                    x,
3876                    input.scroll_by(y as f32, content_box_width, content_box_height) as f64,
3877                )
3878            } else {
3879                (
3880                    input.scroll_by(x as f32, content_box_width, content_box_height) as f64,
3881                    y,
3882                )
3883            };
3884
3885            let has_changed = bubble_x != x || bubble_y != y;
3886
3887            if bubble_x != 0.0 || bubble_y != 0.0 {
3888                let bubbled = if let Some(parent) = parent {
3889                    self.scroll_node_by_inner(parent, bubble_x, bubble_y, dispatch_event)
3890                } else {
3891                    self.scroll_viewport_by_has_changed(bubble_x, bubble_y)
3892                };
3893                return bubbled | has_changed;
3894            }
3895
3896            return has_changed;
3897        }
3898
3899        let (can_x_scroll, can_y_scroll) = node
3900            .primary_styles()
3901            .map(|styles| {
3902                (
3903                    matches!(styles.clone_overflow_x(), Overflow::Scroll | Overflow::Auto),
3904                    matches!(styles.clone_overflow_y(), Overflow::Scroll | Overflow::Auto),
3905                )
3906            })
3907            .unwrap_or((false, false));
3908
3909        let initial = *node.scroll_offset();
3910        let new_x = node.scroll_offset().x - x;
3911        let new_y = node.scroll_offset().y - y;
3912
3913        let mut bubble_x = 0.0;
3914        let mut bubble_y = 0.0;
3915
3916        let scroll_width = node.final_layout().scroll_width() as f64;
3917        let scroll_height = node.final_layout().scroll_height() as f64;
3918
3919        // Handle sub document case
3920        if let Some(mut sub_doc) = node.subdoc_mut().map(|doc| doc.inner_mut()) {
3921            let has_changed = if let Some(hover_node_id) = sub_doc.get_hover_node_id() {
3922                sub_doc.scroll_node_by_has_changed(hover_node_id, x, y, dispatch_event)
3923            } else {
3924                sub_doc.scroll_viewport_by_has_changed(x, y)
3925            };
3926
3927            // TODO: propagate remaining scroll to parent
3928            return has_changed;
3929        }
3930
3931        // If we're past our scroll bounds, transfer remainder of scrolling to parent/viewport
3932        if !can_x_scroll {
3933            bubble_x = x
3934        } else if new_x < 0.0 {
3935            bubble_x = -new_x;
3936            node.scroll_offset_mut().x = 0.0;
3937        } else if new_x > scroll_width {
3938            bubble_x = scroll_width - new_x;
3939            node.scroll_offset_mut().x = scroll_width;
3940        } else {
3941            node.scroll_offset_mut().x = new_x;
3942        }
3943
3944        if !can_y_scroll {
3945            bubble_y = y
3946        } else if new_y < 0.0 {
3947            bubble_y = -new_y;
3948            node.scroll_offset_mut().y = 0.0;
3949        } else if new_y > scroll_height {
3950            bubble_y = scroll_height - new_y;
3951            node.scroll_offset_mut().y = scroll_height;
3952        } else {
3953            node.scroll_offset_mut().y = new_y;
3954        }
3955
3956        let has_changed = *node.scroll_offset() != initial;
3957
3958        if has_changed {
3959            let layout = *node.final_layout();
3960            let event = BlitzScrollEvent {
3961                scroll_top: node.scroll_offset().y,
3962                scroll_left: node.scroll_offset().x,
3963                scroll_width: layout.scroll_width() as i32,
3964                scroll_height: layout.scroll_height() as i32,
3965                client_width: layout.size.width as i32,
3966                client_height: layout.size.height as i32,
3967            };
3968
3969            dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
3970        }
3971
3972        let parent = node.parent;
3973        if has_changed {
3974            self.show_scrollbars(node_id);
3975        }
3976
3977        if bubble_x != 0.0 || bubble_y != 0.0 {
3978            if let Some(parent) = parent {
3979                return self.scroll_node_by_inner(parent, bubble_x, bubble_y, dispatch_event)
3980                    | has_changed;
3981            } else {
3982                return self.scroll_viewport_by_has_changed(bubble_x, bubble_y) | has_changed;
3983            }
3984        }
3985
3986        has_changed
3987    }
3988
3989    pub fn scroll_viewport_by(&mut self, x: f64, y: f64) {
3990        self.scroll_viewport_by_has_changed(x, y);
3991    }
3992
3993    /// Scroll the viewport by the given values
3994    pub fn scroll_viewport_by_has_changed(&mut self, x: f64, y: f64) -> bool {
3995        // The viewport scrolls the root element's scrollable overflow, which includes both
3996        // the root element itself and any content which overflows it (e.g. when the root
3997        // element has a fixed height but its content is taller). A document without a root
3998        // element has no scrollable content, so its content size is zero.
3999        let (content_width, content_height) = match self.try_root_element() {
4000            Some(root) => {
4001                let root_layout = root.final_layout();
4002                (
4003                    root_layout.size.width.max(root_layout.content_size.width) as f64,
4004                    root_layout.size.height.max(root_layout.content_size.height) as f64,
4005                )
4006            }
4007            None => (0.0, 0.0),
4008        };
4009        let new_scroll = (self.viewport_scroll.x - x, self.viewport_scroll.y - y);
4010        let window_width = self.viewport.window_size.0 as f64 / self.viewport.scale() as f64;
4011        let window_height = self.viewport.window_size.1 as f64 / self.viewport.scale() as f64;
4012
4013        let initial = self.viewport_scroll;
4014        self.viewport_scroll.x =
4015            f64::max(0.0, f64::min(new_scroll.0, content_width - window_width));
4016        self.viewport_scroll.y =
4017            f64::max(0.0, f64::min(new_scroll.1, content_height - window_height));
4018
4019        let has_changed = self.viewport_scroll != initial;
4020        if has_changed {
4021            // The viewport is the scrollport a page-level sticky box is held
4022            // against, and the containing block a fixed box is pinned to, so
4023            // both move with this and not with the next relayout. See
4024            // `resolve_sticky_positions` and `resolve_fixed_positions`.
4025            self.resolve_positioned_scroll();
4026        }
4027        has_changed
4028    }
4029
4030    pub fn scroll_by(
4031        &mut self,
4032        anchor_node_id: Option<NodeId>,
4033        scroll_x: f64,
4034        scroll_y: f64,
4035        dispatch_event: &mut dyn FnMut(DomEvent),
4036    ) -> bool {
4037        if let Some(anchor_node_id) = anchor_node_id {
4038            self.scroll_node_by_has_changed(anchor_node_id, scroll_x, scroll_y, dispatch_event)
4039        } else {
4040            self.scroll_viewport_by_has_changed(scroll_x, scroll_y)
4041        }
4042    }
4043
4044    pub fn viewport_scroll(&self) -> crate::Point<f64> {
4045        self.viewport_scroll
4046    }
4047
4048    pub fn set_viewport_scroll(&mut self, scroll: crate::Point<f64>) {
4049        self.viewport_scroll = scroll;
4050    }
4051
4052    /// Find the node targeted by a URL fragment (the `#...` part of a URL).
4053    ///
4054    /// Per the HTML spec, this is the element whose `id` matches the fragment, falling
4055    /// back to the first `<a>` element whose `name` attribute matches.
4056    pub fn get_fragment_target(&self, fragment: &str) -> Option<NodeId> {
4057        if let Some(node_id) = self.get_element_by_id(fragment) {
4058            return Some(node_id);
4059        }
4060
4061        // Fall back to a named anchor: `<a name="...">`
4062        self.nodes.iter().find_map(|(id, node)| {
4063            let el = node.element_data()?;
4064            (el.name.local == local_name!("a") && el.attr(local_name!("name")) == Some(fragment))
4065                .then_some(id)
4066        })
4067    }
4068
4069    /// Scroll the viewport so that the given node is aligned with the top of the viewport.
4070    /// Scroll the nearest scroll container at or above `node_id`.
4071    ///
4072    /// "Scroll this panel" is the operation callers actually want, and
4073    /// `scroll_node_by` only moves the node itself, so naming any inner element
4074    /// silently did nothing. Wheel events are no help either: they are
4075    /// delivered to whatever the document last saw hovered, which an injected
4076    /// pointer move does not set, so an automated caller had no way to scroll
4077    /// anything at all.
4078    /// The nearest scroll container at or above `node_id`, if there is one.
4079    pub fn nearest_scroll_container(&self, node_id: NodeId) -> Option<NodeId> {
4080        let mut current = Some(node_id);
4081        for _ in 0..64 {
4082            let id = current?;
4083            let node = self.nodes.get(id)?;
4084            if node.style().overflow.x.is_scroll_container()
4085                || node.style().overflow.y.is_scroll_container()
4086            {
4087                return Some(id);
4088            }
4089            current = node.parent;
4090        }
4091        None
4092    }
4093
4094    pub fn scroll_nearest_container_by(&mut self, node_id: NodeId, x: f64, y: f64) -> bool {
4095        self.scroll_nearest_container_by_with_events(node_id, x, y, |_| {})
4096    }
4097
4098    pub fn scroll_nearest_container_by_with_events<F: FnMut(DomEvent)>(
4099        &mut self,
4100        node_id: NodeId,
4101        x: f64,
4102        y: f64,
4103        mut dispatch_event: F,
4104    ) -> bool {
4105        let mut current = Some(node_id);
4106        for _ in 0..64 {
4107            let Some(id) = current else { break };
4108            let Some(node) = self.nodes.get(id) else {
4109                break;
4110            };
4111            let scrolls = node.style().overflow.x.is_scroll_container()
4112                || node.style().overflow.y.is_scroll_container();
4113            if scrolls {
4114                self.scroll_node_by(id, x, y, &mut dispatch_event);
4115                return true;
4116            }
4117            current = node.parent;
4118        }
4119        self.scroll_viewport_by(x, y);
4120        false
4121    }
4122
4123    pub fn scroll_to_node(&mut self, node_id: NodeId) {
4124        self.scroll_to_node_with_events(node_id, |_| {});
4125    }
4126
4127    pub fn scroll_to_node_with_events<F: FnMut(DomEvent)>(
4128        &mut self,
4129        node_id: NodeId,
4130        dispatch_event: F,
4131    ) {
4132        self.scroll_to_node_aligned_with_events(node_id, false, dispatch_event);
4133    }
4134
4135    /// Reveal a node near the centre of each scrollport and the viewport.
4136    ///
4137    /// Semantic input uses this placement so fixed or sticky chrome cannot
4138    /// cover the point an automation client will drive. Ordinary DOM
4139    /// `scrollIntoView` and fragment navigation retain their start alignment.
4140    pub fn scroll_to_node_centered_with_events<F: FnMut(DomEvent)>(
4141        &mut self,
4142        node_id: NodeId,
4143        dispatch_event: F,
4144    ) {
4145        self.scroll_to_node_aligned_with_events(node_id, true, dispatch_event);
4146    }
4147
4148    fn scroll_to_node_aligned_with_events<F: FnMut(DomEvent)>(
4149        &mut self,
4150        node_id: NodeId,
4151        centered: bool,
4152        mut dispatch_event: F,
4153    ) {
4154        // Semantic snapshots expose visible text as addressable nodes, but a
4155        // text node does not own a Taffy layout box. `absolute_position` and
4156        // the scrolling calculations below require one, so resolve such a
4157        // target to its nearest layout ancestor first. Without this, asking an
4158        // automation client to reveal a label panics the entire host at
4159        // `Node::final_layout` instead of scrolling the label's box into view.
4160        let mut scroll_target = Some(node_id);
4161        let node_id = loop {
4162            let Some(candidate) = scroll_target else {
4163                return;
4164            };
4165            let Some(node) = self.nodes.get(candidate) else {
4166                return;
4167            };
4168            if matches!(
4169                node.data,
4170                NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
4171            ) {
4172                break candidate;
4173            }
4174            scroll_target = node.layout_parent.get().or(node.parent);
4175        };
4176
4177        // Every scroll container between the node and the root, innermost
4178        // first. Scrolling only the viewport is not `scrollIntoView`: it does
4179        // nothing at all for a node inside a nested scroller, which is what an
4180        // application's own scrolling panes are.
4181        //
4182        // This was not academic. A transcript pane held its "Show 12 earlier
4183        // messages" button at y=-9463 and neither wheel events, Page Up nor
4184        // this call moved it by a single pixel, so a layout bug that only
4185        // appears further up the thread could not be reached from outside the
4186        // app at all. Every measurement of it had to come from a human
4187        // scrolling by hand and saying "now".
4188        let mut chain = Vec::new();
4189        let mut current = self.nodes.get(node_id).and_then(|node| node.parent);
4190        while let Some(id) = current {
4191            let Some(node) = self.nodes.get(id) else {
4192                break;
4193            };
4194            let scrolls = node.style().overflow.x.is_scroll_container()
4195                || node.style().overflow.y.is_scroll_container();
4196            if scrolls {
4197                chain.push(id);
4198            }
4199            current = node.parent;
4200        }
4201
4202        // Innermost first: scrolling an outer container moves the inner one, so
4203        // the inner offsets have to be settled before the outer ones are
4204        // measured, and each step re-reads the node's position.
4205        for container in chain {
4206            let Some(node) = self.nodes.get(node_id) else {
4207                return;
4208            };
4209            let target = node.absolute_position(0.0, 0.0);
4210            let Some(scroller) = self.nodes.get(container) else {
4211                continue;
4212            };
4213            let box_ = scroller.absolute_position(0.0, 0.0);
4214            let layout = scroller.final_layout();
4215            let target_layout = node.final_layout();
4216            // `scroll_node_by` subtracts its delta from the offset. Semantic
4217            // automation centres the target to avoid chrome; DOM behavior
4218            // keeps its historical start alignment.
4219            let (dx, dy) = if centered {
4220                (
4221                    f64::from(
4222                        box_.x + layout.size.width / 2.0
4223                            - (target.x + target_layout.size.width / 2.0),
4224                    ),
4225                    f64::from(
4226                        box_.y + layout.size.height / 2.0
4227                            - (target.y + target_layout.size.height / 2.0),
4228                    ),
4229                )
4230            } else {
4231                (f64::from(box_.x - target.x), f64::from(box_.y - target.y))
4232            };
4233            self.scroll_node_by(container, dx, dy, &mut dispatch_event);
4234        }
4235
4236        // `absolute_position` gives the node's position in document space (it does not
4237        // account for the viewport scroll), so it is the scroll offset we want to land on.
4238        let Some(node) = self.nodes.get(node_id) else {
4239            return;
4240        };
4241        let target = node.absolute_position(0.0, 0.0);
4242        let current = self.viewport_scroll;
4243        let (desired_x, desired_y) = if centered {
4244            let target_layout = node.final_layout();
4245            let scale = self.viewport.scale();
4246            let viewport_width = self.viewport.window_size.0 as f32 / scale;
4247            let viewport_height = self.viewport.window_size.1 as f32 / scale;
4248            (
4249                f64::from(target.x + target_layout.size.width / 2.0 - viewport_width / 2.0)
4250                    .max(0.0),
4251                f64::from(target.y + target_layout.size.height / 2.0 - viewport_height / 2.0)
4252                    .max(0.0),
4253            )
4254        } else {
4255            (f64::from(target.x), f64::from(target.y))
4256        };
4257
4258        // `scroll_viewport_by` subtracts the delta from the current scroll
4259        // offset, so pass current minus the centred destination.
4260        let dx = current.x - desired_x;
4261        let dy = current.y - desired_y;
4262        if let Some(root) = self.try_root_element().map(|element| element.id) {
4263            self.scroll_node_by(root, dx, dy, dispatch_event);
4264        } else {
4265            self.scroll_viewport_by(dx, dy);
4266        }
4267    }
4268
4269    /// Scroll to the element targeted by the given URL fragment (the `#...` part of a URL).
4270    ///
4271    /// An empty fragment (or a `top` fragment that matches no element) scrolls to the top
4272    /// of the document, matching browser behaviour. Returns `true` if a scroll target was
4273    /// found.
4274    pub fn scroll_to_fragment(&mut self, fragment: &str) -> bool {
4275        // Fragments are percent-encoded in URLs (e.g. `%20`); decode before matching.
4276        let decoded = percent_encoding::percent_decode_str(fragment)
4277            .decode_utf8_lossy()
4278            .into_owned();
4279
4280        if !decoded.is_empty() {
4281            if let Some(node_id) = self.get_fragment_target(&decoded) {
4282                self.scroll_to_node(node_id);
4283                return true;
4284            }
4285        }
4286
4287        // An empty fragment, or the special "top" fragment when no matching element exists,
4288        // scrolls to the top of the document.
4289        if decoded.is_empty() || decoded.eq_ignore_ascii_case("top") {
4290            let current = self.viewport_scroll;
4291            self.scroll_viewport_by(current.x, current.y);
4292            return true;
4293        }
4294
4295        false
4296    }
4297
4298    /// SVG graphics bounds in the element's user coordinate system.
4299    /// Callers must flush layout before reading, as for client rectangles.
4300    pub fn get_svg_bbox(&self, node_id: NodeId) -> Option<BoundingRect> {
4301        #[cfg(feature = "svg")]
4302        {
4303            self.svg_user_bbox(node_id)
4304        }
4305        #[cfg(not(feature = "svg"))]
4306        {
4307            let _ = node_id;
4308            None
4309        }
4310    }
4311
4312    /// Computes the size and position of the `Node` relative to the viewport
4313    pub fn get_client_bounding_rect(&self, node_id: NodeId) -> Option<BoundingRect> {
4314        #[cfg(feature = "svg")]
4315        if let Some(root) = self.svg_geometry_root(node_id) {
4316            return self.svg_client_rect(root, node_id);
4317        }
4318
4319        // Non-atomic inline elements have no layout box of their own: return
4320        // the union of their per-line-box fragment rects.
4321        if let Some(rects) = self.inline_fragment_rects(node_id) {
4322            let rects: Vec<_> = rects
4323                .into_iter()
4324                .map(|rect| self.transformed_client_rect(node_id, rect))
4325                .collect();
4326            let x0 = rects.iter().map(|r| r.x).fold(f64::INFINITY, f64::min);
4327            let y0 = rects.iter().map(|r| r.y).fold(f64::INFINITY, f64::min);
4328            let x1 = rects
4329                .iter()
4330                .map(|r| r.x + r.width)
4331                .fold(f64::NEG_INFINITY, f64::max);
4332            let y1 = rects
4333                .iter()
4334                .map(|r| r.y + r.height)
4335                .fold(f64::NEG_INFINITY, f64::max);
4336            return match rects.is_empty() {
4337                true => None,
4338                false => Some(BoundingRect {
4339                    x: x0,
4340                    y: y0,
4341                    width: x1 - x0,
4342                    height: y1 - y0,
4343                }),
4344            };
4345        }
4346
4347        let node = self.get_node(node_id)?;
4348        if !matches!(
4349            node.data,
4350            NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
4351        ) {
4352            return None;
4353        }
4354        let pos = node.absolute_position(0.0, 0.0);
4355
4356        Some(self.transformed_client_rect(
4357            node_id,
4358            BoundingRect {
4359                x: pos.x as f64 - self.viewport_scroll.x,
4360                y: pos.y as f64 - self.viewport_scroll.y,
4361                width: node.unrounded_layout().size.width as f64,
4362                height: node.unrounded_layout().size.height as f64,
4363            },
4364        ))
4365    }
4366
4367    /// Map the layout rectangle through the same two-dimensional transforms as paint.
4368    /// Layout coordinates deliberately exclude transforms; CSSOM client rectangles do not.
4369    pub(crate) fn transformed_client_rect(
4370        &self,
4371        node_id: NodeId,
4372        rect: BoundingRect,
4373    ) -> BoundingRect {
4374        let mut matrix = kurbo::Affine::IDENTITY;
4375        let mut current = self.get_node(node_id);
4376        let scale = self.viewport.scale_f64();
4377        while let Some(node) = current {
4378            if matches!(
4379                node.data,
4380                NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
4381            ) && let Some(transform) = *node.transform()
4382            {
4383                let [a, b, c, d, e, f] = transform.as_coeffs();
4384                let css_transform = kurbo::Affine::new([a, b, c, d, e / scale, f / scale]);
4385                let origin = node.absolute_position(0.0, 0.0);
4386                let translate = kurbo::Affine::translate((
4387                    origin.x as f64 - self.viewport_scroll.x,
4388                    origin.y as f64 - self.viewport_scroll.y,
4389                ));
4390                matrix = translate * css_transform * translate.inverse() * matrix;
4391            }
4392            current = node
4393                .layout_parent
4394                .get()
4395                .or(node.parent)
4396                .and_then(|id| self.get_node(id));
4397        }
4398        let transformed = matrix.transform_rect_bbox(kurbo::Rect::new(
4399            rect.x,
4400            rect.y,
4401            rect.x + rect.width,
4402            rect.y + rect.height,
4403        ));
4404        BoundingRect {
4405            x: transformed.x0,
4406            y: transformed.y0,
4407            width: transformed.width(),
4408            height: transformed.height(),
4409        }
4410    }
4411
4412    /// Computes the sizes and positions of the `Node`'s box fragments relative to the
4413    /// viewport (CSSOM `getClientRects()` semantics). Nodes with their own layout box
4414    /// return a single rect. Non-atomic inline elements (which are laid out as style
4415    /// spans within an inline root's text layout) return one rect per line box.
4416    pub fn node_client_rects(&self, node_id: NodeId) -> Vec<BoundingRect> {
4417        #[cfg(feature = "svg")]
4418        if self.svg_geometry_root(node_id).is_some() {
4419            return self.get_client_bounding_rect(node_id).into_iter().collect();
4420        }
4421        match self.inline_fragment_rects(node_id) {
4422            Some(rects) => rects
4423                .into_iter()
4424                .map(|rect| self.transformed_client_rect(node_id, rect))
4425                .collect(),
4426            None => self.get_client_bounding_rect(node_id).into_iter().collect(),
4427        }
4428    }
4429
4430    /// Computes per-line-box fragment rects for a non-atomic inline element by walking
4431    /// the containing inline root's text layout. Returns `None` for nodes that have
4432    /// their own layout box (which should use `get_client_bounding_rect` instead).
4433    /// Report inline elements whose fragment rects lie outside the inline root
4434    /// that owns them. `BLITZ_TRACE_INLINE=1`, once per resolve.
4435    ///
4436    /// A non-atomic inline element has no layout box of its own: its geometry
4437    /// is read back out of the containing inline root's text layout on demand.
4438    /// So "the chip is 900px to the right of its block" is a statement about
4439    /// that text layout, and the only way to see it is from in here, with both
4440    /// the fragment and the root in hand. Every earlier attempt to chase this
4441    /// from outside was reading a number the engine computes on the fly and
4442    /// could not say where it came from.
4443    pub(crate) fn trace_escaped_inline_fragments(&self) {
4444        static TRACE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4445        if !*TRACE.get_or_init(|| std::env::var_os("BLITZ_TRACE_INLINE").is_some()) {
4446            return;
4447        }
4448        let mut reported = 0;
4449        for (id, node) in self.nodes.iter() {
4450            if !node.is_element() {
4451                continue;
4452            }
4453            let Some(rects) = self.inline_fragment_rects(id) else {
4454                continue;
4455            };
4456            let Some(root) = node.inline_root_ancestor() else {
4457                continue;
4458            };
4459            let root_layout = root.final_layout();
4460            let root_pos = root.absolute_position(0.0, 0.0);
4461            let root_right =
4462                root_pos.x as f64 + root_layout.size.width as f64 - self.viewport_scroll.x;
4463            for rect in &rects {
4464                if rect.x + rect.width > root_right + 1.0 {
4465                    reported += 1;
4466                    if reported <= 12 {
4467                        eprintln!(
4468                            "escaped-fragment node={id:?} rect=[{:.1},{:.1} {:.1}x{:.1}] \
4469root={:?} root_right={root_right:.1} root_w={:.1} lines={} layout_scale={:.2} vp_scale={:.2} layout_w={:.1}",
4470                            rect.x,
4471                            rect.y,
4472                            rect.width,
4473                            rect.height,
4474                            root.id,
4475                            root_layout.size.width,
4476                            root.element_data()
4477                                .and_then(|e| e.inline_layout_data.as_ref())
4478                                .map(|i| i.layout.len())
4479                                .unwrap_or(0),
4480                            root.element_data()
4481                                .and_then(|e| e.inline_layout_data.as_ref())
4482                                .map(|i| i.layout.scale())
4483                                .unwrap_or(0.0),
4484                            self.viewport.scale(),
4485                            root.element_data()
4486                                .and_then(|e| e.inline_layout_data.as_ref())
4487                                .map(|i| i.layout.width())
4488                                .unwrap_or(0.0),
4489                        );
4490                    }
4491                    break;
4492                }
4493            }
4494        }
4495        if reported > 0 {
4496            eprintln!("escaped-fragment total={reported}");
4497        }
4498
4499        // The opposite failure, and the one that reads as "first load is
4500        // broken": lines broken far narrower than the box they sit in, so a
4501        // paragraph comes out as a column of one or two words inside a
4502        // full-width bubble. Nothing escapes, so the check above never sees it.
4503        let mut narrow = 0;
4504        for (id, node) in self.nodes.iter() {
4505            let Some(inline) = node
4506                .data
4507                .downcast_element()
4508                .and_then(|element| element.inline_layout_data.as_ref())
4509            else {
4510                continue;
4511            };
4512            let box_width = node.final_layout().size.width as f64 * self.viewport.scale() as f64;
4513            let broken_at = inline.layout.width() as f64;
4514            // Only interesting when the text had more to give: a short string
4515            // legitimately measures narrower than its box.
4516            let full = inline.layout.calculate_content_widths().max as f64;
4517            if box_width > 40.0 && broken_at < box_width * 0.6 && full > box_width * 0.9 {
4518                narrow += 1;
4519                if narrow <= 12 {
4520                    eprintln!(
4521                        "narrow-break node={id:?} broken_at={broken_at:.1} box={box_width:.1} \
4522                         max_content={full:.1} lines={} text={:?}",
4523                        inline.layout.len(),
4524                        inline.text.chars().take(40).collect::<String>(),
4525                    );
4526                }
4527            }
4528        }
4529        if narrow > 0 {
4530            eprintln!("narrow-break total={narrow}");
4531        }
4532    }
4533
4534    pub fn inline_fragment_rects(&self, node_id: NodeId) -> Option<Vec<BoundingRect>> {
4535        use parley::PositionedLayoutItem;
4536
4537        let node = self.get_node(node_id)?;
4538
4539        // Text and non-atomic inline elements live in the inline root's glyph
4540        // runs rather than owning layout boxes.
4541        let is_text = node.is_text_node();
4542        if !is_text {
4543            if !node.is_element() || node.flags.is_inline_root() {
4544                return None;
4545            }
4546            let display = node.primary_styles()?.clone_display();
4547            if !(display.outside() == DisplayOutside::Inline
4548                && display.inside() == DisplayInside::Flow)
4549            {
4550                return None;
4551            }
4552        }
4553
4554        let inline_root = node.inline_root_ancestor()?;
4555        let inline_layout = inline_root.element_data()?.inline_layout_data.as_ref()?;
4556        let layout = &inline_layout.layout;
4557        let scale = layout.scale() as f64;
4558
4559        // Walk up the DOM parent chain from `id` to check whether it is (or is
4560        // inside) the target node, stopping at the inline root.
4561        let is_in_target = |mut id: NodeId| -> bool {
4562            loop {
4563                if id == node_id {
4564                    return true;
4565                }
4566                if id == inline_root.id {
4567                    return false;
4568                }
4569                match self.get_node(id).and_then(|n| n.parent) {
4570                    Some(parent) => id = parent,
4571                    None => return false,
4572                }
4573            }
4574        };
4575
4576        // Fragment rects are relative to the inline root's content box.
4577        let root_layout = inline_root.final_layout();
4578        let root_pos = inline_root.absolute_position(0.0, 0.0);
4579        let origin_x = root_pos.x as f64
4580            + (root_layout.padding.left + root_layout.border.left) as f64
4581            - self.viewport_scroll.x;
4582        let origin_y = root_pos.y as f64
4583            + (root_layout.padding.top + root_layout.border.top) as f64
4584            - self.viewport_scroll.y;
4585
4586        let mut rects: Vec<BoundingRect> = Vec::new();
4587        for line in layout.lines() {
4588            let line_metrics = line.metrics();
4589            // Union all of the target's fragments on this line into a single rect
4590            let mut line_rect: Option<(f64, f64, f64, f64)> = None;
4591            let mut add = |x0: f64, y0: f64, x1: f64, y1: f64| {
4592                line_rect = Some(match line_rect {
4593                    Some((lx0, ly0, lx1, ly1)) => {
4594                        (lx0.min(x0), ly0.min(y0), lx1.max(x1), ly1.max(y1))
4595                    }
4596                    None => (x0, y0, x1, y1),
4597                });
4598            };
4599
4600            for item in line.items() {
4601                match item {
4602                    PositionedLayoutItem::GlyphRun(glyph_run) => {
4603                        let brush = glyph_run.style().brush;
4604                        let matches = if is_text {
4605                            brush.text_node == Some(node_id)
4606                        } else {
4607                            is_in_target(brush.id)
4608                        };
4609                        if !matches {
4610                            continue;
4611                        }
4612                        let x0 = glyph_run.offset() as f64;
4613                        let x1 = x0 + glyph_run.advance() as f64;
4614                        // Use the line box's block extent rather than the
4615                        // run's font ascent/descent: fonts with small
4616                        // typographic metrics would otherwise produce rects
4617                        // that clip the rendered glyphs. This matches the
4618                        // geometry used for text selection highlights.
4619                        let y0 = line_metrics.block_min_coord as f64;
4620                        let y1 = line_metrics.block_max_coord as f64;
4621                        add(x0, y0, x1, y1);
4622                    }
4623                    PositionedLayoutItem::InlineBox(inline_box) => {
4624                        if is_text || !is_in_target(NodeId::from_u64(inline_box.id)) {
4625                            continue;
4626                        }
4627                        let x0 = inline_box.x as f64;
4628                        let y0 = inline_box.y as f64;
4629                        add(
4630                            x0,
4631                            y0,
4632                            x0 + inline_box.width as f64,
4633                            y0 + inline_box.height as f64,
4634                        );
4635                    }
4636                }
4637            }
4638
4639            if let Some((x0, y0, x1, y1)) = line_rect {
4640                rects.push(BoundingRect {
4641                    x: origin_x + x0 / scale,
4642                    y: origin_y + y0 / scale,
4643                    width: (x1 - x0) / scale,
4644                    height: (y1 - y0) / scale,
4645                });
4646            }
4647        }
4648
4649        Some(rects)
4650    }
4651
4652    pub fn find_title_node(&self) -> Option<&Node> {
4653        TreeTraverser::new(self)
4654            .find(|node_id| {
4655                let node = &self.nodes[*node_id];
4656                let Some(element) = node.element_data() else {
4657                    return false;
4658                };
4659                if element.name.ns != ns!(html) || element.name.local != local_name!("title") {
4660                    return false;
4661                }
4662                node.parent
4663                    .and_then(|parent_id| self.nodes.get(parent_id))
4664                    .and_then(Node::element_data)
4665                    .is_some_and(|parent| {
4666                        parent.name.ns == ns!(html) && parent.name.local == local_name!("head")
4667                    })
4668            })
4669            .map(|node_id| &self.nodes[node_id])
4670    }
4671
4672    pub fn with_text_input(
4673        &mut self,
4674        node_id: NodeId,
4675        cb: impl FnOnce(PlainEditorDriver<TextBrush>),
4676    ) {
4677        let Some(node) = self.nodes.get_mut(node_id) else {
4678            return;
4679        };
4680
4681        if let Some(text_input) = node
4682            .element_data_mut()
4683            .and_then(|el| el.text_input_data_mut())
4684        {
4685            let mut font_ctx = self.font_ctx.lock().unwrap();
4686            let layout_ctx = &mut self.layout_ctx;
4687            let driver = text_input.editor.driver(&mut font_ctx, layout_ctx);
4688            cb(driver)
4689        }
4690    }
4691
4692    /// Recompute the scroll offset of the text input at `node_id` (if any) so that its caret
4693    /// remains visible within the input's content box.
4694    pub(crate) fn clamp_text_input_scroll(&mut self, node_id: NodeId) {
4695        let Some(node) = self.nodes.get_mut(node_id) else {
4696            return;
4697        };
4698
4699        let content_box_width = node.final_layout().content_box_width();
4700        let content_box_height = node.final_layout().content_box_height();
4701
4702        if let Some(text_input) = node
4703            .element_data_mut()
4704            .and_then(|el| el.text_input_data_mut())
4705        {
4706            text_input.clamp_scroll_offset(content_box_width, content_box_height);
4707        }
4708    }
4709
4710    pub(crate) fn compute_has_canvas(&self) -> bool {
4711        TreeTraverser::new(self).any(|node_id| {
4712            let node = &self.nodes[node_id];
4713            let Some(element) = node.element_data() else {
4714                return false;
4715            };
4716            if element.name.local == local_name!("canvas") && element.has_attr(local_name!("src")) {
4717                return true;
4718            }
4719
4720            false
4721        })
4722    }
4723
4724    // Text selection methods
4725
4726    /// Find the text position (inline_root_id, byte_offset) at a given point.
4727    /// Uses hit() for proper coordinate transformation, then finds the inline root
4728    /// and byte offset.
4729    pub fn find_text_position(&self, x: f32, y: f32) -> Option<(NodeId, usize)> {
4730        let hit = self.hit(x, y)?;
4731        let hit_node = self.get_node(hit.node_id)?;
4732        let inline_root = hit_node.inline_root_ancestor()?;
4733        let byte_offset = inline_root.text_offset_at_point(hit.x, hit.y)?;
4734        Some((inline_root.id, byte_offset))
4735    }
4736
4737    /// Find the word or line at a point, as `(inline_root_id, start, end)`.
4738    ///
4739    /// The multi-click counterpart of
4740    /// [`find_text_position`](Self::find_text_position): that one answers where
4741    /// a caret goes, this one answers what a double or triple click selects.
4742    pub fn find_text_range(
4743        &self,
4744        x: f32,
4745        y: f32,
4746        granularity: TextGranularity,
4747    ) -> Option<(NodeId, usize, usize)> {
4748        let hit = self.hit(x, y)?;
4749        let hit_node = self.get_node(hit.node_id)?;
4750        let inline_root = hit_node.inline_root_ancestor()?;
4751        let range = inline_root.text_range_at_point(hit.x, hit.y, granularity)?;
4752        Some((inline_root.id, range.start, range.end))
4753    }
4754
4755    /// Set the text selection range (creates a new selection from anchor to focus)
4756    pub fn set_text_selection(
4757        &mut self,
4758        anchor_node: NodeId,
4759        anchor_offset: usize,
4760        focus_node: NodeId,
4761        focus_offset: usize,
4762    ) {
4763        self.dom_selection = None;
4764        self.text_selection =
4765            TextSelection::new(anchor_node, anchor_offset, focus_node, focus_offset);
4766
4767        // For anonymous blocks, switch to storing parent+sibling_index (stable reference)
4768        if let (Some(parent), Some(idx)) = self.anonymous_block_location(anchor_node) {
4769            self.text_selection
4770                .anchor
4771                .set_anonymous(parent, idx, anchor_offset);
4772        }
4773        if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
4774            self.text_selection
4775                .focus
4776                .set_anonymous(parent, idx, focus_offset);
4777        }
4778    }
4779
4780    /// Get the parent ID and sibling index for a node if it's an anonymous block.
4781    /// Returns (None, None) for non-anonymous blocks.
4782    fn anonymous_block_location(&self, node_id: NodeId) -> (Option<NodeId>, Option<usize>) {
4783        let Some(node) = self.get_node(node_id) else {
4784            return (None, None);
4785        };
4786
4787        if !node.is_anonymous() {
4788            return (None, None);
4789        }
4790
4791        let Some(parent_id) = node.parent else {
4792            return (None, None);
4793        };
4794
4795        let Some(parent) = self.get_node(parent_id) else {
4796            return (Some(parent_id), None);
4797        };
4798
4799        let layout_children = parent.layout_children.borrow();
4800        let Some(children) = layout_children.as_ref() else {
4801            return (Some(parent_id), None);
4802        };
4803
4804        // Find the index of this anonymous block among siblings
4805        let mut anon_index = 0;
4806        for &child_id in children.iter() {
4807            if child_id == node_id {
4808                return (Some(parent_id), Some(anon_index));
4809            }
4810            if self.get_node(child_id).is_some_and(|n| n.is_anonymous()) {
4811                anon_index += 1;
4812            }
4813        }
4814
4815        (Some(parent_id), None)
4816    }
4817
4818    /// Clear the text selection
4819    pub fn clear_text_selection(&mut self) {
4820        self.dom_selection = None;
4821        self.text_selection.clear();
4822    }
4823
4824    /// Update the selection focus point (used during mouse drag to extend selection).
4825    pub fn update_selection_focus(&mut self, focus_node: NodeId, focus_offset: usize) {
4826        self.dom_selection = None;
4827        // For anonymous blocks, store parent+sibling_index; otherwise store node directly
4828        if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
4829            self.text_selection
4830                .focus
4831                .set_anonymous(parent, idx, focus_offset);
4832        } else {
4833            self.text_selection.set_focus(focus_node, focus_offset);
4834        }
4835    }
4836
4837    /// Extend text selection to the given point. Returns true if selection was updated.
4838    /// This is a convenience method that combines find_text_position and update_selection_focus.
4839    pub fn extend_text_selection_to_point(&mut self, x: f32, y: f32) -> bool {
4840        if !self.text_selection.anchor.is_some() {
4841            return false;
4842        }
4843
4844        if let Some((node, offset)) = self.find_text_position(x, y) {
4845            self.update_selection_focus(node, offset);
4846            self.shell_provider.request_redraw();
4847            true
4848        } else {
4849            false
4850        }
4851    }
4852
4853    /// Find the Nth anonymous block under a parent.
4854    fn find_anonymous_block_by_index(
4855        &self,
4856        parent_id: NodeId,
4857        target_index: usize,
4858    ) -> Option<NodeId> {
4859        let parent = self.get_node(parent_id)?;
4860        let layout_children = parent.layout_children.borrow();
4861        let children = layout_children.as_ref()?;
4862
4863        children
4864            .iter()
4865            .filter(|&&child_id| self.get_node(child_id).is_some_and(|n| n.is_anonymous()))
4866            .nth(target_index)
4867            .copied()
4868    }
4869
4870    /// Check if there is an active (non-empty) text selection
4871    pub fn has_text_selection(&self) -> bool {
4872        self.dom_selection.as_ref().map_or_else(
4873            || self.text_selection.is_active(),
4874            |range| !range.bounds().collapsed(),
4875        )
4876    }
4877
4878    /// Get the selected text content, supporting selection across multiple inline roots.
4879    pub fn get_selected_text(&self) -> Option<String> {
4880        if let Some(range) = &self.dom_selection {
4881            let text = self.range_string(range.bounds());
4882            return (!text.is_empty()).then_some(text);
4883        }
4884        let ranges = self.get_text_selection_ranges();
4885        if ranges.is_empty() {
4886            return None;
4887        }
4888
4889        let mut result = String::new();
4890        for (node_id, start, end) in &ranges {
4891            let node = self.get_node(*node_id)?;
4892            let element_data = node.element_data()?;
4893            let inline_layout = element_data.inline_layout_data.as_ref()?;
4894
4895            if *end > inline_layout.text.len() {
4896                continue;
4897            }
4898
4899            if !result.is_empty() {
4900                result.push(' ');
4901            }
4902            result.push_str(&inline_layout.text[*start..*end]);
4903        }
4904
4905        if result.is_empty() {
4906            None
4907        } else {
4908            Some(result)
4909        }
4910    }
4911
4912    /// The selection as HTML, for a clipboard that can carry formatting.
4913    ///
4914    /// The same ranges [`Self::get_selected_text`] walks, with each one wrapped
4915    /// in the tag of the element it came from, so bold stays bold and a code
4916    /// span stays a code span when it lands in another application. Pasting
4917    /// formatted text out of a Blitz window was impossible before this: plain
4918    /// text was the only payload ever written.
4919    ///
4920    /// Only the element's own tag is reproduced, not its ancestors or its
4921    /// styles. That is deliberate: a paste should carry the structure the author
4922    /// wrote, not this window's theme, and dragging a `class` into another
4923    /// application's document would reference styles that do not exist there.
4924    pub fn get_selected_html(&self) -> Option<String> {
4925        let ranges = self.get_text_selection_ranges();
4926        if ranges.is_empty() {
4927            return None;
4928        }
4929
4930        let mut result = String::new();
4931        for (node_id, start, end) in &ranges {
4932            let Some(node) = self.get_node(*node_id) else {
4933                continue;
4934            };
4935            let Some(element_data) = node.element_data() else {
4936                continue;
4937            };
4938            let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
4939                continue;
4940            };
4941            if *end > inline_layout.text.len() {
4942                continue;
4943            }
4944
4945            if !result.is_empty() {
4946                result.push(' ');
4947            }
4948
4949            let tag = element_data.name.local.as_ref();
4950            let fragment = escape_html(&inline_layout.text[*start..*end]);
4951            // A block tag around a fragment of a paragraph would be wrong: the
4952            // range is a run of text, and the element it belongs to may be only
4953            // partly selected. Inline tags carry meaning a paste should keep;
4954            // anything else contributes its text and nothing more.
4955            if matches!(
4956                tag,
4957                "b" | "strong" | "i" | "em" | "code" | "kbd" | "mark" | "s" | "u" | "sub" | "sup"
4958            ) {
4959                result.push_str(&format!("<{tag}>{fragment}</{tag}>"));
4960            } else {
4961                result.push_str(&fragment);
4962            }
4963        }
4964
4965        if result.is_empty() {
4966            None
4967        } else {
4968            Some(result)
4969        }
4970    }
4971
4972    /// Get all selection ranges as Vec<(node_id, start_offset, end_offset)>.
4973    /// Returns empty vec if no selection.
4974    pub fn get_text_selection_ranges(&self) -> Vec<(NodeId, usize, usize)> {
4975        if let Some(range) = &self.dom_selection {
4976            return self.range_layout_ranges(range.bounds());
4977        }
4978        let lookup = |parent_id, idx| self.find_anonymous_block_by_index(parent_id, idx);
4979
4980        let anchor_node = match self.text_selection.anchor.resolve_node_id(lookup) {
4981            Some(id) => id,
4982            None => return Vec::new(),
4983        };
4984        let focus_node = match self.text_selection.focus.resolve_node_id(lookup) {
4985            Some(id) => id,
4986            None => return Vec::new(),
4987        };
4988
4989        // Guard against stale selection endpoints: nodes may have been removed from
4990        // the document (e.g. by script) since the selection was made.
4991        let node_is_in_doc = |node_id: NodeId| {
4992            self.nodes
4993                .get(node_id)
4994                .is_some_and(|node| node.flags.is_in_document())
4995        };
4996        if !node_is_in_doc(anchor_node) || !node_is_in_doc(focus_node) {
4997            return Vec::new();
4998        }
4999
5000        // Single node selection
5001        if anchor_node == focus_node {
5002            let start = self
5003                .text_selection
5004                .anchor
5005                .offset
5006                .min(self.text_selection.focus.offset);
5007            let end = self
5008                .text_selection
5009                .anchor
5010                .offset
5011                .max(self.text_selection.focus.offset);
5012
5013            if start == end {
5014                return Vec::new();
5015            }
5016            return vec![(anchor_node, start, end)];
5017        }
5018
5019        // Multi-node selection: collect all inline roots between anchor and focus
5020        let inline_roots = self.collect_inline_roots_in_range(anchor_node, focus_node);
5021        if inline_roots.is_empty() {
5022            return Vec::new();
5023        }
5024
5025        // Determine document order using the collected inline_roots order
5026        // (inline_roots is already in document order from first to last)
5027        let first_in_roots = inline_roots[0];
5028
5029        let (first_node, first_offset, last_node, last_offset) =
5030            if first_in_roots == anchor_node || (first_in_roots != focus_node) {
5031                // anchor is first (or neither endpoint is in roots, which shouldn't happen)
5032                (
5033                    anchor_node,
5034                    self.text_selection.anchor.offset,
5035                    focus_node,
5036                    self.text_selection.focus.offset,
5037                )
5038            } else {
5039                // focus is first
5040                (
5041                    focus_node,
5042                    self.text_selection.focus.offset,
5043                    anchor_node,
5044                    self.text_selection.anchor.offset,
5045                )
5046            };
5047
5048        let mut ranges = Vec::with_capacity(inline_roots.len());
5049
5050        for &node_id in &inline_roots {
5051            let Some(node) = self.get_node(node_id) else {
5052                continue;
5053            };
5054            let Some(element_data) = node.element_data() else {
5055                continue;
5056            };
5057            let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
5058                continue;
5059            };
5060
5061            let text_len = inline_layout.text.len();
5062
5063            if node_id == first_node && node_id == last_node {
5064                let start = first_offset.min(last_offset);
5065                let end = first_offset.max(last_offset);
5066                if start < end && end <= text_len {
5067                    ranges.push((node_id, start, end));
5068                }
5069            } else if node_id == first_node {
5070                if first_offset < text_len {
5071                    ranges.push((node_id, first_offset, text_len));
5072                }
5073            } else if node_id == last_node {
5074                if last_offset > 0 && last_offset <= text_len {
5075                    ranges.push((node_id, 0, last_offset));
5076                }
5077            } else if text_len > 0 {
5078                ranges.push((node_id, 0, text_len));
5079            }
5080        }
5081
5082        ranges
5083    }
5084}
5085
5086#[derive(Debug, Clone, Copy, PartialEq)]
5087pub struct BoundingRect {
5088    pub x: f64,
5089    pub y: f64,
5090    pub width: f64,
5091    pub height: f64,
5092}
5093
5094/// The record of changes a script's `MutationObserver` reads. See
5095/// [`crate::DomMutation`] for what is recorded and why.
5096impl BaseDocument {
5097    /// Note a `<details open>` presence change for the toggle event. `present` is whether the
5098    /// attribute exists after this write.
5099    pub(crate) fn record_details_open(
5100        &mut self,
5101        node_id: NodeId,
5102        name: &markup5ever::QualName,
5103        present: bool,
5104    ) {
5105        if name.local != local_name!("open") || !name.ns.is_empty() {
5106            return;
5107        }
5108        let Some(element) = self.nodes[node_id].element_data() else {
5109            return;
5110        };
5111        if element.name.local != local_name!("details") || element.name.ns != ns!(html) {
5112            return;
5113        }
5114        let was_open = element.attr(local_name!("open")).is_some();
5115        if was_open != present && !self.details_toggles.iter().any(|(id, _)| *id == node_id) {
5116            self.details_toggles.push((node_id, was_open));
5117        }
5118    }
5119
5120    /// The inert document template contents belong to.
5121    pub(crate) fn template_contents_owner(&mut self) -> NodeId {
5122        if let Some(owner) = self.template_contents_owner {
5123            return owner;
5124        }
5125        let mut data = crate::node::DocumentData::default();
5126        data.content_type = "text/html";
5127        let owner = self.create_node(NodeData::Document(Box::new(data)));
5128        self.template_contents_owner = Some(owner);
5129        owner
5130    }
5131
5132    /// Changes whenever the device media queries are evaluated against changes.
5133    pub fn viewport_generation(&self) -> u64 {
5134        self.viewport_generation
5135    }
5136
5137    /// The `<details>` toggles recorded since the last call, each with its old open state.
5138    pub fn take_details_toggles(&mut self) -> Vec<(NodeId, bool)> {
5139        std::mem::take(&mut self.details_toggles)
5140    }
5141
5142    /// Update the raw content handler state at the attribute mutation boundary.
5143    /// Equal-value writes still replace a compiled or script-assigned handler.
5144    pub(crate) fn record_inline_handler_attribute(
5145        &mut self,
5146        node_id: NodeId,
5147        name: &crate::QualName,
5148        value: Option<&str>,
5149    ) {
5150        if name.ns != markup5ever::ns!() || !name.local.as_ref().starts_with("on") {
5151            return;
5152        }
5153        let Some(element) = self
5154            .nodes
5155            .get_mut(node_id)
5156            .and_then(|node| node.element_data_mut())
5157        else {
5158            return;
5159        };
5160        let value = value.map(crate::node::AttrAtom::from);
5161        if let Some(value) = &value {
5162            let attributes = element
5163                .inline_event_attributes
5164                .get_or_insert_with(|| Box::new(Vec::new()));
5165            if let Some(attribute) = attributes
5166                .iter_mut()
5167                .find(|attribute| attribute.name.local == name.local)
5168            {
5169                attribute.name = name.clone();
5170                attribute.value = value.clone();
5171            } else {
5172                attributes.push(crate::node::Attribute {
5173                    name: name.clone(),
5174                    value: value.clone(),
5175                });
5176            }
5177        } else {
5178            let Some(attributes) = element.inline_event_attributes.as_mut() else {
5179                return;
5180            };
5181            let Some(index) = attributes
5182                .iter()
5183                .position(|attribute| attribute.name.local == name.local)
5184            else {
5185                return;
5186            };
5187            attributes.remove(index);
5188            if attributes.is_empty() {
5189                element.inline_event_attributes = None;
5190            }
5191        }
5192        self.inline_handler_changes
5193            .push((node_id, name.clone(), value));
5194    }
5195
5196    /// Take content handler changes in mutation order.
5197    pub fn take_inline_handler_changes(
5198        &mut self,
5199    ) -> Vec<(NodeId, crate::QualName, Option<crate::node::AttrAtom>)> {
5200        std::mem::take(&mut self.inline_handler_changes)
5201    }
5202
5203    /// Start or stop recording changes. Stopping discards what was recorded
5204    /// and not yet taken.
5205    pub fn set_recording_mutations(&mut self, recording: bool) {
5206        match (recording, self.mutation_log.is_some()) {
5207            (true, false) => self.mutation_log = Some(Vec::new()),
5208            (false, true) => self.mutation_log = None,
5209            _ => {}
5210        }
5211    }
5212
5213    /// Whether changes are being recorded.
5214    pub fn is_recording_mutations(&self) -> bool {
5215        self.mutation_log.is_some()
5216    }
5217
5218    /// Take every change recorded since the last call, oldest first.
5219    pub fn take_mutations(&mut self) -> Vec<crate::DomMutation> {
5220        self.mutation_log
5221            .as_mut()
5222            .map(std::mem::take)
5223            .unwrap_or_default()
5224    }
5225
5226    pub(crate) fn record_mutation(&mut self, mutation: crate::DomMutation) {
5227        if let Some(log) = self.mutation_log.as_mut() {
5228            if log.len() < crate::mutation_record::MUTATION_LOG_CAP {
5229                log.push(mutation);
5230            }
5231        }
5232    }
5233
5234    /// Where `node_id` sits among its siblings: its parent and the siblings on
5235    /// either side, as a child-list record reports them.
5236    pub(crate) fn sibling_context(
5237        &self,
5238        node_id: NodeId,
5239    ) -> Option<(NodeId, Option<NodeId>, Option<NodeId>)> {
5240        let parent_id = self.nodes.get(node_id)?.parent?;
5241        let siblings = &self.nodes.get(parent_id)?.children;
5242        let index = siblings.iter().position(|id| *id == node_id)?;
5243        let previous = index.checked_sub(1).map(|i| siblings[i]);
5244        let next = siblings.get(index + 1).copied();
5245        Some((parent_id, previous, next))
5246    }
5247}
5248
5249impl AsRef<BaseDocument> for BaseDocument {
5250    fn as_ref(&self) -> &BaseDocument {
5251        self
5252    }
5253}
5254
5255impl AsMut<BaseDocument> for BaseDocument {
5256    fn as_mut(&mut self) -> &mut BaseDocument {
5257        self
5258    }
5259}
5260
5261#[cfg(test)]
5262mod hover_state_tests {
5263    use super::*;
5264    use crate::{Attribute, qual_name};
5265    use blitz_traits::shell::ColorScheme;
5266
5267    /// Build `<html><body style="margin:0"><div style="width:300px">some text
5268    /// <div style="height:50px"></div></div></body></html>` manually (the HTML
5269    /// parser lives in blitz-html, which would be a circular dev-dependency).
5270    /// The bare text next to a block sibling gets wrapped in an anonymous
5271    /// block, which becomes the inline root: text hits report the anonymous
5272    /// block as the hit node.
5273    fn make_doc() -> (BaseDocument, NodeId) {
5274        let mut doc = BaseDocument::new(DocumentConfig {
5275            viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
5276            ..Default::default()
5277        });
5278        let root_id = doc.root_node().id;
5279        let style = |value: &str| Attribute {
5280            name: qual_name!("style"),
5281            value: value.into(),
5282        };
5283
5284        let mut mutator = doc.mutate();
5285        let html = mutator.create_element(qual_name!("html"), vec![]);
5286        let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
5287        let container = mutator.create_element(qual_name!("div"), vec![style("width:300px")]);
5288        let text = mutator.create_text_node("some text");
5289        let block = mutator.create_element(qual_name!("div"), vec![style("height:50px")]);
5290        mutator.append_children(container, &[text, block]);
5291        mutator.append_children(body, &[container]);
5292        mutator.append_children(html, &[body]);
5293        mutator.append_children(root_id, &[html]);
5294        drop(mutator);
5295
5296        doc.resolve(0.0);
5297        (doc, container)
5298    }
5299
5300    /// Whether text laid out with a real (non-zero-metric) font. Without the
5301    /// `system-fonts` feature text measures 0x0 and text hits are impossible,
5302    /// making these tests vacuous.
5303    fn text_has_size(doc: &BaseDocument, container: NodeId) -> bool {
5304        doc.nodes[container].final_layout().size.height > 50.0
5305    }
5306
5307    /// Regression test: hovering bare text wrapped in an anonymous block must
5308    /// report a text cursor. The hit node for such text is the anonymous
5309    /// inline root itself, while the *stored* hover target is canonicalized to
5310    /// the containing element — the cursor must be derived from the precise
5311    /// hit node, not the canonical target.
5312    #[test]
5313    fn hovering_text_in_anonymous_block_reports_text_cursor() {
5314        let (mut doc, container) = make_doc();
5315        if !text_has_size(&doc, container) {
5316            eprintln!("skipping: no usable font (text measures 0x0)");
5317            return;
5318        }
5319
5320        doc.set_hover_to(5.0, 8.0);
5321        assert!(doc.hover_node_is_text, "expected a text hit");
5322        let hit_id = doc.hover_hit_node_id.expect("expected a hit node");
5323        assert!(
5324            doc.nodes[hit_id].is_anonymous(),
5325            "expected the hit node to be the anonymous inline root"
5326        );
5327        assert_eq!(
5328            doc.get_hover_node_id(),
5329            Some(container),
5330            "expected the stored hover target to be the containing element"
5331        );
5332        assert_eq!(doc.get_cursor(), Some(CursorIcon::Text));
5333    }
5334
5335    #[test]
5336    fn semantic_hover_keeps_the_resolved_node_instead_of_hit_testing_again() {
5337        let (mut doc, container) = make_doc();
5338
5339        // This coordinate is outside the 300px-wide container. A coordinate
5340        // hit test therefore cannot select it, but semantic automation has
5341        // already selected the container by id and must preserve that target.
5342        doc.set_hover_to_node(container, 350.0, 250.0);
5343
5344        assert_eq!(doc.get_hover_node_id(), Some(container));
5345        assert_eq!(doc.hover_hit_node_id, Some(container));
5346
5347        doc.resolve(0.0);
5348        assert_eq!(
5349            doc.get_hover_node_id(),
5350            Some(container),
5351            "a resolve must not turn semantic identity back into a coordinate hit"
5352        );
5353    }
5354
5355    /// Hovering the empty region of the anonymous block (right of the text) is
5356    /// not a text hit: default cursor, same canonical hover target.
5357    #[test]
5358    fn hovering_anonymous_block_whitespace_reports_default_cursor() {
5359        let (mut doc, container) = make_doc();
5360        if !text_has_size(&doc, container) {
5361            eprintln!("skipping: no usable font (text measures 0x0)");
5362            return;
5363        }
5364
5365        doc.set_hover_to(250.0, 8.0);
5366        assert!(!doc.hover_node_is_text);
5367        assert_eq!(doc.get_hover_node_id(), Some(container));
5368        assert_eq!(doc.get_cursor(), Some(CursorIcon::Default));
5369    }
5370}
5371
5372#[cfg(test)]
5373mod control_scroll_tests {
5374    use super::*;
5375    use crate::{Attribute, qual_name};
5376    use blitz_traits::shell::ColorScheme;
5377
5378    #[test]
5379    fn controlled_scroll_dispatches_the_dom_scroll_event() {
5380        let mut doc = BaseDocument::new(DocumentConfig {
5381            viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
5382            ..Default::default()
5383        });
5384        let root_id = doc.root_node().id;
5385        let style = |value: &str| Attribute {
5386            name: qual_name!("style"),
5387            value: value.into(),
5388        };
5389
5390        let mut mutator = doc.mutate();
5391        let html = mutator.create_element(qual_name!("html"), vec![]);
5392        let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
5393        let scroller = mutator.create_element(
5394            qual_name!("div"),
5395            vec![style("width:200px;height:100px;overflow-y:scroll")],
5396        );
5397        let spacer = mutator.create_element(qual_name!("div"), vec![style("height:400px")]);
5398        let target = mutator.create_element(qual_name!("button"), vec![style("height:40px")]);
5399        let target_text = mutator.create_text_node("Reveal me");
5400        mutator.append_children(target, &[target_text]);
5401        mutator.append_children(scroller, &[spacer, target]);
5402        mutator.append_children(body, &[scroller]);
5403        mutator.append_children(html, &[body]);
5404        mutator.append_children(root_id, &[html]);
5405        drop(mutator);
5406        doc.resolve(0.0);
5407
5408        // The manual mutator deliberately bypasses the HTML/style parser used
5409        // by loaded documents. Give the fixture explicit post-layout geometry
5410        // so this unit test isolates event forwarding rather than CSS parsing.
5411        doc.nodes[html].final_layout_mut().size.height = 300.0;
5412        doc.nodes[html].final_layout_mut().content_size.height = 600.0;
5413        doc.nodes[target].final_layout_mut().location.y = 400.0;
5414
5415        let mut events = Vec::new();
5416        // A semantic client may target the exposed text rather than its
5417        // element. Text has no `final_layout`, so this also pins the host-crash
5418        // regression from revealing a named label.
5419        doc.scroll_to_node_centered_with_events(target_text, |event| events.push(event));
5420
5421        assert!(
5422            (doc.viewport_scroll.y - 270.0).abs() < 0.1,
5423            "the 40px target should be centred in the 300px viewport"
5424        );
5425        assert!(
5426            events
5427                .iter()
5428                .any(|event| { event.target == html && event.name() == "scroll" })
5429        );
5430    }
5431}
5432
5433#[cfg(test)]
5434mod font_face_override_tests {
5435    use super::*;
5436    use crate::net::{FontFaceOverrides, Resource, ResourceLoadResponse};
5437
5438    /// Regression-pin for the `@font-face` descriptor-honouring fix.
5439    ///
5440    /// The bug was that `Resource::Font` carried only the raw font bytes,
5441    /// so `load_resource` registered fonts with `info_override = None` and
5442    /// parley fell back to the TTF's internal `name` table. After the fix,
5443    /// `Resource::Font` carries `FontFaceOverrides` and `load_resource`
5444    /// builds a `FontInfoOverride` from them — meaning a CSS-declared
5445    /// `font-family` alias wins over the file's own metadata.
5446    ///
5447    /// We drive `load_resource` directly with a fabricated response rather
5448    /// than go through HTML parsing → `fetch_font_face`, because the
5449    /// downstream HTML parser lives in `blitz-html` (would be a circular
5450    /// crate dependency). The mapping from `@font-face` descriptors into
5451    /// `FontFaceOverrides` is covered by the unit tests in `net.rs`; this
5452    /// test pins the load-side of the pipeline.
5453    #[test]
5454    fn font_face_overrides_alias_family_name() {
5455        const ALIAS: &str = "AliasedFamily";
5456
5457        let mut document = BaseDocument::new(DocumentConfig::default());
5458
5459        // Sanity: the alias name is not registered before we feed the font.
5460        {
5461            let mut ctx = document.font_ctx.lock().unwrap();
5462            assert!(
5463                ctx.collection.family_id(ALIAS).is_none(),
5464                "alias must not exist before registration",
5465            );
5466        }
5467
5468        // Drive `load_resource` with a `Resource::Font` whose overrides
5469        // assert the CSS-side family name. We use the bullet font as a
5470        // valid font payload — its internal `name` table is irrelevant to
5471        // the assertion; what matters is whether the override wins.
5472        let response = ResourceLoadResponse {
5473            request_id: 0,
5474            node_id: None,
5475            resolved_url: Some(String::from("test://aliased-family")),
5476            result: Ok(Resource::Font(
5477                blitz_traits::net::Bytes::from_static(crate::BULLET_FONT),
5478                FontFaceOverrides {
5479                    family_name: Some(String::from(ALIAS)),
5480                    weight: Some(800.0),
5481                    style: Some(parley::fontique::FontStyle::Italic),
5482                },
5483            )),
5484        };
5485        document.load_resource(response);
5486
5487        // The override must have taken effect: parley's `Collection` now
5488        // resolves the CSS-declared alias to a registered family.
5489        let mut ctx = document.font_ctx.lock().unwrap();
5490        let family_id = ctx
5491            .collection
5492            .family_id(ALIAS)
5493            .expect("CSS-declared family name should be registered as a family alias");
5494        let resolved_name = ctx
5495            .collection
5496            .family_name(family_id)
5497            .expect("family id should resolve back to a name");
5498        assert_eq!(
5499            resolved_name, ALIAS,
5500            "registered family should report the CSS-declared name, \
5501             not the font file's internal `name` table entry",
5502        );
5503    }
5504}
5505
5506#[cfg(test)]
5507mod clipboard_html_tests {
5508    use super::escape_html;
5509
5510    /// A selection is document text, so markup characters in it are content.
5511    /// Unescaped, `a < b` opens a tag in whatever receives the paste.
5512    #[test]
5513    fn markup_characters_in_a_selection_are_escaped() {
5514        assert_eq!(
5515            escape_html("a < b && c > d"),
5516            "a &lt; b &amp;&amp; c &gt; d"
5517        );
5518    }
5519
5520    /// The ampersand has to go first, or escaping the others re-escapes the
5521    /// entities just written and `<` arrives as `&amp;lt;`.
5522    #[test]
5523    fn an_ampersand_is_not_double_escaped() {
5524        assert_eq!(escape_html("&lt;"), "&amp;lt;");
5525    }
5526
5527    /// Ordinary prose is left exactly as it is, including the apostrophes and
5528    /// quotes that a paste should carry through unchanged.
5529    #[test]
5530    fn prose_is_untouched() {
5531        assert_eq!(escape_html("it's \"fine\""), "it's &quot;fine&quot;");
5532    }
5533}