Skip to main content

blitz_dom/
document.rs

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