Skip to main content

ps_blitz_dom/
document.rs

1use crate::events::{DragMode, ScrollAnimationState, handle_dom_event};
2use crate::font_metrics::BlitzFontMetricsProvider;
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::selection::TextSelection;
11use crate::stylo_to_cursor_icon::stylo_to_cursor_icon;
12use crate::traversal::TreeTraverser;
13use crate::url::DocumentUrl;
14use crate::util::ImageType;
15use crate::{
16    DEFAULT_CSS, DocumentConfig, DocumentMutator, DummyHtmlParserProvider, ElementData,
17    EventDriver, HtmlParserProvider, Node, NodeData, NoopEventHandler, StyleThreading,
18    TextNodeData,
19};
20use blitz_traits::devtools::DevtoolSettings;
21use blitz_traits::events::{BlitzScrollEvent, DomEvent, DomEventData, HitResult, UiEvent};
22use blitz_traits::navigation::{DummyNavigationProvider, NavigationProvider};
23use blitz_traits::net::{AbortSignal, DummyNetProvider, NetProvider, Request};
24use blitz_traits::shell::{ColorScheme, DummyShellProvider, ShellProvider, Viewport};
25use cursor_icon::CursorIcon;
26use linebender_resource_handle::Blob;
27use markup5ever::{local_name, ns};
28use parley::{FontContext, PlainEditorDriver};
29use selectors::{Element, matching::QuirksMode};
30use slab::Slab;
31use std::any::Any;
32use std::cell::RefCell;
33use std::collections::{BTreeMap, Bound, HashMap, HashSet};
34use std::ops::{Deref, DerefMut};
35use std::rc::Rc;
36use std::str::FromStr;
37use std::sync::atomic::{AtomicUsize, Ordering};
38use std::sync::mpsc::{Receiver, Sender, channel};
39use std::sync::{Arc, Mutex, MutexGuard, OnceLock, RwLockReadGuard, RwLockWriteGuard};
40use std::task::{Context as TaskContext, Waker};
41use style::Atom;
42use style::animation::DocumentAnimationSet;
43use style::attr::{AttrIdentifier, AttrValue};
44use style::data::{ElementData as StyloElementData, ElementStyles};
45use style::media_queries::MediaType;
46use style::properties::ComputedValues;
47use style::properties::style_structs::Font;
48use style::queries::values::PrefersColorScheme;
49use style::selector_parser::ServoElementSnapshot;
50use style::servo::media_features::PointerCapabilities;
51use style::servo_arc::Arc as ServoArc;
52use style::values::GenericAtomIdent;
53use style::values::computed::ui::CursorKind;
54use style::values::computed::{Overflow, UserSelect};
55use style::{
56    device::Device,
57    dom::{TDocument, TNode},
58    media_queries::MediaList,
59    selector_parser::SnapshotMap,
60    shared_lock::{SharedRwLock, StylesheetGuards},
61    stylesheets::{AllowImportRules, DocumentStyleSheet, Origin, Stylesheet},
62    stylist::Stylist,
63};
64use url::Url;
65use web_time::Instant;
66
67#[cfg(feature = "parallel-construct")]
68use thread_local::ThreadLocal;
69
70pub enum DocGuard<'a> {
71    Ref(&'a BaseDocument),
72    RefCell(std::cell::Ref<'a, BaseDocument>),
73    RwLock(RwLockReadGuard<'a, BaseDocument>),
74    Mutex(MutexGuard<'a, BaseDocument>),
75}
76
77impl Deref for DocGuard<'_> {
78    type Target = BaseDocument;
79    #[inline(always)]
80    fn deref(&self) -> &Self::Target {
81        match self {
82            Self::Ref(base_document) => base_document,
83            Self::RefCell(refcell_guard) => refcell_guard,
84            Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
85            Self::Mutex(mutex_guard) => mutex_guard,
86        }
87    }
88}
89
90pub enum DocGuardMut<'a> {
91    Ref(&'a mut BaseDocument),
92    RefCell(std::cell::RefMut<'a, BaseDocument>),
93    RwLock(RwLockWriteGuard<'a, BaseDocument>),
94    Mutex(MutexGuard<'a, BaseDocument>),
95}
96
97impl Deref for DocGuardMut<'_> {
98    type Target = BaseDocument;
99    #[inline(always)]
100    fn deref(&self) -> &Self::Target {
101        match self {
102            Self::Ref(base_document) => base_document,
103            Self::RefCell(refcell_guard) => refcell_guard,
104            Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
105            Self::Mutex(mutex_guard) => mutex_guard,
106        }
107    }
108}
109
110impl DerefMut for DocGuardMut<'_> {
111    #[inline(always)]
112    fn deref_mut(&mut self) -> &mut Self::Target {
113        match self {
114            Self::Ref(base_document) => base_document,
115            Self::RefCell(refcell_guard) => &mut *refcell_guard,
116            Self::RwLock(rw_lock_read_guard) => &mut *rw_lock_read_guard,
117            Self::Mutex(mutex_guard) => &mut *mutex_guard,
118        }
119    }
120}
121
122/// Abstraction over wrappers around [`BaseDocument`] to allow for them all to
123/// be driven by [`blitz-shell`](https://docs.rs/blitz-shell)
124pub trait Document: Any + 'static {
125    fn inner(&self) -> DocGuard<'_>;
126    fn inner_mut(&mut self) -> DocGuardMut<'_>;
127
128    /// Update the [`Document`] in response to a [`UiEvent`] (click, keypress, etc)
129    fn handle_ui_event(&mut self, event: UiEvent) {
130        let mut doc = self.inner_mut();
131        let mut driver = EventDriver::new(&mut *doc, NoopEventHandler);
132        driver.handle_ui_event(event);
133    }
134
135    /// Poll any pending async operations, and flush changes to the underlying [`BaseDocument`]
136    fn poll(&mut self, task_context: Option<TaskContext>) -> bool {
137        // Default implementation does nothing
138        let _ = task_context;
139        false
140    }
141
142    /// Get the [`Document`]'s id
143    fn id(&self) -> usize {
144        self.inner().id
145    }
146}
147
148pub struct PlainDocument(pub BaseDocument);
149impl Document for PlainDocument {
150    fn inner(&self) -> DocGuard<'_> {
151        DocGuard::Ref(&self.0)
152    }
153    fn inner_mut(&mut self) -> DocGuardMut<'_> {
154        DocGuardMut::Ref(&mut self.0)
155    }
156}
157
158impl Document for BaseDocument {
159    fn inner(&self) -> DocGuard<'_> {
160        DocGuard::Ref(self)
161    }
162    fn inner_mut(&mut self) -> DocGuardMut<'_> {
163        DocGuardMut::Ref(self)
164    }
165}
166
167impl Document for Rc<RefCell<BaseDocument>> {
168    fn inner(&self) -> DocGuard<'_> {
169        DocGuard::RefCell(self.borrow())
170    }
171
172    fn inner_mut(&mut self) -> DocGuardMut<'_> {
173        DocGuardMut::RefCell(self.borrow_mut())
174    }
175}
176
177pub enum DocumentEvent {
178    ResourceLoad(ResourceLoadResponse),
179}
180
181pub struct BaseDocument {
182    /// ID of the document
183    id: usize,
184
185    // Config
186    /// Base url for resolving linked resources (stylesheets, images, fonts, etc)
187    pub(crate) url: DocumentUrl,
188    // Devtool settings. Currently used to render debug overlays
189    pub(crate) devtool_settings: DevtoolSettings,
190    // Viewport details such as the dimensions, HiDPI scale, and zoom factor,
191    pub(crate) viewport: Viewport,
192    // Scroll within our viewport
193    pub(crate) viewport_scroll: crate::Point<f64>,
194    /// CSS media type used to evaluate `@media` rules.
195    pub(crate) media_type: MediaType,
196    /// Strategy for Stylo's style traversal during `resolve`.
197    pub(crate) style_threading: StyleThreading,
198    /// Whether incremental layout is enabled for this document. Defaults to
199    /// whether the `incremental` feature is compiled in. Incremental layout can
200    /// only function when the feature is enabled, so toggling this on has no
201    /// effect in builds compiled without it.
202    pub(crate) incremental_layout: bool,
203
204    // Events
205    pub(crate) tx: Sender<DocumentEvent>,
206    // rx will always be Some, except temporarily while processing events
207    pub(crate) rx: Option<Receiver<DocumentEvent>>,
208
209    /// A slab-backed tree of nodes
210    ///
211    /// We pin the tree to a guarantee to the nodes it creates that the tree is stable in memory.
212    /// There is no way to create the tree - publicly or privately - that would invalidate that invariant.
213    pub(crate) nodes: Box<Slab<Node>>,
214
215    // Stylo
216    /// The Stylo engine
217    pub(crate) stylist: Stylist,
218    pub(crate) animations: DocumentAnimationSet,
219    /// Stylo shared lock
220    pub(crate) guard: SharedRwLock,
221    /// Stylo invalidation map. We insert into this map prior to mutating nodes.
222    pub(crate) snapshots: SnapshotMap,
223
224    // Parley contexts
225    /// A Parley font context
226    pub(crate) font_ctx: Arc<Mutex<parley::FontContext>>,
227    #[cfg(feature = "parallel-construct")]
228    /// Thread-and-document-local copies to the font context
229    pub(crate) thread_font_contexts: ThreadLocal<RefCell<Box<FontContext>>>,
230    /// A Parley layout context
231    pub(crate) layout_ctx: parley::LayoutContext<TextBrush>,
232
233    /// The node which is currently hovered (if any)
234    pub(crate) hover_node_id: Option<usize>,
235    /// Whether the node which is currently hovered is a text node/span
236    pub(crate) hover_node_is_text: bool,
237    /// The node which is currently focussed (if any)
238    pub(crate) focus_node_id: Option<usize>,
239    /// The node which is currently active (if any)
240    pub(crate) active_node_id: Option<usize>,
241    /// The node which recieved a mousedown event (if any)
242    pub(crate) mousedown_node_id: Option<usize>,
243    /// The last time a mousedown was made (for double-click detection)
244    pub(crate) last_mousedown_time: Option<Instant>,
245    /// The position where mousedown occurred (for selection drags and double-click detection)
246    pub(crate) mousedown_position: taffy::Point<f32>,
247    /// How many clicks have been made in quick succession
248    pub(crate) click_count: u16,
249    /// Whether we're currently in a text selection drag (moved 2px+ from mousedown)
250    pub(crate) drag_mode: DragMode,
251    /// The scrollbar thumb currently under the pointer, if any
252    pub(crate) hovered_scrollbar: Option<crate::node::ScrollbarRef>,
253    /// When each scroll container's overlay scrollbars were last shown
254    /// (scrolled, or the pointer left the thumb); drives their fade-out
255    pub(crate) scrollbar_activity: HashMap<usize, Instant>,
256    /// Whether and what kind of scroll animation is currently in progress
257    pub(crate) scroll_animation: ScrollAnimationState,
258
259    /// Text selection state (for non-input text)
260    pub(crate) text_selection: TextSelection,
261
262    // TODO: collapse animating state into a bitflags
263    /// Whether there are active CSS animations/transitions (so we should re-render every frame)
264    pub(crate) has_active_animations: bool,
265    /// Whether there is a `<canvas>` element in the DOM (so we should re-render every frame)
266    pub(crate) has_canvas: bool,
267    /// Whether there are subdocuments that are animating (so we should re-render every frame)
268    pub(crate) subdoc_is_animating: bool,
269
270    /// Map of node ID's for fast lookups
271    pub(crate) nodes_to_id: HashMap<String, usize>,
272    /// Map of `<style>` and `<link>` node IDs to their associated stylesheet
273    pub(crate) nodes_to_stylesheet: BTreeMap<usize, DocumentStyleSheet>,
274    /// Stylesheets added by the useragent
275    /// where the key is the hashed CSS
276    pub(crate) ua_stylesheets: HashMap<String, DocumentStyleSheet>,
277    /// Map from form control node ID's to their associated forms node ID's
278    pub(crate) controls_to_form: HashMap<usize, usize>,
279    /// Nodes that contain sub documents
280    pub(crate) sub_document_nodes: HashSet<usize>,
281    /// Set of changed nodes for updating the accessibility tree
282    pub(crate) changed_nodes: HashSet<usize>,
283    /// Set of changed nodes for updating the accessibility tree
284    pub(crate) deferred_construction_nodes: Vec<ConstructionTask>,
285
286    /// Nodes that contain custom widgets
287    #[cfg(feature = "custom-widget")]
288    pub(crate) custom_widget_nodes: HashSet<usize>,
289    /// Rendering resources allocated by custom widgets that should be deallocated during the next render
290    #[cfg(feature = "custom-widget")]
291    pub(crate) pending_resource_deallocations: Vec<anyrender::ResourceId>,
292
293    /// Cache of loaded images, keyed by URL. Allows reusing images across multiple
294    /// elements without re-fetching from the network.
295    pub(crate) image_cache: HashMap<String, ImageData>,
296
297    /// Tracks in-flight image requests. When an image is being fetched, additional
298    /// requests for the same URL are queued here instead of starting new fetches.
299    /// Value is a list of (node_id, image_type) pairs waiting for the image.
300    pub(crate) pending_images: HashMap<String, Vec<(usize, ImageType)>>,
301
302    // Tracks in-flight "critical" resources (e.g. stylesheets linked from the `<head>`)
303    pub(crate) pending_critical_resources: HashSet<usize>,
304
305    // Service providers
306    /// Network provider. Can be used to fetch assets.
307    pub net_provider: Arc<dyn NetProvider>,
308    /// Navigation provider. Can be used to navigate to a new page (bubbles up the event
309    /// on e.g. clicking a Link)
310    pub navigation_provider: Arc<dyn NavigationProvider>,
311    /// Shell provider. Can be used to request a redraw or set the cursor icon
312    pub shell_provider: Arc<dyn ShellProvider>,
313    /// HTML parser provider. Used to parse HTML for setInnerHTML
314    pub html_parser_provider: Arc<dyn HtmlParserProvider>,
315    /// Carried on every sub-resource `Request` this document issues; aborting
316    /// it cancels all in-flight fetches tied to this document. Set via
317    /// [`DocumentConfig::abort_signal`].
318    pub(crate) abort_signal: Option<AbortSignal>,
319}
320
321pub(crate) fn make_device(
322    viewport: &Viewport,
323    media_type: MediaType,
324    font_ctx: Arc<Mutex<FontContext>>,
325) -> Device {
326    let width = viewport.window_size.0 as f32 / viewport.scale();
327    let height = viewport.window_size.1 as f32 / viewport.scale();
328    let viewport_size = euclid::Size2D::new(width, height);
329    let device_size = euclid::Size2D::new(width, height) * viewport.scale();
330    let device_pixel_ratio = euclid::Scale::new(viewport.scale());
331
332    Device::new(
333        media_type,
334        selectors::matching::QuirksMode::NoQuirks,
335        viewport_size,
336        device_size,
337        device_pixel_ratio,
338        Box::new(BlitzFontMetricsProvider { font_ctx }),
339        ComputedValues::initial_values_with_font_override(Font::initial_values()),
340        match viewport.color_scheme {
341            ColorScheme::Light => PrefersColorScheme::Light,
342            ColorScheme::Dark => PrefersColorScheme::Dark,
343        },
344        PointerCapabilities::default(),
345        PointerCapabilities::default(),
346    )
347}
348
349/// Whether layout reuses its caches, and how that can be overridden at runtime.
350///
351/// Compiled default comes from the `incremental` feature. The environment
352/// override exists so a single build can be measured both ways: with the flag
353/// off every `resolve` clears the Taffy cache and re-shapes every inline root
354/// from scratch, so comparing the two in separate binaries would also compare
355/// two different compilations. `BLITZ_INCREMENTAL=0` forces the old behaviour,
356/// `=1` forces the new one.
357fn incremental_layout_default() -> bool {
358    match std::env::var("BLITZ_INCREMENTAL").ok().as_deref() {
359        Some("0" | "false" | "off") => false,
360        Some(_) => true,
361        None => cfg!(feature = "incremental"),
362    }
363}
364
365impl BaseDocument {
366    /// Create a new (empty) [`BaseDocument`] with the specified configuration
367    pub fn new(config: DocumentConfig) -> Self {
368        static ID_GENERATOR: AtomicUsize = AtomicUsize::new(1);
369
370        let id = ID_GENERATOR.fetch_add(1, Ordering::SeqCst);
371
372        let font_ctx = config
373            .font_ctx
374            .map(|mut font_ctx| {
375                font_ctx.source_cache.make_shared();
376                // font_ctx.collection.make_shared();
377                font_ctx
378            })
379            .unwrap_or_else(|| {
380                use parley::fontique::{Collection, CollectionOptions, SourceCache};
381                let mut font_ctx = FontContext {
382                    source_cache: SourceCache::new_shared(),
383                    collection: Collection::new(CollectionOptions {
384                        shared: false,
385                        system_fonts: cfg!(all(
386                            feature = "system-fonts",
387                            not(target_arch = "wasm32")
388                        )),
389                    }),
390                };
391                font_ctx
392                    .collection
393                    .register_fonts(Blob::new(Arc::new(crate::BULLET_FONT) as _), None);
394                font_ctx
395            });
396        let font_ctx = Arc::new(Mutex::new(font_ctx));
397
398        // Make sure we turn on stylo features *before* creating the Stylist
399        style_config::set_pref!("layout.grid.enabled", true);
400        style_config::set_pref!("layout.unimplemented", true);
401        style_config::set_pref!("layout.columns.enabled", true);
402        style_config::set_pref!("layout.css.basic-shape-shape.enabled", true);
403        style_config::set_pref!("layout.threads", -1);
404
405        let viewport = config.viewport.unwrap_or_default();
406        let media_type = config.media_type.unwrap_or_else(MediaType::screen);
407        let device = make_device(&viewport, media_type.clone(), font_ctx.clone());
408        let stylist = Stylist::new(device, QuirksMode::NoQuirks);
409        let snapshots = SnapshotMap::new();
410        let nodes = Box::new(Slab::new());
411        let guard = SharedRwLock::new();
412        let nodes_to_id = HashMap::new();
413
414        let base_url = config
415            .base_url
416            .and_then(|url| DocumentUrl::from_str(&url).ok())
417            .unwrap_or_default();
418
419        let net_provider = config
420            .net_provider
421            .unwrap_or_else(|| Arc::new(DummyNetProvider));
422        let navigation_provider = config
423            .navigation_provider
424            .unwrap_or_else(|| Arc::new(DummyNavigationProvider));
425        let shell_provider = config
426            .shell_provider
427            .unwrap_or_else(|| Arc::new(DummyShellProvider));
428        let html_parser_provider = config
429            .html_parser_provider
430            .unwrap_or_else(|| Arc::new(DummyHtmlParserProvider));
431
432        let (tx, rx) = channel();
433
434        let mut doc = Self {
435            id,
436            tx,
437            rx: Some(rx),
438
439            guard,
440            nodes,
441            stylist,
442            animations: DocumentAnimationSet::default(),
443            snapshots,
444            nodes_to_id,
445            viewport,
446            media_type,
447            style_threading: config.style_threading,
448            incremental_layout: incremental_layout_default(),
449            devtool_settings: DevtoolSettings::default(),
450            viewport_scroll: crate::Point::ZERO,
451            url: base_url,
452            ua_stylesheets: HashMap::new(),
453            nodes_to_stylesheet: BTreeMap::new(),
454            font_ctx,
455            #[cfg(feature = "parallel-construct")]
456            thread_font_contexts: ThreadLocal::new(),
457            layout_ctx: parley::LayoutContext::new(),
458
459            hover_node_id: None,
460            hover_node_is_text: false,
461            focus_node_id: None,
462            active_node_id: None,
463            mousedown_node_id: None,
464            has_active_animations: false,
465            subdoc_is_animating: false,
466            has_canvas: false,
467            sub_document_nodes: HashSet::new(),
468
469            #[cfg(feature = "custom-widget")]
470            custom_widget_nodes: HashSet::new(),
471            #[cfg(feature = "custom-widget")]
472            pending_resource_deallocations: Vec::new(),
473
474            changed_nodes: HashSet::new(),
475            deferred_construction_nodes: Vec::new(),
476            image_cache: HashMap::new(),
477            pending_images: HashMap::new(),
478            pending_critical_resources: HashSet::new(),
479            controls_to_form: HashMap::new(),
480            net_provider,
481            navigation_provider,
482            shell_provider,
483            html_parser_provider,
484            abort_signal: config.abort_signal,
485            last_mousedown_time: None,
486            mousedown_position: taffy::Point::ZERO,
487            click_count: 0,
488            drag_mode: DragMode::None,
489            hovered_scrollbar: None,
490            scrollbar_activity: HashMap::new(),
491            scroll_animation: ScrollAnimationState::None,
492            text_selection: TextSelection::default(),
493        };
494
495        // Initialise document with root Document node
496        doc.create_node(NodeData::Document);
497        doc.root_node_mut().flags.insert(NodeFlags::IS_IN_DOCUMENT);
498
499        match config.ua_stylesheets {
500            Some(stylesheets) => {
501                for ss in &stylesheets {
502                    doc.add_user_agent_stylesheet(ss);
503                }
504            }
505            None => doc.add_user_agent_stylesheet(DEFAULT_CSS),
506        }
507
508        // Stylo data on the root node container is needed to render the node
509        let stylo_element_data = StyloElementData {
510            styles: ElementStyles {
511                primary: Some(
512                    ComputedValues::initial_values_with_font_override(Font::initial_values())
513                        .to_arc(),
514                ),
515                ..Default::default()
516            },
517            ..Default::default()
518        };
519        let stylo_data = &mut doc.root_node_mut().stylo_element_data;
520        *stylo_data.ensure_init_mut() = stylo_element_data;
521
522        doc
523    }
524
525    /// Set the Document's networking provider
526    pub fn set_net_provider(&mut self, net_provider: Arc<dyn NetProvider>) {
527        self.net_provider = net_provider;
528    }
529
530    /// Set the Document's navigation provider
531    pub fn set_navigation_provider(&mut self, navigation_provider: Arc<dyn NavigationProvider>) {
532        self.navigation_provider = navigation_provider;
533    }
534
535    /// Set the Document's shell provider
536    pub fn set_shell_provider(&mut self, shell_provider: Arc<dyn ShellProvider>) {
537        self.shell_provider = shell_provider;
538    }
539
540    /// Set the Document's html parser provider
541    pub fn set_html_parser_provider(&mut self, html_parser_provider: Arc<dyn HtmlParserProvider>) {
542        self.html_parser_provider = html_parser_provider;
543    }
544
545    /// Set base url for resolving linked resources (stylesheets, images, fonts, etc)
546    pub fn set_base_url(&mut self, url: &str) {
547        self.url = DocumentUrl::from(Url::parse(url).unwrap());
548    }
549
550    pub fn guard(&self) -> &SharedRwLock {
551        &self.guard
552    }
553
554    pub fn tree(&self) -> &Slab<Node> {
555        &self.nodes
556    }
557
558    pub fn id(&self) -> usize {
559        self.id
560    }
561
562    /// Wrapper around [`crate::net::stamped_request`]. Use the free function
563    /// when `&self` would conflict with a held `&mut` borrow on a field.
564    pub(crate) fn build_request(&self, url: url::Url) -> Request {
565        crate::net::stamped_request(url, self.abort_signal.as_ref())
566    }
567
568    pub fn favicon_url(&self) -> Option<String> {
569        self.tree().iter().find_map(|(_, node)| {
570            let data = &node.data;
571            if !data.is_element_with_tag_name(&local_name!("link")) {
572                return None;
573            }
574            let rel = data.attr(local_name!("rel"))?;
575            if !rel
576                .split_ascii_whitespace()
577                .any(|v| v.eq_ignore_ascii_case("icon"))
578            {
579                return None;
580            }
581            data.attr(local_name!("href")).map(|s| s.to_string())
582        })
583    }
584
585    pub fn get_node(&self, node_id: usize) -> Option<&Node> {
586        self.nodes.get(node_id)
587    }
588
589    pub fn get_node_mut(&mut self, node_id: usize) -> Option<&mut Node> {
590        self.nodes.get_mut(node_id)
591    }
592
593    pub fn get_focussed_node_id(&self) -> Option<usize> {
594        self.focus_node_id
595            .or(self.try_root_element().map(|el| el.id))
596    }
597
598    pub fn mutate<'doc>(&'doc mut self) -> DocumentMutator<'doc> {
599        DocumentMutator::new(self)
600    }
601
602    pub fn handle_dom_event<F: FnMut(DomEvent)>(
603        &mut self,
604        event: &mut DomEvent,
605        dispatch_event: F,
606    ) {
607        handle_dom_event(self, event, dispatch_event)
608    }
609
610    pub fn as_any_mut(&mut self) -> &mut dyn Any {
611        self
612    }
613
614    /// Find the label's bound input elements:
615    /// the element id referenced by the "for" attribute of a given label element
616    /// or the first input element which is nested in the label
617    /// Note that although there should only be one bound element,
618    /// we return all possibilities instead of just the first
619    /// in order to allow the caller to decide which one is correct
620    pub fn label_bound_input_element(&self, label_node_id: usize) -> Option<&Node> {
621        let label_element = self.nodes[label_node_id].element_data()?;
622        if let Some(target_element_dom_id) = label_element.attr(local_name!("for")) {
623            TreeTraverser::new(self)
624                .filter_map(|id| {
625                    let node = self.get_node(id)?;
626                    let element_data = node.element_data()?;
627                    if element_data.name.local != local_name!("input") {
628                        return None;
629                    }
630                    let id = element_data.id.as_ref()?;
631                    if *id == *target_element_dom_id {
632                        Some(node)
633                    } else {
634                        None
635                    }
636                })
637                .next()
638        } else {
639            TreeTraverser::new_with_root(self, label_node_id)
640                .filter_map(|child_id| {
641                    let node = self.get_node(child_id)?;
642                    let element_data = node.element_data()?;
643                    if element_data.name.local == local_name!("input") {
644                        Some(node)
645                    } else {
646                        None
647                    }
648                })
649                .next()
650        }
651    }
652
653    pub fn toggle_checkbox(el: &mut ElementData) -> bool {
654        let Some(is_checked) = el.checkbox_input_checked_mut() else {
655            return false;
656        };
657        *is_checked = !*is_checked;
658
659        *is_checked
660    }
661
662    pub fn toggle_radio(&mut self, radio_set_name: String, target_radio_id: usize) {
663        for i in 0..self.nodes.len() {
664            let node = &mut self.nodes[i];
665            if let Some(node_data) = node.data.downcast_element_mut() {
666                if node_data.attr(local_name!("name")) == Some(&radio_set_name) {
667                    let was_clicked = i == target_radio_id;
668                    let Some(is_checked) = node_data.checkbox_input_checked_mut() else {
669                        continue;
670                    };
671                    *is_checked = was_clicked;
672                }
673            }
674        }
675    }
676
677    /// Toggle the `open` attribute of a `<details>` element, expanding or
678    /// collapsing it. This is the default action triggered when the element's
679    /// first `<summary>` child is activated.
680    pub fn toggle_details_open(&mut self, details_id: usize) {
681        use crate::qual_name;
682
683        let node = &self.nodes[details_id];
684        if !node.data.is_element_with_tag_name(&local_name!("details")) {
685            return;
686        }
687        let is_open = node.data.has_attr(local_name!("open"));
688
689        // Note: HTML attributes are in the empty (null) namespace, so the
690        // QualName must not use the html namespace here, else it won't match
691        // an `open` attribute created by the HTML parser.
692        let mut mutator = self.mutate();
693        if is_open {
694            mutator.clear_attribute(details_id, qual_name!("open"));
695        } else {
696            mutator.set_attribute(details_id, qual_name!("open"), "");
697        }
698        drop(mutator);
699
700        self.shell_provider.request_redraw();
701    }
702
703    pub fn set_style_property(&mut self, node_id: usize, name: &str, value: &str) {
704        let node = &mut self.nodes[node_id];
705        let did_change = node.element_data_mut().unwrap().set_style_property(
706            name,
707            value,
708            &self.guard,
709            self.url.url_extra_data(),
710        );
711        if did_change {
712            node.mark_style_attr_updated();
713        }
714    }
715
716    pub fn remove_style_property(&mut self, node_id: usize, name: &str) {
717        let node = &mut self.nodes[node_id];
718        let did_change = node.element_data_mut().unwrap().remove_style_property(
719            name,
720            &self.guard,
721            self.url.url_extra_data(),
722        );
723        if did_change {
724            node.mark_style_attr_updated();
725        }
726    }
727
728    pub fn sub_document_node_ids(&self) -> Vec<usize> {
729        self.sub_document_nodes.iter().copied().collect()
730    }
731
732    pub fn set_sub_document(&mut self, node_id: usize, sub_document: Box<dyn Document>) {
733        self.nodes[node_id]
734            .element_data_mut()
735            .unwrap()
736            .set_sub_document(sub_document);
737        self.sub_document_nodes.insert(node_id);
738    }
739
740    pub fn remove_sub_document(&mut self, node_id: usize) {
741        self.nodes[node_id]
742            .element_data_mut()
743            .unwrap()
744            .remove_sub_document();
745        self.sub_document_nodes.remove(&node_id);
746    }
747
748    /// Poll all sub-documents (see [`Document::poll`]), allowing them to make progress
749    /// on any pending async operations (e.g. JavaScript timers). Hosts which poll a
750    /// wrapper around a [`BaseDocument`] should call this from their `poll` implementation.
751    ///
752    /// Returns `true` if any sub-document reported changes.
753    pub fn poll_subdocuments(&mut self, waker: Option<&Waker>) -> bool {
754        let mut has_changes = false;
755        let node_ids: Vec<usize> = self.sub_document_nodes.iter().copied().collect();
756        for node_id in node_ids {
757            let Some(sub_doc) = self
758                .nodes
759                .get_mut(node_id)
760                .and_then(|node| node.subdoc_mut())
761            else {
762                continue;
763            };
764            let task_context = waker.map(TaskContext::from_waker);
765            has_changes |= sub_doc.poll(task_context);
766        }
767        has_changes
768    }
769
770    #[cfg(feature = "custom-widget")]
771    pub fn custom_widget_node_ids(&self) -> Vec<usize> {
772        self.custom_widget_nodes.iter().copied().collect()
773    }
774
775    #[cfg(feature = "custom-widget")]
776    pub fn take_pending_resource_deallocations(&mut self) -> Vec<anyrender::ResourceId> {
777        std::mem::take(&mut self.pending_resource_deallocations)
778    }
779
780    #[cfg(feature = "custom-widget")]
781    pub fn set_custom_widget(&mut self, node_id: usize, widget: Box<dyn crate::Widget>) {
782        self.nodes[node_id]
783            .element_data_mut()
784            .unwrap()
785            .set_custom_widget(widget);
786        self.custom_widget_nodes.insert(node_id);
787    }
788
789    #[cfg(feature = "custom-widget")]
790    pub fn remove_custom_widget(&mut self, node_id: usize) {
791        let resources_to_deallocate = self.nodes[node_id]
792            .element_data_mut()
793            .unwrap()
794            .remove_custom_widget();
795        self.pending_resource_deallocations
796            .extend_from_slice(&resources_to_deallocate);
797        self.custom_widget_nodes.remove(&node_id);
798    }
799
800    pub fn root_node(&self) -> &Node {
801        &self.nodes[0]
802    }
803
804    pub fn root_node_mut(&mut self) -> &mut Node {
805        &mut self.nodes[0]
806    }
807
808    pub fn try_root_element(&self) -> Option<&Node> {
809        TDocument::as_node(&self.root_node()).first_element_child()
810    }
811
812    pub fn root_element(&self) -> &Node {
813        TDocument::as_node(&self.root_node())
814            .first_element_child()
815            .unwrap()
816            .as_element()
817            .unwrap()
818    }
819
820    pub fn create_node(&mut self, node_data: NodeData) -> usize {
821        let slab_ptr = self.nodes.as_mut() as *mut Slab<Node>;
822        let guard = self.guard.clone();
823
824        let entry = self.nodes.vacant_entry();
825        let id = entry.key();
826        entry.insert(Node::new(slab_ptr, id, guard, node_data));
827
828        // Mark the new node as changed.
829        self.changed_nodes.insert(id);
830        id
831    }
832
833    pub(crate) fn drop_node_ignoring_parent(&mut self, node_id: usize) -> Option<Node> {
834        self.drop_node_ignoring_parent_with(node_id, &mut |_| {})
835    }
836
837    /// Like [`Self::drop_node_ignoring_parent`], but calls `on_drop` with the id of
838    /// every dropped node (the node itself and all of its descendants).
839    pub(crate) fn drop_node_ignoring_parent_with(
840        &mut self,
841        node_id: usize,
842        on_drop: &mut dyn FnMut(usize),
843    ) -> Option<Node> {
844        let mut node = self.nodes.try_remove(node_id);
845        if let Some(node) = &mut node {
846            on_drop(node_id);
847            if let Some(before) = node.before {
848                self.drop_node_ignoring_parent_with(before, on_drop);
849            }
850            if let Some(after) = node.after {
851                self.drop_node_ignoring_parent_with(after, on_drop);
852            }
853
854            for &child in &node.children {
855                self.drop_node_ignoring_parent_with(child, on_drop);
856            }
857        }
858        node
859    }
860
861    /// Whether the document has been mutated
862    pub fn has_changes(&self) -> bool {
863        self.changed_nodes.is_empty()
864    }
865
866    pub fn create_text_node(&mut self, text: &str) -> usize {
867        let content = text.to_string();
868        let data = NodeData::Text(TextNodeData::new(content));
869        self.create_node(data)
870    }
871
872    pub fn deep_clone_node(&mut self, node_id: usize) -> usize {
873        // Load existing node
874        let node = &self.nodes[node_id];
875        let mut data = node.data.clone();
876
877        match &mut data {
878            NodeData::Element(elem) | NodeData::AnonymousBlock(elem) => {
879                if let Some(arc) = elem.style_attribute.as_mut() {
880                    let read_guard = self.guard().read();
881                    let block = arc.read_with(&read_guard);
882                    *arc = ServoArc::new(self.guard().wrap(block.clone()));
883                }
884            }
885            _ => {}
886        }
887
888        let children = node.children.clone();
889
890        // Create new node
891        let new_node_id = self.create_node(data);
892
893        // Recursively clone children
894        let new_children: Vec<usize> = children
895            .into_iter()
896            .map(|child_id| self.deep_clone_node(child_id))
897            .collect();
898        for &child_id in &new_children {
899            self.nodes[child_id].parent = Some(new_node_id);
900        }
901        self.nodes[new_node_id].children = new_children;
902
903        new_node_id
904    }
905
906    pub(crate) fn remove_and_drop_pe(&mut self, node_id: usize) -> Option<Node> {
907        fn remove_pe_ignoring_parent(doc: &mut BaseDocument, node_id: usize) -> Option<Node> {
908            let mut node = doc.nodes.try_remove(node_id);
909            if let Some(node) = &mut node {
910                for &child in &node.children {
911                    remove_pe_ignoring_parent(doc, child);
912                }
913            }
914            node
915        }
916
917        let node = remove_pe_ignoring_parent(self, node_id);
918
919        // Update child_idx values
920        if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
921            let parent = &mut self.nodes[parent_id];
922            parent.children.retain(|id| *id != node_id);
923        }
924
925        node
926    }
927
928    pub(crate) fn resolve_url(&self, raw: &str) -> url::Url {
929        self.url.resolve_relative(raw).unwrap_or_else(|| {
930            panic!(
931                "to be able to resolve {raw} with the base_url: {:?}",
932                *self.url
933            )
934        })
935    }
936
937    pub fn print_tree(&self) {
938        crate::util::walk_tree(0, self.root_node());
939    }
940
941    pub fn print_subtree(&self, node_id: usize) {
942        crate::util::walk_tree(0, &self.nodes[node_id]);
943    }
944
945    pub fn reload_resource_by_href(&mut self, href_to_reload: &str) {
946        for &node_id in self.nodes_to_stylesheet.keys() {
947            let node = &self.nodes[node_id];
948            let Some(element) = node.element_data() else {
949                continue;
950            };
951
952            if element.name.local == local_name!("link") {
953                if let Some(href) = element.attr(local_name!("href")) {
954                    // println!("Node {node_id} {href} {href_to_reload} {} {}", resolved_href.as_str(), resolved_href.as_str() == url_to_reload);
955                    if href == href_to_reload {
956                        let resolved_href = self.resolve_url(href);
957                        self.net_provider.fetch(
958                            self.id(),
959                            self.build_request(resolved_href.clone()),
960                            ResourceHandler::boxed(
961                                self.tx.clone(),
962                                self.id,
963                                Some(node_id),
964                                self.shell_provider.clone(),
965                                StylesheetHandler {
966                                    source_url: resolved_href,
967                                    guard: self.guard.clone(),
968                                    net_provider: self.net_provider.clone(),
969                                    abort_signal: self.abort_signal.clone(),
970                                },
971                            ),
972                        );
973                    }
974                }
975            }
976        }
977    }
978
979    pub fn process_style_element(&mut self, target_id: usize) {
980        let css = self.nodes[target_id].text_content();
981        let css = html_escape::decode_html_entities(&css);
982        let sheet = self.make_stylesheet(&css, Origin::Author);
983        self.add_stylesheet_for_node(sheet, target_id);
984    }
985
986    pub fn remove_user_agent_stylesheet(&mut self, contents: &str) {
987        if let Some(sheet) = self.ua_stylesheets.remove(contents) {
988            self.stylist.remove_stylesheet(sheet, &self.guard.read());
989        }
990    }
991
992    pub fn add_user_agent_stylesheet(&mut self, css: &str) {
993        let sheet = self.make_stylesheet(css, Origin::UserAgent);
994        self.ua_stylesheets.insert(css.to_string(), sheet.clone());
995        self.stylist.append_stylesheet(sheet, &self.guard.read());
996    }
997
998    pub fn make_stylesheet(&self, css: impl AsRef<str>, origin: Origin) -> DocumentStyleSheet {
999        let data = Stylesheet::from_str(
1000            css.as_ref(),
1001            self.url.url_extra_data(),
1002            origin,
1003            ServoArc::new(self.guard.wrap(MediaList::empty())),
1004            self.guard.clone(),
1005            Some(&StylesheetLoader {
1006                tx: self.tx.clone(),
1007                doc_id: self.id,
1008                net_provider: self.net_provider.clone(),
1009                shell_provider: self.shell_provider.clone(),
1010                abort_signal: self.abort_signal.clone(),
1011            }),
1012            None,
1013            QuirksMode::NoQuirks,
1014            AllowImportRules::Yes,
1015        );
1016
1017        DocumentStyleSheet(ServoArc::new(data))
1018    }
1019
1020    pub fn upsert_stylesheet_for_node(&mut self, node_id: usize) {
1021        let raw_styles = self.nodes[node_id].text_content();
1022        let sheet = self.make_stylesheet(raw_styles, Origin::Author);
1023        self.add_stylesheet_for_node(sheet, node_id);
1024    }
1025
1026    pub fn add_stylesheet_for_node(&mut self, stylesheet: DocumentStyleSheet, node_id: usize) {
1027        let old = self.nodes_to_stylesheet.insert(node_id, stylesheet.clone());
1028
1029        if let Some(old) = old {
1030            self.stylist.remove_stylesheet(old, &self.guard.read())
1031        }
1032
1033        // Fetch @font-face fonts
1034        crate::net::fetch_font_face(
1035            self.tx.clone(),
1036            self.id,
1037            Some(node_id),
1038            &stylesheet.0,
1039            &self.net_provider,
1040            &self.shell_provider,
1041            &self.guard.read(),
1042            self.abort_signal.as_ref(),
1043        );
1044
1045        // Store data on element
1046        let element = &mut self.nodes[node_id].element_data_mut().unwrap();
1047        element.special_data = SpecialElementData::Stylesheet(stylesheet.clone());
1048
1049        // TODO: Nodes could potentially get reused so ordering by node_id might be wrong.
1050        let insertion_point = self
1051            .nodes_to_stylesheet
1052            .range((Bound::Excluded(node_id), Bound::Unbounded))
1053            .next()
1054            .map(|(_, sheet)| sheet);
1055
1056        if let Some(insertion_point) = insertion_point {
1057            self.stylist.insert_stylesheet_before(
1058                stylesheet,
1059                insertion_point.clone(),
1060                &self.guard.read(),
1061            )
1062        } else {
1063            self.stylist
1064                .append_stylesheet(stylesheet, &self.guard.read())
1065        }
1066    }
1067
1068    pub fn handle_messages(&mut self) {
1069        // Remove event Reciever from the Document so that we can process events
1070        // without holding a borrow to the Document
1071        let rx = self.rx.take().unwrap();
1072
1073        while let Ok(msg) = rx.try_recv() {
1074            self.handle_message(msg);
1075        }
1076
1077        // Put Reciever back
1078        self.rx = Some(rx);
1079    }
1080
1081    pub fn handle_message(&mut self, msg: DocumentEvent) {
1082        match msg {
1083            DocumentEvent::ResourceLoad(resource) => self.load_resource(resource),
1084        }
1085    }
1086
1087    /// Whether the Document has pending requests for "critical" resources (that should block rendering)
1088    pub fn has_pending_critical_resources(&self) -> bool {
1089        !self.pending_critical_resources.is_empty()
1090    }
1091
1092    pub fn load_resource(&mut self, res: ResourceLoadResponse) {
1093        self.pending_critical_resources.remove(&res.request_id);
1094
1095        let resource = match res.result {
1096            Ok(resource) => resource,
1097            Err(err) => {
1098                if let Some(url) = res.resolved_url.as_ref() {
1099                    let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1100                    #[cfg(feature = "tracing")]
1101                    tracing::warn!(
1102                        url = url.as_str(),
1103                        waiting_nodes = waiting_nodes.len(),
1104                        error = err.as_str(),
1105                        "Resource load failed"
1106                    );
1107                    #[cfg(not(feature = "tracing"))]
1108                    let _ = (waiting_nodes, err);
1109                } else {
1110                    #[cfg(feature = "tracing")]
1111                    tracing::warn!(error = err.as_str(), "Resource load failed (no url)");
1112                    #[cfg(not(feature = "tracing"))]
1113                    let _ = err;
1114                }
1115                return;
1116            }
1117        };
1118
1119        match resource {
1120            Resource::Css(css) => {
1121                let node_id = res.node_id.unwrap();
1122                self.add_stylesheet_for_node(css, node_id);
1123            }
1124            Resource::Image(_kind, width, height, image_data) => {
1125                // Create the ImageData and cache it
1126                let image = ImageData::Raster(RasterImageData::new(width, height, image_data));
1127
1128                let Some(url) = res.resolved_url.as_ref() else {
1129                    return;
1130                };
1131
1132                self.apply_loaded_image(url, image);
1133            }
1134            #[cfg(feature = "svg")]
1135            Resource::Svg(_kind, svg) => {
1136                // Create the ImageData and cache it
1137                let image = ImageData::Svg(svg);
1138
1139                let Some(url) = res.resolved_url.as_ref() else {
1140                    return;
1141                };
1142
1143                self.apply_loaded_image(url, image);
1144            }
1145            Resource::Font(bytes, overrides) => {
1146                let font = Blob::new(Arc::new(bytes));
1147
1148                // Build a `FontInfoOverride` from the `@font-face` descriptors
1149                // captured during stylesheet parsing. Without this, parley
1150                // reads the family name from the TTF's own metadata, which
1151                // means CSS `font-family: 'Avenir Book'` won't match a font
1152                // file that internally identifies as `Avenir 45 Book`.
1153                let weight_override = overrides.weight.map(parley::fontique::FontWeight::new);
1154                let info_override = parley::fontique::FontInfoOverride {
1155                    family_name: overrides.family_name.as_deref(),
1156                    weight: weight_override,
1157                    style: overrides.style,
1158                    ..Default::default()
1159                };
1160
1161                // TODO: Investigate eliminating double-box
1162                let mut global_font_ctx = self.font_ctx.lock().unwrap();
1163                global_font_ctx
1164                    .collection
1165                    .register_fonts(font.clone(), Some(info_override));
1166
1167                #[cfg(feature = "parallel-construct")]
1168                {
1169                    rayon::broadcast(|_ctx| {
1170                        let mut font_ctx = self
1171                            .thread_font_contexts
1172                            .get_or(|| RefCell::new(Box::new(global_font_ctx.clone())))
1173                            .borrow_mut();
1174                        font_ctx
1175                            .collection
1176                            .register_fonts(font.clone(), Some(info_override));
1177                    });
1178                }
1179                drop(global_font_ctx);
1180
1181                // TODO: see if we can only invalidate if resolved fonts may have changed
1182                self.invalidate_inline_contexts();
1183            }
1184            Resource::None => {
1185                // Do nothing
1186            }
1187        }
1188    }
1189
1190    /// Cache a loaded image and apply it to all nodes waiting on it
1191    /// (`<img>` elements, `background-image` layers and `mask-image` layers).
1192    fn apply_loaded_image(&mut self, url: &str, image: ImageData) {
1193        // Get all nodes waiting for this image
1194        let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1195
1196        #[cfg(feature = "tracing")]
1197        tracing::info!(
1198            "Image {url} loaded, applying to {} nodes",
1199            waiting_nodes.len()
1200        );
1201
1202        // Cache the image
1203        self.image_cache.insert(url.to_string(), image.clone());
1204
1205        // Apply to all waiting nodes
1206        for (node_id, image_type) in waiting_nodes {
1207            let Some(node) = self.get_node_mut(node_id) else {
1208                continue;
1209            };
1210
1211            match image_type {
1212                ImageType::Image => {
1213                    node.element_data_mut().unwrap().special_data =
1214                        SpecialElementData::Image(Box::new(image.clone()));
1215
1216                    // Clear layout cache
1217                    node.cache.clear();
1218                    node.insert_damage(ALL_DAMAGE);
1219                }
1220                ImageType::Background(idx) | ImageType::Mask(idx) => {
1221                    let layer_image = node.element_data_mut().and_then(|el| {
1222                        let images = match image_type {
1223                            ImageType::Background(_) => &mut el.background_images,
1224                            ImageType::Mask(_) => &mut el.mask_images,
1225                            ImageType::Image => unreachable!(),
1226                        };
1227                        images.get_mut(idx)
1228                    });
1229                    if let Some(Some(layer_image)) = layer_image {
1230                        layer_image.status = Status::Ok;
1231                        layer_image.image = image.clone();
1232                    }
1233                }
1234            }
1235        }
1236    }
1237
1238    pub fn snapshot_node(&mut self, node_id: usize) {
1239        let node = &mut self.nodes[node_id];
1240
1241        // Do not snapshot nodes that have never been styled. A snapshot records an element's
1242        // pre-mutation state so a restyle can diff selector matches then-vs-now. An element
1243        // that has never been styled has no "then" to diff against. Snapshotting it anyway
1244        // makes Stylo's invalidation unwrap its (absent) primary style and panic.
1245        let has_been_styled = node.primary_styles().is_some();
1246        if !has_been_styled {
1247            return;
1248        }
1249
1250        let opaque_node_id = TNode::opaque(&&*node);
1251        node.has_snapshot = true;
1252        node.snapshot_handled
1253            .store(false, std::sync::atomic::Ordering::SeqCst);
1254
1255        // TODO: handle invalidations other than hover
1256        if let Some(_existing_snapshot) = self.snapshots.get_mut(&opaque_node_id) {
1257            // Do nothing
1258            // TODO: update snapshot
1259        } else {
1260            let attrs: Option<Vec<_>> = node.attrs().map(|attrs| {
1261                attrs
1262                    .iter()
1263                    .map(|attr| {
1264                        let ident = AttrIdentifier {
1265                            local_name: GenericAtomIdent(attr.name.local.clone()),
1266                            name: GenericAtomIdent(attr.name.local.clone()),
1267                            namespace: GenericAtomIdent(attr.name.ns.clone()),
1268                            prefix: None,
1269                        };
1270
1271                        let value = if attr.name.local == local_name!("id") {
1272                            AttrValue::Atom(Atom::from(&*attr.value))
1273                        } else if attr.name.local == local_name!("class") {
1274                            let classes = attr
1275                                .value
1276                                .split_ascii_whitespace()
1277                                .map(Atom::from)
1278                                .collect();
1279                            AttrValue::TokenList(OnceLock::from(attr.value.clone()), classes)
1280                        } else {
1281                            AttrValue::String(attr.value.clone())
1282                        };
1283
1284                        (ident, value)
1285                    })
1286                    .collect()
1287            });
1288
1289            let changed_attrs = attrs
1290                .as_ref()
1291                .map(|attrs| attrs.iter().map(|attr| attr.0.name.clone()).collect())
1292                .unwrap_or_default();
1293
1294            self.snapshots.insert(
1295                opaque_node_id,
1296                ServoElementSnapshot {
1297                    state: Some(node.element_state),
1298                    attrs,
1299                    changed_attrs,
1300                    class_changed: true,
1301                    id_changed: true,
1302                    other_attributes_changed: true,
1303                },
1304            );
1305        }
1306    }
1307
1308    pub fn snapshot_node_and(&mut self, node_id: usize, cb: impl FnOnce(&mut Node)) {
1309        self.snapshot_node(node_id);
1310        cb(&mut self.nodes[node_id]);
1311    }
1312
1313    // Takes (x, y) co-ordinates (relative to the )
1314    pub fn hit(&self, x: f32, y: f32) -> Option<HitResult> {
1315        self.hit_with_scrollbar(x, y).0
1316    }
1317
1318    pub fn focus_next_node(&mut self) -> Option<usize> {
1319        let focussed_node_id = self.get_focussed_node_id()?;
1320        let id = self.next_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
1321        self.set_focus_to(id);
1322        Some(id)
1323    }
1324
1325    /// Clear the focussed node
1326    pub fn clear_focus(&mut self) {
1327        if let Some(id) = self.focus_node_id {
1328            let shell_provider = self.shell_provider.clone();
1329            self.snapshot_node_and(id, |node| node.blur(shell_provider));
1330            self.focus_node_id = None;
1331        }
1332    }
1333
1334    pub fn set_mousedown_node_id(&mut self, node_id: Option<usize>) {
1335        self.mousedown_node_id = node_id;
1336    }
1337    pub fn set_focus_to(&mut self, focus_node_id: usize) -> bool {
1338        if Some(focus_node_id) == self.focus_node_id {
1339            return false;
1340        }
1341
1342        #[cfg(feature = "tracing")]
1343        tracing::info!("Focussed node {focus_node_id}");
1344
1345        let shell_provider = self.shell_provider.clone();
1346
1347        // Remove focus from the old node
1348        if let Some(id) = self.focus_node_id {
1349            self.snapshot_node_and(id, |node| node.blur(shell_provider.clone()));
1350        }
1351
1352        // Focus the new node
1353        self.snapshot_node_and(focus_node_id, |node| node.focus(shell_provider));
1354
1355        self.focus_node_id = Some(focus_node_id);
1356
1357        true
1358    }
1359
1360    pub fn active_node(&mut self) -> bool {
1361        let Some(hover_node_id) = self.get_hover_node_id() else {
1362            return false;
1363        };
1364
1365        if let Some(active_node_id) = self.active_node_id {
1366            if active_node_id == hover_node_id {
1367                return true;
1368            }
1369            self.unactive_node();
1370        }
1371
1372        let active_node_id = Some(hover_node_id);
1373
1374        let node_path = self.maybe_node_layout_ancestors(active_node_id);
1375        for &id in node_path.iter() {
1376            self.snapshot_node_and(id, |node| node.active());
1377        }
1378
1379        self.active_node_id = active_node_id;
1380
1381        true
1382    }
1383
1384    pub fn unactive_node(&mut self) -> bool {
1385        let Some(active_node_id) = self.active_node_id.take() else {
1386            return false;
1387        };
1388
1389        let node_path = self.maybe_node_layout_ancestors(Some(active_node_id));
1390        for &id in node_path.iter() {
1391            self.snapshot_node_and(id, |node| node.unactive());
1392        }
1393
1394        true
1395    }
1396
1397    /// The scrollbar thumb currently under the pointer, if any.
1398    pub fn hovered_scrollbar(&self) -> Option<crate::node::ScrollbarRef> {
1399        self.hovered_scrollbar
1400    }
1401
1402    /// The scrollbar thumb currently being dragged, if any.
1403    pub fn scrollbar_drag_target(&self) -> Option<crate::node::ScrollbarRef> {
1404        match &self.drag_mode {
1405            DragMode::ScrollbarDrag(state) => Some(state.scrollbar),
1406            _ => None,
1407        }
1408    }
1409
1410    /// The current opacity of `node_id`'s overlay scrollbars. They show at
1411    /// full opacity before their first interaction so newly overflowing
1412    /// content remains discoverable. After the first scroll they fade out on
1413    /// Chromium's overlay timings; hovering or dragging holds them visible.
1414    pub fn scrollbar_opacity(&self, node_id: usize) -> f32 {
1415        let interacting = |scrollbar: &crate::node::ScrollbarRef| scrollbar.node_id == node_id;
1416        if self.hovered_scrollbar.as_ref().is_some_and(interacting)
1417            || self
1418                .scrollbar_drag_target()
1419                .as_ref()
1420                .is_some_and(interacting)
1421        {
1422            return 1.0;
1423        }
1424        self.scrollbar_activity.get(&node_id).map_or(1.0, |last| {
1425            crate::node::scrollbar::opacity_at(last.elapsed())
1426        })
1427    }
1428
1429    /// Show `node_id`'s overlay scrollbars at full opacity and restart their
1430    /// fade-out delay.
1431    pub(crate) fn show_scrollbars(&mut self, node_id: usize) {
1432        if cfg!(feature = "scrollbars") {
1433            self.scrollbar_activity.insert(node_id, Instant::now());
1434        }
1435    }
1436
1437    /// Whether any overlay scrollbars are awaiting or animating their
1438    /// fade-out (so frames must keep rendering until they finish).
1439    fn scrollbars_animating(&self) -> bool {
1440        use crate::node::scrollbar::{FADE_DELAY, FADE_DURATION};
1441        self.scrollbar_activity
1442            .values()
1443            .any(|last| last.elapsed() < FADE_DELAY + FADE_DURATION)
1444    }
1445
1446    /// [`hit`](Self::hit), also resolving the innermost overlay scrollbar
1447    /// thumb under the point (shares the traversal, so it costs nothing
1448    /// extra).
1449    pub(crate) fn hit_with_scrollbar(
1450        &self,
1451        x: f32,
1452        y: f32,
1453    ) -> (Option<HitResult>, Option<crate::node::ScrollbarRef>) {
1454        if TDocument::as_node(&&self.nodes[0])
1455            .first_element_child()
1456            .is_none()
1457        {
1458            #[cfg(feature = "tracing")]
1459            tracing::warn!("No DOM - not resolving hit test");
1460            return (None, None);
1461        }
1462        let mut scrollbar = None;
1463        let hit = self
1464            .root_element()
1465            .hit_inner(x, y, self.viewport().scale_f64(), &mut scrollbar);
1466        (hit, scrollbar)
1467    }
1468
1469    pub fn set_hover_to(&mut self, x: f32, y: f32) -> bool {
1470        let (hit, hovered_scrollbar) = self.hit_with_scrollbar(x, y);
1471        // A faded-out thumb is not interactive: pointer moves never fade
1472        // overlay scrollbars back in (only scrolling shows them).
1473        let hovered_scrollbar =
1474            hovered_scrollbar.filter(|scrollbar| self.scrollbar_opacity(scrollbar.node_id) > 0.0);
1475        // Scrollbar-thumb hover is part of hover state: track it here so a
1476        // pointer crossing a thumb restyles it even when the hit node (the
1477        // content under the overlay thumb) is unchanged.
1478        let scrollbar_changed = hovered_scrollbar != self.hovered_scrollbar;
1479        if scrollbar_changed {
1480            // Entering a thumb restores full opacity mid-fade; leaving one
1481            // restarts the fade-out delay.
1482            for scrollbar in [self.hovered_scrollbar, hovered_scrollbar]
1483                .into_iter()
1484                .flatten()
1485            {
1486                self.show_scrollbars(scrollbar.node_id);
1487            }
1488        }
1489        self.hovered_scrollbar = hovered_scrollbar;
1490        let hover_node_id = hit.map(|hit| hit.node_id);
1491        let new_is_text = hit.map(|hit| hit.is_text).unwrap_or(false);
1492
1493        // Return early if the new node is the same as the already-hovered node
1494        if hover_node_id == self.hover_node_id {
1495            return scrollbar_changed;
1496        }
1497
1498        let old_node_path = self.maybe_node_layout_ancestors(self.hover_node_id);
1499        let new_node_path = self.maybe_node_layout_ancestors(hover_node_id);
1500        let same_count = old_node_path
1501            .iter()
1502            .zip(&new_node_path)
1503            .take_while(|(o, n)| o == n)
1504            .count();
1505        for &id in old_node_path.iter().skip(same_count) {
1506            self.snapshot_node_and(id, |node| node.unhover());
1507        }
1508        for &id in new_node_path.iter().skip(same_count) {
1509            self.snapshot_node_and(id, |node| node.hover());
1510        }
1511
1512        self.hover_node_id = hover_node_id;
1513        self.hover_node_is_text = new_is_text;
1514
1515        // Update the cursor
1516        self.shell_provider.set_cursor(self.get_cursor());
1517
1518        // Request redraw
1519        self.shell_provider.request_redraw();
1520
1521        true
1522    }
1523
1524    pub fn clear_hover(&mut self) -> bool {
1525        let Some(hover_node_id) = self.hover_node_id else {
1526            return false;
1527        };
1528
1529        let old_node_path = self.maybe_node_layout_ancestors(Some(hover_node_id));
1530        for &id in old_node_path.iter() {
1531            self.snapshot_node_and(id, |node| node.unhover());
1532        }
1533
1534        self.hover_node_id = None;
1535        self.hover_node_is_text = false;
1536
1537        // Update the cursor
1538        self.shell_provider.set_cursor(self.get_cursor());
1539
1540        // Request redraw
1541        self.shell_provider.request_redraw();
1542
1543        true
1544    }
1545
1546    pub fn get_hover_node_id(&self) -> Option<usize> {
1547        self.hover_node_id
1548    }
1549
1550    pub fn set_viewport(&mut self, viewport: Viewport) {
1551        let scale_has_changed = viewport.scale_f64() != self.viewport.scale_f64();
1552        self.viewport = viewport;
1553        self.set_stylist_device(make_device(
1554            &self.viewport,
1555            self.media_type.clone(),
1556            self.font_ctx.clone(),
1557        ));
1558        self.scroll_viewport_by(0.0, 0.0); // Clamp scroll offset
1559
1560        if scale_has_changed {
1561            self.invalidate_inline_contexts();
1562            self.shell_provider.request_redraw();
1563        }
1564    }
1565
1566    /// Returns the current CSS media type used to evaluate `@media` rules.
1567    pub fn media_type(&self) -> &MediaType {
1568        &self.media_type
1569    }
1570
1571    /// Sets the CSS media type used to evaluate `@media` rules (e.g. `screen` or `print`)
1572    /// and rebuilds the stylist device so updated rules apply on the next restyle.
1573    pub fn set_media_type(&mut self, media_type: MediaType) {
1574        if self.media_type == media_type {
1575            return;
1576        }
1577        self.media_type = media_type;
1578        self.set_stylist_device(make_device(
1579            &self.viewport,
1580            self.media_type.clone(),
1581            self.font_ctx.clone(),
1582        ));
1583    }
1584
1585    pub fn viewport(&self) -> &Viewport {
1586        &self.viewport
1587    }
1588
1589    pub fn viewport_mut(&mut self) -> ViewportMut<'_> {
1590        ViewportMut::new(self)
1591    }
1592
1593    pub fn zoom_by(&mut self, increment: f32) {
1594        *self.viewport.zoom_mut() += increment;
1595        self.set_viewport(self.viewport.clone());
1596    }
1597
1598    pub fn zoom_to(&mut self, zoom: f32) {
1599        *self.viewport.zoom_mut() = zoom;
1600        self.set_viewport(self.viewport.clone());
1601    }
1602
1603    pub fn get_viewport(&self) -> Viewport {
1604        self.viewport.clone()
1605    }
1606
1607    /// Returns whether incremental layout is currently enabled for this document.
1608    pub fn incremental_layout(&self) -> bool {
1609        self.incremental_layout
1610    }
1611
1612    /// Enables or disables incremental layout for this document.
1613    ///
1614    /// Note that incremental layout only works when the `incremental` feature is
1615    /// compiled in; enabling it at runtime has no effect otherwise.
1616    pub fn set_incremental_layout(&mut self, enabled: bool) {
1617        self.incremental_layout = enabled;
1618    }
1619
1620    pub fn devtools(&self) -> &DevtoolSettings {
1621        &self.devtool_settings
1622    }
1623
1624    pub fn devtools_mut(&mut self) -> &mut DevtoolSettings {
1625        &mut self.devtool_settings
1626    }
1627
1628    pub fn subdoc(&self, node_id: usize) -> Option<&dyn Document> {
1629        self.get_node(node_id)
1630            .and_then(|node| node.element_data())
1631            .and_then(|el| el.sub_doc_data())
1632    }
1633
1634    pub fn subdoc_mut(&mut self, node_id: usize) -> Option<&mut dyn Document> {
1635        self.get_node_mut(node_id)
1636            .and_then(|node| node.element_data_mut())
1637            .and_then(|el| el.sub_doc_data_mut())
1638    }
1639
1640    pub fn is_animating(&self) -> bool {
1641        #[cfg(feature = "custom-widget")]
1642        let has_custom_widgets = !self.custom_widget_nodes.is_empty();
1643        #[cfg(not(feature = "custom-widget"))]
1644        let has_custom_widgets = false;
1645
1646        self.has_canvas
1647            | self.has_active_animations
1648            | self.subdoc_is_animating
1649            | has_custom_widgets
1650            | (self.scroll_animation != ScrollAnimationState::None)
1651            | self.scrollbars_animating()
1652    }
1653
1654    /// Update the device and reset the stylist to process the new size
1655    pub fn set_stylist_device(&mut self, device: Device) {
1656        // Seed the new device with the root element's current style and font-relative
1657        // unit state (used to resolve rem/rlh/rex/rch/rcap/ric units). Stylo only
1658        // updates this state when the root element's style *changes* during a restyle,
1659        // so a freshly-built device would otherwise resolve these units against the
1660        // default font-size (16px) until the root's font-size next changes.
1661        let root_styles = self
1662            .try_root_element()
1663            .and_then(|root| root.primary_styles());
1664        if let Some(root_style) = root_styles.as_deref() {
1665            device.set_root_style(root_style);
1666
1667            let font = root_style.get_font();
1668            let font_size = font.clone_font_size().computed_size();
1669            device.set_root_font_size(root_style.effective_zoom.unzoom(font_size.px()));
1670
1671            let line_height = device
1672                .calc_line_height(font, root_style.writing_mode, None)
1673                .0;
1674            device.set_root_line_height(root_style.effective_zoom.unzoom(line_height.px()));
1675        }
1676        drop(root_styles);
1677
1678        let origins = {
1679            let guard = &self.guard;
1680            let guards = StylesheetGuards {
1681                author: &guard.read(),
1682                ua_or_user: &guard.read(),
1683            };
1684            self.stylist.set_device(device, &guards)
1685        };
1686        self.stylist.force_stylesheet_origins_dirty(origins);
1687    }
1688
1689    pub fn stylist_device(&mut self) -> &Device {
1690        self.stylist.device()
1691    }
1692
1693    pub fn get_cursor(&self) -> Option<CursorIcon> {
1694        let node = &self.nodes[self.get_hover_node_id()?];
1695
1696        if let Some(subdoc) = node.subdoc().map(|doc| doc.inner()) {
1697            return subdoc.get_cursor();
1698        }
1699
1700        let style = node.primary_styles()?;
1701        let user_select = style.clone_user_select();
1702        let keyword = style.clone_cursor().keyword;
1703
1704        // Return cursor from style if it is non-auto
1705        if keyword != CursorKind::Auto {
1706            return stylo_to_cursor_icon(keyword);
1707        }
1708
1709        // Return text cursor for text inputs
1710        if node
1711            .element_data()
1712            .is_some_and(|e| e.text_input_data().is_some())
1713        {
1714            return Some(CursorIcon::Text);
1715        }
1716
1717        // Use "pointer" cursor if any ancestor is a link
1718        let mut maybe_node = Some(node);
1719        while let Some(node) = maybe_node {
1720            if node.is_link() {
1721                return Some(CursorIcon::Pointer);
1722            }
1723
1724            maybe_node = node.layout_parent.get().map(|node_id| node.with(node_id));
1725        }
1726
1727        // Return text cursor for text nodes
1728        if self.hover_node_is_text {
1729            return Some(match user_select {
1730                UserSelect::Text | UserSelect::All | UserSelect::Auto => CursorIcon::Text,
1731                UserSelect::None => CursorIcon::Default,
1732            });
1733        }
1734
1735        // Else fallback to default cursor
1736        Some(CursorIcon::Default)
1737    }
1738
1739    pub fn scroll_node_by<F: FnMut(DomEvent)>(
1740        &mut self,
1741        node_id: usize,
1742        x: f64,
1743        y: f64,
1744        dispatch_event: F,
1745    ) {
1746        self.scroll_node_by_has_changed(node_id, x, y, dispatch_event);
1747    }
1748
1749    /// Scroll a node by given x and y
1750    /// Will bubble scrolling up to parent node once it can no longer scroll further
1751    /// If we're already at the root node, bubbles scrolling up to the viewport
1752    pub fn scroll_node_by_has_changed<F: FnMut(DomEvent)>(
1753        &mut self,
1754        node_id: usize,
1755        x: f64,
1756        y: f64,
1757        mut dispatch_event: F,
1758    ) -> bool {
1759        // Per the CSS overflow propagation rules, the root element's overflow (and usually
1760        // the <body>'s) is applied to the viewport, and the element itself must not have
1761        // a scrolling mechanism of its own. So scrolls that reach the root element are
1762        // forwarded to the viewport rather than scrolling the root element itself.
1763        if self.try_root_element().is_some_and(|el| el.id == node_id) {
1764            let has_changed = self.scroll_viewport_by_has_changed(x, y);
1765            if has_changed {
1766                let layout = self.root_element().final_layout;
1767                let scale = self.viewport.scale() as f64;
1768                let event = BlitzScrollEvent {
1769                    scroll_top: self.viewport_scroll.y,
1770                    scroll_left: self.viewport_scroll.x,
1771                    scroll_width: layout.size.width.max(layout.content_size.width) as i32,
1772                    scroll_height: layout.size.height.max(layout.content_size.height) as i32,
1773                    client_width: (self.viewport.window_size.0 as f64 / scale) as i32,
1774                    client_height: (self.viewport.window_size.1 as f64 / scale) as i32,
1775                };
1776                dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
1777            }
1778            return has_changed;
1779        }
1780
1781        let Some(node) = self.nodes.get_mut(node_id) else {
1782            return false;
1783        };
1784
1785        // Text inputs scroll their own internal text content rather than using the generic
1786        // overflow mechanism: single-line inputs scroll horizontally, multi-line inputs scroll
1787        // vertically. Any delta the input cannot consume is bubbled up to an ancestor scroller.
1788        if node
1789            .element_data()
1790            .is_some_and(|el| el.text_input_data().is_some())
1791        {
1792            let parent = node.parent;
1793            let content_box_width = node.final_layout.content_box_width();
1794            let content_box_height = node.final_layout.content_box_height();
1795            let input = node
1796                .element_data_mut()
1797                .and_then(|el| el.text_input_data_mut())
1798                .unwrap();
1799
1800            let (bubble_x, bubble_y) = if input.is_multiline {
1801                (
1802                    x,
1803                    input.scroll_by(y as f32, content_box_width, content_box_height) as f64,
1804                )
1805            } else {
1806                (
1807                    input.scroll_by(x as f32, content_box_width, content_box_height) as f64,
1808                    y,
1809                )
1810            };
1811
1812            let has_changed = bubble_x != x || bubble_y != y;
1813
1814            if bubble_x != 0.0 || bubble_y != 0.0 {
1815                let bubbled = if let Some(parent) = parent {
1816                    self.scroll_node_by_has_changed(parent, bubble_x, bubble_y, dispatch_event)
1817                } else {
1818                    self.scroll_viewport_by_has_changed(bubble_x, bubble_y)
1819                };
1820                return bubbled | has_changed;
1821            }
1822
1823            return has_changed;
1824        }
1825
1826        let (can_x_scroll, can_y_scroll) = node
1827            .primary_styles()
1828            .map(|styles| {
1829                (
1830                    matches!(styles.clone_overflow_x(), Overflow::Scroll | Overflow::Auto),
1831                    matches!(styles.clone_overflow_y(), Overflow::Scroll | Overflow::Auto),
1832                )
1833            })
1834            .unwrap_or((false, false));
1835
1836        let initial = node.scroll_offset;
1837        let new_x = node.scroll_offset.x - x;
1838        let new_y = node.scroll_offset.y - y;
1839
1840        let mut bubble_x = 0.0;
1841        let mut bubble_y = 0.0;
1842
1843        let scroll_width = node.final_layout.scroll_width() as f64;
1844        let scroll_height = node.final_layout.scroll_height() as f64;
1845
1846        // Handle sub document case
1847        if let Some(mut sub_doc) = node.subdoc_mut().map(|doc| doc.inner_mut()) {
1848            let has_changed = if let Some(hover_node_id) = sub_doc.get_hover_node_id() {
1849                sub_doc.scroll_node_by_has_changed(hover_node_id, x, y, dispatch_event)
1850            } else {
1851                sub_doc.scroll_viewport_by_has_changed(x, y)
1852            };
1853
1854            // TODO: propagate remaining scroll to parent
1855            return has_changed;
1856        }
1857
1858        // If we're past our scroll bounds, transfer remainder of scrolling to parent/viewport
1859        if !can_x_scroll {
1860            bubble_x = x
1861        } else if new_x < 0.0 {
1862            bubble_x = -new_x;
1863            node.scroll_offset.x = 0.0;
1864        } else if new_x > scroll_width {
1865            bubble_x = scroll_width - new_x;
1866            node.scroll_offset.x = scroll_width;
1867        } else {
1868            node.scroll_offset.x = new_x;
1869        }
1870
1871        if !can_y_scroll {
1872            bubble_y = y
1873        } else if new_y < 0.0 {
1874            bubble_y = -new_y;
1875            node.scroll_offset.y = 0.0;
1876        } else if new_y > scroll_height {
1877            bubble_y = scroll_height - new_y;
1878            node.scroll_offset.y = scroll_height;
1879        } else {
1880            node.scroll_offset.y = new_y;
1881        }
1882
1883        let has_changed = node.scroll_offset != initial;
1884
1885        if has_changed {
1886            let layout = node.final_layout;
1887            let event = BlitzScrollEvent {
1888                scroll_top: node.scroll_offset.y,
1889                scroll_left: node.scroll_offset.x,
1890                scroll_width: layout.scroll_width() as i32,
1891                scroll_height: layout.scroll_height() as i32,
1892                client_width: layout.size.width as i32,
1893                client_height: layout.size.height as i32,
1894            };
1895
1896            dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
1897        }
1898
1899        let parent = node.parent;
1900        if has_changed {
1901            self.show_scrollbars(node_id);
1902        }
1903
1904        if bubble_x != 0.0 || bubble_y != 0.0 {
1905            if let Some(parent) = parent {
1906                return self.scroll_node_by_has_changed(parent, bubble_x, bubble_y, dispatch_event)
1907                    | has_changed;
1908            } else {
1909                return self.scroll_viewport_by_has_changed(bubble_x, bubble_y) | has_changed;
1910            }
1911        }
1912
1913        has_changed
1914    }
1915
1916    pub fn scroll_viewport_by(&mut self, x: f64, y: f64) {
1917        self.scroll_viewport_by_has_changed(x, y);
1918    }
1919
1920    /// Scroll the viewport by the given values
1921    pub fn scroll_viewport_by_has_changed(&mut self, x: f64, y: f64) -> bool {
1922        // The viewport scrolls the root element's scrollable overflow, which includes both
1923        // the root element itself and any content which overflows it (e.g. when the root
1924        // element has a fixed height but its content is taller).
1925        let root_layout = &self.root_element().final_layout;
1926        let content_width = root_layout.size.width.max(root_layout.content_size.width) as f64;
1927        let content_height = root_layout.size.height.max(root_layout.content_size.height) as f64;
1928        let new_scroll = (self.viewport_scroll.x - x, self.viewport_scroll.y - y);
1929        let window_width = self.viewport.window_size.0 as f64 / self.viewport.scale() as f64;
1930        let window_height = self.viewport.window_size.1 as f64 / self.viewport.scale() as f64;
1931
1932        let initial = self.viewport_scroll;
1933        self.viewport_scroll.x =
1934            f64::max(0.0, f64::min(new_scroll.0, content_width - window_width));
1935        self.viewport_scroll.y =
1936            f64::max(0.0, f64::min(new_scroll.1, content_height - window_height));
1937
1938        self.viewport_scroll != initial
1939    }
1940
1941    pub fn scroll_by(
1942        &mut self,
1943        anchor_node_id: Option<usize>,
1944        scroll_x: f64,
1945        scroll_y: f64,
1946        dispatch_event: &mut dyn FnMut(DomEvent),
1947    ) -> bool {
1948        if let Some(anchor_node_id) = anchor_node_id {
1949            self.scroll_node_by_has_changed(anchor_node_id, scroll_x, scroll_y, dispatch_event)
1950        } else {
1951            self.scroll_viewport_by_has_changed(scroll_x, scroll_y)
1952        }
1953    }
1954
1955    pub fn viewport_scroll(&self) -> crate::Point<f64> {
1956        self.viewport_scroll
1957    }
1958
1959    pub fn set_viewport_scroll(&mut self, scroll: crate::Point<f64>) {
1960        self.viewport_scroll = scroll;
1961    }
1962
1963    /// Find the node targeted by a URL fragment (the `#...` part of a URL).
1964    ///
1965    /// Per the HTML spec, this is the element whose `id` matches the fragment, falling
1966    /// back to the first `<a>` element whose `name` attribute matches.
1967    pub fn get_fragment_target(&self, fragment: &str) -> Option<usize> {
1968        if let Some(node_id) = self.get_element_by_id(fragment) {
1969            return Some(node_id);
1970        }
1971
1972        // Fall back to a named anchor: `<a name="...">`
1973        self.nodes.iter().find_map(|(id, node)| {
1974            let el = node.element_data()?;
1975            (el.name.local == local_name!("a") && el.attr(local_name!("name")) == Some(fragment))
1976                .then_some(id)
1977        })
1978    }
1979
1980    /// Scroll the viewport so that the given node is aligned with the top of the viewport.
1981    pub fn scroll_to_node(&mut self, node_id: usize) {
1982        let Some(node) = self.nodes.get(node_id) else {
1983            return;
1984        };
1985
1986        // `absolute_position` gives the node's position in document space (it does not
1987        // account for the viewport scroll), so it is the scroll offset we want to land on.
1988        let target = node.absolute_position(0.0, 0.0);
1989        let current = self.viewport_scroll;
1990
1991        // `scroll_viewport_by` subtracts the delta from the current scroll offset, so pass
1992        // `current - target` in order to land on `target`.
1993        self.scroll_viewport_by(current.x - target.x as f64, current.y - target.y as f64);
1994    }
1995
1996    /// Scroll to the element targeted by the given URL fragment (the `#...` part of a URL).
1997    ///
1998    /// An empty fragment (or a `top` fragment that matches no element) scrolls to the top
1999    /// of the document, matching browser behaviour. Returns `true` if a scroll target was
2000    /// found.
2001    pub fn scroll_to_fragment(&mut self, fragment: &str) -> bool {
2002        // Fragments are percent-encoded in URLs (e.g. `%20`); decode before matching.
2003        let decoded = percent_encoding::percent_decode_str(fragment)
2004            .decode_utf8_lossy()
2005            .into_owned();
2006
2007        if !decoded.is_empty() {
2008            if let Some(node_id) = self.get_fragment_target(&decoded) {
2009                self.scroll_to_node(node_id);
2010                return true;
2011            }
2012        }
2013
2014        // An empty fragment, or the special "top" fragment when no matching element exists,
2015        // scrolls to the top of the document.
2016        if decoded.is_empty() || decoded.eq_ignore_ascii_case("top") {
2017            let current = self.viewport_scroll;
2018            self.scroll_viewport_by(current.x, current.y);
2019            return true;
2020        }
2021
2022        false
2023    }
2024
2025    /// Computes the size and position of the `Node` relative to the viewport
2026    pub fn get_client_bounding_rect(&self, node_id: usize) -> Option<BoundingRect> {
2027        let node = self.get_node(node_id)?;
2028        let pos = node.absolute_position(0.0, 0.0);
2029
2030        Some(BoundingRect {
2031            x: pos.x as f64 - self.viewport_scroll.x,
2032            y: pos.y as f64 - self.viewport_scroll.y,
2033            width: node.unrounded_layout.size.width as f64,
2034            height: node.unrounded_layout.size.height as f64,
2035        })
2036    }
2037
2038    pub fn find_title_node(&self) -> Option<&Node> {
2039        TreeTraverser::new(self)
2040            .find(|node_id| {
2041                let node = &self.nodes[*node_id];
2042                let Some(element) = node.element_data() else {
2043                    return false;
2044                };
2045                if element.name.ns != ns!(html) || element.name.local != local_name!("title") {
2046                    return false;
2047                }
2048                node.parent
2049                    .and_then(|parent_id| self.nodes.get(parent_id))
2050                    .and_then(Node::element_data)
2051                    .is_some_and(|parent| {
2052                        parent.name.ns == ns!(html) && parent.name.local == local_name!("head")
2053                    })
2054            })
2055            .map(|node_id| &self.nodes[node_id])
2056    }
2057
2058    pub fn with_text_input(
2059        &mut self,
2060        node_id: usize,
2061        cb: impl FnOnce(PlainEditorDriver<TextBrush>),
2062    ) {
2063        let Some(node) = self.nodes.get_mut(node_id) else {
2064            return;
2065        };
2066
2067        if let Some(text_input) = node
2068            .element_data_mut()
2069            .and_then(|el| el.text_input_data_mut())
2070        {
2071            let mut font_ctx = self.font_ctx.lock().unwrap();
2072            let layout_ctx = &mut self.layout_ctx;
2073            let driver = text_input.editor.driver(&mut font_ctx, layout_ctx);
2074            cb(driver)
2075        }
2076    }
2077
2078    /// Recompute the scroll offset of the text input at `node_id` (if any) so that its caret
2079    /// remains visible within the input's content box.
2080    pub(crate) fn clamp_text_input_scroll(&mut self, node_id: usize) {
2081        let Some(node) = self.nodes.get_mut(node_id) else {
2082            return;
2083        };
2084
2085        let content_box_width = node.final_layout.content_box_width();
2086        let content_box_height = node.final_layout.content_box_height();
2087
2088        if let Some(text_input) = node
2089            .element_data_mut()
2090            .and_then(|el| el.text_input_data_mut())
2091        {
2092            text_input.clamp_scroll_offset(content_box_width, content_box_height);
2093        }
2094    }
2095
2096    pub(crate) fn compute_has_canvas(&self) -> bool {
2097        TreeTraverser::new(self).any(|node_id| {
2098            let node = &self.nodes[node_id];
2099            let Some(element) = node.element_data() else {
2100                return false;
2101            };
2102            if element.name.local == local_name!("canvas") && element.has_attr(local_name!("src")) {
2103                return true;
2104            }
2105
2106            false
2107        })
2108    }
2109
2110    // Text selection methods
2111
2112    /// Find the text position (inline_root_id, byte_offset) at a given point.
2113    /// Uses hit() for proper coordinate transformation, then finds the inline root
2114    /// and byte offset.
2115    pub fn find_text_position(&self, x: f32, y: f32) -> Option<(usize, usize)> {
2116        let hit = self.hit(x, y)?;
2117        let hit_node = self.get_node(hit.node_id)?;
2118        let inline_root = hit_node.inline_root_ancestor()?;
2119        let byte_offset = inline_root.text_offset_at_point(hit.x, hit.y)?;
2120        Some((inline_root.id, byte_offset))
2121    }
2122
2123    /// Set the text selection range (creates a new selection from anchor to focus)
2124    pub fn set_text_selection(
2125        &mut self,
2126        anchor_node: usize,
2127        anchor_offset: usize,
2128        focus_node: usize,
2129        focus_offset: usize,
2130    ) {
2131        self.text_selection =
2132            TextSelection::new(anchor_node, anchor_offset, focus_node, focus_offset);
2133
2134        // For anonymous blocks, switch to storing parent+sibling_index (stable reference)
2135        if let (Some(parent), Some(idx)) = self.anonymous_block_location(anchor_node) {
2136            self.text_selection
2137                .anchor
2138                .set_anonymous(parent, idx, anchor_offset);
2139        }
2140        if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
2141            self.text_selection
2142                .focus
2143                .set_anonymous(parent, idx, focus_offset);
2144        }
2145    }
2146
2147    /// Get the parent ID and sibling index for a node if it's an anonymous block.
2148    /// Returns (None, None) for non-anonymous blocks.
2149    fn anonymous_block_location(&self, node_id: usize) -> (Option<usize>, Option<usize>) {
2150        let Some(node) = self.get_node(node_id) else {
2151            return (None, None);
2152        };
2153
2154        if !node.is_anonymous() {
2155            return (None, None);
2156        }
2157
2158        let Some(parent_id) = node.parent else {
2159            return (None, None);
2160        };
2161
2162        let Some(parent) = self.get_node(parent_id) else {
2163            return (Some(parent_id), None);
2164        };
2165
2166        let layout_children = parent.layout_children.borrow();
2167        let Some(children) = layout_children.as_ref() else {
2168            return (Some(parent_id), None);
2169        };
2170
2171        // Find the index of this anonymous block among siblings
2172        let mut anon_index = 0;
2173        for &child_id in children.iter() {
2174            if child_id == node_id {
2175                return (Some(parent_id), Some(anon_index));
2176            }
2177            if self.get_node(child_id).is_some_and(|n| n.is_anonymous()) {
2178                anon_index += 1;
2179            }
2180        }
2181
2182        (Some(parent_id), None)
2183    }
2184
2185    /// Clear the text selection
2186    pub fn clear_text_selection(&mut self) {
2187        self.text_selection.clear();
2188    }
2189
2190    /// Update the selection focus point (used during mouse drag to extend selection).
2191    pub fn update_selection_focus(&mut self, focus_node: usize, focus_offset: usize) {
2192        // For anonymous blocks, store parent+sibling_index; otherwise store node directly
2193        if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
2194            self.text_selection
2195                .focus
2196                .set_anonymous(parent, idx, focus_offset);
2197        } else {
2198            self.text_selection.set_focus(focus_node, focus_offset);
2199        }
2200    }
2201
2202    /// Extend text selection to the given point. Returns true if selection was updated.
2203    /// This is a convenience method that combines find_text_position and update_selection_focus.
2204    pub fn extend_text_selection_to_point(&mut self, x: f32, y: f32) -> bool {
2205        if !self.text_selection.anchor.is_some() {
2206            return false;
2207        }
2208
2209        if let Some((node, offset)) = self.find_text_position(x, y) {
2210            self.update_selection_focus(node, offset);
2211            self.shell_provider.request_redraw();
2212            true
2213        } else {
2214            false
2215        }
2216    }
2217
2218    /// Find the Nth anonymous block under a parent.
2219    fn find_anonymous_block_by_index(
2220        &self,
2221        parent_id: usize,
2222        target_index: usize,
2223    ) -> Option<usize> {
2224        let parent = self.get_node(parent_id)?;
2225        let layout_children = parent.layout_children.borrow();
2226        let children = layout_children.as_ref()?;
2227
2228        children
2229            .iter()
2230            .filter(|&&child_id| self.get_node(child_id).is_some_and(|n| n.is_anonymous()))
2231            .nth(target_index)
2232            .copied()
2233    }
2234
2235    /// Check if there is an active (non-empty) text selection
2236    pub fn has_text_selection(&self) -> bool {
2237        self.text_selection.is_active()
2238    }
2239
2240    /// Get the selected text content, supporting selection across multiple inline roots.
2241    pub fn get_selected_text(&self) -> Option<String> {
2242        let ranges = self.get_text_selection_ranges();
2243        if ranges.is_empty() {
2244            return None;
2245        }
2246
2247        let mut result = String::new();
2248        for (node_id, start, end) in &ranges {
2249            let node = self.get_node(*node_id)?;
2250            let element_data = node.element_data()?;
2251            let inline_layout = element_data.inline_layout_data.as_ref()?;
2252
2253            if *end > inline_layout.text.len() {
2254                continue;
2255            }
2256
2257            if !result.is_empty() {
2258                result.push(' ');
2259            }
2260            result.push_str(&inline_layout.text[*start..*end]);
2261        }
2262
2263        if result.is_empty() {
2264            None
2265        } else {
2266            Some(result)
2267        }
2268    }
2269
2270    /// Get all selection ranges as Vec<(node_id, start_offset, end_offset)>.
2271    /// Returns empty vec if no selection.
2272    pub fn get_text_selection_ranges(&self) -> Vec<(usize, usize, usize)> {
2273        let lookup = |parent_id, idx| self.find_anonymous_block_by_index(parent_id, idx);
2274
2275        let anchor_node = match self.text_selection.anchor.resolve_node_id(lookup) {
2276            Some(id) => id,
2277            None => return Vec::new(),
2278        };
2279        let focus_node = match self.text_selection.focus.resolve_node_id(lookup) {
2280            Some(id) => id,
2281            None => return Vec::new(),
2282        };
2283
2284        // Guard against stale selection endpoints: nodes may have been removed from
2285        // the document (e.g. by script) since the selection was made.
2286        let node_is_in_doc = |node_id: usize| {
2287            self.nodes
2288                .get(node_id)
2289                .is_some_and(|node| node.flags.is_in_document())
2290        };
2291        if !node_is_in_doc(anchor_node) || !node_is_in_doc(focus_node) {
2292            return Vec::new();
2293        }
2294
2295        // Single node selection
2296        if anchor_node == focus_node {
2297            let start = self
2298                .text_selection
2299                .anchor
2300                .offset
2301                .min(self.text_selection.focus.offset);
2302            let end = self
2303                .text_selection
2304                .anchor
2305                .offset
2306                .max(self.text_selection.focus.offset);
2307
2308            if start == end {
2309                return Vec::new();
2310            }
2311            return vec![(anchor_node, start, end)];
2312        }
2313
2314        // Multi-node selection: collect all inline roots between anchor and focus
2315        let inline_roots = self.collect_inline_roots_in_range(anchor_node, focus_node);
2316        if inline_roots.is_empty() {
2317            return Vec::new();
2318        }
2319
2320        // Determine document order using the collected inline_roots order
2321        // (inline_roots is already in document order from first to last)
2322        let first_in_roots = inline_roots[0];
2323
2324        let (first_node, first_offset, last_node, last_offset) =
2325            if first_in_roots == anchor_node || (first_in_roots != focus_node) {
2326                // anchor is first (or neither endpoint is in roots, which shouldn't happen)
2327                (
2328                    anchor_node,
2329                    self.text_selection.anchor.offset,
2330                    focus_node,
2331                    self.text_selection.focus.offset,
2332                )
2333            } else {
2334                // focus is first
2335                (
2336                    focus_node,
2337                    self.text_selection.focus.offset,
2338                    anchor_node,
2339                    self.text_selection.anchor.offset,
2340                )
2341            };
2342
2343        let mut ranges = Vec::with_capacity(inline_roots.len());
2344
2345        for &node_id in &inline_roots {
2346            let Some(node) = self.get_node(node_id) else {
2347                continue;
2348            };
2349            let Some(element_data) = node.element_data() else {
2350                continue;
2351            };
2352            let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
2353                continue;
2354            };
2355
2356            let text_len = inline_layout.text.len();
2357
2358            if node_id == first_node && node_id == last_node {
2359                let start = first_offset.min(last_offset);
2360                let end = first_offset.max(last_offset);
2361                if start < end && end <= text_len {
2362                    ranges.push((node_id, start, end));
2363                }
2364            } else if node_id == first_node {
2365                if first_offset < text_len {
2366                    ranges.push((node_id, first_offset, text_len));
2367                }
2368            } else if node_id == last_node {
2369                if last_offset > 0 && last_offset <= text_len {
2370                    ranges.push((node_id, 0, last_offset));
2371                }
2372            } else if text_len > 0 {
2373                ranges.push((node_id, 0, text_len));
2374            }
2375        }
2376
2377        ranges
2378    }
2379}
2380
2381pub struct BoundingRect {
2382    pub x: f64,
2383    pub y: f64,
2384    pub width: f64,
2385    pub height: f64,
2386}
2387
2388impl AsRef<BaseDocument> for BaseDocument {
2389    fn as_ref(&self) -> &BaseDocument {
2390        self
2391    }
2392}
2393
2394impl AsMut<BaseDocument> for BaseDocument {
2395    fn as_mut(&mut self) -> &mut BaseDocument {
2396        self
2397    }
2398}
2399
2400#[cfg(test)]
2401mod font_face_override_tests {
2402    use super::*;
2403    use crate::net::{FontFaceOverrides, Resource, ResourceLoadResponse};
2404
2405    /// Regression-pin for the `@font-face` descriptor-honouring fix.
2406    ///
2407    /// The bug was that `Resource::Font` carried only the raw font bytes,
2408    /// so `load_resource` registered fonts with `info_override = None` and
2409    /// parley fell back to the TTF's internal `name` table. After the fix,
2410    /// `Resource::Font` carries `FontFaceOverrides` and `load_resource`
2411    /// builds a `FontInfoOverride` from them — meaning a CSS-declared
2412    /// `font-family` alias wins over the file's own metadata.
2413    ///
2414    /// We drive `load_resource` directly with a fabricated response rather
2415    /// than go through HTML parsing → `fetch_font_face`, because the
2416    /// downstream HTML parser lives in `blitz-html` (would be a circular
2417    /// crate dependency). The mapping from `@font-face` descriptors into
2418    /// `FontFaceOverrides` is covered by the unit tests in `net.rs`; this
2419    /// test pins the load-side of the pipeline.
2420    #[test]
2421    fn font_face_overrides_alias_family_name() {
2422        const ALIAS: &str = "AliasedFamily";
2423
2424        let mut document = BaseDocument::new(DocumentConfig::default());
2425
2426        // Sanity: the alias name is not registered before we feed the font.
2427        {
2428            let mut ctx = document.font_ctx.lock().unwrap();
2429            assert!(
2430                ctx.collection.family_id(ALIAS).is_none(),
2431                "alias must not exist before registration",
2432            );
2433        }
2434
2435        // Drive `load_resource` with a `Resource::Font` whose overrides
2436        // assert the CSS-side family name. We use the bullet font as a
2437        // valid font payload — its internal `name` table is irrelevant to
2438        // the assertion; what matters is whether the override wins.
2439        let response = ResourceLoadResponse {
2440            request_id: 0,
2441            node_id: None,
2442            resolved_url: Some(String::from("test://aliased-family")),
2443            result: Ok(Resource::Font(
2444                blitz_traits::net::Bytes::from_static(crate::BULLET_FONT),
2445                FontFaceOverrides {
2446                    family_name: Some(String::from(ALIAS)),
2447                    weight: Some(800.0),
2448                    style: Some(parley::fontique::FontStyle::Italic),
2449                },
2450            )),
2451        };
2452        document.load_resource(response);
2453
2454        // The override must have taken effect: parley's `Collection` now
2455        // resolves the CSS-declared alias to a registered family.
2456        let mut ctx = document.font_ctx.lock().unwrap();
2457        let family_id = ctx
2458            .collection
2459            .family_id(ALIAS)
2460            .expect("CSS-declared family name should be registered as a family alias");
2461        let resolved_name = ctx
2462            .collection
2463            .family_name(family_id)
2464            .expect("family id should resolve back to a name");
2465        assert_eq!(
2466            resolved_name, ALIAS,
2467            "registered family should report the CSS-declared name, \
2468             not the font file's internal `name` table entry",
2469        );
2470    }
2471}