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