Skip to main content

ps_blitz_dom/
document.rs

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