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