Skip to main content

blitz_dom/
document.rs

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