Skip to main content

blitz_dom/
document.rs

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