Skip to main content

blitz_dom/
document.rs

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