Skip to main content

blitz_dom/
document.rs

1use crate::NodeTree;
2use crate::events::{DragMode, ScrollAnimationState, handle_dom_event};
3use crate::font_metrics::BlitzFontMetricsProvider;
4use crate::layout::construct::ConstructionTask;
5use crate::layout::damage::ALL_DAMAGE;
6use crate::mutator::ViewportMut;
7use crate::net::{
8    Resource, ResourceHandler, ResourceLoadResponse, StylesheetHandler, StylesheetLoader,
9};
10use crate::node::{
11    ImageData, NodeFlags, RasterImageData, SpecialElementData, Status, TextBrush, TextGranularity,
12};
13use crate::selection::TextSelection;
14use crate::stylo_to_cursor_icon::stylo_to_cursor_icon;
15use crate::traversal::TreeTraverser;
16use crate::url::DocumentUrl;
17use crate::util::ImageType;
18use crate::{
19    DEFAULT_CSS, DocumentConfig, DocumentMutator, DummyHtmlParserProvider, ElementData,
20    EventDriver, HtmlParserProvider, Node, NodeData, NoopEventHandler, StyleThreading,
21    TextNodeData,
22};
23use blitz_traits::devtools::DevtoolSettings;
24use blitz_traits::events::{BlitzScrollEvent, DomEvent, DomEventData, HitResult, UiEvent};
25use blitz_traits::navigation::{DummyNavigationProvider, NavigationProvider};
26use blitz_traits::net::{AbortSignal, DummyNetProvider, NetProvider, Request};
27use blitz_traits::node_id::NodeId;
28use blitz_traits::shell::{ColorScheme, DummyShellProvider, ShellProvider, Viewport};
29use cursor_icon::CursorIcon;
30use linebender_resource_handle::Blob;
31use markup5ever::{local_name, ns};
32use parley::{FontContext, PlainEditorDriver};
33use selectors::{Element, matching::QuirksMode};
34use smallvec::SmallVec;
35use std::any::Any;
36use std::cell::RefCell;
37use std::collections::{BTreeMap, Bound, HashMap, HashSet};
38use std::ops::{Deref, DerefMut};
39use std::rc::Rc;
40use std::str::FromStr;
41use std::sync::atomic::{AtomicUsize, Ordering};
42use std::sync::mpsc::{Receiver, Sender, channel};
43use std::sync::{Arc, Mutex, MutexGuard, OnceLock, RwLockReadGuard, RwLockWriteGuard};
44use std::task::{Context as TaskContext, Waker};
45use style::Atom;
46use style::animation::{AnimationState, DocumentAnimationSet};
47use style::attr::{AttrIdentifier, AttrValue};
48use style::data::{ElementData as StyloElementData, ElementStyles};
49use style::media_queries::MediaType;
50use style::properties::ComputedValues;
51use style::properties::style_structs::Font;
52use style::queries::values::PrefersColorScheme;
53use style::selector_parser::ServoElementSnapshot;
54use style::servo::media_features::PointerCapabilities;
55use style::servo_arc::Arc as ServoArc;
56use style::values::GenericAtomIdent;
57use style::values::computed::ui::CursorKind;
58use style::values::computed::{Overflow, UserSelect};
59use style::values::specified::box_::{DisplayInside, DisplayOutside};
60use style::{
61    device::Device,
62    dom::{TDocument, TNode},
63    media_queries::MediaList,
64    selector_parser::SnapshotMap,
65    shared_lock::{SharedRwLock, StylesheetGuards},
66    stylesheets::{AllowImportRules, DocumentStyleSheet, Origin, Stylesheet},
67    stylist::Stylist,
68};
69use thin_vec::ThinVec;
70use url::Url;
71use web_time::Instant;
72
73#[cfg(feature = "parallel-construct")]
74use thread_local::ThreadLocal;
75
76pub enum DocGuard<'a> {
77    Ref(&'a BaseDocument),
78    RefCell(std::cell::Ref<'a, BaseDocument>),
79    RwLock(RwLockReadGuard<'a, BaseDocument>),
80    Mutex(MutexGuard<'a, BaseDocument>),
81}
82
83impl Deref for DocGuard<'_> {
84    type Target = BaseDocument;
85    #[inline(always)]
86    fn deref(&self) -> &Self::Target {
87        match self {
88            Self::Ref(base_document) => base_document,
89            Self::RefCell(refcell_guard) => refcell_guard,
90            Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
91            Self::Mutex(mutex_guard) => mutex_guard,
92        }
93    }
94}
95
96pub enum DocGuardMut<'a> {
97    Ref(&'a mut BaseDocument),
98    RefCell(std::cell::RefMut<'a, BaseDocument>),
99    RwLock(RwLockWriteGuard<'a, BaseDocument>),
100    Mutex(MutexGuard<'a, BaseDocument>),
101}
102
103impl Deref for DocGuardMut<'_> {
104    type Target = BaseDocument;
105    #[inline(always)]
106    fn deref(&self) -> &Self::Target {
107        match self {
108            Self::Ref(base_document) => base_document,
109            Self::RefCell(refcell_guard) => refcell_guard,
110            Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
111            Self::Mutex(mutex_guard) => mutex_guard,
112        }
113    }
114}
115
116impl DerefMut for DocGuardMut<'_> {
117    #[inline(always)]
118    fn deref_mut(&mut self) -> &mut Self::Target {
119        match self {
120            Self::Ref(base_document) => base_document,
121            Self::RefCell(refcell_guard) => &mut *refcell_guard,
122            Self::RwLock(rw_lock_read_guard) => &mut *rw_lock_read_guard,
123            Self::Mutex(mutex_guard) => &mut *mutex_guard,
124        }
125    }
126}
127
128/// Abstraction over wrappers around [`BaseDocument`] to allow for them all to
129/// be driven by [`blitz-shell`](https://docs.rs/blitz-shell)
130pub trait Document: Any + 'static {
131    fn inner(&self) -> DocGuard<'_>;
132    fn inner_mut(&mut self) -> DocGuardMut<'_>;
133
134    /// Update the [`Document`] in response to a [`UiEvent`] (click, keypress, etc)
135    fn handle_ui_event(&mut self, event: UiEvent) {
136        let mut doc = self.inner_mut();
137        let mut driver = EventDriver::new(&mut *doc, NoopEventHandler);
138        driver.handle_ui_event(event);
139    }
140
141    /// Poll any pending async operations, and flush changes to the underlying [`BaseDocument`]
142    fn poll(&mut self, task_context: Option<TaskContext>) -> bool {
143        // Default implementation does nothing
144        let _ = task_context;
145        false
146    }
147
148    /// Get the [`Document`]'s id
149    fn id(&self) -> usize {
150        self.inner().id
151    }
152}
153
154/// What the pre-click activation steps changed, so a cancelled click can put
155/// it back. Produced by [`BaseDocument::run_pre_click_activation`].
156#[derive(Debug, Clone, PartialEq)]
157pub struct PreClickActivation {
158    /// Every node whose checkedness moved, paired with the value it held.
159    previous: Vec<(NodeId, bool)>,
160}
161
162/// Escape text for inclusion in an HTML clipboard payload.
163///
164/// The five characters that would otherwise be read as markup. A selection is
165/// document text, so a paragraph containing `a < b` must arrive as `a &lt; b`
166/// rather than opening a tag in whatever receives the paste.
167fn escape_html(text: &str) -> String {
168    let mut out = String::with_capacity(text.len());
169    for ch in text.chars() {
170        match ch {
171            '&' => out.push_str("&amp;"),
172            '<' => out.push_str("&lt;"),
173            '>' => out.push_str("&gt;"),
174            '"' => out.push_str("&quot;"),
175            _ => out.push(ch),
176        }
177    }
178    out
179}
180
181pub struct PlainDocument(pub BaseDocument);
182impl Document for PlainDocument {
183    fn inner(&self) -> DocGuard<'_> {
184        DocGuard::Ref(&self.0)
185    }
186    fn inner_mut(&mut self) -> DocGuardMut<'_> {
187        DocGuardMut::Ref(&mut self.0)
188    }
189}
190
191impl Document for BaseDocument {
192    fn inner(&self) -> DocGuard<'_> {
193        DocGuard::Ref(self)
194    }
195    fn inner_mut(&mut self) -> DocGuardMut<'_> {
196        DocGuardMut::Ref(self)
197    }
198}
199
200impl Document for Rc<RefCell<BaseDocument>> {
201    fn inner(&self) -> DocGuard<'_> {
202        DocGuard::RefCell(self.borrow())
203    }
204
205    fn inner_mut(&mut self) -> DocGuardMut<'_> {
206        DocGuardMut::RefCell(self.borrow_mut())
207    }
208}
209
210pub enum DocumentEvent {
211    ResourceLoad(ResourceLoadResponse),
212    /// A navigation originating from within an iframe's sub-document
213    /// (e.g. a link click), to be applied to the iframe identified by `node_id`.
214    NavigateIframe {
215        node_id: NodeId,
216        url: Url,
217    },
218}
219
220/// How urgently a document needs another animation frame.
221#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
222pub enum AnimationPacing {
223    Idle,
224    Caret,
225    SlowCss,
226    Interactive,
227}
228
229pub struct BaseDocument {
230    /// ID of the document
231    id: usize,
232
233    // Config
234    /// Base url for resolving linked resources (stylesheets, images, fonts, etc)
235    pub(crate) url: DocumentUrl,
236    // Devtool settings. Currently used to render debug overlays
237    pub(crate) devtool_settings: DevtoolSettings,
238    // Viewport details such as the dimensions, HiDPI scale, and zoom factor,
239    pub(crate) viewport: Viewport,
240    // Scroll within our viewport
241    pub(crate) viewport_scroll: crate::Point<f64>,
242    /// Changes recorded for a script's `MutationObserver`, or `None` while no
243    /// observer is registered. See [`crate::DomMutation`].
244    pub(crate) mutation_log: Option<Vec<crate::DomMutation>>,
245    /// CSS media type used to evaluate `@media` rules.
246    pub(crate) media_type: MediaType,
247    /// Strategy for Stylo's style traversal during `resolve`.
248    pub(crate) style_threading: StyleThreading,
249    /// Whether incremental layout is enabled for this document.
250    pub(crate) incremental_layout: bool,
251    /// How deeply this document is nested within other documents
252    /// (0 for a root document). Used to limit `<iframe>` nesting depth.
253    pub(crate) subdocument_depth: usize,
254
255    // Events
256    pub(crate) tx: Sender<DocumentEvent>,
257    // rx will always be Some, except temporarily while processing events
258    pub(crate) rx: Option<Receiver<DocumentEvent>>,
259
260    /// A slotmap-backed tree of nodes
261    ///
262    /// We pin the tree to a guarantee to the nodes it creates that the tree is stable in memory.
263    /// There is no way to create the tree - publicly or privately - that would invalidate that invariant.
264    pub(crate) nodes: Box<NodeTree>,
265
266    /// The id of the root node (a Document node)
267    pub(crate) root_node_id: NodeId,
268
269    /// For each `position: fixed` node reparented onto the root element, the
270    /// layout parent it was taken from.
271    ///
272    /// Hoisting gives a fixed node the viewport as its containing block, which
273    /// is what CSS asks for. It must not also decide which stacking context the
274    /// node paints in: that follows the box tree, and the two are independent.
275    /// Without this record the node joins the root's stacking context, so a
276    /// negative z-index fixed layer inside an `isolation: isolate` ancestor
277    /// paints beneath every background between them and disappears.
278    pub(crate) hoisted_fixed_parents: HashMap<NodeId, NodeId>,
279
280    /// Every `position: fixed` node whose containing block is the viewport,
281    /// which is every one of them except those under a transformed ancestor.
282    ///
283    /// Collected by the walk that hoists them, and used by
284    /// `resolve_fixed_positions` to hold them still while the page scrolls.
285    pub(crate) fixed_nodes: Vec<NodeId>,
286
287    /// The viewport scroll currently baked into those nodes' locations.
288    ///
289    /// One value for the whole document rather than one per node: the pin is
290    /// the same displacement for every fixed box, because they all share the
291    /// viewport as their containing block. Reset by `resolve_layout`, which
292    /// rewrites the locations it was added to.
293    pub(crate) fixed_scroll_offset: crate::Point<f64>,
294
295    /// Every `position: sticky` node in the document, in tree order.
296    ///
297    /// Collected by the same walk that hoists fixed nodes, because both need
298    /// one pre-order pass over the box tree and a second one would cost the
299    /// same on every frame of every document, sticky or not. Tree order matters:
300    /// a sticky box inside another sticky box is adjusted on top of its
301    /// ancestor's adjustment, so the ancestor has to be settled first.
302    pub(crate) sticky_nodes: Vec<NodeId>,
303
304    /// For each sticky node, the offset currently baked into its
305    /// `final_layout().location`.
306    ///
307    /// The adjustment is written into the box itself so that paint, hit testing
308    /// and `absolute_position` cannot disagree about where the box is. That
309    /// makes the pass non-idempotent unless it can recover the flow position it
310    /// started from, which is what this records. Cleared by `resolve_layout`,
311    /// which rewrites every location from taffy and so discards the offsets
312    /// along with them.
313    pub(crate) sticky_offsets: HashMap<NodeId, taffy::Point<f32>>,
314
315    /// Stacking contexts holding a hoisted child that an ancestor clips.
316    ///
317    /// Collected while flushing styles so that `resolve_hoisted_clips` visits
318    /// those contexts alone, rather than scanning every node in the document
319    /// after every layout to find the handful that hoist anything at all.
320    pub(crate) hoisted_clip_hosts: Vec<NodeId>,
321
322    // Stylo
323    /// The Stylo engine
324    pub(crate) stylist: Stylist,
325    pub(crate) animations: DocumentAnimationSet,
326    /// Monotonic animation clock used by the most recent resolve.
327    ///
328    /// Embedders may inspect or capture between window frames. A diagnostic
329    /// caller historically passed `0.0` and rewound every CSS transition in
330    /// the document; retaining the high-water mark makes that impossible at
331    /// the document boundary.
332    pub(crate) last_resolve_animation_time: f64,
333    /// Stylo shared lock
334    pub(crate) guard: SharedRwLock,
335    /// Stylo invalidation map. We insert into this map prior to mutating nodes.
336    pub(crate) snapshots: SnapshotMap,
337
338    // Parley contexts
339    /// A Parley font context
340    pub(crate) font_ctx: Arc<Mutex<parley::FontContext>>,
341    #[cfg(feature = "parallel-construct")]
342    /// Thread-and-document-local copies to the font context
343    pub(crate) thread_font_contexts: ThreadLocal<RefCell<Box<FontContext>>>,
344    /// A Parley layout context
345    pub(crate) layout_ctx: parley::LayoutContext<TextBrush>,
346
347    /// The real (non-anonymous) node which is currently hovered (if any).
348    /// This is never a layout-generated (anonymous) node, so it remains valid
349    /// across box-tree reconstruction.
350    pub(crate) hover_node_id: Option<NodeId>,
351    /// The precise (may be anonymous) layout node under the pointer (if any).
352    /// This can be invalidated by box-tree reconstruction, and is re-resolved against
353    /// fresh layout at the end of every `resolve` pass.
354    pub(crate) hover_hit_node_id: Option<NodeId>,
355    /// Whether the node which is currently hovered is a text node/span
356    pub(crate) hover_node_is_text: bool,
357    /// The last known pointer position in client coordinates (viewport-relative, unscrolled).
358    pub(crate) last_client_pointer_position: Option<taffy::Point<f32>>,
359    /// Exact DOM target selected by semantic automation.
360    ///
361    /// Window pointers leave this empty and are re-hit-tested after layout.
362    /// An inspector already resolved identity, so re-hit-testing its synthetic
363    /// centre coordinate would silently replace that target with an overlap.
364    pub(crate) semantic_hover_node_id: Option<NodeId>,
365    /// The node which is currently focussed (if any)
366    pub(crate) focus_node_id: Option<NodeId>,
367    /// The node which is currently active (if any)
368    pub(crate) active_node_id: Option<NodeId>,
369    /// The node which recieved a mousedown event (if any)
370    pub(crate) mousedown_node_id: Option<NodeId>,
371    /// The last time a mousedown was made (for double-click detection)
372    pub(crate) last_mousedown_time: Option<Instant>,
373    /// The position where mousedown occurred (for selection drags and double-click detection)
374    pub(crate) mousedown_position: taffy::Point<f32>,
375    /// How many clicks have been made in quick succession
376    pub(crate) click_count: u16,
377    /// Whether we're currently in a text selection drag (moved 2px+ from mousedown)
378    pub(crate) drag_mode: DragMode,
379    /// The scrollbar thumb currently under the pointer, if any
380    pub(crate) hovered_scrollbar: Option<crate::node::ScrollbarRef>,
381    /// When each scroll container's overlay scrollbars were last shown
382    /// (scrolled, or the pointer left the thumb); drives their fade-out
383    pub(crate) scrollbar_activity: HashMap<NodeId, Instant>,
384    /// Whether and what kind of scroll animation is currently in progress
385    pub(crate) scroll_animation: ScrollAnimationState,
386
387    /// Text selection state (for non-input text)
388    pub(crate) text_selection: TextSelection,
389
390    // TODO: collapse animating state into a bitflags
391    /// Whether there are active CSS animations/transitions (so we should re-render every frame)
392    pub(crate) has_active_animations: bool,
393    /// Whether there is a `<canvas>` element in the DOM (so we should re-render every frame)
394    pub(crate) has_canvas: bool,
395    /// The most urgent animation cadence required by any subdocument.
396    pub(crate) subdoc_animation_pacing: AnimationPacing,
397
398    /// Map of id attribute values to node IDs for fast lookups.
399    /// May contain multiple nodes for the same id: `get_element_by_id`
400    /// returns the first in tree order.
401    pub(crate) nodes_to_id: HashMap<String, SmallVec<[NodeId; 1]>>,
402    /// Map of `<style>` and `<link>` node IDs to their associated stylesheet
403    pub(crate) nodes_to_stylesheet: BTreeMap<NodeId, DocumentStyleSheet>,
404    /// Stylesheets added by the useragent
405    /// where the key is the hashed CSS
406    pub(crate) ua_stylesheets: HashMap<String, DocumentStyleSheet>,
407    /// Map from form control node ID's to their associated forms node ID's
408    pub(crate) controls_to_form: HashMap<NodeId, NodeId>,
409    /// Nodes that contain sub documents
410    pub(crate) sub_document_nodes: HashSet<NodeId>,
411    /// Load state (abort controller and in-flight request id) for each
412    /// `<iframe>` element whose sub-document is loaded automatically
413    pub(crate) iframe_loads: HashMap<NodeId, crate::iframe::IframeLoad>,
414    /// Nodes whose layout construction is waiting for a later pass.
415    pub(crate) deferred_construction_nodes: Vec<ConstructionTask>,
416    /// Which parts of the document differ from the previously painted frame.
417    ///
418    /// Off unless a consumer asks for it, so a document that never questions
419    /// its own frames does not pay to answer. See
420    /// [`set_paint_damage_tracking`](Self::set_paint_damage_tracking).
421    pub(crate) paint_damage: crate::paint_damage::PaintDamageTracker,
422
423    /// Nodes that contain custom widgets
424    #[cfg(feature = "custom-widget")]
425    pub(crate) custom_widget_nodes: HashSet<NodeId>,
426    /// Rendering resources allocated by custom widgets that should be deallocated during the next render
427    #[cfg(feature = "custom-widget")]
428    pub(crate) pending_resource_deallocations: Vec<anyrender::ResourceId>,
429
430    /// Registry of custom element definitions keyed by tag name
431    #[cfg(feature = "shadow-dom")]
432    pub(crate) custom_element_registry: crate::node::CustomElementRegistry,
433    /// Nodes that are shadow hosts (have an attached shadow root)
434    #[cfg(feature = "shadow-dom")]
435    pub(crate) shadow_host_nodes: HashSet<NodeId>,
436    /// Nodes that have an attached custom element controller
437    #[cfg(feature = "shadow-dom")]
438    pub(crate) custom_element_nodes: HashSet<NodeId>,
439
440    /// Cache of loaded images, keyed by URL. Allows reusing images across multiple
441    /// elements without re-fetching from the network.
442    pub(crate) image_cache: HashMap<String, ImageData>,
443
444    /// Tracks in-flight image requests. When an image is being fetched, additional
445    /// requests for the same URL are queued here instead of starting new fetches.
446    /// Value is a list of (node_id, image_type) pairs waiting for the image.
447    pub(crate) pending_images: HashMap<String, Vec<(NodeId, ImageType)>>,
448
449    // Tracks in-flight "critical" resources (e.g. stylesheets linked from the `<head>`),
450    // keyed by request id
451    pub(crate) pending_critical_resources: HashSet<usize>,
452
453    // Service providers
454    /// Network provider. Can be used to fetch assets.
455    pub net_provider: Arc<dyn NetProvider>,
456    /// Navigation provider. Can be used to navigate to a new page (bubbles up the event
457    /// on e.g. clicking a Link)
458    pub navigation_provider: Arc<dyn NavigationProvider>,
459    /// Shell provider. Can be used to request a redraw or set the cursor icon
460    pub shell_provider: Arc<dyn ShellProvider>,
461    /// HTML parser provider. Used to parse HTML for setInnerHTML
462    pub html_parser_provider: Arc<dyn HtmlParserProvider>,
463    /// Carried on every sub-resource `Request` this document issues; aborting
464    /// it cancels all in-flight fetches tied to this document. Set via
465    /// [`DocumentConfig::abort_signal`].
466    pub(crate) abort_signal: Option<AbortSignal>,
467}
468
469pub(crate) fn make_device(
470    viewport: &Viewport,
471    media_type: MediaType,
472    font_ctx: Arc<Mutex<FontContext>>,
473) -> Device {
474    let width = viewport.window_size.0 as f32 / viewport.scale();
475    let height = viewport.window_size.1 as f32 / viewport.scale();
476    let viewport_size = euclid::Size2D::new(width, height);
477    let device_size = euclid::Size2D::new(width, height) * viewport.scale();
478    let device_pixel_ratio = euclid::Scale::new(viewport.scale());
479
480    Device::new(
481        media_type,
482        selectors::matching::QuirksMode::NoQuirks,
483        viewport_size,
484        device_size,
485        device_pixel_ratio,
486        Box::new(BlitzFontMetricsProvider { font_ctx }),
487        ComputedValues::initial_values_with_font_override(Font::initial_values()),
488        match viewport.color_scheme {
489            ColorScheme::Light => PrefersColorScheme::Light,
490            ColorScheme::Dark => PrefersColorScheme::Dark,
491        },
492        PointerCapabilities::default(),
493        PointerCapabilities::default(),
494    )
495}
496
497/// Whether layout reuses its caches, and how that can be overridden at runtime.
498///
499/// Incremental layout is on unless a caller or the environment turns it off.
500///
501/// The environment override exists so a single build can be measured both ways:
502/// with it off every `resolve` clears the Taffy cache and re-shapes every inline
503/// root from scratch, so comparing the two in separate binaries would also
504/// compare two different compilations. `BLITZ_INCREMENTAL=0` forces the old
505/// behaviour, `=1` forces the new one.
506///
507/// This used to fall back to `cfg!(feature = "incremental")`. That feature is
508/// gone, replaced by `DocumentConfig::incremental`, and for a while afterwards
509/// this function was never called at all: the config read
510/// `unwrap_or(true)` directly, so `BLITZ_INCREMENTAL` was accepted and ignored.
511fn incremental_layout_default() -> bool {
512    !matches!(
513        std::env::var("BLITZ_INCREMENTAL").ok().as_deref(),
514        Some("0" | "false" | "off")
515    )
516}
517
518impl BaseDocument {
519    /// Create a new (empty) [`BaseDocument`] with the specified configuration
520    pub fn new(config: DocumentConfig) -> Self {
521        static ID_GENERATOR: AtomicUsize = AtomicUsize::new(1);
522
523        let id = ID_GENERATOR.fetch_add(1, Ordering::SeqCst);
524
525        let font_ctx = config
526            .font_ctx
527            .map(|mut font_ctx| {
528                font_ctx.source_cache.make_shared();
529                // font_ctx.collection.make_shared();
530                font_ctx
531            })
532            .unwrap_or_else(|| {
533                use parley::fontique::{Collection, CollectionOptions, SourceCache};
534                let mut font_ctx = FontContext {
535                    source_cache: SourceCache::new_shared(),
536                    collection: Collection::new(CollectionOptions {
537                        shared: false,
538                        system_fonts: cfg!(all(
539                            feature = "system-fonts",
540                            not(target_arch = "wasm32")
541                        )),
542                    }),
543                };
544                font_ctx
545                    .collection
546                    .register_fonts(Blob::new(Arc::new(crate::BULLET_FONT) as _), None);
547                font_ctx
548            });
549        let font_ctx = Arc::new(Mutex::new(font_ctx));
550
551        // Make sure we turn on stylo features *before* creating the Stylist
552        style_config::set_pref!("layout.grid.enabled", true);
553        style_config::set_pref!("layout.unimplemented", true);
554        style_config::set_pref!("layout.columns.enabled", true);
555        style_config::set_pref!("layout.css.basic-shape-shape.enabled", true);
556        style_config::set_pref!("layout.threads", -1);
557
558        let viewport = config.viewport.unwrap_or_default();
559        let media_type = config.media_type.unwrap_or_else(MediaType::screen);
560        let device = make_device(&viewport, media_type.clone(), font_ctx.clone());
561        let stylist = Stylist::new(device, QuirksMode::NoQuirks);
562        let snapshots = SnapshotMap::new();
563        let nodes = Box::new(NodeTree::new());
564        let guard = SharedRwLock::new();
565        let nodes_to_id = HashMap::new();
566
567        let base_url = config
568            .base_url
569            .and_then(|url| DocumentUrl::from_str(&url).ok())
570            .unwrap_or_default();
571
572        let net_provider = config
573            .net_provider
574            .unwrap_or_else(|| Arc::new(DummyNetProvider));
575        let navigation_provider = config
576            .navigation_provider
577            .unwrap_or_else(|| Arc::new(DummyNavigationProvider));
578        let shell_provider = config
579            .shell_provider
580            .unwrap_or_else(|| Arc::new(DummyShellProvider));
581        let html_parser_provider = config
582            .html_parser_provider
583            .unwrap_or_else(|| Arc::new(DummyHtmlParserProvider));
584
585        let (tx, rx) = channel();
586
587        let mut doc = Self {
588            hoisted_fixed_parents: HashMap::new(),
589            fixed_nodes: Vec::new(),
590            fixed_scroll_offset: crate::Point::ZERO,
591            sticky_nodes: Vec::new(),
592            sticky_offsets: HashMap::new(),
593            hoisted_clip_hosts: Vec::new(),
594            id,
595            tx,
596            rx: Some(rx),
597
598            guard,
599            nodes,
600            root_node_id: NodeId::default(),
601            stylist,
602            animations: DocumentAnimationSet::default(),
603            last_resolve_animation_time: 0.0,
604            snapshots,
605            nodes_to_id,
606            viewport,
607            media_type,
608            style_threading: config.style_threading,
609            incremental_layout: config
610                .incremental
611                .unwrap_or_else(incremental_layout_default),
612            subdocument_depth: config.subdocument_depth,
613            devtool_settings: DevtoolSettings::default(),
614            viewport_scroll: crate::Point::ZERO,
615            mutation_log: None,
616            url: base_url,
617            ua_stylesheets: HashMap::new(),
618            nodes_to_stylesheet: BTreeMap::new(),
619            font_ctx,
620            #[cfg(feature = "parallel-construct")]
621            thread_font_contexts: ThreadLocal::new(),
622            layout_ctx: parley::LayoutContext::new(),
623
624            hover_node_id: None,
625            hover_hit_node_id: None,
626            hover_node_is_text: false,
627            last_client_pointer_position: None,
628            semantic_hover_node_id: None,
629            focus_node_id: None,
630            active_node_id: None,
631            mousedown_node_id: None,
632            has_active_animations: false,
633            subdoc_animation_pacing: AnimationPacing::Idle,
634            has_canvas: false,
635            sub_document_nodes: HashSet::new(),
636            iframe_loads: HashMap::new(),
637
638            #[cfg(feature = "custom-widget")]
639            custom_widget_nodes: HashSet::new(),
640            #[cfg(feature = "custom-widget")]
641            pending_resource_deallocations: Vec::new(),
642
643            #[cfg(feature = "shadow-dom")]
644            custom_element_registry: crate::node::CustomElementRegistry::new(),
645            #[cfg(feature = "shadow-dom")]
646            shadow_host_nodes: HashSet::new(),
647            #[cfg(feature = "shadow-dom")]
648            custom_element_nodes: HashSet::new(),
649
650            deferred_construction_nodes: Vec::new(),
651            paint_damage: Default::default(),
652            image_cache: HashMap::new(),
653            pending_images: HashMap::new(),
654            pending_critical_resources: HashSet::new(),
655            controls_to_form: HashMap::new(),
656            net_provider,
657            navigation_provider,
658            shell_provider,
659            html_parser_provider,
660            abort_signal: config.abort_signal,
661            last_mousedown_time: None,
662            mousedown_position: taffy::Point::ZERO,
663            click_count: 0,
664            drag_mode: DragMode::None,
665            hovered_scrollbar: None,
666            scrollbar_activity: HashMap::new(),
667            scroll_animation: ScrollAnimationState::None,
668            text_selection: TextSelection::default(),
669        };
670
671        // Initialise document with root Document node
672        doc.root_node_id = doc.create_node(NodeData::Document(Box::default()));
673        doc.root_node_mut().flags.insert(NodeFlags::IS_IN_DOCUMENT);
674
675        match config.ua_stylesheets {
676            Some(stylesheets) => {
677                for ss in &stylesheets {
678                    doc.add_user_agent_stylesheet(ss);
679                }
680            }
681            None => doc.add_user_agent_stylesheet(DEFAULT_CSS),
682        }
683
684        // Stylo data on the root node container is needed to render the node
685        let stylo_element_data = StyloElementData {
686            styles: ElementStyles {
687                primary: Some(
688                    ComputedValues::initial_values_with_font_override(Font::initial_values())
689                        .to_arc(),
690                ),
691                ..Default::default()
692            },
693            ..Default::default()
694        };
695        let stylo_data = doc.root_node_mut().stylo_element_data_mut();
696        *stylo_data.ensure_init_mut() = stylo_element_data;
697
698        doc
699    }
700
701    /// Set the Document's networking provider
702    pub fn set_net_provider(&mut self, net_provider: Arc<dyn NetProvider>) {
703        self.net_provider = net_provider;
704    }
705
706    /// Set the Document's navigation provider
707    pub fn set_navigation_provider(&mut self, navigation_provider: Arc<dyn NavigationProvider>) {
708        self.navigation_provider = navigation_provider;
709    }
710
711    /// Set the Document's shell provider
712    pub fn set_shell_provider(&mut self, shell_provider: Arc<dyn ShellProvider>) {
713        self.shell_provider = shell_provider;
714    }
715
716    /// Set the Document's html parser provider
717    pub fn set_html_parser_provider(&mut self, html_parser_provider: Arc<dyn HtmlParserProvider>) {
718        self.html_parser_provider = html_parser_provider;
719    }
720
721    /// Set base url for resolving linked resources (stylesheets, images, fonts, etc)
722    pub fn set_base_url(&mut self, url: &str) {
723        self.url = DocumentUrl::from(Url::parse(url).unwrap());
724    }
725
726    pub fn guard(&self) -> &SharedRwLock {
727        &self.guard
728    }
729
730    pub fn tree(&self) -> &NodeTree {
731        &self.nodes
732    }
733
734    pub fn id(&self) -> usize {
735        self.id
736    }
737
738    /// Wrapper around [`crate::net::stamped_request`]. Use the free function
739    /// when `&self` would conflict with a held `&mut` borrow on a field.
740    pub(crate) fn build_request(&self, url: url::Url) -> Request {
741        crate::net::stamped_request(url, self.abort_signal.as_ref())
742    }
743
744    pub fn favicon_url(&self) -> Option<String> {
745        self.tree().iter().find_map(|(_, node)| {
746            let data = &node.data;
747            if !data.is_element_with_tag_name(&local_name!("link")) {
748                return None;
749            }
750            let rel = data.attr(local_name!("rel"))?;
751            if !rel
752                .split_ascii_whitespace()
753                .any(|v| v.eq_ignore_ascii_case("icon"))
754            {
755                return None;
756            }
757            data.attr(local_name!("href")).map(|s| s.to_string())
758        })
759    }
760
761    pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
762        self.nodes.get(node_id)
763    }
764
765    pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
766        self.nodes.get_mut(node_id)
767    }
768
769    pub fn get_focussed_node_id(&self) -> Option<NodeId> {
770        self.focus_node_id
771            .or(self.try_root_element().map(|el| el.id))
772    }
773
774    pub fn mutate<'doc>(&'doc mut self) -> DocumentMutator<'doc> {
775        DocumentMutator::new(self)
776    }
777
778    pub fn handle_dom_event<F: FnMut(DomEvent)>(
779        &mut self,
780        event: &mut DomEvent,
781        dispatch_event: F,
782    ) {
783        handle_dom_event(self, event, dispatch_event)
784    }
785
786    pub fn as_any_mut(&mut self) -> &mut dyn Any {
787        self
788    }
789
790    /// Find the label's bound input elements:
791    /// the element id referenced by the "for" attribute of a given label element
792    /// or the first input element which is nested in the label
793    /// Note that although there should only be one bound element,
794    /// we return all possibilities instead of just the first
795    /// in order to allow the caller to decide which one is correct
796    pub fn label_bound_input_element(&self, label_node_id: NodeId) -> Option<&Node> {
797        let label_element = self.nodes[label_node_id].element_data()?;
798        if let Some(target_element_dom_id) = label_element.attr(local_name!("for")) {
799            TreeTraverser::new(self)
800                .filter_map(|id| {
801                    let node = self.get_node(id)?;
802                    let element_data = node.element_data()?;
803                    if element_data.name.local != local_name!("input") {
804                        return None;
805                    }
806                    let id = element_data.id.as_ref()?;
807                    if *id == *target_element_dom_id {
808                        Some(node)
809                    } else {
810                        None
811                    }
812                })
813                .next()
814        } else {
815            TreeTraverser::new_with_root(self, label_node_id)
816                .filter_map(|child_id| {
817                    let node = self.get_node(child_id)?;
818                    let element_data = node.element_data()?;
819                    if element_data.name.local == local_name!("input") {
820                        Some(node)
821                    } else {
822                        None
823                    }
824                })
825                .next()
826        }
827    }
828
829    /// The checkedness a click changed, kept so the click's *canceled
830    /// activation steps* can put it back when a listener calls
831    /// `preventDefault()`.
832    ///
833    /// A radio carries its whole set, because selecting one clears the others.
834    pub fn run_pre_click_activation(&mut self, target: NodeId) -> Option<PreClickActivation> {
835        let node_id = crate::events::pointer::checkable_activation_target(self, target)?;
836        let el = self.get_node(node_id)?.data.downcast_element()?;
837        let is_radio = el.attr(local_name!("type")) == Some("radio");
838
839        if !is_radio {
840            let previous = el.checkbox_input_checked()?;
841            let el = self.get_node_mut(node_id)?.data.downcast_element_mut()?;
842            Self::toggle_checkbox(el);
843            return Some(PreClickActivation {
844                previous: vec![(node_id, previous)],
845            });
846        }
847
848        let radio_set = el.attr(local_name!("name")).map(str::to_string);
849        let Some(radio_set) = radio_set else {
850            let previous = el.checkbox_input_checked()?;
851            let el = self.get_node_mut(node_id)?.data.downcast_element_mut()?;
852            *el.checkbox_input_checked_mut()? = true;
853            return Some(PreClickActivation {
854                previous: vec![(node_id, previous)],
855            });
856        };
857
858        // Recorded *while* selecting rather than before it. Selecting one radio
859        // clears every other in the set, so cancelling has to restore all of
860        // them, and reading them first meant walking the whole arena twice for
861        // one press. The membership test is `toggle_radio`'s, deliberately: two
862        // predicates that disagreed would restore a different set than the one
863        // that changed.
864        //
865        // That predicate is name plus "has checkbox state", which is neither
866        // scoped to `type=radio` nor to a form owner. Wrong per HTML, and
867        // longstanding; matching it here keeps this change to the ordering it
868        // is about.
869        let mut previous: Vec<(NodeId, bool)> = Vec::new();
870        for (id, node) in self.nodes.iter_mut() {
871            let Some(el) = node.data.downcast_element_mut() else {
872                continue;
873            };
874            if el.attr(local_name!("name")) != Some(&*radio_set) {
875                continue;
876            }
877            let Some(is_checked) = el.checkbox_input_checked_mut() else {
878                continue;
879            };
880            previous.push((id, *is_checked));
881            *is_checked = id == node_id;
882        }
883        Some(PreClickActivation { previous })
884    }
885
886    /// Undo [`Self::run_pre_click_activation`]. The click's *canceled
887    /// activation steps*.
888    pub fn undo_pre_click_activation(&mut self, activation: PreClickActivation) {
889        for (node_id, was_checked) in activation.previous {
890            let Some(node) = self.get_node_mut(node_id) else {
891                continue;
892            };
893            let Some(el) = node.data.downcast_element_mut() else {
894                continue;
895            };
896            if let Some(is_checked) = el.checkbox_input_checked_mut() {
897                *is_checked = was_checked;
898            }
899        }
900    }
901
902    pub fn toggle_checkbox(el: &mut ElementData) -> bool {
903        let Some(is_checked) = el.checkbox_input_checked_mut() else {
904            return false;
905        };
906        *is_checked = !*is_checked;
907
908        *is_checked
909    }
910
911    pub fn toggle_radio(&mut self, radio_set_name: String, target_radio_id: NodeId) {
912        for (i, node) in self.nodes.iter_mut() {
913            if let Some(node_data) = node.data.downcast_element_mut() {
914                if node_data.attr(local_name!("name")) == Some(&radio_set_name) {
915                    let was_clicked = i == target_radio_id;
916                    let Some(is_checked) = node_data.checkbox_input_checked_mut() else {
917                        continue;
918                    };
919                    *is_checked = was_clicked;
920                }
921            }
922        }
923    }
924
925    /// Toggle the `open` attribute of a `<details>` element, expanding or
926    /// collapsing it. This is the default action triggered when the element's
927    /// first `<summary>` child is activated.
928    pub fn toggle_details_open(&mut self, details_id: NodeId) {
929        use crate::qual_name;
930
931        let node = &self.nodes[details_id];
932        if !node.data.is_element_with_tag_name(&local_name!("details")) {
933            return;
934        }
935        let is_open = node.data.has_attr(local_name!("open"));
936
937        // Note: HTML attributes are in the empty (null) namespace, so the
938        // QualName must not use the html namespace here, else it won't match
939        // an `open` attribute created by the HTML parser.
940        let mut mutator = self.mutate();
941        if is_open {
942            mutator.clear_attribute(details_id, qual_name!("open"));
943        } else {
944            mutator.set_attribute(details_id, qual_name!("open"), "");
945        }
946        drop(mutator);
947
948        self.shell_provider.request_redraw();
949    }
950
951    pub fn set_style_property(&mut self, node_id: NodeId, name: &str, value: &str) {
952        let node = &mut self.nodes[node_id];
953        let did_change = node.element_data_mut().unwrap().set_style_property(
954            name,
955            value,
956            &self.guard,
957            self.url.url_extra_data(),
958        );
959        if did_change {
960            node.mark_style_attr_updated();
961        }
962    }
963
964    pub fn remove_style_property(&mut self, node_id: NodeId, name: &str) {
965        let node = &mut self.nodes[node_id];
966        let did_change = node.element_data_mut().unwrap().remove_style_property(
967            name,
968            &self.guard,
969            self.url.url_extra_data(),
970        );
971        if did_change {
972            node.mark_style_attr_updated();
973        }
974    }
975
976    pub fn sub_document_node_ids(&self) -> Vec<NodeId> {
977        self.sub_document_nodes.iter().copied().collect()
978    }
979
980    pub fn set_sub_document(&mut self, node_id: NodeId, sub_document: Box<dyn Document>) {
981        self.nodes[node_id]
982            .element_data_mut()
983            .unwrap()
984            .set_sub_document(sub_document);
985        self.sub_document_nodes.insert(node_id);
986    }
987
988    pub fn remove_sub_document(&mut self, node_id: NodeId) {
989        self.nodes[node_id]
990            .element_data_mut()
991            .unwrap()
992            .remove_sub_document();
993        self.sub_document_nodes.remove(&node_id);
994        if let Some(load) = self.iframe_loads.remove(&node_id) {
995            load.abort_controller.abort();
996        }
997    }
998
999    /// Poll all sub-documents (see [`Document::poll`]), allowing them to make progress
1000    /// on any pending async operations (e.g. JavaScript timers). Hosts which poll a
1001    /// wrapper around a [`BaseDocument`] should call this from their `poll` implementation.
1002    ///
1003    /// Returns `true` if any sub-document reported changes.
1004    pub fn poll_subdocuments(&mut self, waker: Option<&Waker>) -> bool {
1005        let mut has_changes = false;
1006        let node_ids: Vec<NodeId> = self.sub_document_nodes.iter().copied().collect();
1007        for node_id in node_ids {
1008            let Some(sub_doc) = self
1009                .nodes
1010                .get_mut(node_id)
1011                .and_then(|node| node.subdoc_mut())
1012            else {
1013                continue;
1014            };
1015            let task_context = waker.map(TaskContext::from_waker);
1016            has_changes |= sub_doc.poll(task_context);
1017        }
1018        has_changes
1019    }
1020
1021    #[cfg(feature = "custom-widget")]
1022    pub fn custom_widget_node_ids(&self) -> Vec<NodeId> {
1023        self.custom_widget_nodes.iter().copied().collect()
1024    }
1025
1026    #[cfg(feature = "custom-widget")]
1027    pub fn take_pending_resource_deallocations(&mut self) -> Vec<anyrender::ResourceId> {
1028        std::mem::take(&mut self.pending_resource_deallocations)
1029    }
1030
1031    #[cfg(feature = "custom-widget")]
1032    pub fn set_custom_widget(&mut self, node_id: NodeId, widget: Box<dyn crate::Widget>) {
1033        self.nodes[node_id]
1034            .element_data_mut()
1035            .unwrap()
1036            .set_custom_widget(widget);
1037        self.custom_widget_nodes.insert(node_id);
1038    }
1039
1040    #[cfg(feature = "custom-widget")]
1041    pub fn remove_custom_widget(&mut self, node_id: NodeId) {
1042        let resources_to_deallocate = self.nodes[node_id]
1043            .element_data_mut()
1044            .unwrap()
1045            .remove_custom_widget();
1046        self.pending_resource_deallocations
1047            .extend_from_slice(&resources_to_deallocate);
1048        self.custom_widget_nodes.remove(&node_id);
1049    }
1050
1051    /// Mutable access to the custom element registry. Use
1052    /// [`CustomElementRegistry::define`](crate::node::CustomElementRegistry::define)
1053    /// to register custom elements by tag name.
1054    #[cfg(feature = "shadow-dom")]
1055    pub fn custom_elements_mut(&mut self) -> &mut crate::node::CustomElementRegistry {
1056        &mut self.custom_element_registry
1057    }
1058
1059    /// Register a custom element definition against a tag name (analogous to
1060    /// `customElements.define`).
1061    #[cfg(feature = "shadow-dom")]
1062    pub fn define_custom_element(
1063        &mut self,
1064        name: markup5ever::LocalName,
1065        definition: crate::node::CustomElementDefinition,
1066    ) {
1067        self.custom_element_registry.define(name, definition);
1068    }
1069
1070    /// The node ids of all shadow hosts in the document.
1071    #[cfg(feature = "shadow-dom")]
1072    pub fn shadow_host_node_ids(&self) -> Vec<NodeId> {
1073        self.shadow_host_nodes.iter().copied().collect()
1074    }
1075
1076    /// If `host_id` is a shadow host, returns the node id of its shadow root.
1077    #[cfg(feature = "shadow-dom")]
1078    pub fn shadow_root_id(&self, host_id: NodeId) -> Option<NodeId> {
1079        self.get_node(host_id)
1080            .and_then(|node| node.shadow_root_id())
1081    }
1082
1083    /// Attach a shadow root to the given host element, returning the node id of
1084    /// the newly-created shadow root. If the host already has a shadow root, its
1085    /// existing shadow root id is returned unchanged.
1086    #[cfg(feature = "shadow-dom")]
1087    pub fn attach_shadow(&mut self, host_id: NodeId, mode: crate::node::ShadowRootMode) -> NodeId {
1088        if let Some(existing) = self.nodes[host_id].shadow_root_id() {
1089            return existing;
1090        }
1091
1092        let shadow_root_id = self.create_node(NodeData::ShadowRoot(
1093            crate::node::ShadowRootData::new(host_id, mode),
1094        ));
1095
1096        // The shadow root's parent is the host. It is *not* added to the host's
1097        // `children` list (which holds light-DOM children); it is referenced via
1098        // the host's `ElementData::shadow_root` field instead.
1099        self.nodes[shadow_root_id].parent = Some(host_id);
1100        if self.nodes[host_id].flags.is_in_document() {
1101            self.nodes[shadow_root_id]
1102                .flags
1103                .insert(NodeFlags::IS_IN_DOCUMENT);
1104        }
1105
1106        self.nodes[host_id]
1107            .element_data_mut()
1108            .expect("Shadow host must be an element")
1109            .shadow_root = Some(shadow_root_id);
1110        self.shadow_host_nodes.insert(host_id);
1111
1112        // Host needs its box tree rebuilt to account for the shadow tree.
1113        self.nodes[host_id].insert_damage(ALL_DAMAGE);
1114        self.nodes[host_id].mark_ancestors_dirty();
1115
1116        shadow_root_id
1117    }
1118
1119    /// Detach (and drop) the shadow root of the given host element, if any.
1120    #[cfg(feature = "shadow-dom")]
1121    pub fn detach_shadow(&mut self, host_id: NodeId) {
1122        let shadow_root_id = self.nodes[host_id]
1123            .element_data_mut()
1124            .and_then(|el| el.shadow_root.take());
1125        if let Some(shadow_root_id) = shadow_root_id {
1126            self.drop_node_ignoring_parent(shadow_root_id);
1127            self.shadow_host_nodes.remove(&host_id);
1128            self.nodes[host_id].insert_damage(ALL_DAMAGE);
1129            self.nodes[host_id].mark_ancestors_dirty();
1130        }
1131    }
1132
1133    /// Attach a custom element controller to the given node.
1134    #[cfg(feature = "shadow-dom")]
1135    pub fn set_custom_element(
1136        &mut self,
1137        node_id: NodeId,
1138        controller: Box<dyn crate::node::CustomElement>,
1139    ) {
1140        use crate::node::{CustomElementData, SpecialElementData};
1141        self.nodes[node_id]
1142            .element_data_mut()
1143            .expect("Custom element host must be an element")
1144            .special_data = SpecialElementData::CustomElement(CustomElementData::new(controller));
1145        self.custom_element_nodes.insert(node_id);
1146    }
1147
1148    /// Detach the custom element controller from the given node (without running
1149    /// the `disconnected` callback). Returns the controller if present.
1150    #[cfg(feature = "shadow-dom")]
1151    pub fn take_custom_element(
1152        &mut self,
1153        node_id: NodeId,
1154    ) -> Option<Box<dyn crate::node::CustomElement>> {
1155        use crate::node::SpecialElementData;
1156        self.custom_element_nodes.remove(&node_id);
1157        let element = self.nodes[node_id].element_data_mut()?;
1158        if matches!(element.special_data, SpecialElementData::CustomElement(_)) {
1159            if let SpecialElementData::CustomElement(mut data) = element.special_data.take() {
1160                return data.controller.take();
1161            }
1162        }
1163        None
1164    }
1165
1166    pub fn root_node(&self) -> &Node {
1167        &self.nodes[self.root_node_id]
1168    }
1169
1170    pub fn root_node_mut(&mut self) -> &mut Node {
1171        &mut self.nodes[self.root_node_id]
1172    }
1173
1174    /// Ask this document to work out which regions differ between frames.
1175    ///
1176    /// Off by default. A consumer that turns it on is charged one pass over the
1177    /// node list per [`resolve`](Self::resolve) - a pass `resolve` already makes
1178    /// to clear damage - plus a hash lookup and a rectangle comparison per node.
1179    /// Nothing else in the document reads the result, so leaving it off costs a
1180    /// single branch.
1181    ///
1182    /// The consumer this exists for is a `backdrop-filter` cache. Blurring what
1183    /// is behind an element costs a render pass and a filter every frame, and
1184    /// the only way that stops being permanent is to skip the elements whose
1185    /// input has not changed. Turning this on is what makes that question
1186    /// answerable.
1187    ///
1188    /// The first frame after enabling reports everything as changed, because
1189    /// there is no previous frame to compare against.
1190    pub fn set_paint_damage_tracking(&mut self, enabled: bool) {
1191        self.paint_damage.set_enabled(enabled);
1192    }
1193
1194    /// Whether [`set_paint_damage_tracking`](Self::set_paint_damage_tracking) is on.
1195    pub fn paint_damage_tracking(&self) -> bool {
1196        self.paint_damage.is_enabled()
1197    }
1198
1199    /// What changed since the previously resolved frame.
1200    ///
1201    /// Empty when tracking is off, which is indistinguishable from "nothing
1202    /// changed" and deliberately so: a consumer that has not asked for the
1203    /// question to be answered must not read the empty answer as a licence to
1204    /// reuse a cache. Check
1205    /// [`paint_damage_tracking`](Self::paint_damage_tracking) first.
1206    pub fn paint_damage(&self) -> &crate::paint_damage::PaintDamage {
1207        self.paint_damage.damage()
1208    }
1209
1210    pub fn try_root_element(&self) -> Option<&Node> {
1211        TDocument::as_node(&self.root_node()).first_element_child()
1212    }
1213
1214    pub fn root_element(&self) -> &Node {
1215        TDocument::as_node(&self.root_node())
1216            .first_element_child()
1217            .unwrap()
1218            .as_element()
1219            .unwrap()
1220    }
1221
1222    pub fn create_node(&mut self, node_data: NodeData) -> NodeId {
1223        let tree_ptr = self.nodes.as_mut() as *mut NodeTree;
1224        let guard = self.guard.clone();
1225
1226        self.nodes
1227            .insert_with_key(|id| Node::new(tree_ptr, id, guard, node_data))
1228    }
1229
1230    /// Remove a node from the node tree, clearing any interaction state
1231    /// (hover/active/focus/mousedown/selection/drag/scrollbar) that references
1232    /// it so that stale NodeIds are never dereferenced after the slot is freed.
1233    pub(crate) fn remove_node_from_tree(&mut self, node_id: NodeId) -> Option<Node> {
1234        self.clear_interaction_state_for_removed_node(node_id);
1235        self.nodes.remove(node_id)
1236    }
1237
1238    /// The nearest element ancestor of `node_id` that is still in the
1239    /// document. Used to retarget hover/active state when the node they
1240    /// reference is removed. Tolerates already-removed ancestors (subtree
1241    /// teardown proceeds root-first) by giving up and returning `None`.
1242    fn nearest_surviving_element_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
1243        let mut current = self.get_node(node_id)?.parent;
1244        while let Some(id) = current {
1245            let node = self.get_node(id)?;
1246            if node.is_element() && node.flags.is_in_document() {
1247                return Some(id);
1248            }
1249            current = node.parent;
1250        }
1251        None
1252    }
1253
1254    /// Clear any interaction state (hover/active/focus/mousedown/selection/
1255    /// drag/scrollbar) that references `node_id`, which is being removed from
1256    /// the document, running the usual teardown steps. `node_id` must still be
1257    /// present in the slab.
1258    ///
1259    /// This matches browser semantics (WebKit `hoveredElementDidDetach` /
1260    /// `elementInActiveChainDidDetach`, Blink `HoveredElementDetached` /
1261    /// `ActiveChainNodeDetached`):
1262    /// - Hover and active retarget to the nearest surviving element ancestor
1263    ///   as a *transient bridge*: the HOVER/ACTIVE element-state bits along
1264    ///   the surviving chain stay lit (no one-frame gap in `:hover`/`:active`
1265    ///   styling), and the subsequent hover diff can unset exactly the right
1266    ///   bits. Hover is then re-resolved against the pointer position by
1267    ///   [`Self::refresh_hover`] at the end of the next resolve pass (the
1268    ///   analogue of WebKit's "fake mouse move"), which corrects the bridge
1269    ///   value — including cases where the removed node overflowed its
1270    ///   ancestor's box, so the ancestor was never truly under the pointer.
1271    /// - Focus resets to the body (encoded as `None`), running blur
1272    ///   side-effects (clearing focus element state and disabling IME for
1273    ///   text inputs).
1274    pub(crate) fn clear_interaction_state_for_removed_node(&mut self, node_id: NodeId) {
1275        if !self.nodes.contains_key(node_id) {
1276            return;
1277        }
1278
1279        if self.hover_node_id == Some(node_id) {
1280            self.hover_node_id = self.nearest_surviving_element_ancestor(node_id);
1281            self.hover_node_is_text = false;
1282        }
1283        if self.hover_hit_node_id == Some(node_id) {
1284            self.hover_hit_node_id = None;
1285        }
1286        if self.active_node_id == Some(node_id) {
1287            self.active_node_id = self.nearest_surviving_element_ancestor(node_id);
1288        }
1289        if self.focus_node_id == Some(node_id) {
1290            let shell_provider = self.shell_provider.clone();
1291            self.nodes[node_id].blur(shell_provider);
1292            self.focus_node_id = None;
1293        }
1294        if self.mousedown_node_id == Some(node_id) {
1295            self.mousedown_node_id = None;
1296        }
1297        if self.text_selection.anchor.node_or_parent == Some(node_id)
1298            || self.text_selection.focus.node_or_parent == Some(node_id)
1299        {
1300            self.text_selection.clear();
1301        }
1302        if self
1303            .hovered_scrollbar
1304            .is_some_and(|scrollbar| scrollbar.node_id == node_id)
1305        {
1306            self.hovered_scrollbar = None;
1307        }
1308        let drag_references_node = match &self.drag_mode {
1309            DragMode::Panning(state) => state.target == node_id,
1310            DragMode::ScrollbarDrag(state) => state.scrollbar.node_id == node_id,
1311            DragMode::Selecting | DragMode::None => false,
1312        };
1313        if drag_references_node {
1314            self.drag_mode = DragMode::None;
1315        }
1316        self.scrollbar_activity.remove(&node_id);
1317
1318        // The form-owner map is keyed by control id and was never pruned, so a
1319        // page that re-renders its fields grew an entry per render, every one
1320        // of them a freed slot. Nothing dereferences those any more, but an
1321        // unbounded map keyed on dead ids is a leak either way.
1322        self.controls_to_form.remove(&node_id);
1323    }
1324
1325    pub(crate) fn drop_node_ignoring_parent(&mut self, node_id: NodeId) -> Option<Node> {
1326        self.drop_node_ignoring_parent_with(node_id, &mut |_| {})
1327    }
1328
1329    /// Like [`Self::drop_node_ignoring_parent`], but calls `on_drop` with the id of
1330    /// every dropped node (the node itself and all of its descendants).
1331    pub(crate) fn drop_node_ignoring_parent_with(
1332        &mut self,
1333        node_id: NodeId,
1334        on_drop: &mut dyn FnMut(NodeId),
1335    ) -> Option<Node> {
1336        let mut node = self.remove_node_from_tree(node_id);
1337        if let Some(node) = &mut node {
1338            on_drop(node_id);
1339            if let Some(before) = node.before() {
1340                self.drop_node_ignoring_parent_with(before, on_drop);
1341            }
1342            if let Some(after) = node.after() {
1343                self.drop_node_ignoring_parent_with(after, on_drop);
1344            }
1345
1346            for &child in &node.children {
1347                self.drop_node_ignoring_parent_with(child, on_drop);
1348            }
1349
1350            // Anonymous blocks live only in the slab, so deallocate the ones this
1351            // node owns rather than leaking them.
1352            for &anon_id in &node.anonymous_blocks {
1353                self.deallocate_anonymous_block(anon_id);
1354            }
1355
1356            // Drop any attached shadow root (its children are dropped recursively
1357            // via the recursive call below).
1358            #[cfg(feature = "shadow-dom")]
1359            if let Some(shadow_root_id) = node.shadow_root_id() {
1360                self.shadow_host_nodes.remove(&node_id);
1361                self.custom_element_nodes.remove(&node_id);
1362                self.drop_node_ignoring_parent(shadow_root_id);
1363            }
1364        }
1365        node
1366    }
1367
1368    /// Deallocate an anonymous block created in a previous construction
1369    /// round, along with any anonymous blocks nested within it.
1370    pub(crate) fn deallocate_anonymous_block(&mut self, anon_id: NodeId) {
1371        // The block may already have been removed from the slab (e.g. a
1372        // whitespace-only anonymous block dropped during construction).
1373        if !self.nodes.contains_key(anon_id) {
1374            return;
1375        }
1376
1377        // Free any anonymous blocks that this block owns before removing it.
1378        let nested = std::mem::take(&mut self.nodes[anon_id].anonymous_blocks);
1379        for nested_id in nested {
1380            self.deallocate_anonymous_block(nested_id);
1381        }
1382
1383        self.remove_node_from_tree(anon_id);
1384    }
1385
1386    pub fn create_text_node(&mut self, text: &str) -> NodeId {
1387        let content = text.to_string();
1388        let data = NodeData::Text(TextNodeData::new(content));
1389        self.create_node(data)
1390    }
1391
1392    pub fn deep_clone_node(&mut self, node_id: NodeId) -> NodeId {
1393        // Load existing node
1394        let node = &self.nodes[node_id];
1395        let mut data = node.data.clone();
1396
1397        match &mut data {
1398            NodeData::Element(elem) | NodeData::AnonymousBlock(elem) => {
1399                if let Some(arc) = elem.style_attribute.as_mut() {
1400                    let read_guard = self.guard().read();
1401                    let block = arc.read_with(&read_guard);
1402                    *arc = ServoArc::new(self.guard().wrap(block.clone()));
1403                }
1404            }
1405            _ => {}
1406        }
1407
1408        let children = node.children.clone();
1409
1410        // Create new node
1411        let new_node_id = self.create_node(data);
1412
1413        // Recursively clone children
1414        let new_children: ThinVec<NodeId> = children
1415            .into_iter()
1416            .map(|child_id| self.deep_clone_node(child_id))
1417            .collect();
1418        for &child_id in &new_children {
1419            self.nodes[child_id].parent = Some(new_node_id);
1420        }
1421        self.nodes[new_node_id].children = new_children;
1422
1423        new_node_id
1424    }
1425
1426    pub(crate) fn remove_and_drop_pe(&mut self, node_id: NodeId) -> Option<Node> {
1427        fn remove_pe_ignoring_parent(doc: &mut BaseDocument, node_id: NodeId) -> Option<Node> {
1428            let mut node = doc.remove_node_from_tree(node_id);
1429            if let Some(node) = &mut node {
1430                for &child in &node.children {
1431                    remove_pe_ignoring_parent(doc, child);
1432                }
1433                for &anon_id in &node.anonymous_blocks {
1434                    doc.deallocate_anonymous_block(anon_id);
1435                }
1436            }
1437            node
1438        }
1439
1440        let node = remove_pe_ignoring_parent(self, node_id);
1441
1442        // Update child_idx values
1443        if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
1444            let parent = &mut self.nodes[parent_id];
1445            parent.children.retain(|id| *id != node_id);
1446        }
1447
1448        node
1449    }
1450
1451    pub(crate) fn resolve_url(&self, raw: &str) -> url::Url {
1452        self.url.resolve_relative(raw).unwrap_or_else(|| {
1453            panic!(
1454                "to be able to resolve {raw} with the base_url: {:?}",
1455                *self.url
1456            )
1457        })
1458    }
1459
1460    /// Navigate to `raw`, resolved against this document's base URL.
1461    ///
1462    /// The same route a link click takes, exposed so that script can reach it:
1463    /// `location.assign`, `location.replace` and `location.reload` had nowhere
1464    /// to go, because `resolve_url` and the navigation provider are both
1465    /// internal to this crate. Returns `false` when `raw` will not resolve,
1466    /// so the caller can report that rather than navigate somewhere wrong.
1467    pub fn navigate_to_url(&self, raw: &str) -> bool {
1468        let Some(url) = self.url.resolve_relative(raw) else {
1469            return false;
1470        };
1471        self.navigation_provider
1472            .navigate_to(blitz_traits::navigation::NavigationOptions::new(
1473                url,
1474                None,
1475                self.id(),
1476            ));
1477        true
1478    }
1479
1480    /// This document's URL, as a page's `location.href` reads it.
1481    pub fn current_url(&self) -> String {
1482        self.url.to_string()
1483    }
1484
1485    pub fn print_tree(&self) {
1486        crate::util::walk_tree(0, self.root_node());
1487    }
1488
1489    pub fn print_subtree(&self, node_id: NodeId) {
1490        crate::util::walk_tree(0, &self.nodes[node_id]);
1491    }
1492
1493    pub fn reload_resource_by_href(&mut self, href_to_reload: &str) {
1494        for &node_id in self.nodes_to_stylesheet.keys() {
1495            let node = &self.nodes[node_id];
1496            let Some(element) = node.element_data() else {
1497                continue;
1498            };
1499
1500            if element.name.local == local_name!("link") {
1501                if let Some(href) = element.attr(local_name!("href")) {
1502                    // println!("Node {node_id} {href} {href_to_reload} {} {}", resolved_href.as_str(), resolved_href.as_str() == url_to_reload);
1503                    if href == href_to_reload {
1504                        let resolved_href = self.resolve_url(href);
1505                        self.net_provider.fetch(
1506                            self.id(),
1507                            self.build_request(resolved_href.clone()),
1508                            ResourceHandler::boxed(
1509                                self.tx.clone(),
1510                                self.id,
1511                                Some(node_id),
1512                                self.shell_provider.clone(),
1513                                StylesheetHandler {
1514                                    source_url: resolved_href,
1515                                    guard: self.guard.clone(),
1516                                    net_provider: self.net_provider.clone(),
1517                                    abort_signal: self.abort_signal.clone(),
1518                                },
1519                            ),
1520                        );
1521                    }
1522                }
1523            }
1524        }
1525    }
1526
1527    pub fn process_style_element(&mut self, target_id: NodeId) {
1528        let css = self.nodes[target_id].text_content();
1529        let css = html_escape::decode_html_entities(&css);
1530        let sheet = self.make_stylesheet(&css, Origin::Author);
1531        self.add_stylesheet_for_node(sheet, target_id);
1532    }
1533
1534    pub fn remove_user_agent_stylesheet(&mut self, contents: &str) {
1535        if let Some(sheet) = self.ua_stylesheets.remove(contents) {
1536            self.stylist.remove_stylesheet(sheet, &self.guard.read());
1537        }
1538    }
1539
1540    /// The document's base URL
1541    pub fn url(&self) -> &url::Url {
1542        &self.url
1543    }
1544
1545    /// Iterate over the author stylesheets (from `<style>` and `<link>` nodes)
1546    /// currently associated with this document
1547    pub fn author_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
1548        self.nodes_to_stylesheet.values()
1549    }
1550
1551    /// Iterate over the user-agent stylesheets currently associated with this document
1552    pub fn useragent_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
1553        self.ua_stylesheets.values()
1554    }
1555
1556    pub fn add_user_agent_stylesheet(&mut self, css: &str) {
1557        let sheet = self.make_stylesheet(css, Origin::UserAgent);
1558        self.ua_stylesheets.insert(css.to_string(), sheet.clone());
1559        self.stylist.append_stylesheet(sheet, &self.guard.read());
1560    }
1561
1562    pub fn make_stylesheet(&self, css: impl AsRef<str>, origin: Origin) -> DocumentStyleSheet {
1563        let data = Stylesheet::from_str(
1564            css.as_ref(),
1565            self.url.url_extra_data(),
1566            origin,
1567            ServoArc::new(self.guard.wrap(MediaList::empty())),
1568            self.guard.clone(),
1569            Some(&StylesheetLoader {
1570                tx: self.tx.clone(),
1571                doc_id: self.id,
1572                net_provider: self.net_provider.clone(),
1573                shell_provider: self.shell_provider.clone(),
1574                abort_signal: self.abort_signal.clone(),
1575            }),
1576            None,
1577            QuirksMode::NoQuirks,
1578            AllowImportRules::Yes,
1579        );
1580
1581        DocumentStyleSheet(ServoArc::new(data))
1582    }
1583
1584    pub fn upsert_stylesheet_for_node(&mut self, node_id: NodeId) {
1585        let raw_styles = self.nodes[node_id].text_content();
1586        let sheet = self.make_stylesheet(raw_styles, Origin::Author);
1587        self.add_stylesheet_for_node(sheet, node_id);
1588    }
1589
1590    pub fn add_stylesheet_for_node(&mut self, stylesheet: DocumentStyleSheet, node_id: NodeId) {
1591        let old = self.nodes_to_stylesheet.insert(node_id, stylesheet.clone());
1592
1593        if let Some(old) = old {
1594            self.stylist.remove_stylesheet(old, &self.guard.read())
1595        }
1596
1597        // Fetch @font-face fonts
1598        crate::net::fetch_font_face(
1599            self.tx.clone(),
1600            self.id,
1601            Some(node_id),
1602            &stylesheet.0,
1603            &self.net_provider,
1604            &self.shell_provider,
1605            &self.guard.read(),
1606            self.abort_signal.as_ref(),
1607        );
1608
1609        // Store data on element
1610        let element = &mut self.nodes[node_id].element_data_mut().unwrap();
1611        element.special_data = SpecialElementData::Stylesheet(stylesheet.clone());
1612
1613        // TODO: Nodes could potentially get reused so ordering by node_id might be wrong.
1614        let insertion_point = self
1615            .nodes_to_stylesheet
1616            .range((Bound::Excluded(node_id), Bound::Unbounded))
1617            .next()
1618            .map(|(_, sheet)| sheet);
1619
1620        if let Some(insertion_point) = insertion_point {
1621            self.stylist.insert_stylesheet_before(
1622                stylesheet,
1623                insertion_point.clone(),
1624                &self.guard.read(),
1625            )
1626        } else {
1627            self.stylist
1628                .append_stylesheet(stylesheet, &self.guard.read())
1629        }
1630    }
1631
1632    pub fn handle_messages(&mut self) {
1633        // Remove event Reciever from the Document so that we can process events
1634        // without holding a borrow to the Document
1635        let rx = self.rx.take().unwrap();
1636
1637        while let Ok(msg) = rx.try_recv() {
1638            self.handle_message(msg);
1639        }
1640
1641        // Put Reciever back
1642        self.rx = Some(rx);
1643    }
1644
1645    pub fn handle_message(&mut self, msg: DocumentEvent) {
1646        match msg {
1647            DocumentEvent::ResourceLoad(resource) => self.load_resource(resource),
1648            DocumentEvent::NavigateIframe { node_id, url } => self.navigate_iframe(node_id, url),
1649        }
1650    }
1651
1652    /// Whether the Document has pending requests for "critical" resources (that should block rendering)
1653    pub fn has_pending_critical_resources(&self) -> bool {
1654        !self.pending_critical_resources.is_empty()
1655    }
1656
1657    /// How many distinct image URLs are still being fetched.
1658    ///
1659    /// Images are deliberately not "critical" resources, so they never block
1660    /// rendering. An embedder that needs a settled page (a screenshot, a test,
1661    /// a print) has no other way to tell an image that is still in flight from
1662    /// one that will never arrive.
1663    pub fn pending_image_count(&self) -> usize {
1664        self.pending_images.len()
1665    }
1666
1667    pub fn load_resource(&mut self, res: ResourceLoadResponse) {
1668        self.pending_critical_resources.remove(&res.request_id);
1669
1670        let resource = match res.result {
1671            Ok(resource) => resource,
1672            Err(err) => {
1673                if let Some(url) = res.resolved_url.as_ref() {
1674                    let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1675                    #[cfg(feature = "tracing")]
1676                    tracing::warn!(
1677                        url = url.as_str(),
1678                        waiting_nodes = waiting_nodes.len(),
1679                        error = err.as_str(),
1680                        "Resource load failed"
1681                    );
1682                    #[cfg(not(feature = "tracing"))]
1683                    let _ = (waiting_nodes, err);
1684                } else {
1685                    #[cfg(feature = "tracing")]
1686                    tracing::warn!(error = err.as_str(), "Resource load failed (no url)");
1687                    #[cfg(not(feature = "tracing"))]
1688                    let _ = err;
1689                }
1690                return;
1691            }
1692        };
1693
1694        match resource {
1695            Resource::Css(css) => {
1696                let node_id = res.node_id.unwrap();
1697                self.add_stylesheet_for_node(css, node_id);
1698            }
1699            Resource::ImportSheet(import_rule, sheet) => {
1700                // The write that used to happen on the network worker. Here it
1701                // is on the thread that owns styling, so it cannot collide
1702                // with a concurrent read of the same lock.
1703                //
1704                // Scoped, because the `@font-face` scan below needs a read of
1705                // the same lock and this is an `AtomicRefCell`: holding the
1706                // write across it would deadlock against itself rather than
1707                // wait.
1708                {
1709                    let mut guard = self.guard.write();
1710                    import_rule.write_with(&mut guard).stylesheet =
1711                        style::stylesheets::import_rule::ImportSheet::Sheet(sheet.clone());
1712                }
1713
1714                // The same scan `add_stylesheet_for_node` does for a top-level
1715                // sheet. An imported sheet may declare fonts too, and until now
1716                // nothing fetched them from a thread allowed to read the lock.
1717                crate::net::fetch_font_face(
1718                    self.tx.clone(),
1719                    self.id,
1720                    res.node_id,
1721                    &sheet,
1722                    &self.net_provider,
1723                    &self.shell_provider,
1724                    &self.guard.read(),
1725                    self.abort_signal.as_ref(),
1726                );
1727            }
1728            Resource::Image(_kind, width, height, image_data) => {
1729                // Create the ImageData and cache it
1730                let image = ImageData::Raster(RasterImageData::new(width, height, image_data));
1731
1732                let Some(url) = res.resolved_url.as_ref() else {
1733                    return;
1734                };
1735
1736                self.apply_loaded_image(url, image);
1737            }
1738            #[cfg(feature = "svg")]
1739            Resource::Svg(_kind, svg) => {
1740                // Create the ImageData and cache it
1741                let image = ImageData::Svg(svg);
1742
1743                let Some(url) = res.resolved_url.as_ref() else {
1744                    return;
1745                };
1746
1747                self.apply_loaded_image(url, image);
1748            }
1749            Resource::DocumentSrc(html) => {
1750                let Some(node_id) = res.node_id else {
1751                    return;
1752                };
1753                self.apply_iframe_html(node_id, res.request_id, res.resolved_url, &html);
1754            }
1755            Resource::Font(bytes, overrides) => {
1756                let font = Blob::new(Arc::new(bytes));
1757
1758                // Build a `FontInfoOverride` from the `@font-face` descriptors
1759                // captured during stylesheet parsing. Without this, parley
1760                // reads the family name from the TTF's own metadata, which
1761                // means CSS `font-family: 'Avenir Book'` won't match a font
1762                // file that internally identifies as `Avenir 45 Book`.
1763                let weight_override = overrides.weight.map(parley::fontique::FontWeight::new);
1764                let info_override = parley::fontique::FontInfoOverride {
1765                    family_name: overrides.family_name.as_deref(),
1766                    weight: weight_override,
1767                    style: overrides.style,
1768                    ..Default::default()
1769                };
1770
1771                // TODO: Investigate eliminating double-box
1772                let mut global_font_ctx = self.font_ctx.lock().unwrap();
1773                global_font_ctx
1774                    .collection
1775                    .register_fonts(font.clone(), Some(info_override));
1776
1777                #[cfg(feature = "parallel-construct")]
1778                {
1779                    rayon::broadcast(|_ctx| {
1780                        let mut font_ctx = self
1781                            .thread_font_contexts
1782                            .get_or(|| RefCell::new(Box::new(global_font_ctx.clone())))
1783                            .borrow_mut();
1784                        font_ctx
1785                            .collection
1786                            .register_fonts(font.clone(), Some(info_override));
1787                    });
1788                }
1789                drop(global_font_ctx);
1790
1791                // TODO: see if we can only invalidate if resolved fonts may have changed
1792                self.invalidate_inline_contexts();
1793            }
1794            Resource::None => {
1795                // Do nothing
1796            }
1797        }
1798    }
1799
1800    /// Cache a loaded image and apply it to all nodes waiting on it
1801    /// (`<img>` elements, `background-image` layers and `mask-image` layers).
1802    fn apply_loaded_image(&mut self, url: &str, image: ImageData) {
1803        // Get all nodes waiting for this image
1804        let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1805
1806        #[cfg(feature = "tracing")]
1807        tracing::info!(
1808            "Image {url} loaded, applying to {} nodes",
1809            waiting_nodes.len()
1810        );
1811
1812        // Cache the image
1813        self.image_cache.insert(url.to_string(), image.clone());
1814
1815        // Apply to all waiting nodes
1816        for (node_id, image_type) in waiting_nodes {
1817            let Some(node) = self.get_node_mut(node_id) else {
1818                continue;
1819            };
1820
1821            match image_type {
1822                ImageType::Image => {
1823                    node.element_data_mut().unwrap().special_data =
1824                        SpecialElementData::Image(Box::new(image.clone()));
1825
1826                    // Clear layout cache
1827                    node.cache_mut().clear();
1828                    node.insert_damage(ALL_DAMAGE);
1829                }
1830                ImageType::Background(idx) | ImageType::Mask(idx) => {
1831                    let layer_image = node.element_data_mut().and_then(|el| {
1832                        let images = match image_type {
1833                            ImageType::Background(_) => &mut el.background_images,
1834                            ImageType::Mask(_) => &mut el.mask_images,
1835                            ImageType::Image => unreachable!(),
1836                        };
1837                        images.get_mut(idx)
1838                    });
1839                    if let Some(Some(layer_image)) = layer_image {
1840                        layer_image.status = Status::Ok;
1841                        layer_image.image = image.clone();
1842                    }
1843                }
1844            }
1845        }
1846    }
1847
1848    pub fn snapshot_node(&mut self, node_id: NodeId) {
1849        let node = &mut self.nodes[node_id];
1850
1851        // Do not snapshot nodes that have never been styled. A snapshot records an element's
1852        // pre-mutation state so a restyle can diff selector matches then-vs-now. An element
1853        // that has never been styled has no "then" to diff against. Snapshotting it anyway
1854        // makes Stylo's invalidation unwrap its (absent) primary style and panic.
1855        let has_been_styled = node.primary_styles().is_some();
1856        if !has_been_styled {
1857            return;
1858        }
1859
1860        let opaque_node_id = TNode::opaque(&&*node);
1861        node.set_has_snapshot(true);
1862        node.snapshot_handled()
1863            .store(false, std::sync::atomic::Ordering::SeqCst);
1864
1865        // TODO: handle invalidations other than hover
1866        if let Some(_existing_snapshot) = self.snapshots.get_mut(&opaque_node_id) {
1867            // Do nothing
1868            // TODO: update snapshot
1869        } else {
1870            let attrs: Option<Vec<_>> = node.attrs().map(|attrs| {
1871                attrs
1872                    .iter()
1873                    .map(|attr| {
1874                        let ident = AttrIdentifier {
1875                            local_name: GenericAtomIdent(attr.name.local.clone()),
1876                            name: GenericAtomIdent(attr.name.local.clone()),
1877                            namespace: GenericAtomIdent(attr.name.ns.clone()),
1878                            prefix: None,
1879                        };
1880
1881                        let value = if attr.name.local == local_name!("id") {
1882                            AttrValue::Atom(Atom::from(&*attr.value))
1883                        } else if attr.name.local == local_name!("class") {
1884                            let classes = attr
1885                                .value
1886                                .split_ascii_whitespace()
1887                                .map(Atom::from)
1888                                .collect();
1889                            // Stylo's `AttrValue` owns a `String`, so the atom
1890                            // is materialised here. This is the one place
1891                            // interning is paid back out, and it is bounded:
1892                            // once per snapshotted attribute, not per element
1893                            // per frame.
1894                            AttrValue::TokenList(OnceLock::from(attr.value.to_string()), classes)
1895                        } else {
1896                            AttrValue::String(attr.value.to_string())
1897                        };
1898
1899                        (ident, value)
1900                    })
1901                    .collect()
1902            });
1903
1904            let changed_attrs = attrs
1905                .as_ref()
1906                .map(|attrs| attrs.iter().map(|attr| attr.0.name.clone()).collect())
1907                .unwrap_or_default();
1908
1909            self.snapshots.insert(
1910                opaque_node_id,
1911                ServoElementSnapshot {
1912                    state: Some(*node.element_state()),
1913                    attrs,
1914                    changed_attrs,
1915                    class_changed: true,
1916                    id_changed: true,
1917                    other_attributes_changed: true,
1918                },
1919            );
1920        }
1921    }
1922
1923    /// Snapshot a node and act on it, if it is still there.
1924    ///
1925    /// Tolerant of a node that has gone, because the ids reaching this are
1926    /// remembered across events — focus, hover, the last press — and the node
1927    /// they name can be removed between one event and the next. Indexing
1928    /// directly turned that ordinary case into a panic inside an event handler.
1929    pub fn snapshot_node_and(&mut self, node_id: NodeId, cb: impl FnOnce(&mut Node)) {
1930        if !self.nodes.contains_key(node_id) {
1931            return;
1932        }
1933        self.snapshot_node(node_id);
1934        cb(&mut self.nodes[node_id]);
1935    }
1936
1937    // Takes (x, y) co-ordinates (relative to the )
1938    pub fn hit(&self, x: f32, y: f32) -> Option<HitResult> {
1939        self.hit_with_scrollbar(x, y).0
1940    }
1941
1942    /// Walk up the tree to the nearest DOM node whose id is stable across
1943    /// box-tree reconstruction, so canonicalized interaction state never goes
1944    /// stale.
1945    ///
1946    /// Layout-generated nodes (anonymous blocks and `::before`/`::after`
1947    /// pseudo-elements, both stored as anonymous blocks) get new ids on every
1948    /// reconstruction, so we skip any anonymous node *and* a non-anonymous node
1949    /// whose parent is anonymous (the pseudo's text content). The first
1950    /// non-anonymous node with a non-anonymous parent is a real DOM node; the
1951    /// root element's `Document` parent guarantees termination.
1952    ///
1953    /// Returns `None` if `node_id` (or an ancestor) no longer exists.
1954    pub fn nearest_non_anonymous_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
1955        // Recurse up the tree keeping a window of the current node and its
1956        // parent, advancing one step per iteration so each node is looked up
1957        // exactly once.
1958        let mut node = self.get_node(node_id)?;
1959        loop {
1960            let parent = match node.parent {
1961                Some(parent_id) => self.get_node(parent_id)?,
1962                None => return Some(node.id),
1963            };
1964            if !node.is_anonymous() && !parent.is_anonymous() {
1965                return Some(node.id);
1966            }
1967            node = parent;
1968        }
1969    }
1970
1971    pub fn focus_next_node(&mut self) -> Option<NodeId> {
1972        let focussed_node_id = self.get_focussed_node_id()?;
1973        let id = self.next_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
1974        self.set_focus_to(id);
1975        Some(id)
1976    }
1977
1978    /// Move focus to the previous focussable node in the document
1979    pub fn focus_prev_node(&mut self) -> Option<NodeId> {
1980        let focussed_node_id = self.get_focussed_node_id()?;
1981        let id = self.prev_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
1982        self.set_focus_to(id);
1983        Some(id)
1984    }
1985
1986    /// Clear the focussed node
1987    pub fn clear_focus(&mut self) {
1988        if let Some(id) = self.focus_node_id {
1989            let shell_provider = self.shell_provider.clone();
1990            self.snapshot_node_and(id, |node| node.blur(shell_provider));
1991            self.focus_node_id = None;
1992        }
1993    }
1994
1995    pub fn set_mousedown_node_id(&mut self, node_id: Option<NodeId>) {
1996        self.mousedown_node_id = node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
1997    }
1998    pub fn set_focus_to(&mut self, focus_node_id: NodeId) -> bool {
1999        let Some(focus_node_id) = self.nearest_non_anonymous_ancestor(focus_node_id) else {
2000            return false;
2001        };
2002        if Some(focus_node_id) == self.focus_node_id {
2003            return false;
2004        }
2005
2006        #[cfg(feature = "tracing")]
2007        tracing::info!("Focussed node {focus_node_id}");
2008
2009        let shell_provider = self.shell_provider.clone();
2010
2011        // Remove focus from the old node
2012        if let Some(id) = self.focus_node_id {
2013            self.snapshot_node_and(id, |node| node.blur(shell_provider.clone()));
2014        }
2015
2016        // Focus the new node
2017        self.snapshot_node_and(focus_node_id, |node| node.focus(shell_provider));
2018
2019        self.focus_node_id = Some(focus_node_id);
2020
2021        true
2022    }
2023
2024    pub fn active_node(&mut self) -> bool {
2025        let Some(hover_node_id) = self.get_hover_node_id() else {
2026            return false;
2027        };
2028
2029        if let Some(active_node_id) = self.active_node_id {
2030            if active_node_id == hover_node_id {
2031                return true;
2032            }
2033            self.unactive_node();
2034        }
2035
2036        // hover_node_id is canonicalized when stored, so this always holds.
2037        debug_assert!(
2038            self.get_node(hover_node_id)
2039                .is_some_and(|node| !node.is_anonymous()),
2040            "interaction state must reference DOM nodes, not layout-generated nodes"
2041        );
2042        let active_node_id = Some(hover_node_id);
2043
2044        let node_path = self.maybe_node_layout_ancestors(active_node_id);
2045        for &id in node_path.iter() {
2046            self.snapshot_node_and(id, |node| node.active());
2047        }
2048
2049        self.active_node_id = active_node_id;
2050
2051        true
2052    }
2053
2054    pub fn unactive_node(&mut self) -> bool {
2055        let Some(active_node_id) = self.active_node_id.take() else {
2056            return false;
2057        };
2058
2059        let node_path = self.maybe_node_layout_ancestors(Some(active_node_id));
2060        for &id in node_path.iter() {
2061            self.snapshot_node_and(id, |node| node.unactive());
2062        }
2063
2064        true
2065    }
2066
2067    /// The scrollbar thumb currently under the pointer, if any.
2068    pub fn hovered_scrollbar(&self) -> Option<crate::node::ScrollbarRef> {
2069        self.hovered_scrollbar
2070    }
2071
2072    /// The scrollbar thumb currently being dragged, if any.
2073    pub fn scrollbar_drag_target(&self) -> Option<crate::node::ScrollbarRef> {
2074        match &self.drag_mode {
2075            DragMode::ScrollbarDrag(state) => Some(state.scrollbar),
2076            _ => None,
2077        }
2078    }
2079
2080    /// The current opacity of `node_id`'s overlay scrollbars. They show at
2081    /// full opacity on scroll and fade out after a delay (Chromium's overlay
2082    /// timings); the pointer resting on a thumb, or dragging it, holds them
2083    /// visible.
2084    pub fn scrollbar_opacity(&self, node_id: NodeId) -> f32 {
2085        let interacting = |scrollbar: &crate::node::ScrollbarRef| scrollbar.node_id == node_id;
2086        if self.hovered_scrollbar.as_ref().is_some_and(interacting)
2087            || self
2088                .scrollbar_drag_target()
2089                .as_ref()
2090                .is_some_and(interacting)
2091        {
2092            return 1.0;
2093        }
2094        self.scrollbar_activity.get(&node_id).map_or(1.0, |last| {
2095            crate::node::scrollbar::opacity_at(last.elapsed())
2096        })
2097    }
2098
2099    /// Show `node_id`'s overlay scrollbars at full opacity and restart their
2100    /// fade-out delay.
2101    pub(crate) fn show_scrollbars(&mut self, node_id: NodeId) {
2102        if cfg!(feature = "scrollbars") {
2103            self.scrollbar_activity.insert(node_id, Instant::now());
2104        }
2105    }
2106
2107    /// Whether any overlay scrollbars are awaiting or animating their
2108    /// fade-out (so frames must keep rendering until they finish).
2109    fn scrollbars_animating(&self) -> bool {
2110        use crate::node::scrollbar::{FADE_DELAY, FADE_DURATION};
2111        self.scrollbar_activity
2112            .values()
2113            .any(|last| last.elapsed() < FADE_DELAY + FADE_DURATION)
2114    }
2115
2116    /// [`hit`](Self::hit), also resolving the innermost overlay scrollbar
2117    /// thumb under the point (shares the traversal, so it costs nothing
2118    /// extra).
2119    pub(crate) fn hit_with_scrollbar(
2120        &self,
2121        x: f32,
2122        y: f32,
2123    ) -> (Option<HitResult>, Option<crate::node::ScrollbarRef>) {
2124        if TDocument::as_node(&self.root_node())
2125            .first_element_child()
2126            .is_none()
2127        {
2128            #[cfg(feature = "tracing")]
2129            tracing::warn!("No DOM - not resolving hit test");
2130            return (None, None);
2131        }
2132        let mut scrollbar = None;
2133        let hit = self
2134            .root_element()
2135            .hit_inner(x, y, self.viewport().scale_f64(), &mut scrollbar);
2136        (hit, scrollbar)
2137    }
2138
2139    pub fn set_hover_to(&mut self, x: f32, y: f32) -> bool {
2140        self.semantic_hover_node_id = None;
2141        // Record the pointer position in client (unscrolled) coordinates so
2142        // that `refresh_hover` can re-resolve hover state after layout or
2143        // scroll changes.
2144        self.last_client_pointer_position = Some(taffy::Point {
2145            x: x - self.viewport_scroll.x as f32,
2146            y: y - self.viewport_scroll.y as f32,
2147        });
2148
2149        let (hit, hovered_scrollbar) = self.hit_with_scrollbar(x, y);
2150        // A faded-out thumb is not interactive: pointer moves never fade
2151        // overlay scrollbars back in (only scrolling shows them).
2152        let hovered_scrollbar =
2153            hovered_scrollbar.filter(|scrollbar| self.scrollbar_opacity(scrollbar.node_id) > 0.0);
2154        // Scrollbar-thumb hover is part of hover state: track it here so a
2155        // pointer crossing a thumb restyles it even when the hit node (the
2156        // content under the overlay thumb) is unchanged.
2157        let scrollbar_changed = hovered_scrollbar != self.hovered_scrollbar;
2158        if scrollbar_changed {
2159            // Entering a thumb restores full opacity mid-fade; leaving one
2160            // restarts the fade-out delay.
2161            for scrollbar in [self.hovered_scrollbar, hovered_scrollbar]
2162                .into_iter()
2163                .flatten()
2164            {
2165                self.show_scrollbars(scrollbar.node_id);
2166            }
2167        }
2168        self.hovered_scrollbar = hovered_scrollbar;
2169
2170        // Store both the precise layout node that was hit (transient: used for
2171        // cursor/style queries) and its canonical DOM target (persistent: must
2172        // not reference layout-generated nodes, whose ids die on box-tree
2173        // reconstruction).
2174        let hit_node_id = hit.map(|hit| hit.node_id);
2175        let hover_node_id = hit_node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
2176        let new_is_text = hit.map(|hit| hit.is_text).unwrap_or(false);
2177
2178        self.apply_hover_target(hit_node_id, hover_node_id, new_is_text, scrollbar_changed)
2179    }
2180
2181    /// Move the authored hover state to an already-resolved DOM node.
2182    ///
2183    /// Semantic automation has selected a node by identity already. Repeating
2184    /// hit testing at its centre can choose an overlapping child or overlay,
2185    /// especially inside nested scrollers, and makes `Hover { node_id }`
2186    /// target something other than the requested node. Pointer coordinates are
2187    /// still recorded for event payloads and later layout refreshes.
2188    pub fn set_hover_to_node(&mut self, node_id: NodeId, x: f32, y: f32) -> bool {
2189        self.semantic_hover_node_id = Some(node_id);
2190        self.last_client_pointer_position = Some(taffy::Point {
2191            x: x - self.viewport_scroll.x as f32,
2192            y: y - self.viewport_scroll.y as f32,
2193        });
2194
2195        let hovered_scrollbar = self.hovered_scrollbar.take();
2196        let scrollbar_changed = hovered_scrollbar.is_some();
2197        if let Some(scrollbar) = hovered_scrollbar {
2198            self.show_scrollbars(scrollbar.node_id);
2199        }
2200        let hover_node_id = self.nearest_non_anonymous_ancestor(node_id);
2201        self.apply_hover_target(Some(node_id), hover_node_id, false, scrollbar_changed)
2202    }
2203
2204    fn apply_hover_target(
2205        &mut self,
2206        hit_node_id: Option<NodeId>,
2207        hover_node_id: Option<NodeId>,
2208        new_is_text: bool,
2209        scrollbar_changed: bool,
2210    ) -> bool {
2211        let hit_changed =
2212            hit_node_id != self.hover_hit_node_id || new_is_text != self.hover_node_is_text;
2213        self.hover_hit_node_id = hit_node_id;
2214        self.hover_node_is_text = new_is_text;
2215
2216        // Return early if the new node is the same as the already-hovered node
2217        if hover_node_id == self.hover_node_id {
2218            if hit_changed {
2219                // The canonical target is unchanged (so no restyle is needed)
2220                // but the precise hit node changed, which can change the cursor
2221                // (e.g. moving between text and non-text within one element).
2222                self.shell_provider.set_cursor(self.get_cursor());
2223            }
2224            return scrollbar_changed;
2225        }
2226
2227        let old_node_path = self.maybe_node_layout_ancestors(self.hover_node_id);
2228        let new_node_path = self.maybe_node_layout_ancestors(hover_node_id);
2229        let same_count = old_node_path
2230            .iter()
2231            .zip(&new_node_path)
2232            .take_while(|(o, n)| o == n)
2233            .count();
2234        for &id in old_node_path.iter().skip(same_count) {
2235            self.snapshot_node_and(id, |node| node.unhover());
2236        }
2237        for &id in new_node_path.iter().skip(same_count) {
2238            self.snapshot_node_and(id, |node| node.hover());
2239        }
2240
2241        self.hover_node_id = hover_node_id;
2242
2243        // Update the cursor
2244        self.shell_provider.set_cursor(self.get_cursor());
2245
2246        // Request redraw
2247        self.shell_provider.request_redraw();
2248
2249        true
2250    }
2251
2252    pub fn clear_hover(&mut self) -> bool {
2253        // The pointer is no longer over the document, so stop re-resolving
2254        // hover state against it.
2255        self.last_client_pointer_position = None;
2256        self.semantic_hover_node_id = None;
2257        self.hover_hit_node_id = None;
2258
2259        let Some(hover_node_id) = self.hover_node_id else {
2260            return false;
2261        };
2262
2263        let old_node_path = self.maybe_node_layout_ancestors(Some(hover_node_id));
2264        for &id in old_node_path.iter() {
2265            self.snapshot_node_and(id, |node| node.unhover());
2266        }
2267
2268        self.hover_node_id = None;
2269        self.hover_node_is_text = false;
2270
2271        // Update the cursor
2272        self.shell_provider.set_cursor(self.get_cursor());
2273
2274        // Request redraw
2275        self.shell_provider.request_redraw();
2276
2277        true
2278    }
2279
2280    /// Re-resolve hover state against the current layout using the last known
2281    /// pointer position.
2282    ///
2283    /// TODO: synthesizing pointerenter/pointerleave DOM events for
2284    /// hover changes caused by layout shifts.
2285    pub fn refresh_hover(&mut self) -> bool {
2286        if let Some(node_id) = self.semantic_hover_node_id {
2287            if self.get_node(node_id).is_some() {
2288                let hover_node_id = self.nearest_non_anonymous_ancestor(node_id);
2289                return self.apply_hover_target(Some(node_id), hover_node_id, false, false);
2290            }
2291            self.semantic_hover_node_id = None;
2292        }
2293        let Some(pos) = self.last_client_pointer_position else {
2294            return false;
2295        };
2296        let x = pos.x + self.viewport_scroll.x as f32;
2297        let y = pos.y + self.viewport_scroll.y as f32;
2298        self.set_hover_to(x, y)
2299    }
2300
2301    pub fn get_hover_node_id(&self) -> Option<NodeId> {
2302        self.hover_node_id
2303    }
2304
2305    pub fn get_mousedown_node_id(&self) -> Option<NodeId> {
2306        self.mousedown_node_id
2307    }
2308
2309    pub fn set_viewport(&mut self, viewport: Viewport) {
2310        let scale_has_changed = viewport.scale_f64() != self.viewport.scale_f64();
2311        self.viewport = viewport;
2312        self.set_stylist_device(make_device(
2313            &self.viewport,
2314            self.media_type.clone(),
2315            self.font_ctx.clone(),
2316        ));
2317        self.scroll_viewport_by(0.0, 0.0); // Clamp scroll offset
2318
2319        if scale_has_changed {
2320            self.invalidate_inline_contexts();
2321            self.shell_provider.request_redraw();
2322        }
2323    }
2324
2325    /// Returns the current CSS media type used to evaluate `@media` rules.
2326    pub fn media_type(&self) -> &MediaType {
2327        &self.media_type
2328    }
2329
2330    /// Sets the CSS media type used to evaluate `@media` rules (e.g. `screen` or `print`)
2331    /// and rebuilds the stylist device so updated rules apply on the next restyle.
2332    pub fn set_media_type(&mut self, media_type: MediaType) {
2333        if self.media_type == media_type {
2334            return;
2335        }
2336        self.media_type = media_type;
2337        self.set_stylist_device(make_device(
2338            &self.viewport,
2339            self.media_type.clone(),
2340            self.font_ctx.clone(),
2341        ));
2342    }
2343
2344    pub fn viewport(&self) -> &Viewport {
2345        &self.viewport
2346    }
2347
2348    pub fn viewport_mut(&mut self) -> ViewportMut<'_> {
2349        ViewportMut::new(self)
2350    }
2351
2352    pub fn zoom_by(&mut self, increment: f32) {
2353        *self.viewport.zoom_mut() += increment;
2354        self.set_viewport(self.viewport.clone());
2355    }
2356
2357    pub fn zoom_to(&mut self, zoom: f32) {
2358        *self.viewport.zoom_mut() = zoom;
2359        self.set_viewport(self.viewport.clone());
2360    }
2361
2362    pub fn get_viewport(&self) -> Viewport {
2363        self.viewport.clone()
2364    }
2365
2366    /// Returns whether incremental layout is currently enabled for this document.
2367    pub fn incremental_layout(&self) -> bool {
2368        self.incremental_layout
2369    }
2370
2371    /// Enables or disables incremental layout for this document.
2372    pub fn set_incremental_layout(&mut self, enabled: bool) {
2373        self.incremental_layout = enabled;
2374    }
2375
2376    pub fn devtools(&self) -> &DevtoolSettings {
2377        &self.devtool_settings
2378    }
2379
2380    pub fn devtools_mut(&mut self) -> &mut DevtoolSettings {
2381        &mut self.devtool_settings
2382    }
2383
2384    pub fn subdoc(&self, node_id: NodeId) -> Option<&dyn Document> {
2385        self.get_node(node_id)
2386            .and_then(|node| node.element_data())
2387            .and_then(|el| el.sub_doc_data())
2388    }
2389
2390    pub fn subdoc_mut(&mut self, node_id: NodeId) -> Option<&mut dyn Document> {
2391        self.get_node_mut(node_id)
2392            .and_then(|node| node.element_data_mut())
2393            .and_then(|el| el.sub_doc_data_mut())
2394    }
2395
2396    pub fn is_animating(&self) -> bool {
2397        #[cfg(feature = "custom-widget")]
2398        let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
2399            self.nodes[node_id]
2400                .element_data()
2401                .and_then(|el| el.custom_widget_data())
2402                .is_some_and(|data| data.widget.requires_redraw())
2403        });
2404        #[cfg(not(feature = "custom-widget"))]
2405        let custom_widget_is_animating = false;
2406
2407        let animating = self.has_canvas
2408            | self.has_active_animations
2409            | (self.subdoc_animation_pacing != AnimationPacing::Idle)
2410            | custom_widget_is_animating
2411            | (self.scroll_animation != ScrollAnimationState::None)
2412            | self.scrollbars_animating();
2413
2414        if animating && crate::debug::animation_reasons_enabled() {
2415            crate::debug::report_animation_reasons(
2416                self.id(),
2417                self.has_canvas,
2418                self.has_active_animations,
2419                self.subdoc_animation_pacing != AnimationPacing::Idle,
2420                custom_widget_is_animating,
2421                self.scroll_animation != ScrollAnimationState::None,
2422                self.scrollbars_animating(),
2423                self.animating_node_names().as_deref(),
2424            );
2425        }
2426
2427        animating
2428    }
2429
2430    /// Return the cadence class for the next animation-only frame.
2431    ///
2432    /// CSS animations are commonly decorative and can use a lower cadence.
2433    /// Canvas, scrolling and custom widgets remain at the interactive cadence.
2434    pub fn animation_pacing(&self) -> AnimationPacing {
2435        let focused_text_input = self.focus_node_id.is_some_and(|node_id| {
2436            self.nodes
2437                .get(node_id)
2438                .and_then(|node| node.element_data())
2439                .is_some_and(|element| element.text_input_data().is_some())
2440        });
2441        #[cfg(feature = "custom-widget")]
2442        let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
2443            self.nodes[node_id]
2444                .element_data()
2445                .and_then(|el| el.custom_widget_data())
2446                .is_some_and(|data| data.widget.requires_redraw())
2447        });
2448        #[cfg(not(feature = "custom-widget"))]
2449        let custom_widget_is_animating = false;
2450
2451        if self.has_canvas
2452            || custom_widget_is_animating
2453            || self.scroll_animation != ScrollAnimationState::None
2454            || self.scrollbars_animating()
2455        {
2456            AnimationPacing::Interactive
2457        } else if self.has_active_animations {
2458            const SLOW_ANIMATION_SECONDS: f64 = 2.0;
2459            let sets = self.animations.sets.read();
2460            let has_fast_animation_or_transition = sets.values().any(|set| {
2461                set.transitions.iter().any(|transition| {
2462                    matches!(
2463                        transition.state,
2464                        AnimationState::Pending | AnimationState::Running
2465                    )
2466                }) || set.animations.iter().any(|animation| {
2467                    matches!(
2468                        animation.state,
2469                        AnimationState::Pending | AnimationState::Running
2470                    ) && animation.duration < SLOW_ANIMATION_SECONDS
2471                })
2472            });
2473            if has_fast_animation_or_transition {
2474                AnimationPacing::Interactive
2475            } else {
2476                AnimationPacing::SlowCss
2477            }
2478        } else if focused_text_input {
2479            AnimationPacing::Caret
2480        } else if self.subdoc_animation_pacing != AnimationPacing::Idle {
2481            self.subdoc_animation_pacing
2482        } else {
2483            AnimationPacing::Idle
2484        }
2485    }
2486
2487    /// Which elements Stylo currently holds animations or transitions for.
2488    ///
2489    /// Only built when the diagnostic is switched on: a frame loop that will
2490    /// not settle is otherwise very hard to attribute, because
2491    /// `has_active_animations` is one bool for the whole document and says
2492    /// nothing about which element is keeping it true.
2493    fn animating_node_names(&self) -> Option<String> {
2494        if !self.has_active_animations {
2495            return None;
2496        }
2497        let sets = self.animations.sets.read();
2498        let mut described: Vec<String> = sets
2499            .iter()
2500            .filter(|(_, state)| state.needs_animation_ticks())
2501            .filter_map(|(key, state)| {
2502                let node_id = NodeId::from_u64(key.node.id() as u64);
2503                let node = self.nodes.get(node_id)?;
2504                let element = node.element_data()?;
2505                let name = element
2506                    .attr(local_name!("id"))
2507                    .map(|id| format!("#{id}"))
2508                    .or_else(|| {
2509                        element
2510                            .attr(local_name!("class"))
2511                            .and_then(|c| c.split_ascii_whitespace().next())
2512                            .map(|c| format!(".{c}"))
2513                    })
2514                    .unwrap_or_else(|| element.name.local.to_string());
2515                Some(format!(
2516                    "{name}(anim={},trans={},in_doc={})",
2517                    state.animations.len(),
2518                    state.transitions.len(),
2519                    node.flags.is_in_document(),
2520                ))
2521            })
2522            .collect();
2523        described.sort();
2524        described.truncate(12);
2525        Some(described.join(" "))
2526    }
2527
2528    /// Update the device and reset the stylist to process the new size
2529    pub fn set_stylist_device(&mut self, device: Device) {
2530        // Seed the new device with the root element's current style and font-relative
2531        // unit state (used to resolve rem/rlh/rex/rch/rcap/ric units). Stylo only
2532        // updates this state when the root element's style *changes* during a restyle,
2533        // so a freshly-built device would otherwise resolve these units against the
2534        // default font-size (16px) until the root's font-size next changes.
2535        let root_styles = self
2536            .try_root_element()
2537            .and_then(|root| root.primary_styles());
2538        if let Some(root_style) = root_styles.as_deref() {
2539            device.set_root_style(root_style);
2540
2541            let font = root_style.get_font();
2542            let font_size = font.clone_font_size().computed_size();
2543            device.set_root_font_size(root_style.effective_zoom.unzoom(font_size.px()));
2544
2545            let line_height = device
2546                .calc_line_height(font, root_style.writing_mode, None)
2547                .0;
2548            device.set_root_line_height(root_style.effective_zoom.unzoom(line_height.px()));
2549        }
2550        drop(root_styles);
2551
2552        let origins = {
2553            let guard = &self.guard;
2554            let guards = StylesheetGuards {
2555                author: &guard.read(),
2556                ua_or_user: &guard.read(),
2557            };
2558            self.stylist.set_device(device, &guards)
2559        };
2560        self.stylist.force_stylesheet_origins_dirty(origins);
2561    }
2562
2563    pub fn stylist_device(&mut self) -> &Device {
2564        self.stylist.device()
2565    }
2566
2567    /// The cursor to show, where `None` means `cursor: none` — hide it.
2568    ///
2569    /// `None` is an answer, not the absence of one. The shell hides the pointer
2570    /// when it sees `None`, so every path that means "nothing to say here" must
2571    /// return `Default` instead. Returning `None` from those made the pointer
2572    /// vanish as it crossed into page content, which is the shape this used to
2573    /// have: three `?`s that each meant "no opinion" and all read as "hide".
2574    pub fn get_cursor(&self) -> Option<CursorIcon> {
2575        // Prefer the precise hit node: `cursor` and `user-select` may be set on
2576        // a pseudo-element or resolved on an anonymous box, and text hits carry
2577        // is_text via the hit node. Fall back to the canonical hover node if
2578        // the hit node has been removed (it is transient across resolves).
2579        let node_id = self
2580            .hover_hit_node_id
2581            .filter(|&id| self.nodes.contains_key(id))
2582            .or(self.get_hover_node_id());
2583        let Some(node_id) = node_id else {
2584            return Some(CursorIcon::Default);
2585        };
2586        let node = &self.nodes[node_id];
2587
2588        if let Some(subdoc) = node.subdoc().map(|doc| doc.inner()) {
2589            // Only delegate when the sub-document has hover state of its own.
2590            // Without this check an embedded document that has not been hovered
2591            // yet answers `None` — meaning "I have no hover node" — and the
2592            // pointer disappears the moment it enters the page area, which is
2593            // every page in a browser built on sub-documents.
2594            if subdoc.hover_hit_node_id.is_some() || subdoc.get_hover_node_id().is_some() {
2595                return subdoc.get_cursor();
2596            }
2597            return Some(CursorIcon::Default);
2598        }
2599
2600        let Some(style) = node.primary_styles() else {
2601            return Some(CursorIcon::Default);
2602        };
2603        let user_select = style.clone_user_select();
2604        let keyword = style.clone_cursor().keyword;
2605
2606        // Return cursor from style if it is non-auto
2607        if keyword != CursorKind::Auto {
2608            return stylo_to_cursor_icon(keyword);
2609        }
2610
2611        // Return text cursor for text inputs
2612        if node
2613            .element_data()
2614            .is_some_and(|e| e.text_input_data().is_some())
2615        {
2616            return Some(CursorIcon::Text);
2617        }
2618
2619        // Use "pointer" cursor if any ancestor is a link
2620        let mut maybe_node = Some(node);
2621        while let Some(node) = maybe_node {
2622            if node.is_link() {
2623                return Some(CursorIcon::Pointer);
2624            }
2625
2626            maybe_node = node.layout_parent.get().map(|node_id| node.with(node_id));
2627        }
2628
2629        // Return text cursor for text nodes
2630        if self.hover_node_is_text {
2631            return Some(match user_select {
2632                UserSelect::Text | UserSelect::All | UserSelect::Auto => CursorIcon::Text,
2633                UserSelect::None => CursorIcon::Default,
2634            });
2635        }
2636
2637        // Else fallback to default cursor
2638        Some(CursorIcon::Default)
2639    }
2640
2641    pub fn scroll_node_by<F: FnMut(DomEvent)>(
2642        &mut self,
2643        node_id: NodeId,
2644        x: f64,
2645        y: f64,
2646        dispatch_event: F,
2647    ) {
2648        self.scroll_node_by_has_changed(node_id, x, y, dispatch_event);
2649    }
2650
2651    /// Scroll a node by given x and y
2652    /// Will bubble scrolling up to parent node once it can no longer scroll further
2653    /// If we're already at the root node, bubbles scrolling up to the viewport
2654    ///
2655    /// A `position: sticky` box is held against the edge of the scrollport it
2656    /// lives in, so the boxes have to be re-adjusted here rather than only in
2657    /// `resolve`: a wheel event does not necessarily produce a style and layout
2658    /// pass, and a header that only unstuck on the next restyle is a header
2659    /// that visibly lags the scroll.
2660    pub fn scroll_node_by_has_changed<F: FnMut(DomEvent)>(
2661        &mut self,
2662        node_id: NodeId,
2663        x: f64,
2664        y: f64,
2665        dispatch_event: F,
2666    ) -> bool {
2667        let has_changed = self.scroll_node_by_inner(node_id, x, y, dispatch_event);
2668        if has_changed {
2669            self.resolve_sticky_positions();
2670        }
2671        has_changed
2672    }
2673
2674    fn scroll_node_by_inner<F: FnMut(DomEvent)>(
2675        &mut self,
2676        node_id: NodeId,
2677        x: f64,
2678        y: f64,
2679        mut dispatch_event: F,
2680    ) -> bool {
2681        // Per the CSS overflow propagation rules, the root element's overflow (and usually
2682        // the <body>'s) is applied to the viewport, and the element itself must not have
2683        // a scrolling mechanism of its own. So scrolls that reach the root element are
2684        // forwarded to the viewport rather than scrolling the root element itself.
2685        if self.try_root_element().is_some_and(|el| el.id == node_id) {
2686            let has_changed = self.scroll_viewport_by_has_changed(x, y);
2687            if has_changed {
2688                let layout = *self.root_element().final_layout();
2689                let scale = self.viewport.scale() as f64;
2690                let event = BlitzScrollEvent {
2691                    scroll_top: self.viewport_scroll.y,
2692                    scroll_left: self.viewport_scroll.x,
2693                    scroll_width: layout.size.width.max(layout.content_size.width) as i32,
2694                    scroll_height: layout.size.height.max(layout.content_size.height) as i32,
2695                    client_width: (self.viewport.window_size.0 as f64 / scale) as i32,
2696                    client_height: (self.viewport.window_size.1 as f64 / scale) as i32,
2697                };
2698                dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
2699            }
2700            return has_changed;
2701        }
2702
2703        let Some(node) = self.nodes.get_mut(node_id) else {
2704            return false;
2705        };
2706
2707        // Text inputs scroll their own internal text content rather than using the generic
2708        // overflow mechanism: single-line inputs scroll horizontally, multi-line inputs scroll
2709        // vertically. Any delta the input cannot consume is bubbled up to an ancestor scroller.
2710        if node
2711            .element_data()
2712            .is_some_and(|el| el.text_input_data().is_some())
2713        {
2714            let parent = node.parent;
2715            let content_box_width = node.final_layout().content_box_width();
2716            let content_box_height = node.final_layout().content_box_height();
2717            let input = node
2718                .element_data_mut()
2719                .and_then(|el| el.text_input_data_mut())
2720                .unwrap();
2721
2722            let (bubble_x, bubble_y) = if input.is_multiline {
2723                (
2724                    x,
2725                    input.scroll_by(y as f32, content_box_width, content_box_height) as f64,
2726                )
2727            } else {
2728                (
2729                    input.scroll_by(x as f32, content_box_width, content_box_height) as f64,
2730                    y,
2731                )
2732            };
2733
2734            let has_changed = bubble_x != x || bubble_y != y;
2735
2736            if bubble_x != 0.0 || bubble_y != 0.0 {
2737                let bubbled = if let Some(parent) = parent {
2738                    self.scroll_node_by_inner(parent, bubble_x, bubble_y, dispatch_event)
2739                } else {
2740                    self.scroll_viewport_by_has_changed(bubble_x, bubble_y)
2741                };
2742                return bubbled | has_changed;
2743            }
2744
2745            return has_changed;
2746        }
2747
2748        let (can_x_scroll, can_y_scroll) = node
2749            .primary_styles()
2750            .map(|styles| {
2751                (
2752                    matches!(styles.clone_overflow_x(), Overflow::Scroll | Overflow::Auto),
2753                    matches!(styles.clone_overflow_y(), Overflow::Scroll | Overflow::Auto),
2754                )
2755            })
2756            .unwrap_or((false, false));
2757
2758        let initial = *node.scroll_offset();
2759        let new_x = node.scroll_offset().x - x;
2760        let new_y = node.scroll_offset().y - y;
2761
2762        let mut bubble_x = 0.0;
2763        let mut bubble_y = 0.0;
2764
2765        let scroll_width = node.final_layout().scroll_width() as f64;
2766        let scroll_height = node.final_layout().scroll_height() as f64;
2767
2768        // Handle sub document case
2769        if let Some(mut sub_doc) = node.subdoc_mut().map(|doc| doc.inner_mut()) {
2770            let has_changed = if let Some(hover_node_id) = sub_doc.get_hover_node_id() {
2771                sub_doc.scroll_node_by_has_changed(hover_node_id, x, y, dispatch_event)
2772            } else {
2773                sub_doc.scroll_viewport_by_has_changed(x, y)
2774            };
2775
2776            // TODO: propagate remaining scroll to parent
2777            return has_changed;
2778        }
2779
2780        // If we're past our scroll bounds, transfer remainder of scrolling to parent/viewport
2781        if !can_x_scroll {
2782            bubble_x = x
2783        } else if new_x < 0.0 {
2784            bubble_x = -new_x;
2785            node.scroll_offset_mut().x = 0.0;
2786        } else if new_x > scroll_width {
2787            bubble_x = scroll_width - new_x;
2788            node.scroll_offset_mut().x = scroll_width;
2789        } else {
2790            node.scroll_offset_mut().x = new_x;
2791        }
2792
2793        if !can_y_scroll {
2794            bubble_y = y
2795        } else if new_y < 0.0 {
2796            bubble_y = -new_y;
2797            node.scroll_offset_mut().y = 0.0;
2798        } else if new_y > scroll_height {
2799            bubble_y = scroll_height - new_y;
2800            node.scroll_offset_mut().y = scroll_height;
2801        } else {
2802            node.scroll_offset_mut().y = new_y;
2803        }
2804
2805        let has_changed = *node.scroll_offset() != initial;
2806
2807        if has_changed {
2808            let layout = *node.final_layout();
2809            let event = BlitzScrollEvent {
2810                scroll_top: node.scroll_offset().y,
2811                scroll_left: node.scroll_offset().x,
2812                scroll_width: layout.scroll_width() as i32,
2813                scroll_height: layout.scroll_height() as i32,
2814                client_width: layout.size.width as i32,
2815                client_height: layout.size.height as i32,
2816            };
2817
2818            dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
2819        }
2820
2821        let parent = node.parent;
2822        if has_changed {
2823            self.show_scrollbars(node_id);
2824        }
2825
2826        if bubble_x != 0.0 || bubble_y != 0.0 {
2827            if let Some(parent) = parent {
2828                return self.scroll_node_by_inner(parent, bubble_x, bubble_y, dispatch_event)
2829                    | has_changed;
2830            } else {
2831                return self.scroll_viewport_by_has_changed(bubble_x, bubble_y) | has_changed;
2832            }
2833        }
2834
2835        has_changed
2836    }
2837
2838    pub fn scroll_viewport_by(&mut self, x: f64, y: f64) {
2839        self.scroll_viewport_by_has_changed(x, y);
2840    }
2841
2842    /// Scroll the viewport by the given values
2843    pub fn scroll_viewport_by_has_changed(&mut self, x: f64, y: f64) -> bool {
2844        // The viewport scrolls the root element's scrollable overflow, which includes both
2845        // the root element itself and any content which overflows it (e.g. when the root
2846        // element has a fixed height but its content is taller). A document without a root
2847        // element has no scrollable content, so its content size is zero.
2848        let (content_width, content_height) = match self.try_root_element() {
2849            Some(root) => {
2850                let root_layout = root.final_layout();
2851                (
2852                    root_layout.size.width.max(root_layout.content_size.width) as f64,
2853                    root_layout.size.height.max(root_layout.content_size.height) as f64,
2854                )
2855            }
2856            None => (0.0, 0.0),
2857        };
2858        let new_scroll = (self.viewport_scroll.x - x, self.viewport_scroll.y - y);
2859        let window_width = self.viewport.window_size.0 as f64 / self.viewport.scale() as f64;
2860        let window_height = self.viewport.window_size.1 as f64 / self.viewport.scale() as f64;
2861
2862        let initial = self.viewport_scroll;
2863        self.viewport_scroll.x =
2864            f64::max(0.0, f64::min(new_scroll.0, content_width - window_width));
2865        self.viewport_scroll.y =
2866            f64::max(0.0, f64::min(new_scroll.1, content_height - window_height));
2867
2868        let has_changed = self.viewport_scroll != initial;
2869        if has_changed {
2870            // The viewport is the scrollport a page-level sticky box is held
2871            // against, and the containing block a fixed box is pinned to, so
2872            // both move with this and not with the next relayout. See
2873            // `resolve_sticky_positions` and `resolve_fixed_positions`.
2874            self.resolve_sticky_positions();
2875            self.resolve_fixed_positions();
2876        }
2877        has_changed
2878    }
2879
2880    pub fn scroll_by(
2881        &mut self,
2882        anchor_node_id: Option<NodeId>,
2883        scroll_x: f64,
2884        scroll_y: f64,
2885        dispatch_event: &mut dyn FnMut(DomEvent),
2886    ) -> bool {
2887        if let Some(anchor_node_id) = anchor_node_id {
2888            self.scroll_node_by_has_changed(anchor_node_id, scroll_x, scroll_y, dispatch_event)
2889        } else {
2890            self.scroll_viewport_by_has_changed(scroll_x, scroll_y)
2891        }
2892    }
2893
2894    pub fn viewport_scroll(&self) -> crate::Point<f64> {
2895        self.viewport_scroll
2896    }
2897
2898    pub fn set_viewport_scroll(&mut self, scroll: crate::Point<f64>) {
2899        self.viewport_scroll = scroll;
2900    }
2901
2902    /// Find the node targeted by a URL fragment (the `#...` part of a URL).
2903    ///
2904    /// Per the HTML spec, this is the element whose `id` matches the fragment, falling
2905    /// back to the first `<a>` element whose `name` attribute matches.
2906    pub fn get_fragment_target(&self, fragment: &str) -> Option<NodeId> {
2907        if let Some(node_id) = self.get_element_by_id(fragment) {
2908            return Some(node_id);
2909        }
2910
2911        // Fall back to a named anchor: `<a name="...">`
2912        self.nodes.iter().find_map(|(id, node)| {
2913            let el = node.element_data()?;
2914            (el.name.local == local_name!("a") && el.attr(local_name!("name")) == Some(fragment))
2915                .then_some(id)
2916        })
2917    }
2918
2919    /// Scroll the viewport so that the given node is aligned with the top of the viewport.
2920    /// Scroll the nearest scroll container at or above `node_id`.
2921    ///
2922    /// "Scroll this panel" is the operation callers actually want, and
2923    /// `scroll_node_by` only moves the node itself, so naming any inner element
2924    /// silently did nothing. Wheel events are no help either: they are
2925    /// delivered to whatever the document last saw hovered, which an injected
2926    /// pointer move does not set, so an automated caller had no way to scroll
2927    /// anything at all.
2928    /// The nearest scroll container at or above `node_id`, if there is one.
2929    pub fn nearest_scroll_container(&self, node_id: NodeId) -> Option<NodeId> {
2930        let mut current = Some(node_id);
2931        for _ in 0..64 {
2932            let id = current?;
2933            let node = self.nodes.get(id)?;
2934            if node.style().overflow.x.is_scroll_container()
2935                || node.style().overflow.y.is_scroll_container()
2936            {
2937                return Some(id);
2938            }
2939            current = node.parent;
2940        }
2941        None
2942    }
2943
2944    pub fn scroll_nearest_container_by(&mut self, node_id: NodeId, x: f64, y: f64) -> bool {
2945        self.scroll_nearest_container_by_with_events(node_id, x, y, |_| {})
2946    }
2947
2948    pub fn scroll_nearest_container_by_with_events<F: FnMut(DomEvent)>(
2949        &mut self,
2950        node_id: NodeId,
2951        x: f64,
2952        y: f64,
2953        mut dispatch_event: F,
2954    ) -> bool {
2955        let mut current = Some(node_id);
2956        for _ in 0..64 {
2957            let Some(id) = current else { break };
2958            let Some(node) = self.nodes.get(id) else {
2959                break;
2960            };
2961            let scrolls = node.style().overflow.x.is_scroll_container()
2962                || node.style().overflow.y.is_scroll_container();
2963            if scrolls {
2964                self.scroll_node_by(id, x, y, &mut dispatch_event);
2965                return true;
2966            }
2967            current = node.parent;
2968        }
2969        self.scroll_viewport_by(x, y);
2970        false
2971    }
2972
2973    pub fn scroll_to_node(&mut self, node_id: NodeId) {
2974        self.scroll_to_node_with_events(node_id, |_| {});
2975    }
2976
2977    pub fn scroll_to_node_with_events<F: FnMut(DomEvent)>(
2978        &mut self,
2979        node_id: NodeId,
2980        dispatch_event: F,
2981    ) {
2982        self.scroll_to_node_aligned_with_events(node_id, false, dispatch_event);
2983    }
2984
2985    /// Reveal a node near the centre of each scrollport and the viewport.
2986    ///
2987    /// Semantic input uses this placement so fixed or sticky chrome cannot
2988    /// cover the point an automation client will drive. Ordinary DOM
2989    /// `scrollIntoView` and fragment navigation retain their start alignment.
2990    pub fn scroll_to_node_centered_with_events<F: FnMut(DomEvent)>(
2991        &mut self,
2992        node_id: NodeId,
2993        dispatch_event: F,
2994    ) {
2995        self.scroll_to_node_aligned_with_events(node_id, true, dispatch_event);
2996    }
2997
2998    fn scroll_to_node_aligned_with_events<F: FnMut(DomEvent)>(
2999        &mut self,
3000        node_id: NodeId,
3001        centered: bool,
3002        mut dispatch_event: F,
3003    ) {
3004        // Semantic snapshots expose visible text as addressable nodes, but a
3005        // text node does not own a Taffy layout box. `absolute_position` and
3006        // the scrolling calculations below require one, so resolve such a
3007        // target to its nearest layout ancestor first. Without this, asking an
3008        // automation client to reveal a label panics the entire host at
3009        // `Node::final_layout` instead of scrolling the label's box into view.
3010        let mut scroll_target = Some(node_id);
3011        let node_id = loop {
3012            let Some(candidate) = scroll_target else {
3013                return;
3014            };
3015            let Some(node) = self.nodes.get(candidate) else {
3016                return;
3017            };
3018            if matches!(
3019                node.data,
3020                NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
3021            ) {
3022                break candidate;
3023            }
3024            scroll_target = node.layout_parent.get().or(node.parent);
3025        };
3026
3027        // Every scroll container between the node and the root, innermost
3028        // first. Scrolling only the viewport is not `scrollIntoView`: it does
3029        // nothing at all for a node inside a nested scroller, which is what an
3030        // application's own scrolling panes are.
3031        //
3032        // This was not academic. A transcript pane held its "Show 12 earlier
3033        // messages" button at y=-9463 and neither wheel events, Page Up nor
3034        // this call moved it by a single pixel, so a layout bug that only
3035        // appears further up the thread could not be reached from outside the
3036        // app at all. Every measurement of it had to come from a human
3037        // scrolling by hand and saying "now".
3038        let mut chain = Vec::new();
3039        let mut current = self.nodes.get(node_id).and_then(|node| node.parent);
3040        while let Some(id) = current {
3041            let Some(node) = self.nodes.get(id) else {
3042                break;
3043            };
3044            let scrolls = node.style().overflow.x.is_scroll_container()
3045                || node.style().overflow.y.is_scroll_container();
3046            if scrolls {
3047                chain.push(id);
3048            }
3049            current = node.parent;
3050        }
3051
3052        // Innermost first: scrolling an outer container moves the inner one, so
3053        // the inner offsets have to be settled before the outer ones are
3054        // measured, and each step re-reads the node's position.
3055        for container in chain {
3056            let Some(node) = self.nodes.get(node_id) else {
3057                return;
3058            };
3059            let target = node.absolute_position(0.0, 0.0);
3060            let Some(scroller) = self.nodes.get(container) else {
3061                continue;
3062            };
3063            let box_ = scroller.absolute_position(0.0, 0.0);
3064            let layout = scroller.final_layout();
3065            let target_layout = node.final_layout();
3066            // `scroll_node_by` subtracts its delta from the offset. Semantic
3067            // automation centres the target to avoid chrome; DOM behavior
3068            // keeps its historical start alignment.
3069            let (dx, dy) = if centered {
3070                (
3071                    f64::from(
3072                        box_.x + layout.size.width / 2.0
3073                            - (target.x + target_layout.size.width / 2.0),
3074                    ),
3075                    f64::from(
3076                        box_.y + layout.size.height / 2.0
3077                            - (target.y + target_layout.size.height / 2.0),
3078                    ),
3079                )
3080            } else {
3081                (f64::from(box_.x - target.x), f64::from(box_.y - target.y))
3082            };
3083            self.scroll_node_by(container, dx, dy, &mut dispatch_event);
3084        }
3085
3086        // `absolute_position` gives the node's position in document space (it does not
3087        // account for the viewport scroll), so it is the scroll offset we want to land on.
3088        let Some(node) = self.nodes.get(node_id) else {
3089            return;
3090        };
3091        let target = node.absolute_position(0.0, 0.0);
3092        let current = self.viewport_scroll;
3093        let (desired_x, desired_y) = if centered {
3094            let target_layout = node.final_layout();
3095            let scale = self.viewport.scale();
3096            let viewport_width = self.viewport.window_size.0 as f32 / scale;
3097            let viewport_height = self.viewport.window_size.1 as f32 / scale;
3098            (
3099                f64::from(target.x + target_layout.size.width / 2.0 - viewport_width / 2.0)
3100                    .max(0.0),
3101                f64::from(target.y + target_layout.size.height / 2.0 - viewport_height / 2.0)
3102                    .max(0.0),
3103            )
3104        } else {
3105            (f64::from(target.x), f64::from(target.y))
3106        };
3107
3108        // `scroll_viewport_by` subtracts the delta from the current scroll
3109        // offset, so pass current minus the centred destination.
3110        let dx = current.x - desired_x;
3111        let dy = current.y - desired_y;
3112        if let Some(root) = self.try_root_element().map(|element| element.id) {
3113            self.scroll_node_by(root, dx, dy, dispatch_event);
3114        } else {
3115            self.scroll_viewport_by(dx, dy);
3116        }
3117    }
3118
3119    /// Scroll to the element targeted by the given URL fragment (the `#...` part of a URL).
3120    ///
3121    /// An empty fragment (or a `top` fragment that matches no element) scrolls to the top
3122    /// of the document, matching browser behaviour. Returns `true` if a scroll target was
3123    /// found.
3124    pub fn scroll_to_fragment(&mut self, fragment: &str) -> bool {
3125        // Fragments are percent-encoded in URLs (e.g. `%20`); decode before matching.
3126        let decoded = percent_encoding::percent_decode_str(fragment)
3127            .decode_utf8_lossy()
3128            .into_owned();
3129
3130        if !decoded.is_empty() {
3131            if let Some(node_id) = self.get_fragment_target(&decoded) {
3132                self.scroll_to_node(node_id);
3133                return true;
3134            }
3135        }
3136
3137        // An empty fragment, or the special "top" fragment when no matching element exists,
3138        // scrolls to the top of the document.
3139        if decoded.is_empty() || decoded.eq_ignore_ascii_case("top") {
3140            let current = self.viewport_scroll;
3141            self.scroll_viewport_by(current.x, current.y);
3142            return true;
3143        }
3144
3145        false
3146    }
3147
3148    /// Computes the size and position of the `Node` relative to the viewport
3149    pub fn get_client_bounding_rect(&self, node_id: NodeId) -> Option<BoundingRect> {
3150        // Non-atomic inline elements have no layout box of their own: return
3151        // the union of their per-line-box fragment rects.
3152        if let Some(rects) = self.inline_fragment_rects(node_id) {
3153            let rects: Vec<_> = rects
3154                .into_iter()
3155                .map(|rect| self.transformed_client_rect(node_id, rect))
3156                .collect();
3157            let x0 = rects.iter().map(|r| r.x).fold(f64::INFINITY, f64::min);
3158            let y0 = rects.iter().map(|r| r.y).fold(f64::INFINITY, f64::min);
3159            let x1 = rects
3160                .iter()
3161                .map(|r| r.x + r.width)
3162                .fold(f64::NEG_INFINITY, f64::max);
3163            let y1 = rects
3164                .iter()
3165                .map(|r| r.y + r.height)
3166                .fold(f64::NEG_INFINITY, f64::max);
3167            return match rects.is_empty() {
3168                true => None,
3169                false => Some(BoundingRect {
3170                    x: x0,
3171                    y: y0,
3172                    width: x1 - x0,
3173                    height: y1 - y0,
3174                }),
3175            };
3176        }
3177
3178        let node = self.get_node(node_id)?;
3179        if !matches!(
3180            node.data,
3181            NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
3182        ) {
3183            return None;
3184        }
3185        let pos = node.absolute_position(0.0, 0.0);
3186
3187        Some(self.transformed_client_rect(
3188            node_id,
3189            BoundingRect {
3190                x: pos.x as f64 - self.viewport_scroll.x,
3191                y: pos.y as f64 - self.viewport_scroll.y,
3192                width: node.unrounded_layout().size.width as f64,
3193                height: node.unrounded_layout().size.height as f64,
3194            },
3195        ))
3196    }
3197
3198    /// Map the layout rectangle through the same two-dimensional transforms as paint.
3199    /// Layout coordinates deliberately exclude transforms; CSSOM client rectangles do not.
3200    fn transformed_client_rect(&self, node_id: NodeId, rect: BoundingRect) -> BoundingRect {
3201        let mut matrix = kurbo::Affine::IDENTITY;
3202        let mut current = self.get_node(node_id);
3203        let scale = self.viewport.scale_f64();
3204        while let Some(node) = current {
3205            if matches!(
3206                node.data,
3207                NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
3208            ) && let Some(transform) = *node.transform()
3209            {
3210                let [a, b, c, d, e, f] = transform.as_coeffs();
3211                let css_transform = kurbo::Affine::new([a, b, c, d, e / scale, f / scale]);
3212                let origin = node.absolute_position(0.0, 0.0);
3213                let translate = kurbo::Affine::translate((
3214                    origin.x as f64 - self.viewport_scroll.x,
3215                    origin.y as f64 - self.viewport_scroll.y,
3216                ));
3217                matrix = translate * css_transform * translate.inverse() * matrix;
3218            }
3219            current = node
3220                .layout_parent
3221                .get()
3222                .or(node.parent)
3223                .and_then(|id| self.get_node(id));
3224        }
3225        let transformed = matrix.transform_rect_bbox(kurbo::Rect::new(
3226            rect.x,
3227            rect.y,
3228            rect.x + rect.width,
3229            rect.y + rect.height,
3230        ));
3231        BoundingRect {
3232            x: transformed.x0,
3233            y: transformed.y0,
3234            width: transformed.width(),
3235            height: transformed.height(),
3236        }
3237    }
3238
3239    /// Computes the sizes and positions of the `Node`'s box fragments relative to the
3240    /// viewport (CSSOM `getClientRects()` semantics). Nodes with their own layout box
3241    /// return a single rect. Non-atomic inline elements (which are laid out as style
3242    /// spans within an inline root's text layout) return one rect per line box.
3243    pub fn node_client_rects(&self, node_id: NodeId) -> Vec<BoundingRect> {
3244        match self.inline_fragment_rects(node_id) {
3245            Some(rects) => rects
3246                .into_iter()
3247                .map(|rect| self.transformed_client_rect(node_id, rect))
3248                .collect(),
3249            None => self.get_client_bounding_rect(node_id).into_iter().collect(),
3250        }
3251    }
3252
3253    /// Computes per-line-box fragment rects for a non-atomic inline element by walking
3254    /// the containing inline root's text layout. Returns `None` for nodes that have
3255    /// their own layout box (which should use `get_client_bounding_rect` instead).
3256    /// Report inline elements whose fragment rects lie outside the inline root
3257    /// that owns them. `BLITZ_TRACE_INLINE=1`, once per resolve.
3258    ///
3259    /// A non-atomic inline element has no layout box of its own: its geometry
3260    /// is read back out of the containing inline root's text layout on demand.
3261    /// So "the chip is 900px to the right of its block" is a statement about
3262    /// that text layout, and the only way to see it is from in here, with both
3263    /// the fragment and the root in hand. Every earlier attempt to chase this
3264    /// from outside was reading a number the engine computes on the fly and
3265    /// could not say where it came from.
3266    pub(crate) fn trace_escaped_inline_fragments(&self) {
3267        static TRACE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3268        if !*TRACE.get_or_init(|| std::env::var_os("BLITZ_TRACE_INLINE").is_some()) {
3269            return;
3270        }
3271        let mut reported = 0;
3272        for (id, node) in self.nodes.iter() {
3273            if !node.is_element() {
3274                continue;
3275            }
3276            let Some(rects) = self.inline_fragment_rects(id) else {
3277                continue;
3278            };
3279            let Some(root) = node.inline_root_ancestor() else {
3280                continue;
3281            };
3282            let root_layout = root.final_layout();
3283            let root_pos = root.absolute_position(0.0, 0.0);
3284            let root_right =
3285                root_pos.x as f64 + root_layout.size.width as f64 - self.viewport_scroll.x;
3286            for rect in &rects {
3287                if rect.x + rect.width > root_right + 1.0 {
3288                    reported += 1;
3289                    if reported <= 12 {
3290                        eprintln!(
3291                            "escaped-fragment node={id:?} rect=[{:.1},{:.1} {:.1}x{:.1}] \
3292root={:?} root_right={root_right:.1} root_w={:.1} lines={} layout_scale={:.2} vp_scale={:.2} layout_w={:.1}",
3293                            rect.x,
3294                            rect.y,
3295                            rect.width,
3296                            rect.height,
3297                            root.id,
3298                            root_layout.size.width,
3299                            root.element_data()
3300                                .and_then(|e| e.inline_layout_data.as_ref())
3301                                .map(|i| i.layout.len())
3302                                .unwrap_or(0),
3303                            root.element_data()
3304                                .and_then(|e| e.inline_layout_data.as_ref())
3305                                .map(|i| i.layout.scale())
3306                                .unwrap_or(0.0),
3307                            self.viewport.scale(),
3308                            root.element_data()
3309                                .and_then(|e| e.inline_layout_data.as_ref())
3310                                .map(|i| i.layout.width())
3311                                .unwrap_or(0.0),
3312                        );
3313                    }
3314                    break;
3315                }
3316            }
3317        }
3318        if reported > 0 {
3319            eprintln!("escaped-fragment total={reported}");
3320        }
3321
3322        // The opposite failure, and the one that reads as "first load is
3323        // broken": lines broken far narrower than the box they sit in, so a
3324        // paragraph comes out as a column of one or two words inside a
3325        // full-width bubble. Nothing escapes, so the check above never sees it.
3326        let mut narrow = 0;
3327        for (id, node) in self.nodes.iter() {
3328            let Some(inline) = node
3329                .data
3330                .downcast_element()
3331                .and_then(|element| element.inline_layout_data.as_ref())
3332            else {
3333                continue;
3334            };
3335            let box_width = node.final_layout().size.width as f64 * self.viewport.scale() as f64;
3336            let broken_at = inline.layout.width() as f64;
3337            // Only interesting when the text had more to give: a short string
3338            // legitimately measures narrower than its box.
3339            let full = inline.layout.calculate_content_widths().max as f64;
3340            if box_width > 40.0 && broken_at < box_width * 0.6 && full > box_width * 0.9 {
3341                narrow += 1;
3342                if narrow <= 12 {
3343                    eprintln!(
3344                        "narrow-break node={id:?} broken_at={broken_at:.1} box={box_width:.1} \
3345                         max_content={full:.1} lines={} text={:?}",
3346                        inline.layout.len(),
3347                        inline.text.chars().take(40).collect::<String>(),
3348                    );
3349                }
3350            }
3351        }
3352        if narrow > 0 {
3353            eprintln!("narrow-break total={narrow}");
3354        }
3355    }
3356
3357    pub fn inline_fragment_rects(&self, node_id: NodeId) -> Option<Vec<BoundingRect>> {
3358        use parley::PositionedLayoutItem;
3359
3360        let node = self.get_node(node_id)?;
3361
3362        // Text and non-atomic inline elements live in the inline root's glyph
3363        // runs rather than owning layout boxes.
3364        let is_text = node.is_text_node();
3365        if !is_text {
3366            if !node.is_element() || node.flags.is_inline_root() {
3367                return None;
3368            }
3369            let display = node.primary_styles()?.clone_display();
3370            if !(display.outside() == DisplayOutside::Inline
3371                && display.inside() == DisplayInside::Flow)
3372            {
3373                return None;
3374            }
3375        }
3376
3377        let inline_root = node.inline_root_ancestor()?;
3378        let inline_layout = inline_root.element_data()?.inline_layout_data.as_ref()?;
3379        let layout = &inline_layout.layout;
3380        let scale = layout.scale() as f64;
3381
3382        // Walk up the DOM parent chain from `id` to check whether it is (or is
3383        // inside) the target node, stopping at the inline root.
3384        let is_in_target = |mut id: NodeId| -> bool {
3385            loop {
3386                if id == node_id {
3387                    return true;
3388                }
3389                if id == inline_root.id {
3390                    return false;
3391                }
3392                match self.get_node(id).and_then(|n| n.parent) {
3393                    Some(parent) => id = parent,
3394                    None => return false,
3395                }
3396            }
3397        };
3398
3399        // Fragment rects are relative to the inline root's content box.
3400        let root_layout = inline_root.final_layout();
3401        let root_pos = inline_root.absolute_position(0.0, 0.0);
3402        let origin_x = root_pos.x as f64
3403            + (root_layout.padding.left + root_layout.border.left) as f64
3404            - self.viewport_scroll.x;
3405        let origin_y = root_pos.y as f64
3406            + (root_layout.padding.top + root_layout.border.top) as f64
3407            - self.viewport_scroll.y;
3408
3409        let mut rects: Vec<BoundingRect> = Vec::new();
3410        for line in layout.lines() {
3411            let line_metrics = line.metrics();
3412            // Union all of the target's fragments on this line into a single rect
3413            let mut line_rect: Option<(f64, f64, f64, f64)> = None;
3414            let mut add = |x0: f64, y0: f64, x1: f64, y1: f64| {
3415                line_rect = Some(match line_rect {
3416                    Some((lx0, ly0, lx1, ly1)) => {
3417                        (lx0.min(x0), ly0.min(y0), lx1.max(x1), ly1.max(y1))
3418                    }
3419                    None => (x0, y0, x1, y1),
3420                });
3421            };
3422
3423            for item in line.items() {
3424                match item {
3425                    PositionedLayoutItem::GlyphRun(glyph_run) => {
3426                        let brush = glyph_run.style().brush;
3427                        let matches = if is_text {
3428                            brush.text_node == Some(node_id)
3429                        } else {
3430                            is_in_target(brush.id)
3431                        };
3432                        if !matches {
3433                            continue;
3434                        }
3435                        let x0 = glyph_run.offset() as f64;
3436                        let x1 = x0 + glyph_run.advance() as f64;
3437                        // Use the line box's block extent rather than the
3438                        // run's font ascent/descent: fonts with small
3439                        // typographic metrics would otherwise produce rects
3440                        // that clip the rendered glyphs. This matches the
3441                        // geometry used for text selection highlights.
3442                        let y0 = line_metrics.block_min_coord as f64;
3443                        let y1 = line_metrics.block_max_coord as f64;
3444                        add(x0, y0, x1, y1);
3445                    }
3446                    PositionedLayoutItem::InlineBox(inline_box) => {
3447                        if is_text || !is_in_target(NodeId::from_u64(inline_box.id)) {
3448                            continue;
3449                        }
3450                        let x0 = inline_box.x as f64;
3451                        let y0 = inline_box.y as f64;
3452                        add(
3453                            x0,
3454                            y0,
3455                            x0 + inline_box.width as f64,
3456                            y0 + inline_box.height as f64,
3457                        );
3458                    }
3459                }
3460            }
3461
3462            if let Some((x0, y0, x1, y1)) = line_rect {
3463                rects.push(BoundingRect {
3464                    x: origin_x + x0 / scale,
3465                    y: origin_y + y0 / scale,
3466                    width: (x1 - x0) / scale,
3467                    height: (y1 - y0) / scale,
3468                });
3469            }
3470        }
3471
3472        Some(rects)
3473    }
3474
3475    pub fn find_title_node(&self) -> Option<&Node> {
3476        TreeTraverser::new(self)
3477            .find(|node_id| {
3478                let node = &self.nodes[*node_id];
3479                let Some(element) = node.element_data() else {
3480                    return false;
3481                };
3482                if element.name.ns != ns!(html) || element.name.local != local_name!("title") {
3483                    return false;
3484                }
3485                node.parent
3486                    .and_then(|parent_id| self.nodes.get(parent_id))
3487                    .and_then(Node::element_data)
3488                    .is_some_and(|parent| {
3489                        parent.name.ns == ns!(html) && parent.name.local == local_name!("head")
3490                    })
3491            })
3492            .map(|node_id| &self.nodes[node_id])
3493    }
3494
3495    pub fn with_text_input(
3496        &mut self,
3497        node_id: NodeId,
3498        cb: impl FnOnce(PlainEditorDriver<TextBrush>),
3499    ) {
3500        let Some(node) = self.nodes.get_mut(node_id) else {
3501            return;
3502        };
3503
3504        if let Some(text_input) = node
3505            .element_data_mut()
3506            .and_then(|el| el.text_input_data_mut())
3507        {
3508            let mut font_ctx = self.font_ctx.lock().unwrap();
3509            let layout_ctx = &mut self.layout_ctx;
3510            let driver = text_input.editor.driver(&mut font_ctx, layout_ctx);
3511            cb(driver)
3512        }
3513    }
3514
3515    /// Recompute the scroll offset of the text input at `node_id` (if any) so that its caret
3516    /// remains visible within the input's content box.
3517    pub(crate) fn clamp_text_input_scroll(&mut self, node_id: NodeId) {
3518        let Some(node) = self.nodes.get_mut(node_id) else {
3519            return;
3520        };
3521
3522        let content_box_width = node.final_layout().content_box_width();
3523        let content_box_height = node.final_layout().content_box_height();
3524
3525        if let Some(text_input) = node
3526            .element_data_mut()
3527            .and_then(|el| el.text_input_data_mut())
3528        {
3529            text_input.clamp_scroll_offset(content_box_width, content_box_height);
3530        }
3531    }
3532
3533    pub(crate) fn compute_has_canvas(&self) -> bool {
3534        TreeTraverser::new(self).any(|node_id| {
3535            let node = &self.nodes[node_id];
3536            let Some(element) = node.element_data() else {
3537                return false;
3538            };
3539            if element.name.local == local_name!("canvas") && element.has_attr(local_name!("src")) {
3540                return true;
3541            }
3542
3543            false
3544        })
3545    }
3546
3547    // Text selection methods
3548
3549    /// Find the text position (inline_root_id, byte_offset) at a given point.
3550    /// Uses hit() for proper coordinate transformation, then finds the inline root
3551    /// and byte offset.
3552    pub fn find_text_position(&self, x: f32, y: f32) -> Option<(NodeId, usize)> {
3553        let hit = self.hit(x, y)?;
3554        let hit_node = self.get_node(hit.node_id)?;
3555        let inline_root = hit_node.inline_root_ancestor()?;
3556        let byte_offset = inline_root.text_offset_at_point(hit.x, hit.y)?;
3557        Some((inline_root.id, byte_offset))
3558    }
3559
3560    /// Find the word or line at a point, as `(inline_root_id, start, end)`.
3561    ///
3562    /// The multi-click counterpart of
3563    /// [`find_text_position`](Self::find_text_position): that one answers where
3564    /// a caret goes, this one answers what a double or triple click selects.
3565    pub fn find_text_range(
3566        &self,
3567        x: f32,
3568        y: f32,
3569        granularity: TextGranularity,
3570    ) -> Option<(NodeId, usize, usize)> {
3571        let hit = self.hit(x, y)?;
3572        let hit_node = self.get_node(hit.node_id)?;
3573        let inline_root = hit_node.inline_root_ancestor()?;
3574        let range = inline_root.text_range_at_point(hit.x, hit.y, granularity)?;
3575        Some((inline_root.id, range.start, range.end))
3576    }
3577
3578    /// Set the text selection range (creates a new selection from anchor to focus)
3579    pub fn set_text_selection(
3580        &mut self,
3581        anchor_node: NodeId,
3582        anchor_offset: usize,
3583        focus_node: NodeId,
3584        focus_offset: usize,
3585    ) {
3586        self.text_selection =
3587            TextSelection::new(anchor_node, anchor_offset, focus_node, focus_offset);
3588
3589        // For anonymous blocks, switch to storing parent+sibling_index (stable reference)
3590        if let (Some(parent), Some(idx)) = self.anonymous_block_location(anchor_node) {
3591            self.text_selection
3592                .anchor
3593                .set_anonymous(parent, idx, anchor_offset);
3594        }
3595        if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
3596            self.text_selection
3597                .focus
3598                .set_anonymous(parent, idx, focus_offset);
3599        }
3600    }
3601
3602    /// Get the parent ID and sibling index for a node if it's an anonymous block.
3603    /// Returns (None, None) for non-anonymous blocks.
3604    fn anonymous_block_location(&self, node_id: NodeId) -> (Option<NodeId>, Option<usize>) {
3605        let Some(node) = self.get_node(node_id) else {
3606            return (None, None);
3607        };
3608
3609        if !node.is_anonymous() {
3610            return (None, None);
3611        }
3612
3613        let Some(parent_id) = node.parent else {
3614            return (None, None);
3615        };
3616
3617        let Some(parent) = self.get_node(parent_id) else {
3618            return (Some(parent_id), None);
3619        };
3620
3621        let layout_children = parent.layout_children.borrow();
3622        let Some(children) = layout_children.as_ref() else {
3623            return (Some(parent_id), None);
3624        };
3625
3626        // Find the index of this anonymous block among siblings
3627        let mut anon_index = 0;
3628        for &child_id in children.iter() {
3629            if child_id == node_id {
3630                return (Some(parent_id), Some(anon_index));
3631            }
3632            if self.get_node(child_id).is_some_and(|n| n.is_anonymous()) {
3633                anon_index += 1;
3634            }
3635        }
3636
3637        (Some(parent_id), None)
3638    }
3639
3640    /// Clear the text selection
3641    pub fn clear_text_selection(&mut self) {
3642        self.text_selection.clear();
3643    }
3644
3645    /// Update the selection focus point (used during mouse drag to extend selection).
3646    pub fn update_selection_focus(&mut self, focus_node: NodeId, focus_offset: usize) {
3647        // For anonymous blocks, store parent+sibling_index; otherwise store node directly
3648        if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
3649            self.text_selection
3650                .focus
3651                .set_anonymous(parent, idx, focus_offset);
3652        } else {
3653            self.text_selection.set_focus(focus_node, focus_offset);
3654        }
3655    }
3656
3657    /// Extend text selection to the given point. Returns true if selection was updated.
3658    /// This is a convenience method that combines find_text_position and update_selection_focus.
3659    pub fn extend_text_selection_to_point(&mut self, x: f32, y: f32) -> bool {
3660        if !self.text_selection.anchor.is_some() {
3661            return false;
3662        }
3663
3664        if let Some((node, offset)) = self.find_text_position(x, y) {
3665            self.update_selection_focus(node, offset);
3666            self.shell_provider.request_redraw();
3667            true
3668        } else {
3669            false
3670        }
3671    }
3672
3673    /// Find the Nth anonymous block under a parent.
3674    fn find_anonymous_block_by_index(
3675        &self,
3676        parent_id: NodeId,
3677        target_index: usize,
3678    ) -> Option<NodeId> {
3679        let parent = self.get_node(parent_id)?;
3680        let layout_children = parent.layout_children.borrow();
3681        let children = layout_children.as_ref()?;
3682
3683        children
3684            .iter()
3685            .filter(|&&child_id| self.get_node(child_id).is_some_and(|n| n.is_anonymous()))
3686            .nth(target_index)
3687            .copied()
3688    }
3689
3690    /// Check if there is an active (non-empty) text selection
3691    pub fn has_text_selection(&self) -> bool {
3692        self.text_selection.is_active()
3693    }
3694
3695    /// Get the selected text content, supporting selection across multiple inline roots.
3696    pub fn get_selected_text(&self) -> Option<String> {
3697        let ranges = self.get_text_selection_ranges();
3698        if ranges.is_empty() {
3699            return None;
3700        }
3701
3702        let mut result = String::new();
3703        for (node_id, start, end) in &ranges {
3704            let node = self.get_node(*node_id)?;
3705            let element_data = node.element_data()?;
3706            let inline_layout = element_data.inline_layout_data.as_ref()?;
3707
3708            if *end > inline_layout.text.len() {
3709                continue;
3710            }
3711
3712            if !result.is_empty() {
3713                result.push(' ');
3714            }
3715            result.push_str(&inline_layout.text[*start..*end]);
3716        }
3717
3718        if result.is_empty() {
3719            None
3720        } else {
3721            Some(result)
3722        }
3723    }
3724
3725    /// The selection as HTML, for a clipboard that can carry formatting.
3726    ///
3727    /// The same ranges [`Self::get_selected_text`] walks, with each one wrapped
3728    /// in the tag of the element it came from, so bold stays bold and a code
3729    /// span stays a code span when it lands in another application. Pasting
3730    /// formatted text out of a Blitz window was impossible before this: plain
3731    /// text was the only payload ever written.
3732    ///
3733    /// Only the element's own tag is reproduced, not its ancestors or its
3734    /// styles. That is deliberate: a paste should carry the structure the author
3735    /// wrote, not this window's theme, and dragging a `class` into another
3736    /// application's document would reference styles that do not exist there.
3737    pub fn get_selected_html(&self) -> Option<String> {
3738        let ranges = self.get_text_selection_ranges();
3739        if ranges.is_empty() {
3740            return None;
3741        }
3742
3743        let mut result = String::new();
3744        for (node_id, start, end) in &ranges {
3745            let Some(node) = self.get_node(*node_id) else {
3746                continue;
3747            };
3748            let Some(element_data) = node.element_data() else {
3749                continue;
3750            };
3751            let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
3752                continue;
3753            };
3754            if *end > inline_layout.text.len() {
3755                continue;
3756            }
3757
3758            if !result.is_empty() {
3759                result.push(' ');
3760            }
3761
3762            let tag = element_data.name.local.as_ref();
3763            let fragment = escape_html(&inline_layout.text[*start..*end]);
3764            // A block tag around a fragment of a paragraph would be wrong: the
3765            // range is a run of text, and the element it belongs to may be only
3766            // partly selected. Inline tags carry meaning a paste should keep;
3767            // anything else contributes its text and nothing more.
3768            if matches!(
3769                tag,
3770                "b" | "strong" | "i" | "em" | "code" | "kbd" | "mark" | "s" | "u" | "sub" | "sup"
3771            ) {
3772                result.push_str(&format!("<{tag}>{fragment}</{tag}>"));
3773            } else {
3774                result.push_str(&fragment);
3775            }
3776        }
3777
3778        if result.is_empty() {
3779            None
3780        } else {
3781            Some(result)
3782        }
3783    }
3784
3785    /// Get all selection ranges as Vec<(node_id, start_offset, end_offset)>.
3786    /// Returns empty vec if no selection.
3787    pub fn get_text_selection_ranges(&self) -> Vec<(NodeId, usize, usize)> {
3788        let lookup = |parent_id, idx| self.find_anonymous_block_by_index(parent_id, idx);
3789
3790        let anchor_node = match self.text_selection.anchor.resolve_node_id(lookup) {
3791            Some(id) => id,
3792            None => return Vec::new(),
3793        };
3794        let focus_node = match self.text_selection.focus.resolve_node_id(lookup) {
3795            Some(id) => id,
3796            None => return Vec::new(),
3797        };
3798
3799        // Guard against stale selection endpoints: nodes may have been removed from
3800        // the document (e.g. by script) since the selection was made.
3801        let node_is_in_doc = |node_id: NodeId| {
3802            self.nodes
3803                .get(node_id)
3804                .is_some_and(|node| node.flags.is_in_document())
3805        };
3806        if !node_is_in_doc(anchor_node) || !node_is_in_doc(focus_node) {
3807            return Vec::new();
3808        }
3809
3810        // Single node selection
3811        if anchor_node == focus_node {
3812            let start = self
3813                .text_selection
3814                .anchor
3815                .offset
3816                .min(self.text_selection.focus.offset);
3817            let end = self
3818                .text_selection
3819                .anchor
3820                .offset
3821                .max(self.text_selection.focus.offset);
3822
3823            if start == end {
3824                return Vec::new();
3825            }
3826            return vec![(anchor_node, start, end)];
3827        }
3828
3829        // Multi-node selection: collect all inline roots between anchor and focus
3830        let inline_roots = self.collect_inline_roots_in_range(anchor_node, focus_node);
3831        if inline_roots.is_empty() {
3832            return Vec::new();
3833        }
3834
3835        // Determine document order using the collected inline_roots order
3836        // (inline_roots is already in document order from first to last)
3837        let first_in_roots = inline_roots[0];
3838
3839        let (first_node, first_offset, last_node, last_offset) =
3840            if first_in_roots == anchor_node || (first_in_roots != focus_node) {
3841                // anchor is first (or neither endpoint is in roots, which shouldn't happen)
3842                (
3843                    anchor_node,
3844                    self.text_selection.anchor.offset,
3845                    focus_node,
3846                    self.text_selection.focus.offset,
3847                )
3848            } else {
3849                // focus is first
3850                (
3851                    focus_node,
3852                    self.text_selection.focus.offset,
3853                    anchor_node,
3854                    self.text_selection.anchor.offset,
3855                )
3856            };
3857
3858        let mut ranges = Vec::with_capacity(inline_roots.len());
3859
3860        for &node_id in &inline_roots {
3861            let Some(node) = self.get_node(node_id) else {
3862                continue;
3863            };
3864            let Some(element_data) = node.element_data() else {
3865                continue;
3866            };
3867            let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
3868                continue;
3869            };
3870
3871            let text_len = inline_layout.text.len();
3872
3873            if node_id == first_node && node_id == last_node {
3874                let start = first_offset.min(last_offset);
3875                let end = first_offset.max(last_offset);
3876                if start < end && end <= text_len {
3877                    ranges.push((node_id, start, end));
3878                }
3879            } else if node_id == first_node {
3880                if first_offset < text_len {
3881                    ranges.push((node_id, first_offset, text_len));
3882                }
3883            } else if node_id == last_node {
3884                if last_offset > 0 && last_offset <= text_len {
3885                    ranges.push((node_id, 0, last_offset));
3886                }
3887            } else if text_len > 0 {
3888                ranges.push((node_id, 0, text_len));
3889            }
3890        }
3891
3892        ranges
3893    }
3894}
3895
3896#[derive(Debug, Clone, Copy, PartialEq)]
3897pub struct BoundingRect {
3898    pub x: f64,
3899    pub y: f64,
3900    pub width: f64,
3901    pub height: f64,
3902}
3903
3904/// The record of changes a script's `MutationObserver` reads. See
3905/// [`crate::DomMutation`] for what is recorded and why.
3906impl BaseDocument {
3907    /// Start or stop recording changes. Stopping discards what was recorded
3908    /// and not yet taken.
3909    pub fn set_recording_mutations(&mut self, recording: bool) {
3910        match (recording, self.mutation_log.is_some()) {
3911            (true, false) => self.mutation_log = Some(Vec::new()),
3912            (false, true) => self.mutation_log = None,
3913            _ => {}
3914        }
3915    }
3916
3917    /// Whether changes are being recorded.
3918    pub fn is_recording_mutations(&self) -> bool {
3919        self.mutation_log.is_some()
3920    }
3921
3922    /// Take every change recorded since the last call, oldest first.
3923    pub fn take_mutations(&mut self) -> Vec<crate::DomMutation> {
3924        self.mutation_log
3925            .as_mut()
3926            .map(std::mem::take)
3927            .unwrap_or_default()
3928    }
3929
3930    pub(crate) fn record_mutation(&mut self, mutation: crate::DomMutation) {
3931        if let Some(log) = self.mutation_log.as_mut() {
3932            if log.len() < crate::mutation_record::MUTATION_LOG_CAP {
3933                log.push(mutation);
3934            }
3935        }
3936    }
3937
3938    /// Where `node_id` sits among its siblings: its parent and the siblings on
3939    /// either side, as a child-list record reports them.
3940    pub(crate) fn sibling_context(
3941        &self,
3942        node_id: NodeId,
3943    ) -> Option<(NodeId, Option<NodeId>, Option<NodeId>)> {
3944        let parent_id = self.nodes.get(node_id)?.parent?;
3945        let siblings = &self.nodes.get(parent_id)?.children;
3946        let index = siblings.iter().position(|id| *id == node_id)?;
3947        let previous = index.checked_sub(1).map(|i| siblings[i]);
3948        let next = siblings.get(index + 1).copied();
3949        Some((parent_id, previous, next))
3950    }
3951}
3952
3953impl AsRef<BaseDocument> for BaseDocument {
3954    fn as_ref(&self) -> &BaseDocument {
3955        self
3956    }
3957}
3958
3959impl AsMut<BaseDocument> for BaseDocument {
3960    fn as_mut(&mut self) -> &mut BaseDocument {
3961        self
3962    }
3963}
3964
3965#[cfg(test)]
3966mod hover_state_tests {
3967    use super::*;
3968    use crate::{Attribute, qual_name};
3969    use blitz_traits::shell::ColorScheme;
3970
3971    /// Build `<html><body style="margin:0"><div style="width:300px">some text
3972    /// <div style="height:50px"></div></div></body></html>` manually (the HTML
3973    /// parser lives in blitz-html, which would be a circular dev-dependency).
3974    /// The bare text next to a block sibling gets wrapped in an anonymous
3975    /// block, which becomes the inline root: text hits report the anonymous
3976    /// block as the hit node.
3977    fn make_doc() -> (BaseDocument, NodeId) {
3978        let mut doc = BaseDocument::new(DocumentConfig {
3979            viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3980            ..Default::default()
3981        });
3982        let root_id = doc.root_node().id;
3983        let style = |value: &str| Attribute {
3984            name: qual_name!("style"),
3985            value: value.into(),
3986        };
3987
3988        let mut mutator = doc.mutate();
3989        let html = mutator.create_element(qual_name!("html"), vec![]);
3990        let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
3991        let container = mutator.create_element(qual_name!("div"), vec![style("width:300px")]);
3992        let text = mutator.create_text_node("some text");
3993        let block = mutator.create_element(qual_name!("div"), vec![style("height:50px")]);
3994        mutator.append_children(container, &[text, block]);
3995        mutator.append_children(body, &[container]);
3996        mutator.append_children(html, &[body]);
3997        mutator.append_children(root_id, &[html]);
3998        drop(mutator);
3999
4000        doc.resolve(0.0);
4001        (doc, container)
4002    }
4003
4004    /// Whether text laid out with a real (non-zero-metric) font. Without the
4005    /// `system-fonts` feature text measures 0x0 and text hits are impossible,
4006    /// making these tests vacuous.
4007    fn text_has_size(doc: &BaseDocument, container: NodeId) -> bool {
4008        doc.nodes[container].final_layout().size.height > 50.0
4009    }
4010
4011    /// Regression test: hovering bare text wrapped in an anonymous block must
4012    /// report a text cursor. The hit node for such text is the anonymous
4013    /// inline root itself, while the *stored* hover target is canonicalized to
4014    /// the containing element — the cursor must be derived from the precise
4015    /// hit node, not the canonical target.
4016    #[test]
4017    fn hovering_text_in_anonymous_block_reports_text_cursor() {
4018        let (mut doc, container) = make_doc();
4019        if !text_has_size(&doc, container) {
4020            eprintln!("skipping: no usable font (text measures 0x0)");
4021            return;
4022        }
4023
4024        doc.set_hover_to(5.0, 8.0);
4025        assert!(doc.hover_node_is_text, "expected a text hit");
4026        let hit_id = doc.hover_hit_node_id.expect("expected a hit node");
4027        assert!(
4028            doc.nodes[hit_id].is_anonymous(),
4029            "expected the hit node to be the anonymous inline root"
4030        );
4031        assert_eq!(
4032            doc.get_hover_node_id(),
4033            Some(container),
4034            "expected the stored hover target to be the containing element"
4035        );
4036        assert_eq!(doc.get_cursor(), Some(CursorIcon::Text));
4037    }
4038
4039    #[test]
4040    fn semantic_hover_keeps_the_resolved_node_instead_of_hit_testing_again() {
4041        let (mut doc, container) = make_doc();
4042
4043        // This coordinate is outside the 300px-wide container. A coordinate
4044        // hit test therefore cannot select it, but semantic automation has
4045        // already selected the container by id and must preserve that target.
4046        doc.set_hover_to_node(container, 350.0, 250.0);
4047
4048        assert_eq!(doc.get_hover_node_id(), Some(container));
4049        assert_eq!(doc.hover_hit_node_id, Some(container));
4050
4051        doc.resolve(0.0);
4052        assert_eq!(
4053            doc.get_hover_node_id(),
4054            Some(container),
4055            "a resolve must not turn semantic identity back into a coordinate hit"
4056        );
4057    }
4058
4059    /// Hovering the empty region of the anonymous block (right of the text) is
4060    /// not a text hit: default cursor, same canonical hover target.
4061    #[test]
4062    fn hovering_anonymous_block_whitespace_reports_default_cursor() {
4063        let (mut doc, container) = make_doc();
4064        if !text_has_size(&doc, container) {
4065            eprintln!("skipping: no usable font (text measures 0x0)");
4066            return;
4067        }
4068
4069        doc.set_hover_to(250.0, 8.0);
4070        assert!(!doc.hover_node_is_text);
4071        assert_eq!(doc.get_hover_node_id(), Some(container));
4072        assert_eq!(doc.get_cursor(), Some(CursorIcon::Default));
4073    }
4074}
4075
4076#[cfg(test)]
4077mod control_scroll_tests {
4078    use super::*;
4079    use crate::{Attribute, qual_name};
4080    use blitz_traits::shell::ColorScheme;
4081
4082    #[test]
4083    fn controlled_scroll_dispatches_the_dom_scroll_event() {
4084        let mut doc = BaseDocument::new(DocumentConfig {
4085            viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
4086            ..Default::default()
4087        });
4088        let root_id = doc.root_node().id;
4089        let style = |value: &str| Attribute {
4090            name: qual_name!("style"),
4091            value: value.into(),
4092        };
4093
4094        let mut mutator = doc.mutate();
4095        let html = mutator.create_element(qual_name!("html"), vec![]);
4096        let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
4097        let scroller = mutator.create_element(
4098            qual_name!("div"),
4099            vec![style("width:200px;height:100px;overflow-y:scroll")],
4100        );
4101        let spacer = mutator.create_element(qual_name!("div"), vec![style("height:400px")]);
4102        let target = mutator.create_element(qual_name!("button"), vec![style("height:40px")]);
4103        let target_text = mutator.create_text_node("Reveal me");
4104        mutator.append_children(target, &[target_text]);
4105        mutator.append_children(scroller, &[spacer, target]);
4106        mutator.append_children(body, &[scroller]);
4107        mutator.append_children(html, &[body]);
4108        mutator.append_children(root_id, &[html]);
4109        drop(mutator);
4110        doc.resolve(0.0);
4111
4112        // The manual mutator deliberately bypasses the HTML/style parser used
4113        // by loaded documents. Give the fixture explicit post-layout geometry
4114        // so this unit test isolates event forwarding rather than CSS parsing.
4115        doc.nodes[html].final_layout_mut().size.height = 300.0;
4116        doc.nodes[html].final_layout_mut().content_size.height = 600.0;
4117        doc.nodes[target].final_layout_mut().location.y = 400.0;
4118
4119        let mut events = Vec::new();
4120        // A semantic client may target the exposed text rather than its
4121        // element. Text has no `final_layout`, so this also pins the host-crash
4122        // regression from revealing a named label.
4123        doc.scroll_to_node_centered_with_events(target_text, |event| events.push(event));
4124
4125        assert!(
4126            (doc.viewport_scroll.y - 270.0).abs() < 0.1,
4127            "the 40px target should be centred in the 300px viewport"
4128        );
4129        assert!(
4130            events
4131                .iter()
4132                .any(|event| { event.target == html && event.name() == "scroll" })
4133        );
4134    }
4135}
4136
4137#[cfg(test)]
4138mod font_face_override_tests {
4139    use super::*;
4140    use crate::net::{FontFaceOverrides, Resource, ResourceLoadResponse};
4141
4142    /// Regression-pin for the `@font-face` descriptor-honouring fix.
4143    ///
4144    /// The bug was that `Resource::Font` carried only the raw font bytes,
4145    /// so `load_resource` registered fonts with `info_override = None` and
4146    /// parley fell back to the TTF's internal `name` table. After the fix,
4147    /// `Resource::Font` carries `FontFaceOverrides` and `load_resource`
4148    /// builds a `FontInfoOverride` from them — meaning a CSS-declared
4149    /// `font-family` alias wins over the file's own metadata.
4150    ///
4151    /// We drive `load_resource` directly with a fabricated response rather
4152    /// than go through HTML parsing → `fetch_font_face`, because the
4153    /// downstream HTML parser lives in `blitz-html` (would be a circular
4154    /// crate dependency). The mapping from `@font-face` descriptors into
4155    /// `FontFaceOverrides` is covered by the unit tests in `net.rs`; this
4156    /// test pins the load-side of the pipeline.
4157    #[test]
4158    fn font_face_overrides_alias_family_name() {
4159        const ALIAS: &str = "AliasedFamily";
4160
4161        let mut document = BaseDocument::new(DocumentConfig::default());
4162
4163        // Sanity: the alias name is not registered before we feed the font.
4164        {
4165            let mut ctx = document.font_ctx.lock().unwrap();
4166            assert!(
4167                ctx.collection.family_id(ALIAS).is_none(),
4168                "alias must not exist before registration",
4169            );
4170        }
4171
4172        // Drive `load_resource` with a `Resource::Font` whose overrides
4173        // assert the CSS-side family name. We use the bullet font as a
4174        // valid font payload — its internal `name` table is irrelevant to
4175        // the assertion; what matters is whether the override wins.
4176        let response = ResourceLoadResponse {
4177            request_id: 0,
4178            node_id: None,
4179            resolved_url: Some(String::from("test://aliased-family")),
4180            result: Ok(Resource::Font(
4181                blitz_traits::net::Bytes::from_static(crate::BULLET_FONT),
4182                FontFaceOverrides {
4183                    family_name: Some(String::from(ALIAS)),
4184                    weight: Some(800.0),
4185                    style: Some(parley::fontique::FontStyle::Italic),
4186                },
4187            )),
4188        };
4189        document.load_resource(response);
4190
4191        // The override must have taken effect: parley's `Collection` now
4192        // resolves the CSS-declared alias to a registered family.
4193        let mut ctx = document.font_ctx.lock().unwrap();
4194        let family_id = ctx
4195            .collection
4196            .family_id(ALIAS)
4197            .expect("CSS-declared family name should be registered as a family alias");
4198        let resolved_name = ctx
4199            .collection
4200            .family_name(family_id)
4201            .expect("family id should resolve back to a name");
4202        assert_eq!(
4203            resolved_name, ALIAS,
4204            "registered family should report the CSS-declared name, \
4205             not the font file's internal `name` table entry",
4206        );
4207    }
4208}
4209
4210#[cfg(test)]
4211mod clipboard_html_tests {
4212    use super::escape_html;
4213
4214    /// A selection is document text, so markup characters in it are content.
4215    /// Unescaped, `a < b` opens a tag in whatever receives the paste.
4216    #[test]
4217    fn markup_characters_in_a_selection_are_escaped() {
4218        assert_eq!(
4219            escape_html("a < b && c > d"),
4220            "a &lt; b &amp;&amp; c &gt; d"
4221        );
4222    }
4223
4224    /// The ampersand has to go first, or escaping the others re-escapes the
4225    /// entities just written and `<` arrives as `&amp;lt;`.
4226    #[test]
4227    fn an_ampersand_is_not_double_escaped() {
4228        assert_eq!(escape_html("&lt;"), "&amp;lt;");
4229    }
4230
4231    /// Ordinary prose is left exactly as it is, including the apostrophes and
4232    /// quotes that a paste should carry through unchanged.
4233    #[test]
4234    fn prose_is_untouched() {
4235        assert_eq!(escape_html("it's \"fine\""), "it's &quot;fine&quot;");
4236    }
4237}