Skip to main content

blitz_dom/
document.rs

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