Skip to main content

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