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