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