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