Skip to main content

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