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