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