Skip to main content

blitz_dom/
document.rs

1use crate::NodeTree;
2use crate::events::{DragMode, 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::{ImageData, NodeFlags, RasterImageData, SpecialElementData, Status, TextBrush};
11use crate::scrolling::ScrollAnimationState;
12use crate::selection::TextSelection;
13use crate::stylo_to_cursor_icon::stylo_to_cursor_icon;
14use crate::traversal::TreeTraverser;
15use crate::url::DocumentUrl;
16use crate::util::ImageType;
17use crate::{
18    DEFAULT_CSS, DocumentConfig, DocumentMutator, DummyHtmlParserProvider, ElementData,
19    EventDriver, HtmlParserProvider, Node, NodeData, NoopEventHandler, StyleThreading,
20    TextNodeData,
21};
22use blitz_traits::devtools::DevtoolSettings;
23use blitz_traits::events::{DomEvent, HitResult, UiEvent};
24use blitz_traits::navigation::{DummyNavigationProvider, NavigationProvider};
25use blitz_traits::net::{AbortSignal, DummyNetProvider, NetProvider, Request};
26use blitz_traits::node_id::NodeId;
27use blitz_traits::shell::{ColorScheme, DummyShellProvider, ShellProvider, Viewport};
28use cursor_icon::CursorIcon;
29use linebender_resource_handle::Blob;
30use markup5ever::{LocalName, local_name};
31use parley::{FontContext, PlainEditorDriver};
32use selectors::{Element, matching::QuirksMode};
33use smallvec::SmallVec;
34use std::any::Any;
35use std::cell::RefCell;
36use std::collections::{BTreeMap, Bound, HashMap, HashSet};
37use std::ops::{Deref, DerefMut};
38use std::rc::Rc;
39use std::str::FromStr;
40use std::sync::atomic::{AtomicUsize, Ordering};
41use std::sync::mpsc::{Receiver, Sender, channel};
42use std::sync::{Arc, Mutex, MutexGuard, OnceLock, RwLockReadGuard, RwLockWriteGuard};
43use std::task::{Context as TaskContext, Waker};
44use style::Atom;
45use style::animation::DocumentAnimationSet;
46use style::attr::{AttrIdentifier, AttrValue};
47use style::data::{ElementData as StyloElementData, ElementStyles};
48use style::media_queries::MediaType;
49use style::properties::ComputedValues;
50use style::properties::style_structs::Font;
51use style::queries::values::PrefersColorScheme;
52use style::selector_parser::ServoElementSnapshot;
53use style::servo::media_features::PointerCapabilities;
54use style::servo_arc::Arc as ServoArc;
55use style::values::GenericAtomIdent;
56use style::values::computed::UserSelect;
57use style::values::computed::ui::CursorKind;
58use style::values::specified::box_::{DisplayInside, DisplayOutside};
59use style::{
60    device::Device,
61    dom::{TDocument, TNode},
62    media_queries::MediaList,
63    selector_parser::SnapshotMap,
64    shared_lock::{SharedRwLock, StylesheetGuards},
65    stylesheets::{AllowImportRules, DocumentStyleSheet, Origin, Stylesheet},
66    stylist::Stylist,
67};
68use style_dom::ElementState;
69use thin_vec::ThinVec;
70use url::Url;
71use web_time::Instant;
72
73#[cfg(feature = "parallel-construct")]
74use thread_local::ThreadLocal;
75
76pub enum DocGuard<'a> {
77    Ref(&'a BaseDocument),
78    RefCell(std::cell::Ref<'a, BaseDocument>),
79    RwLock(RwLockReadGuard<'a, BaseDocument>),
80    Mutex(MutexGuard<'a, BaseDocument>),
81}
82
83impl Deref for DocGuard<'_> {
84    type Target = BaseDocument;
85    #[inline(always)]
86    fn deref(&self) -> &Self::Target {
87        match self {
88            Self::Ref(base_document) => base_document,
89            Self::RefCell(refcell_guard) => refcell_guard,
90            Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
91            Self::Mutex(mutex_guard) => mutex_guard,
92        }
93    }
94}
95
96pub enum DocGuardMut<'a> {
97    Ref(&'a mut BaseDocument),
98    RefCell(std::cell::RefMut<'a, BaseDocument>),
99    RwLock(RwLockWriteGuard<'a, BaseDocument>),
100    Mutex(MutexGuard<'a, BaseDocument>),
101}
102
103impl Deref for DocGuardMut<'_> {
104    type Target = BaseDocument;
105    #[inline(always)]
106    fn deref(&self) -> &Self::Target {
107        match self {
108            Self::Ref(base_document) => base_document,
109            Self::RefCell(refcell_guard) => refcell_guard,
110            Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
111            Self::Mutex(mutex_guard) => mutex_guard,
112        }
113    }
114}
115
116impl DerefMut for DocGuardMut<'_> {
117    #[inline(always)]
118    fn deref_mut(&mut self) -> &mut Self::Target {
119        match self {
120            Self::Ref(base_document) => base_document,
121            Self::RefCell(refcell_guard) => &mut *refcell_guard,
122            Self::RwLock(rw_lock_read_guard) => &mut *rw_lock_read_guard,
123            Self::Mutex(mutex_guard) => &mut *mutex_guard,
124        }
125    }
126}
127
128/// Abstraction over wrappers around [`BaseDocument`] to allow for them all to
129/// be driven by [`blitz-shell`](https://docs.rs/blitz-shell)
130pub trait Document: Any + 'static {
131    fn inner(&self) -> DocGuard<'_>;
132    fn inner_mut(&mut self) -> DocGuardMut<'_>;
133
134    /// Update the [`Document`] in response to a [`UiEvent`] (click, keypress, etc)
135    fn handle_ui_event(&mut self, event: UiEvent) {
136        let mut doc = self.inner_mut();
137        let mut driver = EventDriver::new(&mut *doc, NoopEventHandler);
138        driver.handle_ui_event(event);
139    }
140
141    /// Poll any pending async operations, and flush changes to the underlying [`BaseDocument`]
142    fn poll(&mut self, task_context: Option<TaskContext>) -> bool {
143        // Default implementation does nothing
144        let _ = task_context;
145        false
146    }
147
148    /// Get the [`Document`]'s id
149    fn id(&self) -> usize {
150        self.inner().id
151    }
152}
153
154pub struct PlainDocument(pub BaseDocument);
155impl Document for PlainDocument {
156    fn inner(&self) -> DocGuard<'_> {
157        DocGuard::Ref(&self.0)
158    }
159    fn inner_mut(&mut self) -> DocGuardMut<'_> {
160        DocGuardMut::Ref(&mut self.0)
161    }
162}
163
164impl Document for BaseDocument {
165    fn inner(&self) -> DocGuard<'_> {
166        DocGuard::Ref(self)
167    }
168    fn inner_mut(&mut self) -> DocGuardMut<'_> {
169        DocGuardMut::Ref(self)
170    }
171}
172
173impl Document for Rc<RefCell<BaseDocument>> {
174    fn inner(&self) -> DocGuard<'_> {
175        DocGuard::RefCell(self.borrow())
176    }
177
178    fn inner_mut(&mut self) -> DocGuardMut<'_> {
179        DocGuardMut::RefCell(self.borrow_mut())
180    }
181}
182
183pub enum DocumentEvent {
184    ResourceLoad(ResourceLoadResponse),
185    /// A navigation originating from within an iframe's sub-document
186    /// (e.g. a link click), to be applied to the iframe identified by `node_id`.
187    NavigateIframe {
188        node_id: NodeId,
189        url: Url,
190    },
191}
192
193pub struct BaseDocument {
194    /// ID of the document
195    id: usize,
196
197    // Config
198    /// Base url for resolving linked resources (stylesheets, images, fonts, etc)
199    pub(crate) url: DocumentUrl,
200    // Devtool settings. Currently used to render debug overlays
201    pub(crate) devtool_settings: DevtoolSettings,
202    // Viewport details such as the dimensions, HiDPI scale, and zoom factor,
203    pub(crate) viewport: Viewport,
204    // Scroll within our viewport
205    pub(crate) viewport_scroll: crate::Point<f64>,
206    /// CSS media type used to evaluate `@media` rules.
207    pub(crate) media_type: MediaType,
208    /// Strategy for Stylo's style traversal during `resolve`.
209    pub(crate) style_threading: StyleThreading,
210    /// Whether incremental layout is enabled for this document.
211    pub(crate) incremental_layout: bool,
212    /// How deeply this document is nested within other documents
213    /// (0 for a root document). Used to limit `<iframe>` nesting depth.
214    pub(crate) subdocument_depth: usize,
215
216    // Events
217    pub(crate) tx: Sender<DocumentEvent>,
218    // rx will always be Some, except temporarily while processing events
219    pub(crate) rx: Option<Receiver<DocumentEvent>>,
220
221    /// A slotmap-backed tree of nodes
222    ///
223    /// We pin the tree to a guarantee to the nodes it creates that the tree is stable in memory.
224    /// There is no way to create the tree - publicly or privately - that would invalidate that invariant.
225    pub(crate) nodes: Box<NodeTree>,
226
227    /// The id of the root node (a Document node)
228    pub(crate) root_node_id: NodeId,
229
230    // Stylo
231    /// The Stylo engine
232    pub(crate) stylist: Stylist,
233    pub(crate) animations: DocumentAnimationSet,
234    /// Stylo shared lock
235    pub(crate) guard: SharedRwLock,
236    /// Stylo invalidation map. We insert into this map prior to mutating nodes.
237    pub(crate) snapshots: SnapshotMap,
238
239    // Parley contexts
240    /// A Parley font context
241    pub(crate) font_ctx: Arc<Mutex<parley::FontContext>>,
242    #[cfg(feature = "parallel-construct")]
243    /// Thread-and-document-local copies to the font context
244    pub(crate) thread_font_contexts: ThreadLocal<RefCell<Box<FontContext>>>,
245    /// A Parley layout context
246    pub(crate) layout_ctx: parley::LayoutContext<TextBrush>,
247
248    /// The real (non-anonymous) node which is currently hovered (if any).
249    /// This is never a layout-generated (anonymous) node, so it remains valid
250    /// across box-tree reconstruction.
251    pub(crate) hover_node_id: Option<NodeId>,
252    /// The precise (may be anonymous) layout node under the pointer (if any).
253    /// This can be invalidated by box-tree reconstruction, and is re-resolved against
254    /// fresh layout at the end of every `resolve` pass.
255    pub(crate) hover_hit_node_id: Option<NodeId>,
256    /// Whether the node which is currently hovered is a text node/span
257    pub(crate) hover_node_is_text: bool,
258    /// The last known pointer position in client coordinates (viewport-relative, unscrolled).
259    pub(crate) last_client_pointer_position: Option<taffy::Point<f32>>,
260    /// The node which is currently focussed (if any)
261    pub(crate) focus_node_id: Option<NodeId>,
262    /// The node which is currently active (if any)
263    pub(crate) active_node_id: Option<NodeId>,
264    /// The node which recieved a mousedown event (if any)
265    pub(crate) mousedown_node_id: Option<NodeId>,
266    /// The last time a mousedown was made (for double-click detection)
267    pub(crate) last_mousedown_time: Option<Instant>,
268    /// The position where mousedown occurred (for selection drags and double-click detection)
269    pub(crate) mousedown_position: taffy::Point<f32>,
270    /// How many clicks have been made in quick succession
271    pub(crate) click_count: u16,
272    /// Whether we're currently in a text selection drag (moved 2px+ from mousedown)
273    pub(crate) drag_mode: DragMode,
274    /// The scrollbar thumb currently under the pointer, if any
275    pub(crate) hovered_scrollbar: Option<crate::node::ScrollbarRef>,
276    /// When each scroll container's overlay scrollbars were last shown
277    /// (scrolled, or the pointer left the thumb); drives their fade-out
278    pub(crate) scrollbar_activity: HashMap<NodeId, Instant>,
279    /// Whether and what kind of scroll animation is currently in progress
280    pub(crate) scroll_animation: ScrollAnimationState,
281
282    /// Text selection state (for non-input text)
283    pub(crate) text_selection: TextSelection,
284
285    // TODO: collapse animating state into a bitflags
286    /// Whether there are active CSS animations/transitions (so we should re-render every frame)
287    pub(crate) has_active_animations: bool,
288    /// Whether there is a `<canvas>` element in the DOM (so we should re-render every frame)
289    pub(crate) has_canvas: bool,
290    /// Whether there are subdocuments that are animating (so we should re-render every frame)
291    pub(crate) subdoc_is_animating: bool,
292
293    /// Map of id attribute values to node IDs for fast lookups.
294    /// May contain multiple nodes for the same id: `get_element_by_id`
295    /// returns the first in tree order.
296    pub(crate) nodes_to_id: HashMap<String, SmallVec<[NodeId; 1]>>,
297    /// Map of `<style>` and `<link>` node IDs to their associated stylesheet
298    pub(crate) nodes_to_stylesheet: BTreeMap<NodeId, DocumentStyleSheet>,
299    /// Stylesheets added by the useragent
300    /// where the key is the hashed CSS
301    pub(crate) ua_stylesheets: HashMap<String, DocumentStyleSheet>,
302    /// Map from form control node ID's to their associated forms node ID's
303    pub(crate) controls_to_form: HashMap<NodeId, NodeId>,
304    /// Nodes that contain sub documents
305    pub(crate) sub_document_nodes: HashSet<NodeId>,
306    /// Load state (abort controller and in-flight request id) for each
307    /// `<iframe>` element whose sub-document is loaded automatically
308    pub(crate) iframe_loads: HashMap<NodeId, crate::iframe::IframeLoad>,
309    /// Set of changed nodes for updating the accessibility tree
310    pub(crate) changed_nodes: HashSet<NodeId>,
311    /// Set of changed nodes for updating the accessibility tree
312    pub(crate) deferred_construction_nodes: Vec<ConstructionTask>,
313
314    /// Nodes that contain custom widgets
315    #[cfg(feature = "custom-widget")]
316    pub(crate) custom_widget_nodes: HashSet<NodeId>,
317    /// Rendering resources allocated by custom widgets that should be deallocated during the next render
318    #[cfg(feature = "custom-widget")]
319    pub(crate) pending_resource_deallocations: Vec<anyrender::ResourceId>,
320
321    /// Cache of loaded images, keyed by URL. Allows reusing images across multiple
322    /// elements without re-fetching from the network.
323    pub(crate) image_cache: HashMap<String, ImageData>,
324
325    /// Tracks in-flight image requests. When an image is being fetched, additional
326    /// requests for the same URL are queued here instead of starting new fetches.
327    /// Value is a list of (node_id, image_type) pairs waiting for the image.
328    pub(crate) pending_images: HashMap<String, Vec<(NodeId, ImageType)>>,
329
330    /// Nodes whose `background-image`/`mask-image` layers need flushing to
331    /// dedicated storage on the node because their style changed (populated by
332    /// the style traversal and by pseudo-element box construction).
333    pub(crate) pending_style_image_nodes: Vec<NodeId>,
334
335    // Tracks in-flight "critical" resources (e.g. stylesheets linked from the `<head>`),
336    // keyed by request id
337    pub(crate) pending_critical_resources: HashSet<usize>,
338
339    // Service providers
340    /// Network provider. Can be used to fetch assets.
341    pub net_provider: Arc<dyn NetProvider>,
342    /// Navigation provider. Can be used to navigate to a new page (bubbles up the event
343    /// on e.g. clicking a Link)
344    pub navigation_provider: Arc<dyn NavigationProvider>,
345    /// Shell provider. Can be used to request a redraw or set the cursor icon
346    pub shell_provider: Arc<dyn ShellProvider>,
347    /// HTML parser provider. Used to parse HTML for setInnerHTML
348    pub html_parser_provider: Arc<dyn HtmlParserProvider>,
349    /// Carried on every sub-resource `Request` this document issues; aborting
350    /// it cancels all in-flight fetches tied to this document. Set via
351    /// [`DocumentConfig::abort_signal`].
352    pub(crate) abort_signal: Option<AbortSignal>,
353}
354
355pub(crate) fn make_device(
356    viewport: &Viewport,
357    media_type: MediaType,
358    font_ctx: Arc<Mutex<FontContext>>,
359) -> Device {
360    let width = viewport.window_size.0 as f32 / viewport.scale();
361    let height = viewport.window_size.1 as f32 / viewport.scale();
362    let viewport_size = euclid::Size2D::new(width, height);
363    let device_size = euclid::Size2D::new(width, height) * viewport.scale();
364    let device_pixel_ratio = euclid::Scale::new(viewport.scale());
365
366    Device::new(
367        media_type,
368        selectors::matching::QuirksMode::NoQuirks,
369        viewport_size,
370        device_size,
371        device_pixel_ratio,
372        Box::new(BlitzFontMetricsProvider { font_ctx }),
373        ComputedValues::initial_values_with_font_override(Font::initial_values()),
374        match viewport.color_scheme {
375            ColorScheme::Light => PrefersColorScheme::Light,
376            ColorScheme::Dark => PrefersColorScheme::Dark,
377        },
378        PointerCapabilities::default(),
379        PointerCapabilities::default(),
380    )
381}
382
383impl BaseDocument {
384    /// Create a new (empty) [`BaseDocument`] with the specified configuration
385    pub fn new(config: DocumentConfig) -> Self {
386        static ID_GENERATOR: AtomicUsize = AtomicUsize::new(1);
387
388        let id = ID_GENERATOR.fetch_add(1, Ordering::SeqCst);
389
390        let font_ctx = config
391            .font_ctx
392            .map(|mut font_ctx| {
393                font_ctx.source_cache.make_shared();
394                // font_ctx.collection.make_shared();
395                font_ctx
396            })
397            .unwrap_or_else(|| {
398                use parley::fontique::{Collection, CollectionOptions, SourceCache};
399                let mut font_ctx = FontContext {
400                    source_cache: SourceCache::new_shared(),
401                    collection: Collection::new(CollectionOptions {
402                        shared: false,
403                        system_fonts: cfg!(all(
404                            feature = "system-fonts",
405                            not(target_arch = "wasm32")
406                        )),
407                    }),
408                };
409                font_ctx
410                    .collection
411                    .register_fonts(Blob::new(Arc::new(crate::BULLET_FONT) as _), None);
412                font_ctx
413            });
414        let font_ctx = Arc::new(Mutex::new(font_ctx));
415
416        // Make sure we turn on stylo features *before* creating the Stylist
417        style_config::set_pref!("layout.grid.enabled", true);
418        style_config::set_pref!("layout.unimplemented", true);
419        style_config::set_pref!("layout.columns.enabled", true);
420        style_config::set_pref!("layout.css.basic-shape-shape.enabled", true);
421        style_config::set_pref!("layout.threads", -1);
422
423        let viewport = config.viewport.unwrap_or_default();
424        let media_type = config.media_type.unwrap_or_else(MediaType::screen);
425        let device = make_device(&viewport, media_type.clone(), font_ctx.clone());
426        let stylist = Stylist::new(device, QuirksMode::NoQuirks);
427        let snapshots = SnapshotMap::new();
428        let nodes = Box::new(NodeTree::new());
429        let guard = SharedRwLock::new();
430        let nodes_to_id = HashMap::new();
431
432        let base_url = config
433            .base_url
434            .and_then(|url| DocumentUrl::from_str(&url).ok())
435            .unwrap_or_default();
436
437        let net_provider = config
438            .net_provider
439            .unwrap_or_else(|| Arc::new(DummyNetProvider));
440        let navigation_provider = config
441            .navigation_provider
442            .unwrap_or_else(|| Arc::new(DummyNavigationProvider));
443        let shell_provider = config
444            .shell_provider
445            .unwrap_or_else(|| Arc::new(DummyShellProvider));
446        let html_parser_provider = config
447            .html_parser_provider
448            .unwrap_or_else(|| Arc::new(DummyHtmlParserProvider));
449
450        let (tx, rx) = channel();
451
452        let mut doc = Self {
453            id,
454            tx,
455            rx: Some(rx),
456
457            guard,
458            nodes,
459            root_node_id: NodeId::default(),
460            stylist,
461            animations: DocumentAnimationSet::default(),
462            snapshots,
463            nodes_to_id,
464            viewport,
465            media_type,
466            style_threading: config.style_threading,
467            incremental_layout: config.incremental.unwrap_or(true),
468            subdocument_depth: config.subdocument_depth,
469            devtool_settings: DevtoolSettings::default(),
470            viewport_scroll: crate::Point::ZERO,
471            url: base_url,
472            ua_stylesheets: HashMap::new(),
473            nodes_to_stylesheet: BTreeMap::new(),
474            font_ctx,
475            #[cfg(feature = "parallel-construct")]
476            thread_font_contexts: ThreadLocal::new(),
477            layout_ctx: parley::LayoutContext::new(),
478
479            hover_node_id: None,
480            hover_hit_node_id: None,
481            hover_node_is_text: false,
482            last_client_pointer_position: None,
483            focus_node_id: None,
484            active_node_id: None,
485            mousedown_node_id: None,
486            has_active_animations: false,
487            subdoc_is_animating: false,
488            has_canvas: false,
489            sub_document_nodes: HashSet::new(),
490            iframe_loads: HashMap::new(),
491
492            #[cfg(feature = "custom-widget")]
493            custom_widget_nodes: HashSet::new(),
494            #[cfg(feature = "custom-widget")]
495            pending_resource_deallocations: Vec::new(),
496
497            changed_nodes: HashSet::new(),
498            deferred_construction_nodes: Vec::new(),
499            image_cache: HashMap::new(),
500            pending_images: HashMap::new(),
501            pending_style_image_nodes: Vec::new(),
502            pending_critical_resources: HashSet::new(),
503            controls_to_form: HashMap::new(),
504            net_provider,
505            navigation_provider,
506            shell_provider,
507            html_parser_provider,
508            abort_signal: config.abort_signal,
509            last_mousedown_time: None,
510            mousedown_position: taffy::Point::ZERO,
511            click_count: 0,
512            drag_mode: DragMode::None,
513            hovered_scrollbar: None,
514            scrollbar_activity: HashMap::new(),
515            scroll_animation: ScrollAnimationState::None,
516            text_selection: TextSelection::default(),
517        };
518
519        // Initialise document with root Document node
520        doc.root_node_id = doc.create_node(NodeData::Document(Box::default()));
521        doc.root_node_mut().flags.insert(NodeFlags::IS_IN_DOCUMENT);
522
523        match config.ua_stylesheets {
524            Some(stylesheets) => {
525                for ss in &stylesheets {
526                    doc.add_user_agent_stylesheet(ss);
527                }
528            }
529            None => doc.add_user_agent_stylesheet(DEFAULT_CSS),
530        }
531
532        // Stylo data on the root node container is needed to render the node
533        let stylo_element_data = StyloElementData {
534            styles: ElementStyles {
535                primary: Some(
536                    ComputedValues::initial_values_with_font_override(Font::initial_values())
537                        .to_arc(),
538                ),
539                ..Default::default()
540            },
541            ..Default::default()
542        };
543        let stylo_data = doc.root_node_mut().stylo_element_data_mut();
544        *stylo_data.ensure_init_mut() = stylo_element_data;
545
546        doc
547    }
548
549    /// Set the Document's networking provider
550    pub fn set_net_provider(&mut self, net_provider: Arc<dyn NetProvider>) {
551        self.net_provider = net_provider;
552    }
553
554    /// Set the Document's navigation provider
555    pub fn set_navigation_provider(&mut self, navigation_provider: Arc<dyn NavigationProvider>) {
556        self.navigation_provider = navigation_provider;
557    }
558
559    /// Set the Document's shell provider
560    pub fn set_shell_provider(&mut self, shell_provider: Arc<dyn ShellProvider>) {
561        self.shell_provider = shell_provider;
562    }
563
564    /// Set the Document's html parser provider
565    pub fn set_html_parser_provider(&mut self, html_parser_provider: Arc<dyn HtmlParserProvider>) {
566        self.html_parser_provider = html_parser_provider;
567    }
568
569    /// Set base url for resolving linked resources (stylesheets, images, fonts, etc)
570    pub fn set_base_url(&mut self, url: &str) {
571        self.url = DocumentUrl::from(Url::parse(url).unwrap());
572    }
573
574    /// The base url used for resolving linked resources (stylesheets, images, fonts, etc)
575    pub fn base_url(&self) -> &Url {
576        &self.url
577    }
578
579    pub fn guard(&self) -> &SharedRwLock {
580        &self.guard
581    }
582
583    pub fn tree(&self) -> &NodeTree {
584        &self.nodes
585    }
586
587    pub fn id(&self) -> usize {
588        self.id
589    }
590
591    /// Wrapper around [`crate::net::stamped_request`]. Use the free function
592    /// when `&self` would conflict with a held `&mut` borrow on a field.
593    pub(crate) fn build_request(&self, url: url::Url) -> Request {
594        crate::net::stamped_request(url, self.abort_signal.as_ref())
595    }
596
597    pub fn favicon_url(&self) -> Option<String> {
598        self.tree().iter().find_map(|(_, node)| {
599            let data = &node.data;
600            if !data.is_element_with_tag_name(&local_name!("link")) {
601                return None;
602            }
603            let rel = data.attr(local_name!("rel"))?;
604            if !rel
605                .split_ascii_whitespace()
606                .any(|v| v.eq_ignore_ascii_case("icon"))
607            {
608                return None;
609            }
610            data.attr(local_name!("href")).map(|s| s.to_string())
611        })
612    }
613
614    pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
615        self.nodes.get(node_id)
616    }
617
618    pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
619        self.nodes.get_mut(node_id)
620    }
621
622    pub fn get_focussed_node_id(&self) -> Option<NodeId> {
623        self.focus_node_id
624            .or(self.try_root_element().map(|el| el.id))
625    }
626
627    pub fn mutate<'doc>(&'doc mut self) -> DocumentMutator<'doc> {
628        DocumentMutator::new(self)
629    }
630
631    pub fn handle_dom_event<F: FnMut(DomEvent)>(
632        &mut self,
633        event: &mut DomEvent,
634        dispatch_event: F,
635    ) {
636        handle_dom_event(self, event, dispatch_event)
637    }
638
639    pub fn as_any_mut(&mut self) -> &mut dyn Any {
640        self
641    }
642
643    /// Find the label's bound input elements:
644    /// the element id referenced by the "for" attribute of a given label element
645    /// or the first input element which is nested in the label
646    /// Note that although there should only be one bound element,
647    /// we return all possibilities instead of just the first
648    /// in order to allow the caller to decide which one is correct
649    pub fn label_bound_input_element(&self, label_node_id: NodeId) -> Option<&Node> {
650        let label_element = self.nodes[label_node_id].element_data()?;
651        if let Some(target_element_dom_id) = label_element.attr(local_name!("for")) {
652            TreeTraverser::new(self)
653                .filter_map(|id| {
654                    let node = self.get_node(id)?;
655                    let element_data = node.element_data()?;
656                    if element_data.name.local != local_name!("input") {
657                        return None;
658                    }
659                    let id = element_data.id.as_ref()?;
660                    if *id == *target_element_dom_id {
661                        Some(node)
662                    } else {
663                        None
664                    }
665                })
666                .next()
667        } else {
668            TreeTraverser::new_with_root(self, label_node_id)
669                .filter_map(|child_id| {
670                    let node = self.get_node(child_id)?;
671                    let element_data = node.element_data()?;
672                    if element_data.name.local == local_name!("input") {
673                        Some(node)
674                    } else {
675                        None
676                    }
677                })
678                .next()
679        }
680    }
681
682    pub fn toggle_checkbox(el: &mut ElementData) -> bool {
683        let Some(is_checked) = el.checkbox_input_checked() else {
684            return false;
685        };
686        let checked = !is_checked;
687        el.set_checkbox_input_checked(checked);
688
689        checked
690    }
691
692    pub fn toggle_radio(&mut self, radio_set_name: String, target_radio_id: NodeId) {
693        let radio_ids: Vec<NodeId> = self
694            .nodes
695            .iter()
696            .filter_map(|(i, node)| {
697                let el = node.data.downcast_element()?;
698                (el.attr(local_name!("name")) == Some(&radio_set_name)
699                    && el.checkbox_input_checked().is_some())
700                .then_some(i)
701            })
702            .collect();
703
704        for i in radio_ids {
705            let checked = i == target_radio_id;
706            self.snapshot_node_and(i, ElementState::CHECKED, |node| {
707                if let Some(el) = node.element_data_mut() {
708                    el.set_checkbox_input_checked(checked);
709                }
710                node.mark_ancestors_dirty();
711            });
712        }
713    }
714
715    /// Toggle the `open` attribute of a `<details>` element, expanding or
716    /// collapsing it. This is the default action triggered when the element's
717    /// first `<summary>` child is activated.
718    pub fn toggle_details_open(&mut self, details_id: NodeId) {
719        use crate::qual_name;
720
721        let node = &self.nodes[details_id];
722        if !node.data.is_element_with_tag_name(&local_name!("details")) {
723            return;
724        }
725        let is_open = node.data.has_attr(local_name!("open"));
726
727        // Note: HTML attributes are in the empty (null) namespace, so the
728        // QualName must not use the html namespace here, else it won't match
729        // an `open` attribute created by the HTML parser.
730        let mut mutator = self.mutate();
731        if is_open {
732            mutator.clear_attribute(details_id, qual_name!("open"));
733        } else {
734            mutator.set_attribute(details_id, qual_name!("open"), "");
735        }
736        drop(mutator);
737
738        self.shell_provider.request_redraw();
739    }
740
741    pub fn set_style_property(&mut self, node_id: NodeId, name: &str, value: &str) {
742        let node = &mut self.nodes[node_id];
743        let did_change = node.element_data_mut().unwrap().set_style_property(
744            name,
745            value,
746            &self.guard,
747            self.url.url_extra_data(),
748        );
749        if did_change {
750            node.mark_style_attr_updated();
751        }
752    }
753
754    pub fn remove_style_property(&mut self, node_id: NodeId, name: &str) {
755        let node = &mut self.nodes[node_id];
756        let did_change = node.element_data_mut().unwrap().remove_style_property(
757            name,
758            &self.guard,
759            self.url.url_extra_data(),
760        );
761        if did_change {
762            node.mark_style_attr_updated();
763        }
764    }
765
766    pub fn sub_document_node_ids(&self) -> Vec<NodeId> {
767        self.sub_document_nodes.iter().copied().collect()
768    }
769
770    pub fn set_sub_document(&mut self, node_id: NodeId, sub_document: Box<dyn Document>) {
771        self.nodes[node_id]
772            .element_data_mut()
773            .unwrap()
774            .set_sub_document(sub_document);
775        self.sub_document_nodes.insert(node_id);
776    }
777
778    pub fn remove_sub_document(&mut self, node_id: NodeId) {
779        self.nodes[node_id]
780            .element_data_mut()
781            .unwrap()
782            .remove_sub_document();
783        self.sub_document_nodes.remove(&node_id);
784        if let Some(load) = self.iframe_loads.remove(&node_id) {
785            load.abort_controller.abort();
786        }
787    }
788
789    /// Poll all sub-documents (see [`Document::poll`]), allowing them to make progress
790    /// on any pending async operations (e.g. JavaScript timers). Hosts which poll a
791    /// wrapper around a [`BaseDocument`] should call this from their `poll` implementation.
792    ///
793    /// Returns `true` if any sub-document reported changes.
794    pub fn poll_subdocuments(&mut self, waker: Option<&Waker>) -> bool {
795        let mut has_changes = false;
796        for node_id in self.sub_document_nodes.iter().copied() {
797            let Some(sub_doc) = self
798                .nodes
799                .get_mut(node_id)
800                .and_then(|node| node.subdoc_mut())
801            else {
802                continue;
803            };
804            let task_context = waker.map(TaskContext::from_waker);
805            has_changes |= sub_doc.poll(task_context);
806        }
807        has_changes
808    }
809
810    #[cfg(feature = "custom-widget")]
811    pub fn custom_widget_node_ids(&self) -> Vec<NodeId> {
812        self.custom_widget_nodes.iter().copied().collect()
813    }
814
815    #[cfg(feature = "custom-widget")]
816    pub fn take_pending_resource_deallocations(&mut self) -> Vec<anyrender::ResourceId> {
817        std::mem::take(&mut self.pending_resource_deallocations)
818    }
819
820    #[cfg(feature = "custom-widget")]
821    pub fn set_custom_widget(&mut self, node_id: NodeId, widget: Box<dyn crate::Widget>) {
822        self.nodes[node_id]
823            .element_data_mut()
824            .unwrap()
825            .set_custom_widget(widget);
826        self.custom_widget_nodes.insert(node_id);
827    }
828
829    #[cfg(feature = "custom-widget")]
830    pub fn remove_custom_widget(&mut self, node_id: NodeId) {
831        let resources_to_deallocate = self.nodes[node_id]
832            .element_data_mut()
833            .unwrap()
834            .remove_custom_widget();
835        self.pending_resource_deallocations
836            .extend_from_slice(&resources_to_deallocate);
837        self.custom_widget_nodes.remove(&node_id);
838    }
839
840    pub fn root_node(&self) -> &Node {
841        &self.nodes[self.root_node_id]
842    }
843
844    pub fn root_node_mut(&mut self) -> &mut Node {
845        &mut self.nodes[self.root_node_id]
846    }
847
848    pub fn try_root_element(&self) -> Option<&Node> {
849        TDocument::as_node(&self.root_node()).first_element_child()
850    }
851
852    pub fn root_element(&self) -> &Node {
853        TDocument::as_node(&self.root_node())
854            .first_element_child()
855            .unwrap()
856            .as_element()
857            .unwrap()
858    }
859
860    pub fn create_node(&mut self, node_data: NodeData) -> NodeId {
861        let tree_ptr = self.nodes.as_mut() as *mut NodeTree;
862        let guard = self.guard.clone();
863
864        let id = self
865            .nodes
866            .insert_with_key(|id| Node::new(tree_ptr, id, guard, node_data));
867
868        // Mark the new node as changed.
869        self.changed_nodes.insert(id);
870        id
871    }
872
873    /// Remove a node from the node tree, clearing any interaction state
874    /// (hover/active/focus/mousedown/selection/drag/scrollbar) that references
875    /// it so that stale NodeIds are never dereferenced after the slot is freed.
876    pub(crate) fn remove_node_from_tree(&mut self, node_id: NodeId) -> Option<Node> {
877        self.clear_interaction_state_for_removed_node(node_id);
878        self.nodes.remove(node_id)
879    }
880
881    /// The nearest element ancestor of `node_id` that is still in the
882    /// document. Used to retarget hover/active state when the node they
883    /// reference is removed. Tolerates already-removed ancestors (subtree
884    /// teardown proceeds root-first) by giving up and returning `None`.
885    fn nearest_surviving_element_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
886        let mut current = self.get_node(node_id)?.parent;
887        while let Some(id) = current {
888            let node = self.get_node(id)?;
889            if node.is_element() && node.flags.is_in_document() {
890                return Some(id);
891            }
892            current = node.parent;
893        }
894        None
895    }
896
897    /// Clear any interaction state (hover/active/focus/mousedown/selection/
898    /// drag/scrollbar) that references `node_id`, which is being removed from
899    /// the document, running the usual teardown steps. `node_id` must still be
900    /// present in the slab.
901    ///
902    /// This matches browser semantics (WebKit `hoveredElementDidDetach` /
903    /// `elementInActiveChainDidDetach`, Blink `HoveredElementDetached` /
904    /// `ActiveChainNodeDetached`):
905    /// - Hover and active retarget to the nearest surviving element ancestor
906    ///   as a *transient bridge*: the HOVER/ACTIVE element-state bits along
907    ///   the surviving chain stay lit (no one-frame gap in `:hover`/`:active`
908    ///   styling), and the subsequent hover diff can unset exactly the right
909    ///   bits. Hover is then re-resolved against the pointer position by
910    ///   [`Self::refresh_hover`] at the end of the next resolve pass (the
911    ///   analogue of WebKit's "fake mouse move"), which corrects the bridge
912    ///   value — including cases where the removed node overflowed its
913    ///   ancestor's box, so the ancestor was never truly under the pointer.
914    /// - Focus resets to the body (encoded as `None`), running blur
915    ///   side-effects (clearing focus element state and disabling IME for
916    ///   text inputs).
917    pub(crate) fn clear_interaction_state_for_removed_node(&mut self, node_id: NodeId) {
918        if !self.nodes.contains_key(node_id) {
919            return;
920        }
921
922        if self.hover_node_id == Some(node_id) {
923            self.hover_node_id = self.nearest_surviving_element_ancestor(node_id);
924            self.hover_node_is_text = false;
925        }
926        if self.hover_hit_node_id == Some(node_id) {
927            self.hover_hit_node_id = None;
928        }
929        if self.active_node_id == Some(node_id) {
930            self.active_node_id = self.nearest_surviving_element_ancestor(node_id);
931        }
932        if self.focus_node_id == Some(node_id) {
933            let shell_provider = self.shell_provider.clone();
934            self.nodes[node_id].blur(shell_provider);
935            self.focus_node_id = None;
936        }
937        if self.mousedown_node_id == Some(node_id) {
938            self.mousedown_node_id = None;
939        }
940        if self.text_selection.anchor.node_or_parent == Some(node_id)
941            || self.text_selection.focus.node_or_parent == Some(node_id)
942        {
943            self.text_selection.clear();
944        }
945        if self
946            .hovered_scrollbar
947            .is_some_and(|scrollbar| scrollbar.node_id == node_id)
948        {
949            self.hovered_scrollbar = None;
950        }
951        let drag_references_node = match &self.drag_mode {
952            DragMode::Panning(state) => state.target == node_id,
953            DragMode::ScrollbarDrag(state) => state.scrollbar.node_id == node_id,
954            DragMode::Selecting | DragMode::None => false,
955        };
956        if drag_references_node {
957            self.drag_mode = DragMode::None;
958        }
959        self.scrollbar_activity.remove(&node_id);
960    }
961
962    pub(crate) fn drop_node_ignoring_parent(&mut self, node_id: NodeId) -> Option<Node> {
963        self.drop_node_ignoring_parent_with(node_id, &mut |_| {})
964    }
965
966    /// Like [`Self::drop_node_ignoring_parent`], but calls `on_drop` with the id of
967    /// every dropped node (the node itself and all of its descendants).
968    pub(crate) fn drop_node_ignoring_parent_with(
969        &mut self,
970        node_id: NodeId,
971        on_drop: &mut dyn FnMut(NodeId),
972    ) -> Option<Node> {
973        let mut node = self.remove_node_from_tree(node_id);
974        if let Some(node) = &mut node {
975            on_drop(node_id);
976            if let Some(before) = node.before() {
977                self.drop_node_ignoring_parent_with(before, on_drop);
978            }
979            if let Some(after) = node.after() {
980                self.drop_node_ignoring_parent_with(after, on_drop);
981            }
982
983            for &child in &node.children {
984                self.drop_node_ignoring_parent_with(child, on_drop);
985            }
986
987            // Anonymous blocks live only in the slab, so deallocate the ones this
988            // node owns rather than leaking them.
989            for &anon_id in &node.anonymous_blocks {
990                self.deallocate_anonymous_block(anon_id);
991            }
992        }
993        node
994    }
995
996    /// Deallocate an anonymous block created in a previous construction
997    /// round, along with any anonymous blocks nested within it.
998    pub(crate) fn deallocate_anonymous_block(&mut self, anon_id: NodeId) {
999        // The block may already have been removed from the slab (e.g. a
1000        // whitespace-only anonymous block dropped during construction).
1001        if !self.nodes.contains_key(anon_id) {
1002            return;
1003        }
1004
1005        // Free any anonymous blocks that this block owns before removing it.
1006        let nested = std::mem::take(&mut self.nodes[anon_id].anonymous_blocks);
1007        for nested_id in nested {
1008            self.deallocate_anonymous_block(nested_id);
1009        }
1010
1011        self.remove_node_from_tree(anon_id);
1012    }
1013
1014    /// Whether the document has been mutated
1015    pub fn has_changes(&self) -> bool {
1016        self.changed_nodes.is_empty()
1017    }
1018
1019    pub fn create_text_node(&mut self, text: &str) -> NodeId {
1020        let content = text.to_string();
1021        let data = NodeData::Text(TextNodeData::new(content));
1022        self.create_node(data)
1023    }
1024
1025    pub fn deep_clone_node(&mut self, node_id: NodeId) -> NodeId {
1026        // Load existing node
1027        let node = &self.nodes[node_id];
1028        let mut data = node.data.clone();
1029
1030        match &mut data {
1031            NodeData::Element(elem) | NodeData::AnonymousBlock(elem) => {
1032                if let Some(arc) = elem.style_attribute.as_mut() {
1033                    let read_guard = self.guard().read();
1034                    let block = arc.read_with(&read_guard);
1035                    *arc = ServoArc::new(self.guard().wrap(block.clone()));
1036                }
1037            }
1038            _ => {}
1039        }
1040
1041        let children = node.children.clone();
1042
1043        // Create new node
1044        let new_node_id = self.create_node(data);
1045
1046        // Recursively clone children
1047        let new_children: ThinVec<NodeId> = children
1048            .into_iter()
1049            .map(|child_id| self.deep_clone_node(child_id))
1050            .collect();
1051        for &child_id in &new_children {
1052            self.nodes[child_id].parent = Some(new_node_id);
1053        }
1054        self.nodes[new_node_id].children = new_children;
1055
1056        new_node_id
1057    }
1058
1059    pub(crate) fn remove_and_drop_pe(&mut self, node_id: NodeId) -> Option<Node> {
1060        fn remove_pe_ignoring_parent(doc: &mut BaseDocument, node_id: NodeId) -> Option<Node> {
1061            let mut node = doc.remove_node_from_tree(node_id);
1062            if let Some(node) = &mut node {
1063                for &child in &node.children {
1064                    remove_pe_ignoring_parent(doc, child);
1065                }
1066                for &anon_id in &node.anonymous_blocks {
1067                    doc.deallocate_anonymous_block(anon_id);
1068                }
1069            }
1070            node
1071        }
1072
1073        let node = remove_pe_ignoring_parent(self, node_id);
1074
1075        // Update child_idx values
1076        if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
1077            let parent = &mut self.nodes[parent_id];
1078            parent.children.retain(|id| *id != node_id);
1079        }
1080
1081        node
1082    }
1083
1084    pub(crate) fn resolve_url(&self, raw: &str) -> url::Url {
1085        self.url.resolve_relative(raw).unwrap_or_else(|| {
1086            panic!(
1087                "to be able to resolve {raw} with the base_url: {:?}",
1088                *self.url
1089            )
1090        })
1091    }
1092
1093    pub fn print_tree(&self) {
1094        crate::util::walk_tree(0, self.root_node());
1095    }
1096
1097    pub fn print_subtree(&self, node_id: NodeId) {
1098        crate::util::walk_tree(0, &self.nodes[node_id]);
1099    }
1100
1101    pub fn reload_resource_by_href(&mut self, href_to_reload: &str) {
1102        for &node_id in self.nodes_to_stylesheet.keys() {
1103            let node = &self.nodes[node_id];
1104            let Some(element) = node.element_data() else {
1105                continue;
1106            };
1107
1108            if element.name.local == local_name!("link") {
1109                if let Some(href) = element.attr(local_name!("href")) {
1110                    // println!("Node {node_id} {href} {href_to_reload} {} {}", resolved_href.as_str(), resolved_href.as_str() == url_to_reload);
1111                    if href == href_to_reload {
1112                        let resolved_href = self.resolve_url(href);
1113                        self.net_provider.fetch(
1114                            self.id(),
1115                            self.build_request(resolved_href.clone()),
1116                            ResourceHandler::boxed(
1117                                self.tx.clone(),
1118                                self.id,
1119                                Some(node_id),
1120                                self.shell_provider.clone(),
1121                                StylesheetHandler {
1122                                    source_url: resolved_href,
1123                                    guard: self.guard.clone(),
1124                                    net_provider: self.net_provider.clone(),
1125                                    abort_signal: self.abort_signal.clone(),
1126                                },
1127                            ),
1128                        );
1129                    }
1130                }
1131            }
1132        }
1133    }
1134
1135    pub fn process_style_element(&mut self, target_id: NodeId) {
1136        let css = self.nodes[target_id].text_content();
1137        let css = html_escape::decode_html_entities(&css);
1138        let sheet = self.make_stylesheet(&css, Origin::Author);
1139        self.add_stylesheet_for_node(sheet, target_id);
1140    }
1141
1142    pub fn remove_user_agent_stylesheet(&mut self, contents: &str) {
1143        if let Some(sheet) = self.ua_stylesheets.remove(contents) {
1144            self.stylist.remove_stylesheet(sheet, &self.guard.read());
1145        }
1146    }
1147
1148    /// The document's base URL
1149    pub fn url(&self) -> &url::Url {
1150        &self.url
1151    }
1152
1153    /// Iterate over the author stylesheets (from `<style>` and `<link>` nodes)
1154    /// currently associated with this document
1155    pub fn author_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
1156        self.nodes_to_stylesheet.values()
1157    }
1158
1159    /// Iterate over the user-agent stylesheets currently associated with this document
1160    pub fn useragent_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
1161        self.ua_stylesheets.values()
1162    }
1163
1164    pub fn add_user_agent_stylesheet(&mut self, css: &str) {
1165        let sheet = self.make_stylesheet(css, Origin::UserAgent);
1166        self.ua_stylesheets.insert(css.to_string(), sheet.clone());
1167        self.stylist.append_stylesheet(sheet, &self.guard.read());
1168    }
1169
1170    pub fn make_stylesheet(&self, css: impl AsRef<str>, origin: Origin) -> DocumentStyleSheet {
1171        let data = Stylesheet::from_str(
1172            css.as_ref(),
1173            self.url.url_extra_data(),
1174            origin,
1175            ServoArc::new(self.guard.wrap(MediaList::empty())),
1176            self.guard.clone(),
1177            Some(&StylesheetLoader {
1178                tx: self.tx.clone(),
1179                doc_id: self.id,
1180                net_provider: self.net_provider.clone(),
1181                shell_provider: self.shell_provider.clone(),
1182                abort_signal: self.abort_signal.clone(),
1183            }),
1184            None,
1185            QuirksMode::NoQuirks,
1186            AllowImportRules::Yes,
1187        );
1188
1189        DocumentStyleSheet(ServoArc::new(data))
1190    }
1191
1192    pub fn upsert_stylesheet_for_node(&mut self, node_id: NodeId) {
1193        let raw_styles = self.nodes[node_id].text_content();
1194        let sheet = self.make_stylesheet(raw_styles, Origin::Author);
1195        self.add_stylesheet_for_node(sheet, node_id);
1196    }
1197
1198    pub fn add_stylesheet_for_node(&mut self, stylesheet: DocumentStyleSheet, node_id: NodeId) {
1199        let old = self.nodes_to_stylesheet.insert(node_id, stylesheet.clone());
1200
1201        if let Some(old) = old {
1202            self.stylist.remove_stylesheet(old, &self.guard.read())
1203        }
1204
1205        // Fetch @font-face fonts
1206        crate::net::fetch_font_face(
1207            self.tx.clone(),
1208            self.id,
1209            Some(node_id),
1210            &stylesheet.0,
1211            &self.net_provider,
1212            &self.shell_provider,
1213            &self.guard.read(),
1214            self.abort_signal.as_ref(),
1215        );
1216
1217        // Store data on element
1218        let element = &mut self.nodes[node_id].element_data_mut().unwrap();
1219        element.special_data = SpecialElementData::Stylesheet(stylesheet.clone());
1220
1221        // TODO: Nodes could potentially get reused so ordering by node_id might be wrong.
1222        let insertion_point = self
1223            .nodes_to_stylesheet
1224            .range((Bound::Excluded(node_id), Bound::Unbounded))
1225            .next()
1226            .map(|(_, sheet)| sheet);
1227
1228        if let Some(insertion_point) = insertion_point {
1229            self.stylist.insert_stylesheet_before(
1230                stylesheet,
1231                insertion_point.clone(),
1232                &self.guard.read(),
1233            )
1234        } else {
1235            self.stylist
1236                .append_stylesheet(stylesheet, &self.guard.read())
1237        }
1238    }
1239
1240    pub fn handle_messages(&mut self) {
1241        // Remove event Reciever from the Document so that we can process events
1242        // without holding a borrow to the Document
1243        let rx = self.rx.take().unwrap();
1244
1245        while let Ok(msg) = rx.try_recv() {
1246            self.handle_message(msg);
1247        }
1248
1249        // Put Reciever back
1250        self.rx = Some(rx);
1251    }
1252
1253    pub fn handle_message(&mut self, msg: DocumentEvent) {
1254        match msg {
1255            DocumentEvent::ResourceLoad(resource) => self.load_resource(resource),
1256            DocumentEvent::NavigateIframe { node_id, url } => self.navigate_iframe(node_id, url),
1257        }
1258    }
1259
1260    /// Whether the Document has pending requests for "critical" resources (that should block rendering)
1261    pub fn has_pending_critical_resources(&self) -> bool {
1262        !self.pending_critical_resources.is_empty()
1263    }
1264
1265    pub fn load_resource(&mut self, res: ResourceLoadResponse) {
1266        self.pending_critical_resources.remove(&res.request_id);
1267
1268        let resource = match res.result {
1269            Ok(resource) => resource,
1270            Err(err) => {
1271                if let Some(url) = res.resolved_url.as_ref() {
1272                    let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1273                    #[cfg(feature = "tracing")]
1274                    tracing::warn!(
1275                        url = url.as_str(),
1276                        waiting_nodes = waiting_nodes.len(),
1277                        error = err.as_str(),
1278                        "Resource load failed"
1279                    );
1280                    #[cfg(not(feature = "tracing"))]
1281                    let _ = (waiting_nodes, err);
1282                } else {
1283                    #[cfg(feature = "tracing")]
1284                    tracing::warn!(error = err.as_str(), "Resource load failed (no url)");
1285                    #[cfg(not(feature = "tracing"))]
1286                    let _ = err;
1287                }
1288                return;
1289            }
1290        };
1291
1292        match resource {
1293            Resource::Css(css) => {
1294                let node_id = res.node_id.unwrap();
1295                self.add_stylesheet_for_node(css, node_id);
1296            }
1297            Resource::Image(_kind, width, height, image_data) => {
1298                // Create the ImageData and cache it
1299                let image = ImageData::Raster(RasterImageData::new(width, height, image_data));
1300
1301                let Some(url) = res.resolved_url.as_ref() else {
1302                    return;
1303                };
1304
1305                self.apply_loaded_image(url, image);
1306            }
1307            #[cfg(feature = "svg")]
1308            Resource::Svg(_kind, svg) => {
1309                // Create the ImageData and cache it
1310                let image = ImageData::Svg(svg);
1311
1312                let Some(url) = res.resolved_url.as_ref() else {
1313                    return;
1314                };
1315
1316                self.apply_loaded_image(url, image);
1317            }
1318            Resource::DocumentSrc(html) => {
1319                let Some(node_id) = res.node_id else {
1320                    return;
1321                };
1322                self.apply_iframe_html(node_id, res.request_id, res.resolved_url, &html);
1323            }
1324            Resource::Font(bytes, overrides) => {
1325                let font = Blob::new(Arc::new(bytes));
1326
1327                // Build a `FontInfoOverride` from the `@font-face` descriptors
1328                // captured during stylesheet parsing. Without this, parley
1329                // reads the family name from the TTF's own metadata, which
1330                // means CSS `font-family: 'Avenir Book'` won't match a font
1331                // file that internally identifies as `Avenir 45 Book`.
1332                let weight_override = overrides.weight.map(parley::fontique::FontWeight::new);
1333                let info_override = parley::fontique::FontInfoOverride {
1334                    family_name: overrides.family_name.as_deref(),
1335                    weight: weight_override,
1336                    style: overrides.style,
1337                    ..Default::default()
1338                };
1339
1340                // TODO: Investigate eliminating double-box
1341                let mut global_font_ctx = self.font_ctx.lock().unwrap();
1342                global_font_ctx
1343                    .collection
1344                    .register_fonts(font.clone(), Some(info_override));
1345
1346                #[cfg(feature = "parallel-construct")]
1347                {
1348                    rayon::broadcast(|_ctx| {
1349                        let mut font_ctx = self
1350                            .thread_font_contexts
1351                            .get_or(|| RefCell::new(Box::new(global_font_ctx.clone())))
1352                            .borrow_mut();
1353                        font_ctx
1354                            .collection
1355                            .register_fonts(font.clone(), Some(info_override));
1356                    });
1357                }
1358                drop(global_font_ctx);
1359
1360                // TODO: see if we can only invalidate if resolved fonts may have changed
1361                self.invalidate_inline_contexts();
1362            }
1363            Resource::None => {
1364                // Do nothing
1365            }
1366        }
1367    }
1368
1369    /// Cache a loaded image and apply it to all nodes waiting on it
1370    /// (`<img>` elements, `background-image` layers and `mask-image` layers).
1371    fn apply_loaded_image(&mut self, url: &str, image: ImageData) {
1372        // Get all nodes waiting for this image
1373        let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1374
1375        #[cfg(feature = "tracing")]
1376        tracing::info!(
1377            "Image {url} loaded, applying to {} nodes",
1378            waiting_nodes.len()
1379        );
1380
1381        // Cache the image
1382        self.image_cache.insert(url.to_string(), image.clone());
1383
1384        // Apply to all waiting nodes
1385        for (node_id, image_type) in waiting_nodes {
1386            let Some(node) = self.get_node_mut(node_id) else {
1387                continue;
1388            };
1389
1390            match image_type {
1391                ImageType::Image => {
1392                    node.element_data_mut().unwrap().special_data =
1393                        SpecialElementData::Image(Box::new(image.clone()));
1394
1395                    // Clear layout cache
1396                    node.cache_mut().clear();
1397                    node.insert_damage(ALL_DAMAGE);
1398                }
1399                ImageType::Background(idx) | ImageType::Mask(idx) => {
1400                    let layer_image = node.element_data_mut().and_then(|el| {
1401                        let images = match image_type {
1402                            ImageType::Background(_) => &mut el.background_images,
1403                            ImageType::Mask(_) => &mut el.mask_images,
1404                            ImageType::Image => unreachable!(),
1405                        };
1406                        images.get_mut(idx)
1407                    });
1408                    if let Some(Some(layer_image)) = layer_image {
1409                        layer_image.status = Status::Ok;
1410                        layer_image.image = image.clone();
1411                    }
1412                }
1413            }
1414        }
1415    }
1416
1417    /// Snapshot the node's pre-mutation state (element state and attributes) ahead of
1418    /// an attribute mutation, so that the next style traversal can diff selector matches
1419    /// then-vs-now and invalidate the affected elements.
1420    pub fn snapshot_node(&mut self, node_id: NodeId) {
1421        self.snapshot_node_impl(node_id, true)
1422    }
1423
1424    /// Snapshot only the node's pre-change [`ElementState`] ahead of a state change
1425    /// (hover/focus/active/etc). Cheaper than [`Self::snapshot_node`] as it does not
1426    /// copy attributes or trigger attribute/class/id invalidation work.
1427    pub fn snapshot_node_state_only(&mut self, node_id: NodeId) {
1428        self.snapshot_node_impl(node_id, false)
1429    }
1430
1431    fn snapshot_node_impl(&mut self, node_id: NodeId, capture_attrs: bool) {
1432        let node = &mut self.nodes[node_id];
1433
1434        // Do not snapshot nodes that have never been styled. A snapshot records an element's
1435        // pre-mutation state so a restyle can diff selector matches then-vs-now. An element
1436        // that has never been styled has no "then" to diff against. Snapshotting it anyway
1437        // makes Stylo's invalidation unwrap its (absent) primary style and panic.
1438        let has_been_styled = node.primary_styles().is_some();
1439        if !has_been_styled {
1440            return;
1441        }
1442
1443        let opaque_node_id = TNode::opaque(&&*node);
1444        node.set_has_snapshot(true);
1445        node.snapshot_handled()
1446            .store(false, std::sync::atomic::Ordering::SeqCst);
1447
1448        // A snapshot records the element's state/attributes as they were *before the first
1449        // mutation* since the last style flush (matching Gecko's ServoElementSnapshot
1450        // semantics): state is captured at most once, and attributes are captured at most
1451        // once, but a state-only snapshot is upgraded to also capture attributes if an
1452        // attribute mutation follows.
1453        let needs_attrs = capture_attrs
1454            && self
1455                .snapshots
1456                .get_mut(&opaque_node_id)
1457                .is_none_or(|snapshot| snapshot.attrs.is_none());
1458
1459        let (attrs, changed_attrs) = if needs_attrs {
1460            let node = &self.nodes[node_id];
1461            let attrs: Option<Vec<_>> = node.attrs().map(|attrs| {
1462                attrs
1463                    .iter()
1464                    .map(|attr| {
1465                        let ident = AttrIdentifier {
1466                            local_name: GenericAtomIdent(attr.name.local.clone()),
1467                            name: GenericAtomIdent(attr.name.local.clone()),
1468                            namespace: GenericAtomIdent(attr.name.ns.clone()),
1469                            prefix: None,
1470                        };
1471
1472                        let value = if attr.name.local == local_name!("id") {
1473                            AttrValue::Atom(Atom::from(&*attr.value))
1474                        } else if attr.name.local == local_name!("class") {
1475                            let classes = attr
1476                                .value
1477                                .split_ascii_whitespace()
1478                                .map(Atom::from)
1479                                .collect();
1480                            AttrValue::TokenList(OnceLock::from(attr.value.clone()), classes)
1481                        } else {
1482                            AttrValue::String(attr.value.clone())
1483                        };
1484
1485                        (ident, value)
1486                    })
1487                    .collect()
1488            });
1489
1490            let changed_attrs: Vec<_> = attrs
1491                .as_ref()
1492                .map(|attrs| attrs.iter().map(|attr| attr.0.name.clone()).collect())
1493                .unwrap_or_default();
1494
1495            (attrs, changed_attrs)
1496        } else {
1497            (None, Vec::new())
1498        };
1499
1500        if let Some(snapshot) = self.snapshots.get_mut(&opaque_node_id) {
1501            // The existing snapshot's state is preserved: it records the state before
1502            // the *first* change since the last style flush.
1503            if needs_attrs {
1504                snapshot.attrs = attrs;
1505                snapshot.changed_attrs = changed_attrs;
1506                snapshot.class_changed = true;
1507                snapshot.id_changed = true;
1508                snapshot.other_attributes_changed = true;
1509            }
1510        } else {
1511            self.snapshots.insert(
1512                opaque_node_id,
1513                ServoElementSnapshot {
1514                    state: Some(*self.nodes[node_id].element_state()),
1515                    attrs,
1516                    changed_attrs,
1517                    class_changed: needs_attrs,
1518                    id_changed: needs_attrs,
1519                    other_attributes_changed: needs_attrs,
1520                },
1521            );
1522        }
1523    }
1524
1525    /// Returns whether any style rule depends on any of the given [`ElementState`] bits.
1526    /// If not, changing those bits cannot affect styling and snapshotting can be skipped.
1527    pub fn style_depends_on_state(&self, state: ElementState) -> bool {
1528        self.stylist.iter_origins().any(|(data, _)| {
1529            data.has_state_dependency(state) || data.has_nth_of_state_dependency(state)
1530        })
1531    }
1532
1533    /// Apply a state change (hover/focus/active/etc affecting the given [`ElementState`]
1534    /// bits) to a node, taking a state-only snapshot beforehand if any style rule
1535    /// depends on those bits.
1536    pub fn snapshot_node_and(
1537        &mut self,
1538        node_id: NodeId,
1539        state: ElementState,
1540        cb: impl FnOnce(&mut Node),
1541    ) {
1542        if self.style_depends_on_state(state) {
1543            self.snapshot_node_state_only(node_id);
1544        }
1545        cb(&mut self.nodes[node_id]);
1546    }
1547
1548    // Takes (x, y) co-ordinates (relative to the )
1549    pub fn hit(&self, x: f32, y: f32) -> Option<HitResult> {
1550        self.hit_with_scrollbar(x, y).0
1551    }
1552
1553    /// The topmost element at viewport coordinates (x, y), or `None` if the point
1554    /// is outside the viewport. Anonymous boxes and text nodes are resolved to
1555    /// their nearest element; a point over the background hits the root element.
1556    ///
1557    /// This implements the hit-testing semantics of `document.elementFromPoint()`.
1558    /// Hit testing consults layout, so [`resolve`](Self::resolve) should be called
1559    /// before this method to ensure layout is up to date.
1560    pub fn element_from_point(&self, x: f32, y: f32) -> Option<NodeId> {
1561        let viewport = self.viewport();
1562        let scale = viewport.scale();
1563        let (viewport_width, viewport_height) = (
1564            viewport.window_size.0 as f32 / scale,
1565            viewport.window_size.1 as f32 / scale,
1566        );
1567        if x < 0.0 || y < 0.0 || x > viewport_width || y > viewport_height {
1568            return None;
1569        }
1570
1571        let Some(hit) = self.hit(x, y) else {
1572            // The point is within the viewport but over no box: the root element
1573            // (which covers the viewport in an HTML document) is the hit target
1574            return self.try_root_element().map(|root| root.id);
1575        };
1576
1577        // Resolve anonymous boxes / pseudo-elements, then text nodes, to elements
1578        let mut node_id = self.nearest_non_anonymous_ancestor(hit.node_id)?;
1579        loop {
1580            let node = self.get_node(node_id)?;
1581            if node.is_element() {
1582                return Some(node_id);
1583            }
1584            node_id = node.parent?;
1585        }
1586    }
1587
1588    /// All elements at viewport coordinates (x, y), from topmost to bottommost,
1589    /// as for `document.elementsFromPoint()`.
1590    ///
1591    /// The spec's paint-order list is approximated with the hit element followed
1592    /// by its ancestor elements (which is correct for non-overlapping content).
1593    /// As with [`element_from_point`](Self::element_from_point), layout should be
1594    /// resolved before calling this method.
1595    pub fn elements_from_point(&self, x: f32, y: f32) -> Vec<NodeId> {
1596        let mut element_ids = Vec::new();
1597        let mut current = self.element_from_point(x, y);
1598        while let Some(node_id) = current {
1599            element_ids.push(node_id);
1600            current = self
1601                .get_node(node_id)
1602                .and_then(|node| node.parent)
1603                .filter(|parent_id| {
1604                    self.get_node(*parent_id)
1605                        .is_some_and(|node| node.is_element())
1606                });
1607        }
1608        element_ids
1609    }
1610
1611    /// Walk up the tree to the nearest DOM node whose id is stable across
1612    /// box-tree reconstruction, so canonicalized interaction state never goes
1613    /// stale.
1614    ///
1615    /// Layout-generated nodes (anonymous blocks and `::before`/`::after`
1616    /// pseudo-elements, both stored as anonymous blocks) get new ids on every
1617    /// reconstruction, so we skip any anonymous node *and* a non-anonymous node
1618    /// whose parent is anonymous (the pseudo's text content). The first
1619    /// non-anonymous node with a non-anonymous parent is a real DOM node; the
1620    /// root element's `Document` parent guarantees termination.
1621    ///
1622    /// Returns `None` if `node_id` (or an ancestor) no longer exists.
1623    pub fn nearest_non_anonymous_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
1624        // Recurse up the tree keeping a window of the current node and its
1625        // parent, advancing one step per iteration so each node is looked up
1626        // exactly once.
1627        let mut node = self.get_node(node_id)?;
1628        loop {
1629            let parent = match node.parent {
1630                Some(parent_id) => self.get_node(parent_id)?,
1631                None => return Some(node.id),
1632            };
1633            if !node.is_anonymous() && !parent.is_anonymous() {
1634                return Some(node.id);
1635            }
1636            node = parent;
1637        }
1638    }
1639
1640    pub fn focus_next_node(&mut self) -> Option<NodeId> {
1641        let focussed_node_id = self.get_focussed_node_id()?;
1642        let id = self.next_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
1643        self.set_focus_to(id);
1644        Some(id)
1645    }
1646
1647    /// Move focus to the previous focussable node in the document
1648    pub fn focus_prev_node(&mut self) -> Option<NodeId> {
1649        let focussed_node_id = self.get_focussed_node_id()?;
1650        let id = self.prev_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
1651        self.set_focus_to(id);
1652        Some(id)
1653    }
1654
1655    /// Clear the focussed node
1656    pub fn clear_focus(&mut self) {
1657        if let Some(id) = self.focus_node_id {
1658            let shell_provider = self.shell_provider.clone();
1659            self.snapshot_node_and(id, ElementState::FOCUS | ElementState::FOCUSRING, |node| {
1660                node.blur(shell_provider)
1661            });
1662            self.focus_node_id = None;
1663        }
1664    }
1665
1666    pub fn set_mousedown_node_id(&mut self, node_id: Option<NodeId>) {
1667        self.mousedown_node_id = node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
1668    }
1669    pub fn set_focus_to(&mut self, focus_node_id: NodeId) -> bool {
1670        let Some(focus_node_id) = self.nearest_non_anonymous_ancestor(focus_node_id) else {
1671            return false;
1672        };
1673        if Some(focus_node_id) == self.focus_node_id {
1674            return false;
1675        }
1676
1677        #[cfg(feature = "tracing")]
1678        tracing::info!("Focussed node {focus_node_id}");
1679
1680        let shell_provider = self.shell_provider.clone();
1681
1682        // Remove focus from the old node
1683        if let Some(id) = self.focus_node_id {
1684            self.snapshot_node_and(id, ElementState::FOCUS | ElementState::FOCUSRING, |node| {
1685                node.blur(shell_provider.clone())
1686            });
1687        }
1688
1689        // Focus the new node
1690        self.snapshot_node_and(
1691            focus_node_id,
1692            ElementState::FOCUS | ElementState::FOCUSRING,
1693            |node| node.focus(shell_provider),
1694        );
1695
1696        self.focus_node_id = Some(focus_node_id);
1697
1698        true
1699    }
1700
1701    pub fn active_node(&mut self) -> bool {
1702        let Some(hover_node_id) = self.get_hover_node_id() else {
1703            return false;
1704        };
1705
1706        if let Some(active_node_id) = self.active_node_id {
1707            if active_node_id == hover_node_id {
1708                return true;
1709            }
1710            self.unactive_node();
1711        }
1712
1713        // hover_node_id is canonicalized when stored, so this always holds.
1714        debug_assert!(
1715            self.get_node(hover_node_id)
1716                .is_some_and(|node| !node.is_anonymous()),
1717            "interaction state must reference DOM nodes, not layout-generated nodes"
1718        );
1719        let active_node_id = Some(hover_node_id);
1720
1721        let node_path = self.maybe_node_layout_ancestors(active_node_id);
1722        for &id in node_path.iter() {
1723            self.snapshot_node_and(id, ElementState::ACTIVE, |node| node.active());
1724        }
1725
1726        self.active_node_id = active_node_id;
1727
1728        true
1729    }
1730
1731    pub fn unactive_node(&mut self) -> bool {
1732        let Some(active_node_id) = self.active_node_id.take() else {
1733            return false;
1734        };
1735
1736        let node_path = self.maybe_node_layout_ancestors(Some(active_node_id));
1737        for &id in node_path.iter() {
1738            self.snapshot_node_and(id, ElementState::ACTIVE, |node| node.unactive());
1739        }
1740
1741        true
1742    }
1743
1744    /// The scrollbar thumb currently under the pointer, if any.
1745    pub fn hovered_scrollbar(&self) -> Option<crate::node::ScrollbarRef> {
1746        self.hovered_scrollbar
1747    }
1748
1749    /// The scrollbar thumb currently being dragged, if any.
1750    pub fn scrollbar_drag_target(&self) -> Option<crate::node::ScrollbarRef> {
1751        match &self.drag_mode {
1752            DragMode::ScrollbarDrag(state) => Some(state.scrollbar),
1753            _ => None,
1754        }
1755    }
1756
1757    /// The current opacity of `node_id`'s overlay scrollbars. They show at
1758    /// full opacity on scroll and fade out after a delay (Chromium's overlay
1759    /// timings); the pointer resting on a thumb, or dragging it, holds them
1760    /// visible.
1761    pub fn scrollbar_opacity(&self, node_id: NodeId) -> f32 {
1762        let interacting = |scrollbar: &crate::node::ScrollbarRef| scrollbar.node_id == node_id;
1763        if self.hovered_scrollbar.as_ref().is_some_and(interacting)
1764            || self
1765                .scrollbar_drag_target()
1766                .as_ref()
1767                .is_some_and(interacting)
1768        {
1769            return 1.0;
1770        }
1771        self.scrollbar_activity.get(&node_id).map_or(0.0, |last| {
1772            crate::node::scrollbar::opacity_at(last.elapsed())
1773        })
1774    }
1775
1776    /// Show `node_id`'s overlay scrollbars at full opacity and restart their
1777    /// fade-out delay.
1778    pub(crate) fn show_scrollbars(&mut self, node_id: NodeId) {
1779        if cfg!(feature = "scrollbars") {
1780            self.scrollbar_activity.insert(node_id, Instant::now());
1781        }
1782    }
1783
1784    /// Whether any overlay scrollbars are awaiting or animating their
1785    /// fade-out (so frames must keep rendering until they finish).
1786    fn scrollbars_animating(&self) -> bool {
1787        use crate::node::scrollbar::{FADE_DELAY, FADE_DURATION};
1788        self.scrollbar_activity
1789            .values()
1790            .any(|last| last.elapsed() < FADE_DELAY + FADE_DURATION)
1791    }
1792
1793    /// [`hit`](Self::hit), also resolving the innermost overlay scrollbar
1794    /// thumb under the point (shares the traversal, so it costs nothing
1795    /// extra).
1796    pub(crate) fn hit_with_scrollbar(
1797        &self,
1798        x: f32,
1799        y: f32,
1800    ) -> (Option<HitResult>, Option<crate::node::ScrollbarRef>) {
1801        if TDocument::as_node(&self.root_node())
1802            .first_element_child()
1803            .is_none()
1804        {
1805            #[cfg(feature = "tracing")]
1806            tracing::warn!("No DOM - not resolving hit test");
1807            return (None, None);
1808        }
1809        let mut scrollbar = None;
1810        let hit = self
1811            .root_element()
1812            .hit_inner(x, y, self.viewport().scale_f64(), &mut scrollbar);
1813        (hit, scrollbar)
1814    }
1815
1816    pub fn set_hover_to(&mut self, x: f32, y: f32) -> bool {
1817        // Record the pointer position in client (unscrolled) coordinates so
1818        // that `refresh_hover` can re-resolve hover state after layout or
1819        // scroll changes.
1820        self.last_client_pointer_position = Some(taffy::Point {
1821            x: x - self.viewport_scroll.x as f32,
1822            y: y - self.viewport_scroll.y as f32,
1823        });
1824
1825        let (hit, hovered_scrollbar) = self.hit_with_scrollbar(x, y);
1826        // A faded-out thumb is not interactive: pointer moves never fade
1827        // overlay scrollbars back in (only scrolling shows them).
1828        let hovered_scrollbar =
1829            hovered_scrollbar.filter(|scrollbar| self.scrollbar_opacity(scrollbar.node_id) > 0.0);
1830        // Scrollbar-thumb hover is part of hover state: track it here so a
1831        // pointer crossing a thumb restyles it even when the hit node (the
1832        // content under the overlay thumb) is unchanged.
1833        let scrollbar_changed = hovered_scrollbar != self.hovered_scrollbar;
1834        if scrollbar_changed {
1835            // Entering a thumb restores full opacity mid-fade; leaving one
1836            // restarts the fade-out delay.
1837            for scrollbar in [self.hovered_scrollbar, hovered_scrollbar]
1838                .into_iter()
1839                .flatten()
1840            {
1841                self.show_scrollbars(scrollbar.node_id);
1842            }
1843        }
1844        self.hovered_scrollbar = hovered_scrollbar;
1845
1846        // Store both the precise layout node that was hit (transient: used for
1847        // cursor/style queries) and its canonical DOM target (persistent: must
1848        // not reference layout-generated nodes, whose ids die on box-tree
1849        // reconstruction).
1850        let hit_node_id = hit.map(|hit| hit.node_id);
1851        let hover_node_id = hit_node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
1852        let new_is_text = hit.map(|hit| hit.is_text).unwrap_or(false);
1853
1854        let hit_changed =
1855            hit_node_id != self.hover_hit_node_id || new_is_text != self.hover_node_is_text;
1856        self.hover_hit_node_id = hit_node_id;
1857        self.hover_node_is_text = new_is_text;
1858
1859        // Return early if the new node is the same as the already-hovered node
1860        if hover_node_id == self.hover_node_id {
1861            if hit_changed {
1862                // The canonical target is unchanged (so no restyle is needed)
1863                // but the precise hit node changed, which can change the cursor
1864                // (e.g. moving between text and non-text within one element).
1865                self.shell_provider.set_cursor(self.get_cursor());
1866            }
1867            return scrollbar_changed;
1868        }
1869
1870        let old_node_path = self.maybe_node_layout_ancestors(self.hover_node_id);
1871        let new_node_path = self.maybe_node_layout_ancestors(hover_node_id);
1872        let same_count = old_node_path
1873            .iter()
1874            .zip(&new_node_path)
1875            .take_while(|(o, n)| o == n)
1876            .count();
1877        for &id in old_node_path.iter().skip(same_count) {
1878            self.snapshot_node_and(id, ElementState::HOVER, |node| node.unhover());
1879        }
1880        for &id in new_node_path.iter().skip(same_count) {
1881            self.snapshot_node_and(id, ElementState::HOVER, |node| node.hover());
1882        }
1883
1884        self.hover_node_id = hover_node_id;
1885
1886        // Update the cursor
1887        self.shell_provider.set_cursor(self.get_cursor());
1888
1889        // Request redraw
1890        self.shell_provider.request_redraw();
1891
1892        true
1893    }
1894
1895    pub fn clear_hover(&mut self) -> bool {
1896        // The pointer is no longer over the document, so stop re-resolving
1897        // hover state against it.
1898        self.last_client_pointer_position = None;
1899        self.hover_hit_node_id = None;
1900
1901        let Some(hover_node_id) = self.hover_node_id else {
1902            return false;
1903        };
1904
1905        let old_node_path = self.maybe_node_layout_ancestors(Some(hover_node_id));
1906        for &id in old_node_path.iter() {
1907            self.snapshot_node_and(id, ElementState::HOVER, |node| node.unhover());
1908        }
1909
1910        self.hover_node_id = None;
1911        self.hover_node_is_text = false;
1912
1913        // Update the cursor
1914        self.shell_provider.set_cursor(self.get_cursor());
1915
1916        // Request redraw
1917        self.shell_provider.request_redraw();
1918
1919        true
1920    }
1921
1922    /// Re-resolve hover state against the current layout using the last known
1923    /// pointer position.
1924    ///
1925    /// TODO: synthesizing pointerenter/pointerleave DOM events for
1926    /// hover changes caused by layout shifts.
1927    pub fn refresh_hover(&mut self) -> bool {
1928        let Some(pos) = self.last_client_pointer_position else {
1929            return false;
1930        };
1931        let x = pos.x + self.viewport_scroll.x as f32;
1932        let y = pos.y + self.viewport_scroll.y as f32;
1933        self.set_hover_to(x, y)
1934    }
1935
1936    pub fn get_hover_node_id(&self) -> Option<NodeId> {
1937        self.hover_node_id
1938    }
1939
1940    pub fn get_mousedown_node_id(&self) -> Option<NodeId> {
1941        self.mousedown_node_id
1942    }
1943
1944    pub fn set_viewport(&mut self, viewport: Viewport) {
1945        let scale_has_changed = viewport.scale_f64() != self.viewport.scale_f64();
1946        self.viewport = viewport;
1947        self.set_stylist_device(make_device(
1948            &self.viewport,
1949            self.media_type.clone(),
1950            self.font_ctx.clone(),
1951        ));
1952        self.scroll_viewport_by(0.0, 0.0); // Clamp scroll offset
1953
1954        if scale_has_changed {
1955            self.invalidate_inline_contexts();
1956            self.shell_provider.request_redraw();
1957        }
1958    }
1959
1960    /// Returns the current CSS media type used to evaluate `@media` rules.
1961    pub fn media_type(&self) -> &MediaType {
1962        &self.media_type
1963    }
1964
1965    /// Sets the CSS media type used to evaluate `@media` rules (e.g. `screen` or `print`)
1966    /// and rebuilds the stylist device so updated rules apply on the next restyle.
1967    pub fn set_media_type(&mut self, media_type: MediaType) {
1968        if self.media_type == media_type {
1969            return;
1970        }
1971        self.media_type = media_type;
1972        self.set_stylist_device(make_device(
1973            &self.viewport,
1974            self.media_type.clone(),
1975            self.font_ctx.clone(),
1976        ));
1977    }
1978
1979    pub fn viewport(&self) -> &Viewport {
1980        &self.viewport
1981    }
1982
1983    pub fn viewport_mut(&mut self) -> ViewportMut<'_> {
1984        ViewportMut::new(self)
1985    }
1986
1987    pub fn zoom_by(&mut self, increment: f32) {
1988        *self.viewport.zoom_mut() += increment;
1989        self.set_viewport(self.viewport.clone());
1990    }
1991
1992    pub fn zoom_to(&mut self, zoom: f32) {
1993        *self.viewport.zoom_mut() = zoom;
1994        self.set_viewport(self.viewport.clone());
1995    }
1996
1997    pub fn get_viewport(&self) -> Viewport {
1998        self.viewport.clone()
1999    }
2000
2001    /// Returns whether incremental layout is currently enabled for this document.
2002    pub fn incremental_layout(&self) -> bool {
2003        self.incremental_layout
2004    }
2005
2006    /// Enables or disables incremental layout for this document.
2007    pub fn set_incremental_layout(&mut self, enabled: bool) {
2008        self.incremental_layout = enabled;
2009    }
2010
2011    pub fn devtools(&self) -> &DevtoolSettings {
2012        &self.devtool_settings
2013    }
2014
2015    pub fn devtools_mut(&mut self) -> &mut DevtoolSettings {
2016        &mut self.devtool_settings
2017    }
2018
2019    pub fn subdoc(&self, node_id: NodeId) -> Option<&dyn Document> {
2020        self.get_node(node_id)
2021            .and_then(|node| node.element_data())
2022            .and_then(|el| el.sub_doc_data())
2023    }
2024
2025    pub fn subdoc_mut(&mut self, node_id: NodeId) -> Option<&mut dyn Document> {
2026        self.get_node_mut(node_id)
2027            .and_then(|node| node.element_data_mut())
2028            .and_then(|el| el.sub_doc_data_mut())
2029    }
2030
2031    pub fn is_animating(&self) -> bool {
2032        #[cfg(feature = "custom-widget")]
2033        let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
2034            self.nodes[node_id]
2035                .element_data()
2036                .and_then(|el| el.custom_widget_data())
2037                .is_some_and(|data| data.widget.requires_redraw())
2038        });
2039        #[cfg(not(feature = "custom-widget"))]
2040        let custom_widget_is_animating = false;
2041
2042        self.has_canvas
2043            | self.has_active_animations
2044            | self.subdoc_is_animating
2045            | custom_widget_is_animating
2046            | (self.scroll_animation != ScrollAnimationState::None)
2047            | self.scrollbars_animating()
2048    }
2049
2050    /// Update the device and reset the stylist to process the new size
2051    pub fn set_stylist_device(&mut self, device: Device) {
2052        // Seed the new device with the root element's current style and font-relative
2053        // unit state (used to resolve rem/rlh/rex/rch/rcap/ric units). Stylo only
2054        // updates this state when the root element's style *changes* during a restyle,
2055        // so a freshly-built device would otherwise resolve these units against the
2056        // default font-size (16px) until the root's font-size next changes.
2057        let root_styles = self
2058            .try_root_element()
2059            .and_then(|root| root.primary_styles());
2060        if let Some(root_style) = root_styles.as_deref() {
2061            device.set_root_style(root_style);
2062
2063            let font = root_style.get_font();
2064            let font_size = font.clone_font_size().computed_size();
2065            device.set_root_font_size(root_style.effective_zoom.unzoom(font_size.px()));
2066
2067            let line_height = device
2068                .calc_line_height(font, root_style.writing_mode, None)
2069                .0;
2070            device.set_root_line_height(root_style.effective_zoom.unzoom(line_height.px()));
2071        }
2072        drop(root_styles);
2073
2074        let origins = {
2075            let guard = &self.guard;
2076            let guards = StylesheetGuards {
2077                author: &guard.read(),
2078                ua_or_user: &guard.read(),
2079            };
2080            self.stylist.set_device(device, &guards)
2081        };
2082        self.stylist.force_stylesheet_origins_dirty(origins);
2083    }
2084
2085    pub fn stylist_device(&mut self) -> &Device {
2086        self.stylist.device()
2087    }
2088
2089    pub fn get_cursor(&self) -> Option<CursorIcon> {
2090        // Prefer the precise hit node: `cursor` and `user-select` may be set on
2091        // a pseudo-element or resolved on an anonymous box, and text hits carry
2092        // is_text via the hit node. Fall back to the canonical hover node if
2093        // the hit node has been removed (it is transient across resolves).
2094        let node_id = self
2095            .hover_hit_node_id
2096            .filter(|&id| self.nodes.contains_key(id))
2097            .or(self.get_hover_node_id())?;
2098        let node = &self.nodes[node_id];
2099
2100        if let Some(subdoc) = node.subdoc().map(|doc| doc.inner()) {
2101            return subdoc.get_cursor();
2102        }
2103
2104        let style = node.primary_styles()?;
2105        let user_select = style.clone_user_select();
2106        let keyword = style.clone_cursor().keyword;
2107
2108        // Return cursor from style if it is non-auto
2109        if keyword != CursorKind::Auto {
2110            return stylo_to_cursor_icon(keyword);
2111        }
2112
2113        // Return text cursor for text inputs
2114        if node
2115            .element_data()
2116            .is_some_and(|e| e.text_input_data().is_some())
2117        {
2118            return Some(CursorIcon::Text);
2119        }
2120
2121        // Use "pointer" cursor if any ancestor is a link
2122        let mut maybe_node = Some(node);
2123        while let Some(node) = maybe_node {
2124            if node.is_link() {
2125                return Some(CursorIcon::Pointer);
2126            }
2127
2128            maybe_node = node.layout_parent.get().map(|node_id| node.with(node_id));
2129        }
2130
2131        // Return text cursor for text nodes
2132        if self.hover_node_is_text {
2133            return Some(match user_select {
2134                UserSelect::Text | UserSelect::All | UserSelect::Auto => CursorIcon::Text,
2135                UserSelect::None => CursorIcon::Default,
2136            });
2137        }
2138
2139        // Else fallback to default cursor
2140        Some(CursorIcon::Default)
2141    }
2142
2143    pub fn viewport_scroll(&self) -> crate::Point<f64> {
2144        self.viewport_scroll
2145    }
2146
2147    pub fn set_viewport_scroll(&mut self, scroll: crate::Point<f64>) {
2148        self.viewport_scroll = scroll;
2149    }
2150
2151    /// Find the node targeted by a URL fragment (the `#...` part of a URL).
2152    ///
2153    /// Per the HTML spec, this is the element whose `id` matches the fragment, falling
2154    /// back to the first `<a>` element whose `name` attribute matches.
2155    pub fn get_fragment_target(&self, fragment: &str) -> Option<NodeId> {
2156        if let Some(node_id) = self.get_element_by_id(fragment) {
2157            return Some(node_id);
2158        }
2159
2160        // Fall back to a named anchor: `<a name="...">`
2161        self.nodes.iter().find_map(|(id, node)| {
2162            let el = node.element_data()?;
2163            (el.name.local == local_name!("a") && el.attr(local_name!("name")) == Some(fragment))
2164                .then_some(id)
2165        })
2166    }
2167
2168    /// Computes the size and position of the `Node` relative to the viewport
2169    pub fn get_client_bounding_rect(&self, node_id: NodeId) -> Option<BoundingRect> {
2170        // Non-atomic inline elements have no layout box of their own: return
2171        // the union of their per-line-box fragment rects.
2172        if let Some(rects) = self.inline_fragment_rects(node_id) {
2173            let x0 = rects.iter().map(|r| r.x).fold(f64::INFINITY, f64::min);
2174            let y0 = rects.iter().map(|r| r.y).fold(f64::INFINITY, f64::min);
2175            let x1 = rects
2176                .iter()
2177                .map(|r| r.x + r.width)
2178                .fold(f64::NEG_INFINITY, f64::max);
2179            let y1 = rects
2180                .iter()
2181                .map(|r| r.y + r.height)
2182                .fold(f64::NEG_INFINITY, f64::max);
2183            return match rects.is_empty() {
2184                true => None,
2185                false => Some(BoundingRect {
2186                    x: x0,
2187                    y: y0,
2188                    width: x1 - x0,
2189                    height: y1 - y0,
2190                }),
2191            };
2192        }
2193
2194        let node = self.get_node(node_id)?;
2195        let pos = node.absolute_position(0.0, 0.0);
2196
2197        Some(BoundingRect {
2198            x: pos.x as f64 - self.viewport_scroll.x,
2199            y: pos.y as f64 - self.viewport_scroll.y,
2200            width: node.unrounded_layout().size.width as f64,
2201            height: node.unrounded_layout().size.height as f64,
2202        })
2203    }
2204
2205    /// Computes the sizes and positions of the `Node`'s box fragments relative to the
2206    /// viewport (CSSOM `getClientRects()` semantics). Nodes with their own layout box
2207    /// return a single rect. Non-atomic inline elements (which are laid out as style
2208    /// spans within an inline root's text layout) return one rect per line box.
2209    pub fn node_client_rects(&self, node_id: NodeId) -> Vec<BoundingRect> {
2210        match self.inline_fragment_rects(node_id) {
2211            Some(rects) => rects,
2212            None => self.get_client_bounding_rect(node_id).into_iter().collect(),
2213        }
2214    }
2215
2216    /// Computes per-line-box fragment rects for a non-atomic inline element by walking
2217    /// the containing inline root's text layout. Returns `None` for nodes that have
2218    /// their own layout box (which should use `get_client_bounding_rect` instead).
2219    pub fn inline_fragment_rects(&self, node_id: NodeId) -> Option<Vec<BoundingRect>> {
2220        use parley::PositionedLayoutItem;
2221
2222        let node = self.get_node(node_id)?;
2223
2224        // Only non-atomic inline elements lack their own layout box: they are
2225        // flattened into the containing inline root's text layout as style spans.
2226        if !node.is_element() || node.flags.is_inline_root() {
2227            return None;
2228        }
2229        let display = node.primary_styles()?.clone_display();
2230        if !(display.outside() == DisplayOutside::Inline && display.inside() == DisplayInside::Flow)
2231        {
2232            return None;
2233        }
2234
2235        let inline_root = node.inline_root_ancestor()?;
2236        let inline_layout = inline_root.element_data()?.inline_layout_data.as_ref()?;
2237        let layout = &inline_layout.layout;
2238        let scale = layout.scale() as f64;
2239
2240        // Walk up the DOM parent chain from `id` to check whether it is (or is
2241        // inside) the target node, stopping at the inline root.
2242        let is_in_target = |mut id: NodeId| -> bool {
2243            loop {
2244                if id == node_id {
2245                    return true;
2246                }
2247                if id == inline_root.id {
2248                    return false;
2249                }
2250                match self.get_node(id).and_then(|n| n.parent) {
2251                    Some(parent) => id = parent,
2252                    None => return false,
2253                }
2254            }
2255        };
2256
2257        // Fragment rects are relative to the inline root's content box.
2258        let root_layout = inline_root.final_layout();
2259        let root_pos = inline_root.absolute_position(0.0, 0.0);
2260        let origin_x = root_pos.x as f64
2261            + (root_layout.padding.left + root_layout.border.left) as f64
2262            - self.viewport_scroll.x;
2263        let origin_y = root_pos.y as f64
2264            + (root_layout.padding.top + root_layout.border.top) as f64
2265            - self.viewport_scroll.y;
2266
2267        let mut rects: Vec<BoundingRect> = Vec::new();
2268        for line in layout.lines() {
2269            let line_metrics = line.metrics();
2270            // Union all of the target's fragments on this line into a single rect
2271            let mut line_rect: Option<(f64, f64, f64, f64)> = None;
2272            let mut add = |x0: f64, y0: f64, x1: f64, y1: f64| {
2273                line_rect = Some(match line_rect {
2274                    Some((lx0, ly0, lx1, ly1)) => {
2275                        (lx0.min(x0), ly0.min(y0), lx1.max(x1), ly1.max(y1))
2276                    }
2277                    None => (x0, y0, x1, y1),
2278                });
2279            };
2280
2281            for item in line.items() {
2282                match item {
2283                    PositionedLayoutItem::GlyphRun(glyph_run) => {
2284                        if !is_in_target(glyph_run.style().brush.id) {
2285                            continue;
2286                        }
2287                        let x0 = glyph_run.offset() as f64;
2288                        let x1 = x0 + glyph_run.advance() as f64;
2289                        // Use the line box's block extent rather than the
2290                        // run's font ascent/descent: fonts with small
2291                        // typographic metrics would otherwise produce rects
2292                        // that clip the rendered glyphs. This matches the
2293                        // geometry used for text selection highlights.
2294                        let y0 = line_metrics.block_min_coord as f64;
2295                        let y1 = line_metrics.block_max_coord as f64;
2296                        add(x0, y0, x1, y1);
2297                    }
2298                    PositionedLayoutItem::InlineBox(inline_box) => {
2299                        if !is_in_target(NodeId::from_u64(inline_box.id)) {
2300                            continue;
2301                        }
2302                        let x0 = inline_box.x as f64;
2303                        let y0 = inline_box.y as f64;
2304                        add(
2305                            x0,
2306                            y0,
2307                            x0 + inline_box.width as f64,
2308                            y0 + inline_box.height as f64,
2309                        );
2310                    }
2311                }
2312            }
2313
2314            if let Some((x0, y0, x1, y1)) = line_rect {
2315                rects.push(BoundingRect {
2316                    x: origin_x + x0 / scale,
2317                    y: origin_y + y0 / scale,
2318                    width: (x1 - x0) / scale,
2319                    height: (y1 - y0) / scale,
2320                });
2321            }
2322        }
2323
2324        Some(rects)
2325    }
2326
2327    /// The first element in tree order with the given tag name. The root element and
2328    /// its children are checked first as a fast path before a full tree search, making
2329    /// this suitable for the `documentElement`/`head`/`body` document accessors.
2330    pub fn find_element_by_tag_name(&self, tag: &LocalName) -> Option<&Node> {
2331        let root = self.try_root_element()?;
2332        if root.data.is_element_with_tag_name(tag) {
2333            return Some(root);
2334        }
2335        root.children
2336            .iter()
2337            .copied()
2338            .find(|child_id| {
2339                self.get_node(*child_id)
2340                    .is_some_and(|child| child.data.is_element_with_tag_name(tag))
2341            })
2342            .or_else(|| {
2343                TreeTraverser::new(self)
2344                    .find(|node_id| self.nodes[*node_id].data.is_element_with_tag_name(tag))
2345            })
2346            .map(|node_id| &self.nodes[node_id])
2347    }
2348
2349    /// The document's `body` element, as for `document.body`
2350    pub fn find_body_node(&self) -> Option<&Node> {
2351        self.find_element_by_tag_name(&local_name!("body"))
2352    }
2353
2354    /// The document's `head` element, as for `document.head`
2355    pub fn find_head_node(&self) -> Option<&Node> {
2356        self.find_element_by_tag_name(&local_name!("head"))
2357    }
2358
2359    /// The document's `title` element
2360    pub fn find_title_node(&self) -> Option<&Node> {
2361        self.find_element_by_tag_name(&local_name!("title"))
2362    }
2363
2364    pub fn with_text_input(
2365        &mut self,
2366        node_id: NodeId,
2367        cb: impl FnOnce(PlainEditorDriver<TextBrush>),
2368    ) {
2369        let Some(node) = self.nodes.get_mut(node_id) else {
2370            return;
2371        };
2372
2373        if let Some(text_input) = node
2374            .element_data_mut()
2375            .and_then(|el| el.text_input_data_mut())
2376        {
2377            let mut font_ctx = self.font_ctx.lock().unwrap();
2378            let layout_ctx = &mut self.layout_ctx;
2379            let driver = text_input.editor.driver(&mut font_ctx, layout_ctx);
2380            cb(driver)
2381        }
2382    }
2383
2384    /// Recompute the scroll offset of the text input at `node_id` (if any) so that its caret
2385    /// remains visible within the input's content box.
2386    pub(crate) fn clamp_text_input_scroll(&mut self, node_id: NodeId) {
2387        let Some(node) = self.nodes.get_mut(node_id) else {
2388            return;
2389        };
2390
2391        let content_box_width = node.final_layout().content_box_width();
2392        let content_box_height = node.final_layout().content_box_height();
2393
2394        if let Some(text_input) = node
2395            .element_data_mut()
2396            .and_then(|el| el.text_input_data_mut())
2397        {
2398            text_input.clamp_scroll_offset(content_box_width, content_box_height);
2399        }
2400    }
2401
2402    pub(crate) fn compute_has_canvas(&self) -> bool {
2403        TreeTraverser::new(self).any(|node_id| {
2404            let node = &self.nodes[node_id];
2405            let Some(element) = node.element_data() else {
2406                return false;
2407            };
2408            if element.name.local == local_name!("canvas") && element.has_attr(local_name!("src")) {
2409                return true;
2410            }
2411
2412            false
2413        })
2414    }
2415
2416    // Text selection methods
2417
2418    /// Find the text position (inline_root_id, byte_offset) at a given point.
2419    /// Uses hit() for proper coordinate transformation, then finds the inline root
2420    /// and byte offset.
2421    pub fn find_text_position(&self, x: f32, y: f32) -> Option<(NodeId, usize)> {
2422        let hit = self.hit(x, y)?;
2423        let hit_node = self.get_node(hit.node_id)?;
2424        let inline_root = hit_node.inline_root_ancestor()?;
2425        let byte_offset = inline_root.text_offset_at_point(hit.x, hit.y)?;
2426        Some((inline_root.id, byte_offset))
2427    }
2428
2429    /// Set the text selection range (creates a new selection from anchor to focus)
2430    pub fn set_text_selection(
2431        &mut self,
2432        anchor_node: NodeId,
2433        anchor_offset: usize,
2434        focus_node: NodeId,
2435        focus_offset: usize,
2436    ) {
2437        self.text_selection =
2438            TextSelection::new(anchor_node, anchor_offset, focus_node, focus_offset);
2439
2440        // For anonymous blocks, switch to storing parent+sibling_index (stable reference)
2441        if let (Some(parent), Some(idx)) = self.anonymous_block_location(anchor_node) {
2442            self.text_selection
2443                .anchor
2444                .set_anonymous(parent, idx, anchor_offset);
2445        }
2446        if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
2447            self.text_selection
2448                .focus
2449                .set_anonymous(parent, idx, focus_offset);
2450        }
2451    }
2452
2453    /// Get the parent ID and sibling index for a node if it's an anonymous block.
2454    /// Returns (None, None) for non-anonymous blocks.
2455    fn anonymous_block_location(&self, node_id: NodeId) -> (Option<NodeId>, Option<usize>) {
2456        let Some(node) = self.get_node(node_id) else {
2457            return (None, None);
2458        };
2459
2460        if !node.is_anonymous() {
2461            return (None, None);
2462        }
2463
2464        let Some(parent_id) = node.parent else {
2465            return (None, None);
2466        };
2467
2468        let Some(parent) = self.get_node(parent_id) else {
2469            return (Some(parent_id), None);
2470        };
2471
2472        let layout_children = parent.layout_children.borrow();
2473        let Some(children) = layout_children.as_ref() else {
2474            return (Some(parent_id), None);
2475        };
2476
2477        // Find the index of this anonymous block among siblings
2478        let mut anon_index = 0;
2479        for &child_id in children.iter() {
2480            if child_id == node_id {
2481                return (Some(parent_id), Some(anon_index));
2482            }
2483            if self.get_node(child_id).is_some_and(|n| n.is_anonymous()) {
2484                anon_index += 1;
2485            }
2486        }
2487
2488        (Some(parent_id), None)
2489    }
2490
2491    /// Clear the text selection
2492    pub fn clear_text_selection(&mut self) {
2493        self.text_selection.clear();
2494    }
2495
2496    /// Update the selection focus point (used during mouse drag to extend selection).
2497    pub fn update_selection_focus(&mut self, focus_node: NodeId, focus_offset: usize) {
2498        // For anonymous blocks, store parent+sibling_index; otherwise store node directly
2499        if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
2500            self.text_selection
2501                .focus
2502                .set_anonymous(parent, idx, focus_offset);
2503        } else {
2504            self.text_selection.set_focus(focus_node, focus_offset);
2505        }
2506    }
2507
2508    /// Extend text selection to the given point. Returns true if selection was updated.
2509    /// This is a convenience method that combines find_text_position and update_selection_focus.
2510    pub fn extend_text_selection_to_point(&mut self, x: f32, y: f32) -> bool {
2511        if !self.text_selection.anchor.is_some() {
2512            return false;
2513        }
2514
2515        if let Some((node, offset)) = self.find_text_position(x, y) {
2516            self.update_selection_focus(node, offset);
2517            self.shell_provider.request_redraw();
2518            true
2519        } else {
2520            false
2521        }
2522    }
2523
2524    /// Find the Nth anonymous block under a parent.
2525    fn find_anonymous_block_by_index(
2526        &self,
2527        parent_id: NodeId,
2528        target_index: usize,
2529    ) -> Option<NodeId> {
2530        let parent = self.get_node(parent_id)?;
2531        let layout_children = parent.layout_children.borrow();
2532        let children = layout_children.as_ref()?;
2533
2534        children
2535            .iter()
2536            .filter(|&&child_id| self.get_node(child_id).is_some_and(|n| n.is_anonymous()))
2537            .nth(target_index)
2538            .copied()
2539    }
2540
2541    /// Check if there is an active (non-empty) text selection
2542    pub fn has_text_selection(&self) -> bool {
2543        self.text_selection.is_active()
2544    }
2545
2546    /// Get the selected text content, supporting selection across multiple inline roots.
2547    pub fn get_selected_text(&self) -> Option<String> {
2548        let ranges = self.get_text_selection_ranges();
2549        if ranges.is_empty() {
2550            return None;
2551        }
2552
2553        let mut result = String::new();
2554        for (node_id, start, end) in &ranges {
2555            let node = self.get_node(*node_id)?;
2556            let element_data = node.element_data()?;
2557            let inline_layout = element_data.inline_layout_data.as_ref()?;
2558
2559            if *end > inline_layout.text.len() {
2560                continue;
2561            }
2562
2563            if !result.is_empty() {
2564                result.push(' ');
2565            }
2566            result.push_str(&inline_layout.text[*start..*end]);
2567        }
2568
2569        if result.is_empty() {
2570            None
2571        } else {
2572            Some(result)
2573        }
2574    }
2575
2576    /// Get all selection ranges as Vec<(node_id, start_offset, end_offset)>.
2577    /// Returns empty vec if no selection.
2578    pub fn get_text_selection_ranges(&self) -> Vec<(NodeId, usize, usize)> {
2579        let lookup = |parent_id, idx| self.find_anonymous_block_by_index(parent_id, idx);
2580
2581        let anchor_node = match self.text_selection.anchor.resolve_node_id(lookup) {
2582            Some(id) => id,
2583            None => return Vec::new(),
2584        };
2585        let focus_node = match self.text_selection.focus.resolve_node_id(lookup) {
2586            Some(id) => id,
2587            None => return Vec::new(),
2588        };
2589
2590        // Guard against stale selection endpoints: nodes may have been removed from
2591        // the document (e.g. by script) since the selection was made.
2592        let node_is_in_doc = |node_id: NodeId| {
2593            self.nodes
2594                .get(node_id)
2595                .is_some_and(|node| node.flags.is_in_document())
2596        };
2597        if !node_is_in_doc(anchor_node) || !node_is_in_doc(focus_node) {
2598            return Vec::new();
2599        }
2600
2601        // Single node selection
2602        if anchor_node == focus_node {
2603            let start = self
2604                .text_selection
2605                .anchor
2606                .offset
2607                .min(self.text_selection.focus.offset);
2608            let end = self
2609                .text_selection
2610                .anchor
2611                .offset
2612                .max(self.text_selection.focus.offset);
2613
2614            if start == end {
2615                return Vec::new();
2616            }
2617            return vec![(anchor_node, start, end)];
2618        }
2619
2620        // Multi-node selection: collect all inline roots between anchor and focus
2621        let inline_roots = self.collect_inline_roots_in_range(anchor_node, focus_node);
2622        if inline_roots.is_empty() {
2623            return Vec::new();
2624        }
2625
2626        // Determine document order using the collected inline_roots order
2627        // (inline_roots is already in document order from first to last)
2628        let first_in_roots = inline_roots[0];
2629
2630        let (first_node, first_offset, last_node, last_offset) =
2631            if first_in_roots == anchor_node || (first_in_roots != focus_node) {
2632                // anchor is first (or neither endpoint is in roots, which shouldn't happen)
2633                (
2634                    anchor_node,
2635                    self.text_selection.anchor.offset,
2636                    focus_node,
2637                    self.text_selection.focus.offset,
2638                )
2639            } else {
2640                // focus is first
2641                (
2642                    focus_node,
2643                    self.text_selection.focus.offset,
2644                    anchor_node,
2645                    self.text_selection.anchor.offset,
2646                )
2647            };
2648
2649        let mut ranges = Vec::with_capacity(inline_roots.len());
2650
2651        for &node_id in &inline_roots {
2652            let Some(node) = self.get_node(node_id) else {
2653                continue;
2654            };
2655            let Some(element_data) = node.element_data() else {
2656                continue;
2657            };
2658            let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
2659                continue;
2660            };
2661
2662            let text_len = inline_layout.text.len();
2663
2664            if node_id == first_node && node_id == last_node {
2665                let start = first_offset.min(last_offset);
2666                let end = first_offset.max(last_offset);
2667                if start < end && end <= text_len {
2668                    ranges.push((node_id, start, end));
2669                }
2670            } else if node_id == first_node {
2671                if first_offset < text_len {
2672                    ranges.push((node_id, first_offset, text_len));
2673                }
2674            } else if node_id == last_node {
2675                if last_offset > 0 && last_offset <= text_len {
2676                    ranges.push((node_id, 0, last_offset));
2677                }
2678            } else if text_len > 0 {
2679                ranges.push((node_id, 0, text_len));
2680            }
2681        }
2682
2683        ranges
2684    }
2685}
2686
2687#[derive(Debug, Clone, Copy, PartialEq)]
2688pub struct BoundingRect {
2689    pub x: f64,
2690    pub y: f64,
2691    pub width: f64,
2692    pub height: f64,
2693}
2694
2695impl AsRef<BaseDocument> for BaseDocument {
2696    fn as_ref(&self) -> &BaseDocument {
2697        self
2698    }
2699}
2700
2701impl AsMut<BaseDocument> for BaseDocument {
2702    fn as_mut(&mut self) -> &mut BaseDocument {
2703        self
2704    }
2705}
2706
2707#[cfg(test)]
2708mod hover_state_tests {
2709    use super::*;
2710    use crate::{Attribute, qual_name};
2711    use blitz_traits::shell::ColorScheme;
2712
2713    /// Build `<html><body style="margin:0"><div style="width:300px">some text
2714    /// <div style="height:50px"></div></div></body></html>` manually (the HTML
2715    /// parser lives in blitz-html, which would be a circular dev-dependency).
2716    /// The bare text next to a block sibling gets wrapped in an anonymous
2717    /// block, which becomes the inline root: text hits report the anonymous
2718    /// block as the hit node.
2719    fn make_doc() -> (BaseDocument, NodeId) {
2720        let mut doc = BaseDocument::new(DocumentConfig {
2721            viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
2722            ..Default::default()
2723        });
2724        let root_id = doc.root_node().id;
2725        let style = |value: &str| Attribute {
2726            name: qual_name!("style"),
2727            value: value.to_string(),
2728        };
2729
2730        let mut mutator = doc.mutate();
2731        let html = mutator.create_element(qual_name!("html"), vec![]);
2732        let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
2733        let container = mutator.create_element(qual_name!("div"), vec![style("width:300px")]);
2734        let text = mutator.create_text_node("some text");
2735        let block = mutator.create_element(qual_name!("div"), vec![style("height:50px")]);
2736        mutator.append_children(container, &[text, block]);
2737        mutator.append_children(body, &[container]);
2738        mutator.append_children(html, &[body]);
2739        mutator.append_children(root_id, &[html]);
2740        drop(mutator);
2741
2742        doc.resolve(0.0);
2743        (doc, container)
2744    }
2745
2746    /// Whether text laid out with a real (non-zero-metric) font. Without the
2747    /// `system-fonts` feature text measures 0x0 and text hits are impossible,
2748    /// making these tests vacuous.
2749    fn text_has_size(doc: &BaseDocument, container: NodeId) -> bool {
2750        doc.nodes[container].final_layout().size.height > 50.0
2751    }
2752
2753    /// Regression test: hovering bare text wrapped in an anonymous block must
2754    /// report a text cursor. The hit node for such text is the anonymous
2755    /// inline root itself, while the *stored* hover target is canonicalized to
2756    /// the containing element — the cursor must be derived from the precise
2757    /// hit node, not the canonical target.
2758    #[test]
2759    fn hovering_text_in_anonymous_block_reports_text_cursor() {
2760        let (mut doc, container) = make_doc();
2761        if !text_has_size(&doc, container) {
2762            eprintln!("skipping: no usable font (text measures 0x0)");
2763            return;
2764        }
2765
2766        doc.set_hover_to(5.0, 8.0);
2767        assert!(doc.hover_node_is_text, "expected a text hit");
2768        let hit_id = doc.hover_hit_node_id.expect("expected a hit node");
2769        assert!(
2770            doc.nodes[hit_id].is_anonymous(),
2771            "expected the hit node to be the anonymous inline root"
2772        );
2773        assert_eq!(
2774            doc.get_hover_node_id(),
2775            Some(container),
2776            "expected the stored hover target to be the containing element"
2777        );
2778        assert_eq!(doc.get_cursor(), Some(CursorIcon::Text));
2779    }
2780
2781    /// Hovering the empty region of the anonymous block (right of the text) is
2782    /// not a text hit: default cursor, same canonical hover target.
2783    #[test]
2784    fn hovering_anonymous_block_whitespace_reports_default_cursor() {
2785        let (mut doc, container) = make_doc();
2786        if !text_has_size(&doc, container) {
2787            eprintln!("skipping: no usable font (text measures 0x0)");
2788            return;
2789        }
2790
2791        doc.set_hover_to(250.0, 8.0);
2792        assert!(!doc.hover_node_is_text);
2793        assert_eq!(doc.get_hover_node_id(), Some(container));
2794        assert_eq!(doc.get_cursor(), Some(CursorIcon::Default));
2795    }
2796}
2797
2798#[cfg(test)]
2799mod hover_invalidation_tests {
2800    use super::*;
2801    use crate::{Attribute, QualName, qual_name};
2802    use blitz_traits::shell::ColorScheme;
2803
2804    /// Build `<html><body style="margin:0"><div style="width:300px;height:100px">
2805    /// <span>text</span></div></body></html>` with a `div:hover` rule.
2806    fn make_doc() -> (BaseDocument, NodeId, NodeId) {
2807        let mut doc = BaseDocument::new(DocumentConfig {
2808            viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
2809            ..Default::default()
2810        });
2811        doc.add_user_agent_stylesheet(
2812            "div:hover { background-color: rgb(255, 0, 0); } div:hover span { color: rgb(0, 255, 0); }",
2813        );
2814        let root_id = doc.root_node().id;
2815        let style = |value: &str| Attribute {
2816            name: qual_name!("style"),
2817            value: value.to_string(),
2818        };
2819
2820        let mut mutator = doc.mutate();
2821        let html = mutator.create_element(qual_name!("html"), vec![]);
2822        let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
2823        let div =
2824            mutator.create_element(qual_name!("div"), vec![style("width:300px;height:100px")]);
2825        let span = mutator.create_element(qual_name!("span"), vec![]);
2826        let text = mutator.create_text_node("some text");
2827        mutator.append_children(span, &[text]);
2828        mutator.append_children(div, &[span]);
2829        mutator.append_children(body, &[div]);
2830        mutator.append_children(html, &[body]);
2831        mutator.append_children(root_id, &[html]);
2832        drop(mutator);
2833
2834        doc.resolve(0.0);
2835        (doc, div, span)
2836    }
2837
2838    fn bg_color(doc: &BaseDocument, id: NodeId) -> String {
2839        format!(
2840            "{:?}",
2841            doc.nodes[id]
2842                .primary_styles()
2843                .unwrap()
2844                .get_background()
2845                .background_color
2846        )
2847    }
2848
2849    fn text_color(doc: &BaseDocument, id: NodeId) -> String {
2850        format!(
2851            "{:?}",
2852            doc.nodes[id].primary_styles().unwrap().clone_color()
2853        )
2854    }
2855
2856    #[test]
2857    fn hover_styles_apply_and_clear() {
2858        let (mut doc, div, span) = make_doc();
2859        let initial_bg = bg_color(&doc, div);
2860        let initial_color = text_color(&doc, span);
2861
2862        // Hover the div
2863        doc.set_hover_to(10.0, 10.0);
2864        assert!(doc.nodes[div].is_hovered());
2865        doc.resolve(0.0);
2866        let hovered_bg = bg_color(&doc, div);
2867        let hovered_color = text_color(&doc, span);
2868        assert_ne!(initial_bg, hovered_bg, "hover should change div background");
2869        assert_ne!(
2870            initial_color, hovered_color,
2871            "hover should change span color"
2872        );
2873
2874        // Move the pointer off the div (below it, over the body)
2875        doc.set_hover_to(10.0, 200.0);
2876        assert!(!doc.nodes[div].is_hovered());
2877        doc.resolve(0.0);
2878        assert_eq!(
2879            bg_color(&doc, div),
2880            initial_bg,
2881            "unhover should restore div background"
2882        );
2883        assert_eq!(
2884            text_color(&doc, span),
2885            initial_color,
2886            "unhover should restore span color"
2887        );
2888    }
2889
2890    /// Mimics BBC's headline hover pattern: `a:link:hover .headline` with the
2891    /// headline several levels below the anchor.
2892    #[test]
2893    fn ancestor_hover_with_link_state_updates_descendant() {
2894        let mut doc = BaseDocument::new(DocumentConfig {
2895            viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
2896            ..Default::default()
2897        });
2898        doc.add_user_agent_stylesheet(
2899            ".promo:link:hover .headline, .promo:visited:hover .headline { color: rgb(184, 0, 0); text-decoration-line: underline; }",
2900        );
2901        let root_id = doc.root_node().id;
2902        let attr = |name: QualName, value: &str| Attribute {
2903            name,
2904            value: value.to_string(),
2905        };
2906
2907        let mut mutator = doc.mutate();
2908        let html = mutator.create_element(qual_name!("html"), vec![]);
2909        let body = mutator.create_element(
2910            qual_name!("body"),
2911            vec![attr(qual_name!("style"), "margin:0")],
2912        );
2913        let a = mutator.create_element(
2914            qual_name!("a"),
2915            vec![
2916                attr(qual_name!("href"), "https://example.com"),
2917                attr(qual_name!("class"), "promo"),
2918                attr(
2919                    qual_name!("style"),
2920                    "display:block;width:300px;height:100px",
2921                ),
2922            ],
2923        );
2924        let p = mutator.create_element(qual_name!("p"), vec![]);
2925        let span = mutator.create_element(
2926            qual_name!("span"),
2927            vec![attr(qual_name!("class"), "headline")],
2928        );
2929        let text = mutator.create_text_node("Headline text");
2930        mutator.append_children(span, &[text]);
2931        mutator.append_children(p, &[span]);
2932        mutator.append_children(a, &[p]);
2933        mutator.append_children(body, &[a]);
2934        mutator.append_children(html, &[body]);
2935        mutator.append_children(root_id, &[html]);
2936        drop(mutator);
2937
2938        doc.resolve(0.0);
2939        let initial_color = text_color(&doc, span);
2940
2941        doc.set_hover_to(10.0, 10.0);
2942        assert!(doc.nodes[a].is_hovered());
2943        doc.resolve(0.0);
2944        let hovered_color = text_color(&doc, span);
2945        assert_ne!(
2946            initial_color, hovered_color,
2947            "hovering the anchor should change the headline color"
2948        );
2949
2950        doc.set_hover_to(10.0, 200.0);
2951        assert!(!doc.nodes[a].is_hovered());
2952        doc.resolve(0.0);
2953        assert_eq!(
2954            text_color(&doc, span),
2955            initial_color,
2956            "unhovering the anchor should restore the headline color"
2957        );
2958    }
2959
2960    /// Toggling a checkbox must invalidate `:checked`-dependent styles.
2961    #[test]
2962    fn checkbox_toggle_updates_checked_styles() {
2963        let mut doc = BaseDocument::new(DocumentConfig {
2964            viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
2965            ..Default::default()
2966        });
2967        doc.add_user_agent_stylesheet(
2968            "input:checked { color: rgb(184, 0, 0); } input:checked + label { color: rgb(0, 184, 0); }",
2969        );
2970        let root_id = doc.root_node().id;
2971        let attr = |name: QualName, value: &str| Attribute {
2972            name,
2973            value: value.to_string(),
2974        };
2975
2976        let mut mutator = doc.mutate();
2977        let html = mutator.create_element(qual_name!("html"), vec![]);
2978        let body = mutator.create_element(
2979            qual_name!("body"),
2980            vec![attr(qual_name!("style"), "margin:0")],
2981        );
2982        let input = mutator.create_element(
2983            qual_name!("input"),
2984            vec![attr(qual_name!("type"), "checkbox")],
2985        );
2986        let label = mutator.create_element(qual_name!("label"), vec![]);
2987        let text = mutator.create_text_node("label text");
2988        mutator.append_children(label, &[text]);
2989        mutator.append_children(body, &[input, label]);
2990        mutator.append_children(html, &[body]);
2991        mutator.append_children(root_id, &[html]);
2992        drop(mutator);
2993
2994        doc.resolve(0.0);
2995        let initial_input_color = text_color(&doc, input);
2996        let initial_label_color = text_color(&doc, label);
2997
2998        // Toggle the checkbox on (as the click handler does)
2999        doc.snapshot_node_and(input, ElementState::CHECKED, |node| {
3000            if let Some(el) = node.element_data_mut() {
3001                BaseDocument::toggle_checkbox(el);
3002            }
3003            node.mark_ancestors_dirty();
3004        });
3005        doc.resolve(0.0);
3006        assert_ne!(
3007            text_color(&doc, input),
3008            initial_input_color,
3009            "checking should change the input color"
3010        );
3011        assert_ne!(
3012            text_color(&doc, label),
3013            initial_label_color,
3014            "checking should change the sibling label color"
3015        );
3016
3017        // Toggle the checkbox back off
3018        doc.snapshot_node_and(input, ElementState::CHECKED, |node| {
3019            if let Some(el) = node.element_data_mut() {
3020                BaseDocument::toggle_checkbox(el);
3021            }
3022            node.mark_ancestors_dirty();
3023        });
3024        doc.resolve(0.0);
3025        assert_eq!(
3026            text_color(&doc, input),
3027            initial_input_color,
3028            "unchecking should restore the input color"
3029        );
3030        assert_eq!(
3031            text_color(&doc, label),
3032            initial_label_color,
3033            "unchecking should restore the sibling label color"
3034        );
3035    }
3036
3037    /// A repaint-only style change to an existing `::before` pseudo-element
3038    /// (no box construction damage) must be flushed to the pseudo-element's
3039    /// anonymous node during the style traversal.
3040    #[test]
3041    fn hover_updates_existing_pseudo_element_style() {
3042        let mut doc = BaseDocument::new(DocumentConfig {
3043            viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3044            ..Default::default()
3045        });
3046        doc.add_user_agent_stylesheet(
3047            "div::before { content: \"x\"; color: rgb(1, 2, 3); } \
3048             div:hover::before { color: rgb(0, 0, 255); }",
3049        );
3050        let root_id = doc.root_node().id;
3051        let style = |value: &str| Attribute {
3052            name: qual_name!("style"),
3053            value: value.to_string(),
3054        };
3055
3056        let mut mutator = doc.mutate();
3057        let html = mutator.create_element(qual_name!("html"), vec![]);
3058        let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
3059        let div = mutator.create_element(
3060            qual_name!("div"),
3061            vec![style("display:block;width:300px;height:100px")],
3062        );
3063        let text = mutator.create_text_node("some text");
3064        mutator.append_children(div, &[text]);
3065        mutator.append_children(body, &[div]);
3066        mutator.append_children(html, &[body]);
3067        mutator.append_children(root_id, &[html]);
3068        drop(mutator);
3069
3070        doc.resolve(0.0);
3071        let before = doc.nodes[div].before().expect("::before node should exist");
3072        let initial_color = text_color(&doc, before);
3073
3074        doc.set_hover_to(10.0, 10.0);
3075        assert!(doc.nodes[div].is_hovered());
3076        doc.resolve(0.0);
3077        assert_ne!(
3078            text_color(&doc, before),
3079            initial_color,
3080            "hover should change the ::before color"
3081        );
3082
3083        doc.set_hover_to(10.0, 200.0);
3084        assert!(!doc.nodes[div].is_hovered());
3085        doc.resolve(0.0);
3086        assert_eq!(
3087            text_color(&doc, before),
3088            initial_color,
3089            "unhover should restore the ::before color"
3090        );
3091    }
3092
3093    /// Background-image layers must be flushed to the node's dedicated image
3094    /// storage when its style changes (queued during the style traversal),
3095    /// now that image flushing no longer runs on every node in the flush pass.
3096    #[test]
3097    fn hover_updates_background_image_layers() {
3098        let mut doc = BaseDocument::new(DocumentConfig {
3099            viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3100            ..Default::default()
3101        });
3102        doc.add_user_agent_stylesheet(
3103            "div { background-image: url(\"https://example.com/a.png\"); } \
3104             div:hover { background-image: url(\"https://example.com/b.png\"); }",
3105        );
3106        let root_id = doc.root_node().id;
3107        let style = |value: &str| Attribute {
3108            name: qual_name!("style"),
3109            value: value.to_string(),
3110        };
3111
3112        let mut mutator = doc.mutate();
3113        let html = mutator.create_element(qual_name!("html"), vec![]);
3114        let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
3115        let div = mutator.create_element(
3116            qual_name!("div"),
3117            vec![style("display:block;width:300px;height:100px")],
3118        );
3119        mutator.append_children(body, &[div]);
3120        mutator.append_children(html, &[body]);
3121        mutator.append_children(root_id, &[html]);
3122        drop(mutator);
3123
3124        let background_image_url = |doc: &BaseDocument, id: NodeId| -> Option<String> {
3125            let elem = doc.nodes[id].data.downcast_element().unwrap();
3126            elem.background_images
3127                .first()
3128                .and_then(|img| img.as_ref())
3129                .map(|img| img.url.as_str().to_string())
3130        };
3131
3132        doc.resolve(0.0);
3133        assert_eq!(
3134            background_image_url(&doc, div).as_deref(),
3135            Some("https://example.com/a.png"),
3136            "initial resolve should flush the background image"
3137        );
3138
3139        doc.set_hover_to(10.0, 10.0);
3140        assert!(doc.nodes[div].is_hovered());
3141        doc.resolve(0.0);
3142        assert_eq!(
3143            background_image_url(&doc, div).as_deref(),
3144            Some("https://example.com/b.png"),
3145            "hover should flush the changed background image"
3146        );
3147
3148        doc.set_hover_to(10.0, 200.0);
3149        assert!(!doc.nodes[div].is_hovered());
3150        doc.resolve(0.0);
3151        assert_eq!(
3152            background_image_url(&doc, div).as_deref(),
3153            Some("https://example.com/a.png"),
3154            "unhover should flush the restored background image"
3155        );
3156    }
3157}
3158
3159#[cfg(test)]
3160mod font_face_override_tests {
3161    use super::*;
3162    use crate::net::{FontFaceOverrides, Resource, ResourceLoadResponse};
3163
3164    /// Regression-pin for the `@font-face` descriptor-honouring fix.
3165    ///
3166    /// The bug was that `Resource::Font` carried only the raw font bytes,
3167    /// so `load_resource` registered fonts with `info_override = None` and
3168    /// parley fell back to the TTF's internal `name` table. After the fix,
3169    /// `Resource::Font` carries `FontFaceOverrides` and `load_resource`
3170    /// builds a `FontInfoOverride` from them — meaning a CSS-declared
3171    /// `font-family` alias wins over the file's own metadata.
3172    ///
3173    /// We drive `load_resource` directly with a fabricated response rather
3174    /// than go through HTML parsing → `fetch_font_face`, because the
3175    /// downstream HTML parser lives in `blitz-html` (would be a circular
3176    /// crate dependency). The mapping from `@font-face` descriptors into
3177    /// `FontFaceOverrides` is covered by the unit tests in `net.rs`; this
3178    /// test pins the load-side of the pipeline.
3179    #[test]
3180    fn font_face_overrides_alias_family_name() {
3181        const ALIAS: &str = "AliasedFamily";
3182
3183        let mut document = BaseDocument::new(DocumentConfig::default());
3184
3185        // Sanity: the alias name is not registered before we feed the font.
3186        {
3187            let mut ctx = document.font_ctx.lock().unwrap();
3188            assert!(
3189                ctx.collection.family_id(ALIAS).is_none(),
3190                "alias must not exist before registration",
3191            );
3192        }
3193
3194        // Drive `load_resource` with a `Resource::Font` whose overrides
3195        // assert the CSS-side family name. We use the bullet font as a
3196        // valid font payload — its internal `name` table is irrelevant to
3197        // the assertion; what matters is whether the override wins.
3198        let response = ResourceLoadResponse {
3199            request_id: 0,
3200            node_id: None,
3201            resolved_url: Some(String::from("test://aliased-family")),
3202            result: Ok(Resource::Font(
3203                blitz_traits::net::Bytes::from_static(crate::BULLET_FONT),
3204                FontFaceOverrides {
3205                    family_name: Some(String::from(ALIAS)),
3206                    weight: Some(800.0),
3207                    style: Some(parley::fontique::FontStyle::Italic),
3208                },
3209            )),
3210        };
3211        document.load_resource(response);
3212
3213        // The override must have taken effect: parley's `Collection` now
3214        // resolves the CSS-declared alias to a registered family.
3215        let mut ctx = document.font_ctx.lock().unwrap();
3216        let family_id = ctx
3217            .collection
3218            .family_id(ALIAS)
3219            .expect("CSS-declared family name should be registered as a family alias");
3220        let resolved_name = ctx
3221            .collection
3222            .family_name(family_id)
3223            .expect("family id should resolve back to a name");
3224        assert_eq!(
3225            resolved_name, ALIAS,
3226            "registered family should report the CSS-declared name, \
3227             not the font file's internal `name` table entry",
3228        );
3229    }
3230}