1use crate::NodeTree;
2use crate::events::{DragMode, handle_dom_event};
3use crate::font_metrics::BlitzFontMetricsProvider;
4use crate::layout::construct::ConstructionTask;
5use crate::layout::damage::ALL_DAMAGE;
6use crate::mutator::ViewportMut;
7use crate::net::{
8 Resource, ResourceHandler, ResourceLoadResponse, StylesheetHandler, StylesheetLoader,
9};
10use crate::node::{ImageData, NodeFlags, RasterImageData, SpecialElementData, Status, TextBrush};
11use crate::scrolling::ScrollAnimationState;
12use crate::selection::TextSelection;
13use crate::stylo_to_cursor_icon::stylo_to_cursor_icon;
14use crate::traversal::TreeTraverser;
15use crate::url::DocumentUrl;
16use crate::util::ImageType;
17use crate::{
18 DEFAULT_CSS, DocumentConfig, DocumentMutator, DummyHtmlParserProvider, ElementData,
19 EventDriver, HtmlParserProvider, Node, NodeData, NoopEventHandler, StyleThreading,
20 TextNodeData,
21};
22use blitz_traits::devtools::DevtoolSettings;
23use blitz_traits::events::{DomEvent, HitResult, UiEvent};
24use blitz_traits::navigation::{DummyNavigationProvider, NavigationProvider};
25use blitz_traits::net::{AbortSignal, DummyNetProvider, NetProvider, Request};
26use blitz_traits::node_id::NodeId;
27use blitz_traits::shell::{ColorScheme, DummyShellProvider, ShellProvider, Viewport};
28use cursor_icon::CursorIcon;
29use linebender_resource_handle::Blob;
30use markup5ever::{LocalName, local_name};
31use parley::{FontContext, PlainEditorDriver};
32use selectors::{Element, matching::QuirksMode};
33use smallvec::SmallVec;
34use std::any::Any;
35use std::cell::RefCell;
36use std::collections::{BTreeMap, Bound, HashMap, HashSet};
37use std::ops::{Deref, DerefMut};
38use std::rc::Rc;
39use std::str::FromStr;
40use std::sync::atomic::{AtomicUsize, Ordering};
41use std::sync::mpsc::{Receiver, Sender, channel};
42use std::sync::{Arc, Mutex, MutexGuard, OnceLock, RwLockReadGuard, RwLockWriteGuard};
43use std::task::{Context as TaskContext, Waker};
44use style::Atom;
45use style::animation::DocumentAnimationSet;
46use style::attr::{AttrIdentifier, AttrValue};
47use style::data::{ElementData as StyloElementData, ElementStyles};
48use style::media_queries::MediaType;
49use style::properties::ComputedValues;
50use style::properties::style_structs::Font;
51use style::queries::values::PrefersColorScheme;
52use style::selector_parser::ServoElementSnapshot;
53use style::servo::media_features::PointerCapabilities;
54use style::servo_arc::Arc as ServoArc;
55use style::values::GenericAtomIdent;
56use style::values::computed::UserSelect;
57use style::values::computed::ui::CursorKind;
58use style::values::specified::box_::{DisplayInside, DisplayOutside};
59use style::{
60 device::Device,
61 dom::{TDocument, TNode},
62 media_queries::MediaList,
63 selector_parser::SnapshotMap,
64 shared_lock::{SharedRwLock, StylesheetGuards},
65 stylesheets::{AllowImportRules, DocumentStyleSheet, Origin, Stylesheet},
66 stylist::Stylist,
67};
68use style_dom::ElementState;
69use thin_vec::ThinVec;
70use url::Url;
71use web_time::Instant;
72
73#[cfg(feature = "parallel-construct")]
74use thread_local::ThreadLocal;
75
76pub enum DocGuard<'a> {
77 Ref(&'a BaseDocument),
78 RefCell(std::cell::Ref<'a, BaseDocument>),
79 RwLock(RwLockReadGuard<'a, BaseDocument>),
80 Mutex(MutexGuard<'a, BaseDocument>),
81}
82
83impl Deref for DocGuard<'_> {
84 type Target = BaseDocument;
85 #[inline(always)]
86 fn deref(&self) -> &Self::Target {
87 match self {
88 Self::Ref(base_document) => base_document,
89 Self::RefCell(refcell_guard) => refcell_guard,
90 Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
91 Self::Mutex(mutex_guard) => mutex_guard,
92 }
93 }
94}
95
96pub enum DocGuardMut<'a> {
97 Ref(&'a mut BaseDocument),
98 RefCell(std::cell::RefMut<'a, BaseDocument>),
99 RwLock(RwLockWriteGuard<'a, BaseDocument>),
100 Mutex(MutexGuard<'a, BaseDocument>),
101}
102
103impl Deref for DocGuardMut<'_> {
104 type Target = BaseDocument;
105 #[inline(always)]
106 fn deref(&self) -> &Self::Target {
107 match self {
108 Self::Ref(base_document) => base_document,
109 Self::RefCell(refcell_guard) => refcell_guard,
110 Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
111 Self::Mutex(mutex_guard) => mutex_guard,
112 }
113 }
114}
115
116impl DerefMut for DocGuardMut<'_> {
117 #[inline(always)]
118 fn deref_mut(&mut self) -> &mut Self::Target {
119 match self {
120 Self::Ref(base_document) => base_document,
121 Self::RefCell(refcell_guard) => &mut *refcell_guard,
122 Self::RwLock(rw_lock_read_guard) => &mut *rw_lock_read_guard,
123 Self::Mutex(mutex_guard) => &mut *mutex_guard,
124 }
125 }
126}
127
128pub trait Document: Any + 'static {
131 fn inner(&self) -> DocGuard<'_>;
132 fn inner_mut(&mut self) -> DocGuardMut<'_>;
133
134 fn handle_ui_event(&mut self, event: UiEvent) {
136 let mut doc = self.inner_mut();
137 let mut driver = EventDriver::new(&mut *doc, NoopEventHandler);
138 driver.handle_ui_event(event);
139 }
140
141 fn poll(&mut self, task_context: Option<TaskContext>) -> bool {
143 let _ = task_context;
145 false
146 }
147
148 fn id(&self) -> usize {
150 self.inner().id
151 }
152}
153
154pub struct PlainDocument(pub BaseDocument);
155impl Document for PlainDocument {
156 fn inner(&self) -> DocGuard<'_> {
157 DocGuard::Ref(&self.0)
158 }
159 fn inner_mut(&mut self) -> DocGuardMut<'_> {
160 DocGuardMut::Ref(&mut self.0)
161 }
162}
163
164impl Document for BaseDocument {
165 fn inner(&self) -> DocGuard<'_> {
166 DocGuard::Ref(self)
167 }
168 fn inner_mut(&mut self) -> DocGuardMut<'_> {
169 DocGuardMut::Ref(self)
170 }
171}
172
173impl Document for Rc<RefCell<BaseDocument>> {
174 fn inner(&self) -> DocGuard<'_> {
175 DocGuard::RefCell(self.borrow())
176 }
177
178 fn inner_mut(&mut self) -> DocGuardMut<'_> {
179 DocGuardMut::RefCell(self.borrow_mut())
180 }
181}
182
183pub enum DocumentEvent {
184 ResourceLoad(ResourceLoadResponse),
185 NavigateIframe {
188 node_id: NodeId,
189 url: Url,
190 },
191}
192
193pub struct BaseDocument {
194 id: usize,
196
197 pub(crate) url: DocumentUrl,
200 pub(crate) devtool_settings: DevtoolSettings,
202 pub(crate) viewport: Viewport,
204 pub(crate) viewport_scroll: crate::Point<f64>,
206 pub(crate) media_type: MediaType,
208 pub(crate) style_threading: StyleThreading,
210 pub(crate) incremental_layout: bool,
212 pub(crate) subdocument_depth: usize,
215
216 pub(crate) tx: Sender<DocumentEvent>,
218 pub(crate) rx: Option<Receiver<DocumentEvent>>,
220
221 pub(crate) nodes: Box<NodeTree>,
226
227 pub(crate) root_node_id: NodeId,
229
230 pub(crate) stylist: Stylist,
233 pub(crate) animations: DocumentAnimationSet,
234 pub(crate) guard: SharedRwLock,
236 pub(crate) snapshots: SnapshotMap,
238
239 pub(crate) font_ctx: Arc<Mutex<parley::FontContext>>,
242 #[cfg(feature = "parallel-construct")]
243 pub(crate) thread_font_contexts: ThreadLocal<RefCell<Box<FontContext>>>,
245 pub(crate) layout_ctx: parley::LayoutContext<TextBrush>,
247
248 pub(crate) hover_node_id: Option<NodeId>,
252 pub(crate) hover_hit_node_id: Option<NodeId>,
256 pub(crate) hover_node_is_text: bool,
258 pub(crate) last_client_pointer_position: Option<taffy::Point<f32>>,
260 pub(crate) focus_node_id: Option<NodeId>,
262 pub(crate) active_node_id: Option<NodeId>,
264 pub(crate) mousedown_node_id: Option<NodeId>,
266 pub(crate) last_mousedown_time: Option<Instant>,
268 pub(crate) mousedown_position: taffy::Point<f32>,
270 pub(crate) click_count: u16,
272 pub(crate) drag_mode: DragMode,
274 pub(crate) hovered_scrollbar: Option<crate::node::ScrollbarRef>,
276 pub(crate) scrollbar_activity: HashMap<NodeId, Instant>,
279 pub(crate) scroll_animation: ScrollAnimationState,
281
282 pub(crate) text_selection: TextSelection,
284
285 pub(crate) has_active_animations: bool,
288 pub(crate) has_canvas: bool,
290 pub(crate) subdoc_is_animating: bool,
292
293 pub(crate) nodes_to_id: HashMap<String, SmallVec<[NodeId; 1]>>,
297 pub(crate) nodes_to_stylesheet: BTreeMap<NodeId, DocumentStyleSheet>,
299 pub(crate) ua_stylesheets: HashMap<String, DocumentStyleSheet>,
302 pub(crate) controls_to_form: HashMap<NodeId, NodeId>,
304 pub(crate) sub_document_nodes: HashSet<NodeId>,
306 pub(crate) iframe_loads: HashMap<NodeId, crate::iframe::IframeLoad>,
309 pub(crate) changed_nodes: HashSet<NodeId>,
311 pub(crate) deferred_construction_nodes: Vec<ConstructionTask>,
313
314 #[cfg(feature = "custom-widget")]
316 pub(crate) custom_widget_nodes: HashSet<NodeId>,
317 #[cfg(feature = "custom-widget")]
319 pub(crate) pending_resource_deallocations: Vec<anyrender::ResourceId>,
320
321 pub(crate) image_cache: HashMap<String, ImageData>,
324
325 pub(crate) pending_images: HashMap<String, Vec<(NodeId, ImageType)>>,
329
330 pub(crate) pending_style_image_nodes: Vec<NodeId>,
334
335 pub(crate) pending_critical_resources: HashSet<usize>,
338
339 pub net_provider: Arc<dyn NetProvider>,
342 pub navigation_provider: Arc<dyn NavigationProvider>,
345 pub shell_provider: Arc<dyn ShellProvider>,
347 pub html_parser_provider: Arc<dyn HtmlParserProvider>,
349 pub(crate) abort_signal: Option<AbortSignal>,
353}
354
355pub(crate) fn make_device(
356 viewport: &Viewport,
357 media_type: MediaType,
358 font_ctx: Arc<Mutex<FontContext>>,
359) -> Device {
360 let width = viewport.window_size.0 as f32 / viewport.scale();
361 let height = viewport.window_size.1 as f32 / viewport.scale();
362 let viewport_size = euclid::Size2D::new(width, height);
363 let device_size = euclid::Size2D::new(width, height) * viewport.scale();
364 let device_pixel_ratio = euclid::Scale::new(viewport.scale());
365
366 Device::new(
367 media_type,
368 selectors::matching::QuirksMode::NoQuirks,
369 viewport_size,
370 device_size,
371 device_pixel_ratio,
372 Box::new(BlitzFontMetricsProvider { font_ctx }),
373 ComputedValues::initial_values_with_font_override(Font::initial_values()),
374 match viewport.color_scheme {
375 ColorScheme::Light => PrefersColorScheme::Light,
376 ColorScheme::Dark => PrefersColorScheme::Dark,
377 },
378 PointerCapabilities::default(),
379 PointerCapabilities::default(),
380 )
381}
382
383impl BaseDocument {
384 pub fn new(config: DocumentConfig) -> Self {
386 static ID_GENERATOR: AtomicUsize = AtomicUsize::new(1);
387
388 let id = ID_GENERATOR.fetch_add(1, Ordering::SeqCst);
389
390 let font_ctx = config
391 .font_ctx
392 .map(|mut font_ctx| {
393 font_ctx.source_cache.make_shared();
394 font_ctx
396 })
397 .unwrap_or_else(|| {
398 use parley::fontique::{Collection, CollectionOptions, SourceCache};
399 let mut font_ctx = FontContext {
400 source_cache: SourceCache::new_shared(),
401 collection: Collection::new(CollectionOptions {
402 shared: false,
403 system_fonts: cfg!(all(
404 feature = "system-fonts",
405 not(target_arch = "wasm32")
406 )),
407 }),
408 };
409 font_ctx
410 .collection
411 .register_fonts(Blob::new(Arc::new(crate::BULLET_FONT) as _), None);
412 font_ctx
413 });
414 let font_ctx = Arc::new(Mutex::new(font_ctx));
415
416 style_config::set_pref!("layout.grid.enabled", true);
418 style_config::set_pref!("layout.unimplemented", true);
419 style_config::set_pref!("layout.columns.enabled", true);
420 style_config::set_pref!("layout.css.basic-shape-shape.enabled", true);
421 style_config::set_pref!("layout.threads", -1);
422
423 let viewport = config.viewport.unwrap_or_default();
424 let media_type = config.media_type.unwrap_or_else(MediaType::screen);
425 let device = make_device(&viewport, media_type.clone(), font_ctx.clone());
426 let stylist = Stylist::new(device, QuirksMode::NoQuirks);
427 let snapshots = SnapshotMap::new();
428 let nodes = Box::new(NodeTree::new());
429 let guard = SharedRwLock::new();
430 let nodes_to_id = HashMap::new();
431
432 let base_url = config
433 .base_url
434 .and_then(|url| DocumentUrl::from_str(&url).ok())
435 .unwrap_or_default();
436
437 let net_provider = config
438 .net_provider
439 .unwrap_or_else(|| Arc::new(DummyNetProvider));
440 let navigation_provider = config
441 .navigation_provider
442 .unwrap_or_else(|| Arc::new(DummyNavigationProvider));
443 let shell_provider = config
444 .shell_provider
445 .unwrap_or_else(|| Arc::new(DummyShellProvider));
446 let html_parser_provider = config
447 .html_parser_provider
448 .unwrap_or_else(|| Arc::new(DummyHtmlParserProvider));
449
450 let (tx, rx) = channel();
451
452 let mut doc = Self {
453 id,
454 tx,
455 rx: Some(rx),
456
457 guard,
458 nodes,
459 root_node_id: NodeId::default(),
460 stylist,
461 animations: DocumentAnimationSet::default(),
462 snapshots,
463 nodes_to_id,
464 viewport,
465 media_type,
466 style_threading: config.style_threading,
467 incremental_layout: config.incremental.unwrap_or(true),
468 subdocument_depth: config.subdocument_depth,
469 devtool_settings: DevtoolSettings::default(),
470 viewport_scroll: crate::Point::ZERO,
471 url: base_url,
472 ua_stylesheets: HashMap::new(),
473 nodes_to_stylesheet: BTreeMap::new(),
474 font_ctx,
475 #[cfg(feature = "parallel-construct")]
476 thread_font_contexts: ThreadLocal::new(),
477 layout_ctx: parley::LayoutContext::new(),
478
479 hover_node_id: None,
480 hover_hit_node_id: None,
481 hover_node_is_text: false,
482 last_client_pointer_position: None,
483 focus_node_id: None,
484 active_node_id: None,
485 mousedown_node_id: None,
486 has_active_animations: false,
487 subdoc_is_animating: false,
488 has_canvas: false,
489 sub_document_nodes: HashSet::new(),
490 iframe_loads: HashMap::new(),
491
492 #[cfg(feature = "custom-widget")]
493 custom_widget_nodes: HashSet::new(),
494 #[cfg(feature = "custom-widget")]
495 pending_resource_deallocations: Vec::new(),
496
497 changed_nodes: HashSet::new(),
498 deferred_construction_nodes: Vec::new(),
499 image_cache: HashMap::new(),
500 pending_images: HashMap::new(),
501 pending_style_image_nodes: Vec::new(),
502 pending_critical_resources: HashSet::new(),
503 controls_to_form: HashMap::new(),
504 net_provider,
505 navigation_provider,
506 shell_provider,
507 html_parser_provider,
508 abort_signal: config.abort_signal,
509 last_mousedown_time: None,
510 mousedown_position: taffy::Point::ZERO,
511 click_count: 0,
512 drag_mode: DragMode::None,
513 hovered_scrollbar: None,
514 scrollbar_activity: HashMap::new(),
515 scroll_animation: ScrollAnimationState::None,
516 text_selection: TextSelection::default(),
517 };
518
519 doc.root_node_id = doc.create_node(NodeData::Document(Box::default()));
521 doc.root_node_mut().flags.insert(NodeFlags::IS_IN_DOCUMENT);
522
523 match config.ua_stylesheets {
524 Some(stylesheets) => {
525 for ss in &stylesheets {
526 doc.add_user_agent_stylesheet(ss);
527 }
528 }
529 None => doc.add_user_agent_stylesheet(DEFAULT_CSS),
530 }
531
532 let stylo_element_data = StyloElementData {
534 styles: ElementStyles {
535 primary: Some(
536 ComputedValues::initial_values_with_font_override(Font::initial_values())
537 .to_arc(),
538 ),
539 ..Default::default()
540 },
541 ..Default::default()
542 };
543 let stylo_data = doc.root_node_mut().stylo_element_data_mut();
544 *stylo_data.ensure_init_mut() = stylo_element_data;
545
546 doc
547 }
548
549 pub fn set_net_provider(&mut self, net_provider: Arc<dyn NetProvider>) {
551 self.net_provider = net_provider;
552 }
553
554 pub fn set_navigation_provider(&mut self, navigation_provider: Arc<dyn NavigationProvider>) {
556 self.navigation_provider = navigation_provider;
557 }
558
559 pub fn set_shell_provider(&mut self, shell_provider: Arc<dyn ShellProvider>) {
561 self.shell_provider = shell_provider;
562 }
563
564 pub fn set_html_parser_provider(&mut self, html_parser_provider: Arc<dyn HtmlParserProvider>) {
566 self.html_parser_provider = html_parser_provider;
567 }
568
569 pub fn set_base_url(&mut self, url: &str) {
571 self.url = DocumentUrl::from(Url::parse(url).unwrap());
572 }
573
574 pub fn base_url(&self) -> &Url {
576 &self.url
577 }
578
579 pub fn guard(&self) -> &SharedRwLock {
580 &self.guard
581 }
582
583 pub fn tree(&self) -> &NodeTree {
584 &self.nodes
585 }
586
587 pub fn id(&self) -> usize {
588 self.id
589 }
590
591 pub(crate) fn build_request(&self, url: url::Url) -> Request {
594 crate::net::stamped_request(url, self.abort_signal.as_ref())
595 }
596
597 pub fn favicon_url(&self) -> Option<String> {
598 self.tree().iter().find_map(|(_, node)| {
599 let data = &node.data;
600 if !data.is_element_with_tag_name(&local_name!("link")) {
601 return None;
602 }
603 let rel = data.attr(local_name!("rel"))?;
604 if !rel
605 .split_ascii_whitespace()
606 .any(|v| v.eq_ignore_ascii_case("icon"))
607 {
608 return None;
609 }
610 data.attr(local_name!("href")).map(|s| s.to_string())
611 })
612 }
613
614 pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
615 self.nodes.get(node_id)
616 }
617
618 pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
619 self.nodes.get_mut(node_id)
620 }
621
622 pub fn get_focussed_node_id(&self) -> Option<NodeId> {
623 self.focus_node_id
624 .or(self.try_root_element().map(|el| el.id))
625 }
626
627 pub fn mutate<'doc>(&'doc mut self) -> DocumentMutator<'doc> {
628 DocumentMutator::new(self)
629 }
630
631 pub fn handle_dom_event<F: FnMut(DomEvent)>(
632 &mut self,
633 event: &mut DomEvent,
634 dispatch_event: F,
635 ) {
636 handle_dom_event(self, event, dispatch_event)
637 }
638
639 pub fn as_any_mut(&mut self) -> &mut dyn Any {
640 self
641 }
642
643 pub fn label_bound_input_element(&self, label_node_id: NodeId) -> Option<&Node> {
650 let label_element = self.nodes[label_node_id].element_data()?;
651 if let Some(target_element_dom_id) = label_element.attr(local_name!("for")) {
652 TreeTraverser::new(self)
653 .filter_map(|id| {
654 let node = self.get_node(id)?;
655 let element_data = node.element_data()?;
656 if element_data.name.local != local_name!("input") {
657 return None;
658 }
659 let id = element_data.id.as_ref()?;
660 if *id == *target_element_dom_id {
661 Some(node)
662 } else {
663 None
664 }
665 })
666 .next()
667 } else {
668 TreeTraverser::new_with_root(self, label_node_id)
669 .filter_map(|child_id| {
670 let node = self.get_node(child_id)?;
671 let element_data = node.element_data()?;
672 if element_data.name.local == local_name!("input") {
673 Some(node)
674 } else {
675 None
676 }
677 })
678 .next()
679 }
680 }
681
682 pub fn toggle_checkbox(el: &mut ElementData) -> bool {
683 let Some(is_checked) = el.checkbox_input_checked() else {
684 return false;
685 };
686 let checked = !is_checked;
687 el.set_checkbox_input_checked(checked);
688
689 checked
690 }
691
692 pub fn toggle_radio(&mut self, radio_set_name: String, target_radio_id: NodeId) {
693 let radio_ids: Vec<NodeId> = self
694 .nodes
695 .iter()
696 .filter_map(|(i, node)| {
697 let el = node.data.downcast_element()?;
698 (el.attr(local_name!("name")) == Some(&radio_set_name)
699 && el.checkbox_input_checked().is_some())
700 .then_some(i)
701 })
702 .collect();
703
704 for i in radio_ids {
705 let checked = i == target_radio_id;
706 self.snapshot_node_and(i, ElementState::CHECKED, |node| {
707 if let Some(el) = node.element_data_mut() {
708 el.set_checkbox_input_checked(checked);
709 }
710 node.mark_ancestors_dirty();
711 });
712 }
713 }
714
715 pub fn toggle_details_open(&mut self, details_id: NodeId) {
719 use crate::qual_name;
720
721 let node = &self.nodes[details_id];
722 if !node.data.is_element_with_tag_name(&local_name!("details")) {
723 return;
724 }
725 let is_open = node.data.has_attr(local_name!("open"));
726
727 let mut mutator = self.mutate();
731 if is_open {
732 mutator.clear_attribute(details_id, qual_name!("open"));
733 } else {
734 mutator.set_attribute(details_id, qual_name!("open"), "");
735 }
736 drop(mutator);
737
738 self.shell_provider.request_redraw();
739 }
740
741 pub fn set_style_property(&mut self, node_id: NodeId, name: &str, value: &str) {
742 let node = &mut self.nodes[node_id];
743 let did_change = node.element_data_mut().unwrap().set_style_property(
744 name,
745 value,
746 &self.guard,
747 self.url.url_extra_data(),
748 );
749 if did_change {
750 node.mark_style_attr_updated();
751 }
752 }
753
754 pub fn remove_style_property(&mut self, node_id: NodeId, name: &str) {
755 let node = &mut self.nodes[node_id];
756 let did_change = node.element_data_mut().unwrap().remove_style_property(
757 name,
758 &self.guard,
759 self.url.url_extra_data(),
760 );
761 if did_change {
762 node.mark_style_attr_updated();
763 }
764 }
765
766 pub fn sub_document_node_ids(&self) -> Vec<NodeId> {
767 self.sub_document_nodes.iter().copied().collect()
768 }
769
770 pub fn set_sub_document(&mut self, node_id: NodeId, sub_document: Box<dyn Document>) {
771 self.nodes[node_id]
772 .element_data_mut()
773 .unwrap()
774 .set_sub_document(sub_document);
775 self.sub_document_nodes.insert(node_id);
776 }
777
778 pub fn remove_sub_document(&mut self, node_id: NodeId) {
779 self.nodes[node_id]
780 .element_data_mut()
781 .unwrap()
782 .remove_sub_document();
783 self.sub_document_nodes.remove(&node_id);
784 if let Some(load) = self.iframe_loads.remove(&node_id) {
785 load.abort_controller.abort();
786 }
787 }
788
789 pub fn poll_subdocuments(&mut self, waker: Option<&Waker>) -> bool {
795 let mut has_changes = false;
796 for node_id in self.sub_document_nodes.iter().copied() {
797 let Some(sub_doc) = self
798 .nodes
799 .get_mut(node_id)
800 .and_then(|node| node.subdoc_mut())
801 else {
802 continue;
803 };
804 let task_context = waker.map(TaskContext::from_waker);
805 has_changes |= sub_doc.poll(task_context);
806 }
807 has_changes
808 }
809
810 #[cfg(feature = "custom-widget")]
811 pub fn custom_widget_node_ids(&self) -> Vec<NodeId> {
812 self.custom_widget_nodes.iter().copied().collect()
813 }
814
815 #[cfg(feature = "custom-widget")]
816 pub fn take_pending_resource_deallocations(&mut self) -> Vec<anyrender::ResourceId> {
817 std::mem::take(&mut self.pending_resource_deallocations)
818 }
819
820 #[cfg(feature = "custom-widget")]
821 pub fn set_custom_widget(&mut self, node_id: NodeId, widget: Box<dyn crate::Widget>) {
822 self.nodes[node_id]
823 .element_data_mut()
824 .unwrap()
825 .set_custom_widget(widget);
826 self.custom_widget_nodes.insert(node_id);
827 }
828
829 #[cfg(feature = "custom-widget")]
830 pub fn remove_custom_widget(&mut self, node_id: NodeId) {
831 let resources_to_deallocate = self.nodes[node_id]
832 .element_data_mut()
833 .unwrap()
834 .remove_custom_widget();
835 self.pending_resource_deallocations
836 .extend_from_slice(&resources_to_deallocate);
837 self.custom_widget_nodes.remove(&node_id);
838 }
839
840 pub fn root_node(&self) -> &Node {
841 &self.nodes[self.root_node_id]
842 }
843
844 pub fn root_node_mut(&mut self) -> &mut Node {
845 &mut self.nodes[self.root_node_id]
846 }
847
848 pub fn try_root_element(&self) -> Option<&Node> {
849 TDocument::as_node(&self.root_node()).first_element_child()
850 }
851
852 pub fn root_element(&self) -> &Node {
853 TDocument::as_node(&self.root_node())
854 .first_element_child()
855 .unwrap()
856 .as_element()
857 .unwrap()
858 }
859
860 pub fn create_node(&mut self, node_data: NodeData) -> NodeId {
861 let tree_ptr = self.nodes.as_mut() as *mut NodeTree;
862 let guard = self.guard.clone();
863
864 let id = self
865 .nodes
866 .insert_with_key(|id| Node::new(tree_ptr, id, guard, node_data));
867
868 self.changed_nodes.insert(id);
870 id
871 }
872
873 pub(crate) fn remove_node_from_tree(&mut self, node_id: NodeId) -> Option<Node> {
877 self.clear_interaction_state_for_removed_node(node_id);
878 self.nodes.remove(node_id)
879 }
880
881 fn nearest_surviving_element_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
886 let mut current = self.get_node(node_id)?.parent;
887 while let Some(id) = current {
888 let node = self.get_node(id)?;
889 if node.is_element() && node.flags.is_in_document() {
890 return Some(id);
891 }
892 current = node.parent;
893 }
894 None
895 }
896
897 pub(crate) fn clear_interaction_state_for_removed_node(&mut self, node_id: NodeId) {
918 if !self.nodes.contains_key(node_id) {
919 return;
920 }
921
922 if self.hover_node_id == Some(node_id) {
923 self.hover_node_id = self.nearest_surviving_element_ancestor(node_id);
924 self.hover_node_is_text = false;
925 }
926 if self.hover_hit_node_id == Some(node_id) {
927 self.hover_hit_node_id = None;
928 }
929 if self.active_node_id == Some(node_id) {
930 self.active_node_id = self.nearest_surviving_element_ancestor(node_id);
931 }
932 if self.focus_node_id == Some(node_id) {
933 let shell_provider = self.shell_provider.clone();
934 self.nodes[node_id].blur(shell_provider);
935 self.focus_node_id = None;
936 }
937 if self.mousedown_node_id == Some(node_id) {
938 self.mousedown_node_id = None;
939 }
940 if self.text_selection.anchor.node_or_parent == Some(node_id)
941 || self.text_selection.focus.node_or_parent == Some(node_id)
942 {
943 self.text_selection.clear();
944 }
945 if self
946 .hovered_scrollbar
947 .is_some_and(|scrollbar| scrollbar.node_id == node_id)
948 {
949 self.hovered_scrollbar = None;
950 }
951 let drag_references_node = match &self.drag_mode {
952 DragMode::Panning(state) => state.target == node_id,
953 DragMode::ScrollbarDrag(state) => state.scrollbar.node_id == node_id,
954 DragMode::Selecting | DragMode::None => false,
955 };
956 if drag_references_node {
957 self.drag_mode = DragMode::None;
958 }
959 self.scrollbar_activity.remove(&node_id);
960 }
961
962 pub(crate) fn drop_node_ignoring_parent(&mut self, node_id: NodeId) -> Option<Node> {
963 self.drop_node_ignoring_parent_with(node_id, &mut |_| {})
964 }
965
966 pub(crate) fn drop_node_ignoring_parent_with(
969 &mut self,
970 node_id: NodeId,
971 on_drop: &mut dyn FnMut(NodeId),
972 ) -> Option<Node> {
973 let mut node = self.remove_node_from_tree(node_id);
974 if let Some(node) = &mut node {
975 on_drop(node_id);
976 if let Some(before) = node.before() {
977 self.drop_node_ignoring_parent_with(before, on_drop);
978 }
979 if let Some(after) = node.after() {
980 self.drop_node_ignoring_parent_with(after, on_drop);
981 }
982
983 for &child in &node.children {
984 self.drop_node_ignoring_parent_with(child, on_drop);
985 }
986
987 for &anon_id in &node.anonymous_blocks {
990 self.deallocate_anonymous_block(anon_id);
991 }
992 }
993 node
994 }
995
996 pub(crate) fn deallocate_anonymous_block(&mut self, anon_id: NodeId) {
999 if !self.nodes.contains_key(anon_id) {
1002 return;
1003 }
1004
1005 let nested = std::mem::take(&mut self.nodes[anon_id].anonymous_blocks);
1007 for nested_id in nested {
1008 self.deallocate_anonymous_block(nested_id);
1009 }
1010
1011 self.remove_node_from_tree(anon_id);
1012 }
1013
1014 pub fn has_changes(&self) -> bool {
1016 self.changed_nodes.is_empty()
1017 }
1018
1019 pub fn create_text_node(&mut self, text: &str) -> NodeId {
1020 let content = text.to_string();
1021 let data = NodeData::Text(TextNodeData::new(content));
1022 self.create_node(data)
1023 }
1024
1025 pub fn deep_clone_node(&mut self, node_id: NodeId) -> NodeId {
1026 let node = &self.nodes[node_id];
1028 let mut data = node.data.clone();
1029
1030 match &mut data {
1031 NodeData::Element(elem) | NodeData::AnonymousBlock(elem) => {
1032 if let Some(arc) = elem.style_attribute.as_mut() {
1033 let read_guard = self.guard().read();
1034 let block = arc.read_with(&read_guard);
1035 *arc = ServoArc::new(self.guard().wrap(block.clone()));
1036 }
1037 }
1038 _ => {}
1039 }
1040
1041 let children = node.children.clone();
1042
1043 let new_node_id = self.create_node(data);
1045
1046 let new_children: ThinVec<NodeId> = children
1048 .into_iter()
1049 .map(|child_id| self.deep_clone_node(child_id))
1050 .collect();
1051 for &child_id in &new_children {
1052 self.nodes[child_id].parent = Some(new_node_id);
1053 }
1054 self.nodes[new_node_id].children = new_children;
1055
1056 new_node_id
1057 }
1058
1059 pub(crate) fn remove_and_drop_pe(&mut self, node_id: NodeId) -> Option<Node> {
1060 fn remove_pe_ignoring_parent(doc: &mut BaseDocument, node_id: NodeId) -> Option<Node> {
1061 let mut node = doc.remove_node_from_tree(node_id);
1062 if let Some(node) = &mut node {
1063 for &child in &node.children {
1064 remove_pe_ignoring_parent(doc, child);
1065 }
1066 for &anon_id in &node.anonymous_blocks {
1067 doc.deallocate_anonymous_block(anon_id);
1068 }
1069 }
1070 node
1071 }
1072
1073 let node = remove_pe_ignoring_parent(self, node_id);
1074
1075 if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
1077 let parent = &mut self.nodes[parent_id];
1078 parent.children.retain(|id| *id != node_id);
1079 }
1080
1081 node
1082 }
1083
1084 pub(crate) fn resolve_url(&self, raw: &str) -> url::Url {
1085 self.url.resolve_relative(raw).unwrap_or_else(|| {
1086 panic!(
1087 "to be able to resolve {raw} with the base_url: {:?}",
1088 *self.url
1089 )
1090 })
1091 }
1092
1093 pub fn print_tree(&self) {
1094 crate::util::walk_tree(0, self.root_node());
1095 }
1096
1097 pub fn print_subtree(&self, node_id: NodeId) {
1098 crate::util::walk_tree(0, &self.nodes[node_id]);
1099 }
1100
1101 pub fn reload_resource_by_href(&mut self, href_to_reload: &str) {
1102 for &node_id in self.nodes_to_stylesheet.keys() {
1103 let node = &self.nodes[node_id];
1104 let Some(element) = node.element_data() else {
1105 continue;
1106 };
1107
1108 if element.name.local == local_name!("link") {
1109 if let Some(href) = element.attr(local_name!("href")) {
1110 if href == href_to_reload {
1112 let resolved_href = self.resolve_url(href);
1113 self.net_provider.fetch(
1114 self.id(),
1115 self.build_request(resolved_href.clone()),
1116 ResourceHandler::boxed(
1117 self.tx.clone(),
1118 self.id,
1119 Some(node_id),
1120 self.shell_provider.clone(),
1121 StylesheetHandler {
1122 source_url: resolved_href,
1123 guard: self.guard.clone(),
1124 net_provider: self.net_provider.clone(),
1125 abort_signal: self.abort_signal.clone(),
1126 },
1127 ),
1128 );
1129 }
1130 }
1131 }
1132 }
1133 }
1134
1135 pub fn process_style_element(&mut self, target_id: NodeId) {
1136 let css = self.nodes[target_id].text_content();
1137 let css = html_escape::decode_html_entities(&css);
1138 let sheet = self.make_stylesheet(&css, Origin::Author);
1139 self.add_stylesheet_for_node(sheet, target_id);
1140 }
1141
1142 pub fn remove_user_agent_stylesheet(&mut self, contents: &str) {
1143 if let Some(sheet) = self.ua_stylesheets.remove(contents) {
1144 self.stylist.remove_stylesheet(sheet, &self.guard.read());
1145 }
1146 }
1147
1148 pub fn url(&self) -> &url::Url {
1150 &self.url
1151 }
1152
1153 pub fn author_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
1156 self.nodes_to_stylesheet.values()
1157 }
1158
1159 pub fn useragent_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
1161 self.ua_stylesheets.values()
1162 }
1163
1164 pub fn add_user_agent_stylesheet(&mut self, css: &str) {
1165 let sheet = self.make_stylesheet(css, Origin::UserAgent);
1166 self.ua_stylesheets.insert(css.to_string(), sheet.clone());
1167 self.stylist.append_stylesheet(sheet, &self.guard.read());
1168 }
1169
1170 pub fn make_stylesheet(&self, css: impl AsRef<str>, origin: Origin) -> DocumentStyleSheet {
1171 let data = Stylesheet::from_str(
1172 css.as_ref(),
1173 self.url.url_extra_data(),
1174 origin,
1175 ServoArc::new(self.guard.wrap(MediaList::empty())),
1176 self.guard.clone(),
1177 Some(&StylesheetLoader {
1178 tx: self.tx.clone(),
1179 doc_id: self.id,
1180 net_provider: self.net_provider.clone(),
1181 shell_provider: self.shell_provider.clone(),
1182 abort_signal: self.abort_signal.clone(),
1183 }),
1184 None,
1185 QuirksMode::NoQuirks,
1186 AllowImportRules::Yes,
1187 );
1188
1189 DocumentStyleSheet(ServoArc::new(data))
1190 }
1191
1192 pub fn upsert_stylesheet_for_node(&mut self, node_id: NodeId) {
1193 let raw_styles = self.nodes[node_id].text_content();
1194 let sheet = self.make_stylesheet(raw_styles, Origin::Author);
1195 self.add_stylesheet_for_node(sheet, node_id);
1196 }
1197
1198 pub fn add_stylesheet_for_node(&mut self, stylesheet: DocumentStyleSheet, node_id: NodeId) {
1199 let old = self.nodes_to_stylesheet.insert(node_id, stylesheet.clone());
1200
1201 if let Some(old) = old {
1202 self.stylist.remove_stylesheet(old, &self.guard.read())
1203 }
1204
1205 crate::net::fetch_font_face(
1207 self.tx.clone(),
1208 self.id,
1209 Some(node_id),
1210 &stylesheet.0,
1211 &self.net_provider,
1212 &self.shell_provider,
1213 &self.guard.read(),
1214 self.abort_signal.as_ref(),
1215 );
1216
1217 let element = &mut self.nodes[node_id].element_data_mut().unwrap();
1219 element.special_data = SpecialElementData::Stylesheet(stylesheet.clone());
1220
1221 let insertion_point = self
1223 .nodes_to_stylesheet
1224 .range((Bound::Excluded(node_id), Bound::Unbounded))
1225 .next()
1226 .map(|(_, sheet)| sheet);
1227
1228 if let Some(insertion_point) = insertion_point {
1229 self.stylist.insert_stylesheet_before(
1230 stylesheet,
1231 insertion_point.clone(),
1232 &self.guard.read(),
1233 )
1234 } else {
1235 self.stylist
1236 .append_stylesheet(stylesheet, &self.guard.read())
1237 }
1238 }
1239
1240 pub fn handle_messages(&mut self) {
1241 let rx = self.rx.take().unwrap();
1244
1245 while let Ok(msg) = rx.try_recv() {
1246 self.handle_message(msg);
1247 }
1248
1249 self.rx = Some(rx);
1251 }
1252
1253 pub fn handle_message(&mut self, msg: DocumentEvent) {
1254 match msg {
1255 DocumentEvent::ResourceLoad(resource) => self.load_resource(resource),
1256 DocumentEvent::NavigateIframe { node_id, url } => self.navigate_iframe(node_id, url),
1257 }
1258 }
1259
1260 pub fn has_pending_critical_resources(&self) -> bool {
1262 !self.pending_critical_resources.is_empty()
1263 }
1264
1265 pub fn load_resource(&mut self, res: ResourceLoadResponse) {
1266 self.pending_critical_resources.remove(&res.request_id);
1267
1268 let resource = match res.result {
1269 Ok(resource) => resource,
1270 Err(err) => {
1271 if let Some(url) = res.resolved_url.as_ref() {
1272 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1273 #[cfg(feature = "tracing")]
1274 tracing::warn!(
1275 url = url.as_str(),
1276 waiting_nodes = waiting_nodes.len(),
1277 error = err.as_str(),
1278 "Resource load failed"
1279 );
1280 #[cfg(not(feature = "tracing"))]
1281 let _ = (waiting_nodes, err);
1282 } else {
1283 #[cfg(feature = "tracing")]
1284 tracing::warn!(error = err.as_str(), "Resource load failed (no url)");
1285 #[cfg(not(feature = "tracing"))]
1286 let _ = err;
1287 }
1288 return;
1289 }
1290 };
1291
1292 match resource {
1293 Resource::Css(css) => {
1294 let node_id = res.node_id.unwrap();
1295 self.add_stylesheet_for_node(css, node_id);
1296 }
1297 Resource::Image(_kind, width, height, image_data) => {
1298 let image = ImageData::Raster(RasterImageData::new(width, height, image_data));
1300
1301 let Some(url) = res.resolved_url.as_ref() else {
1302 return;
1303 };
1304
1305 self.apply_loaded_image(url, image);
1306 }
1307 #[cfg(feature = "svg")]
1308 Resource::Svg(_kind, svg) => {
1309 let image = ImageData::Svg(svg);
1311
1312 let Some(url) = res.resolved_url.as_ref() else {
1313 return;
1314 };
1315
1316 self.apply_loaded_image(url, image);
1317 }
1318 Resource::DocumentSrc(html) => {
1319 let Some(node_id) = res.node_id else {
1320 return;
1321 };
1322 self.apply_iframe_html(node_id, res.request_id, res.resolved_url, &html);
1323 }
1324 Resource::Font(bytes, overrides) => {
1325 let font = Blob::new(Arc::new(bytes));
1326
1327 let weight_override = overrides.weight.map(parley::fontique::FontWeight::new);
1333 let info_override = parley::fontique::FontInfoOverride {
1334 family_name: overrides.family_name.as_deref(),
1335 weight: weight_override,
1336 style: overrides.style,
1337 ..Default::default()
1338 };
1339
1340 let mut global_font_ctx = self.font_ctx.lock().unwrap();
1342 global_font_ctx
1343 .collection
1344 .register_fonts(font.clone(), Some(info_override));
1345
1346 #[cfg(feature = "parallel-construct")]
1347 {
1348 rayon::broadcast(|_ctx| {
1349 let mut font_ctx = self
1350 .thread_font_contexts
1351 .get_or(|| RefCell::new(Box::new(global_font_ctx.clone())))
1352 .borrow_mut();
1353 font_ctx
1354 .collection
1355 .register_fonts(font.clone(), Some(info_override));
1356 });
1357 }
1358 drop(global_font_ctx);
1359
1360 self.invalidate_inline_contexts();
1362 }
1363 Resource::None => {
1364 }
1366 }
1367 }
1368
1369 fn apply_loaded_image(&mut self, url: &str, image: ImageData) {
1372 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1374
1375 #[cfg(feature = "tracing")]
1376 tracing::info!(
1377 "Image {url} loaded, applying to {} nodes",
1378 waiting_nodes.len()
1379 );
1380
1381 self.image_cache.insert(url.to_string(), image.clone());
1383
1384 for (node_id, image_type) in waiting_nodes {
1386 let Some(node) = self.get_node_mut(node_id) else {
1387 continue;
1388 };
1389
1390 match image_type {
1391 ImageType::Image => {
1392 node.element_data_mut().unwrap().special_data =
1393 SpecialElementData::Image(Box::new(image.clone()));
1394
1395 node.cache_mut().clear();
1397 node.insert_damage(ALL_DAMAGE);
1398 }
1399 ImageType::Background(idx) | ImageType::Mask(idx) => {
1400 let layer_image = node.element_data_mut().and_then(|el| {
1401 let images = match image_type {
1402 ImageType::Background(_) => &mut el.background_images,
1403 ImageType::Mask(_) => &mut el.mask_images,
1404 ImageType::Image => unreachable!(),
1405 };
1406 images.get_mut(idx)
1407 });
1408 if let Some(Some(layer_image)) = layer_image {
1409 layer_image.status = Status::Ok;
1410 layer_image.image = image.clone();
1411 }
1412 }
1413 }
1414 }
1415 }
1416
1417 pub fn snapshot_node(&mut self, node_id: NodeId) {
1421 self.snapshot_node_impl(node_id, true)
1422 }
1423
1424 pub fn snapshot_node_state_only(&mut self, node_id: NodeId) {
1428 self.snapshot_node_impl(node_id, false)
1429 }
1430
1431 fn snapshot_node_impl(&mut self, node_id: NodeId, capture_attrs: bool) {
1432 let node = &mut self.nodes[node_id];
1433
1434 let has_been_styled = node.primary_styles().is_some();
1439 if !has_been_styled {
1440 return;
1441 }
1442
1443 let opaque_node_id = TNode::opaque(&&*node);
1444 node.set_has_snapshot(true);
1445 node.snapshot_handled()
1446 .store(false, std::sync::atomic::Ordering::SeqCst);
1447
1448 let needs_attrs = capture_attrs
1454 && self
1455 .snapshots
1456 .get_mut(&opaque_node_id)
1457 .is_none_or(|snapshot| snapshot.attrs.is_none());
1458
1459 let (attrs, changed_attrs) = if needs_attrs {
1460 let node = &self.nodes[node_id];
1461 let attrs: Option<Vec<_>> = node.attrs().map(|attrs| {
1462 attrs
1463 .iter()
1464 .map(|attr| {
1465 let ident = AttrIdentifier {
1466 local_name: GenericAtomIdent(attr.name.local.clone()),
1467 name: GenericAtomIdent(attr.name.local.clone()),
1468 namespace: GenericAtomIdent(attr.name.ns.clone()),
1469 prefix: None,
1470 };
1471
1472 let value = if attr.name.local == local_name!("id") {
1473 AttrValue::Atom(Atom::from(&*attr.value))
1474 } else if attr.name.local == local_name!("class") {
1475 let classes = attr
1476 .value
1477 .split_ascii_whitespace()
1478 .map(Atom::from)
1479 .collect();
1480 AttrValue::TokenList(OnceLock::from(attr.value.clone()), classes)
1481 } else {
1482 AttrValue::String(attr.value.clone())
1483 };
1484
1485 (ident, value)
1486 })
1487 .collect()
1488 });
1489
1490 let changed_attrs: Vec<_> = attrs
1491 .as_ref()
1492 .map(|attrs| attrs.iter().map(|attr| attr.0.name.clone()).collect())
1493 .unwrap_or_default();
1494
1495 (attrs, changed_attrs)
1496 } else {
1497 (None, Vec::new())
1498 };
1499
1500 if let Some(snapshot) = self.snapshots.get_mut(&opaque_node_id) {
1501 if needs_attrs {
1504 snapshot.attrs = attrs;
1505 snapshot.changed_attrs = changed_attrs;
1506 snapshot.class_changed = true;
1507 snapshot.id_changed = true;
1508 snapshot.other_attributes_changed = true;
1509 }
1510 } else {
1511 self.snapshots.insert(
1512 opaque_node_id,
1513 ServoElementSnapshot {
1514 state: Some(*self.nodes[node_id].element_state()),
1515 attrs,
1516 changed_attrs,
1517 class_changed: needs_attrs,
1518 id_changed: needs_attrs,
1519 other_attributes_changed: needs_attrs,
1520 },
1521 );
1522 }
1523 }
1524
1525 pub fn style_depends_on_state(&self, state: ElementState) -> bool {
1528 self.stylist.iter_origins().any(|(data, _)| {
1529 data.has_state_dependency(state) || data.has_nth_of_state_dependency(state)
1530 })
1531 }
1532
1533 pub fn snapshot_node_and(
1537 &mut self,
1538 node_id: NodeId,
1539 state: ElementState,
1540 cb: impl FnOnce(&mut Node),
1541 ) {
1542 if self.style_depends_on_state(state) {
1543 self.snapshot_node_state_only(node_id);
1544 }
1545 cb(&mut self.nodes[node_id]);
1546 }
1547
1548 pub fn hit(&self, x: f32, y: f32) -> Option<HitResult> {
1550 self.hit_with_scrollbar(x, y).0
1551 }
1552
1553 pub fn element_from_point(&self, x: f32, y: f32) -> Option<NodeId> {
1561 let viewport = self.viewport();
1562 let scale = viewport.scale();
1563 let (viewport_width, viewport_height) = (
1564 viewport.window_size.0 as f32 / scale,
1565 viewport.window_size.1 as f32 / scale,
1566 );
1567 if x < 0.0 || y < 0.0 || x > viewport_width || y > viewport_height {
1568 return None;
1569 }
1570
1571 let Some(hit) = self.hit(x, y) else {
1572 return self.try_root_element().map(|root| root.id);
1575 };
1576
1577 let mut node_id = self.nearest_non_anonymous_ancestor(hit.node_id)?;
1579 loop {
1580 let node = self.get_node(node_id)?;
1581 if node.is_element() {
1582 return Some(node_id);
1583 }
1584 node_id = node.parent?;
1585 }
1586 }
1587
1588 pub fn elements_from_point(&self, x: f32, y: f32) -> Vec<NodeId> {
1596 let mut element_ids = Vec::new();
1597 let mut current = self.element_from_point(x, y);
1598 while let Some(node_id) = current {
1599 element_ids.push(node_id);
1600 current = self
1601 .get_node(node_id)
1602 .and_then(|node| node.parent)
1603 .filter(|parent_id| {
1604 self.get_node(*parent_id)
1605 .is_some_and(|node| node.is_element())
1606 });
1607 }
1608 element_ids
1609 }
1610
1611 pub fn nearest_non_anonymous_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
1624 let mut node = self.get_node(node_id)?;
1628 loop {
1629 let parent = match node.parent {
1630 Some(parent_id) => self.get_node(parent_id)?,
1631 None => return Some(node.id),
1632 };
1633 if !node.is_anonymous() && !parent.is_anonymous() {
1634 return Some(node.id);
1635 }
1636 node = parent;
1637 }
1638 }
1639
1640 pub fn focus_next_node(&mut self) -> Option<NodeId> {
1641 let focussed_node_id = self.get_focussed_node_id()?;
1642 let id = self.next_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
1643 self.set_focus_to(id);
1644 Some(id)
1645 }
1646
1647 pub fn focus_prev_node(&mut self) -> Option<NodeId> {
1649 let focussed_node_id = self.get_focussed_node_id()?;
1650 let id = self.prev_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
1651 self.set_focus_to(id);
1652 Some(id)
1653 }
1654
1655 pub fn clear_focus(&mut self) {
1657 if let Some(id) = self.focus_node_id {
1658 let shell_provider = self.shell_provider.clone();
1659 self.snapshot_node_and(id, ElementState::FOCUS | ElementState::FOCUSRING, |node| {
1660 node.blur(shell_provider)
1661 });
1662 self.focus_node_id = None;
1663 }
1664 }
1665
1666 pub fn set_mousedown_node_id(&mut self, node_id: Option<NodeId>) {
1667 self.mousedown_node_id = node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
1668 }
1669 pub fn set_focus_to(&mut self, focus_node_id: NodeId) -> bool {
1670 let Some(focus_node_id) = self.nearest_non_anonymous_ancestor(focus_node_id) else {
1671 return false;
1672 };
1673 if Some(focus_node_id) == self.focus_node_id {
1674 return false;
1675 }
1676
1677 #[cfg(feature = "tracing")]
1678 tracing::info!("Focussed node {focus_node_id}");
1679
1680 let shell_provider = self.shell_provider.clone();
1681
1682 if let Some(id) = self.focus_node_id {
1684 self.snapshot_node_and(id, ElementState::FOCUS | ElementState::FOCUSRING, |node| {
1685 node.blur(shell_provider.clone())
1686 });
1687 }
1688
1689 self.snapshot_node_and(
1691 focus_node_id,
1692 ElementState::FOCUS | ElementState::FOCUSRING,
1693 |node| node.focus(shell_provider),
1694 );
1695
1696 self.focus_node_id = Some(focus_node_id);
1697
1698 true
1699 }
1700
1701 pub fn active_node(&mut self) -> bool {
1702 let Some(hover_node_id) = self.get_hover_node_id() else {
1703 return false;
1704 };
1705
1706 if let Some(active_node_id) = self.active_node_id {
1707 if active_node_id == hover_node_id {
1708 return true;
1709 }
1710 self.unactive_node();
1711 }
1712
1713 debug_assert!(
1715 self.get_node(hover_node_id)
1716 .is_some_and(|node| !node.is_anonymous()),
1717 "interaction state must reference DOM nodes, not layout-generated nodes"
1718 );
1719 let active_node_id = Some(hover_node_id);
1720
1721 let node_path = self.maybe_node_layout_ancestors(active_node_id);
1722 for &id in node_path.iter() {
1723 self.snapshot_node_and(id, ElementState::ACTIVE, |node| node.active());
1724 }
1725
1726 self.active_node_id = active_node_id;
1727
1728 true
1729 }
1730
1731 pub fn unactive_node(&mut self) -> bool {
1732 let Some(active_node_id) = self.active_node_id.take() else {
1733 return false;
1734 };
1735
1736 let node_path = self.maybe_node_layout_ancestors(Some(active_node_id));
1737 for &id in node_path.iter() {
1738 self.snapshot_node_and(id, ElementState::ACTIVE, |node| node.unactive());
1739 }
1740
1741 true
1742 }
1743
1744 pub fn hovered_scrollbar(&self) -> Option<crate::node::ScrollbarRef> {
1746 self.hovered_scrollbar
1747 }
1748
1749 pub fn scrollbar_drag_target(&self) -> Option<crate::node::ScrollbarRef> {
1751 match &self.drag_mode {
1752 DragMode::ScrollbarDrag(state) => Some(state.scrollbar),
1753 _ => None,
1754 }
1755 }
1756
1757 pub fn scrollbar_opacity(&self, node_id: NodeId) -> f32 {
1762 let interacting = |scrollbar: &crate::node::ScrollbarRef| scrollbar.node_id == node_id;
1763 if self.hovered_scrollbar.as_ref().is_some_and(interacting)
1764 || self
1765 .scrollbar_drag_target()
1766 .as_ref()
1767 .is_some_and(interacting)
1768 {
1769 return 1.0;
1770 }
1771 self.scrollbar_activity.get(&node_id).map_or(0.0, |last| {
1772 crate::node::scrollbar::opacity_at(last.elapsed())
1773 })
1774 }
1775
1776 pub(crate) fn show_scrollbars(&mut self, node_id: NodeId) {
1779 if cfg!(feature = "scrollbars") {
1780 self.scrollbar_activity.insert(node_id, Instant::now());
1781 }
1782 }
1783
1784 fn scrollbars_animating(&self) -> bool {
1787 use crate::node::scrollbar::{FADE_DELAY, FADE_DURATION};
1788 self.scrollbar_activity
1789 .values()
1790 .any(|last| last.elapsed() < FADE_DELAY + FADE_DURATION)
1791 }
1792
1793 pub(crate) fn hit_with_scrollbar(
1797 &self,
1798 x: f32,
1799 y: f32,
1800 ) -> (Option<HitResult>, Option<crate::node::ScrollbarRef>) {
1801 if TDocument::as_node(&self.root_node())
1802 .first_element_child()
1803 .is_none()
1804 {
1805 #[cfg(feature = "tracing")]
1806 tracing::warn!("No DOM - not resolving hit test");
1807 return (None, None);
1808 }
1809 let mut scrollbar = None;
1810 let hit = self
1811 .root_element()
1812 .hit_inner(x, y, self.viewport().scale_f64(), &mut scrollbar);
1813 (hit, scrollbar)
1814 }
1815
1816 pub fn set_hover_to(&mut self, x: f32, y: f32) -> bool {
1817 self.last_client_pointer_position = Some(taffy::Point {
1821 x: x - self.viewport_scroll.x as f32,
1822 y: y - self.viewport_scroll.y as f32,
1823 });
1824
1825 let (hit, hovered_scrollbar) = self.hit_with_scrollbar(x, y);
1826 let hovered_scrollbar =
1829 hovered_scrollbar.filter(|scrollbar| self.scrollbar_opacity(scrollbar.node_id) > 0.0);
1830 let scrollbar_changed = hovered_scrollbar != self.hovered_scrollbar;
1834 if scrollbar_changed {
1835 for scrollbar in [self.hovered_scrollbar, hovered_scrollbar]
1838 .into_iter()
1839 .flatten()
1840 {
1841 self.show_scrollbars(scrollbar.node_id);
1842 }
1843 }
1844 self.hovered_scrollbar = hovered_scrollbar;
1845
1846 let hit_node_id = hit.map(|hit| hit.node_id);
1851 let hover_node_id = hit_node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
1852 let new_is_text = hit.map(|hit| hit.is_text).unwrap_or(false);
1853
1854 let hit_changed =
1855 hit_node_id != self.hover_hit_node_id || new_is_text != self.hover_node_is_text;
1856 self.hover_hit_node_id = hit_node_id;
1857 self.hover_node_is_text = new_is_text;
1858
1859 if hover_node_id == self.hover_node_id {
1861 if hit_changed {
1862 self.shell_provider.set_cursor(self.get_cursor());
1866 }
1867 return scrollbar_changed;
1868 }
1869
1870 let old_node_path = self.maybe_node_layout_ancestors(self.hover_node_id);
1871 let new_node_path = self.maybe_node_layout_ancestors(hover_node_id);
1872 let same_count = old_node_path
1873 .iter()
1874 .zip(&new_node_path)
1875 .take_while(|(o, n)| o == n)
1876 .count();
1877 for &id in old_node_path.iter().skip(same_count) {
1878 self.snapshot_node_and(id, ElementState::HOVER, |node| node.unhover());
1879 }
1880 for &id in new_node_path.iter().skip(same_count) {
1881 self.snapshot_node_and(id, ElementState::HOVER, |node| node.hover());
1882 }
1883
1884 self.hover_node_id = hover_node_id;
1885
1886 self.shell_provider.set_cursor(self.get_cursor());
1888
1889 self.shell_provider.request_redraw();
1891
1892 true
1893 }
1894
1895 pub fn clear_hover(&mut self) -> bool {
1896 self.last_client_pointer_position = None;
1899 self.hover_hit_node_id = None;
1900
1901 let Some(hover_node_id) = self.hover_node_id else {
1902 return false;
1903 };
1904
1905 let old_node_path = self.maybe_node_layout_ancestors(Some(hover_node_id));
1906 for &id in old_node_path.iter() {
1907 self.snapshot_node_and(id, ElementState::HOVER, |node| node.unhover());
1908 }
1909
1910 self.hover_node_id = None;
1911 self.hover_node_is_text = false;
1912
1913 self.shell_provider.set_cursor(self.get_cursor());
1915
1916 self.shell_provider.request_redraw();
1918
1919 true
1920 }
1921
1922 pub fn refresh_hover(&mut self) -> bool {
1928 let Some(pos) = self.last_client_pointer_position else {
1929 return false;
1930 };
1931 let x = pos.x + self.viewport_scroll.x as f32;
1932 let y = pos.y + self.viewport_scroll.y as f32;
1933 self.set_hover_to(x, y)
1934 }
1935
1936 pub fn get_hover_node_id(&self) -> Option<NodeId> {
1937 self.hover_node_id
1938 }
1939
1940 pub fn get_mousedown_node_id(&self) -> Option<NodeId> {
1941 self.mousedown_node_id
1942 }
1943
1944 pub fn set_viewport(&mut self, viewport: Viewport) {
1945 let scale_has_changed = viewport.scale_f64() != self.viewport.scale_f64();
1946 self.viewport = viewport;
1947 self.set_stylist_device(make_device(
1948 &self.viewport,
1949 self.media_type.clone(),
1950 self.font_ctx.clone(),
1951 ));
1952 self.scroll_viewport_by(0.0, 0.0); if scale_has_changed {
1955 self.invalidate_inline_contexts();
1956 self.shell_provider.request_redraw();
1957 }
1958 }
1959
1960 pub fn media_type(&self) -> &MediaType {
1962 &self.media_type
1963 }
1964
1965 pub fn set_media_type(&mut self, media_type: MediaType) {
1968 if self.media_type == media_type {
1969 return;
1970 }
1971 self.media_type = media_type;
1972 self.set_stylist_device(make_device(
1973 &self.viewport,
1974 self.media_type.clone(),
1975 self.font_ctx.clone(),
1976 ));
1977 }
1978
1979 pub fn viewport(&self) -> &Viewport {
1980 &self.viewport
1981 }
1982
1983 pub fn viewport_mut(&mut self) -> ViewportMut<'_> {
1984 ViewportMut::new(self)
1985 }
1986
1987 pub fn zoom_by(&mut self, increment: f32) {
1988 *self.viewport.zoom_mut() += increment;
1989 self.set_viewport(self.viewport.clone());
1990 }
1991
1992 pub fn zoom_to(&mut self, zoom: f32) {
1993 *self.viewport.zoom_mut() = zoom;
1994 self.set_viewport(self.viewport.clone());
1995 }
1996
1997 pub fn get_viewport(&self) -> Viewport {
1998 self.viewport.clone()
1999 }
2000
2001 pub fn incremental_layout(&self) -> bool {
2003 self.incremental_layout
2004 }
2005
2006 pub fn set_incremental_layout(&mut self, enabled: bool) {
2008 self.incremental_layout = enabled;
2009 }
2010
2011 pub fn devtools(&self) -> &DevtoolSettings {
2012 &self.devtool_settings
2013 }
2014
2015 pub fn devtools_mut(&mut self) -> &mut DevtoolSettings {
2016 &mut self.devtool_settings
2017 }
2018
2019 pub fn subdoc(&self, node_id: NodeId) -> Option<&dyn Document> {
2020 self.get_node(node_id)
2021 .and_then(|node| node.element_data())
2022 .and_then(|el| el.sub_doc_data())
2023 }
2024
2025 pub fn subdoc_mut(&mut self, node_id: NodeId) -> Option<&mut dyn Document> {
2026 self.get_node_mut(node_id)
2027 .and_then(|node| node.element_data_mut())
2028 .and_then(|el| el.sub_doc_data_mut())
2029 }
2030
2031 pub fn is_animating(&self) -> bool {
2032 #[cfg(feature = "custom-widget")]
2033 let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
2034 self.nodes[node_id]
2035 .element_data()
2036 .and_then(|el| el.custom_widget_data())
2037 .is_some_and(|data| data.widget.requires_redraw())
2038 });
2039 #[cfg(not(feature = "custom-widget"))]
2040 let custom_widget_is_animating = false;
2041
2042 self.has_canvas
2043 | self.has_active_animations
2044 | self.subdoc_is_animating
2045 | custom_widget_is_animating
2046 | (self.scroll_animation != ScrollAnimationState::None)
2047 | self.scrollbars_animating()
2048 }
2049
2050 pub fn set_stylist_device(&mut self, device: Device) {
2052 let root_styles = self
2058 .try_root_element()
2059 .and_then(|root| root.primary_styles());
2060 if let Some(root_style) = root_styles.as_deref() {
2061 device.set_root_style(root_style);
2062
2063 let font = root_style.get_font();
2064 let font_size = font.clone_font_size().computed_size();
2065 device.set_root_font_size(root_style.effective_zoom.unzoom(font_size.px()));
2066
2067 let line_height = device
2068 .calc_line_height(font, root_style.writing_mode, None)
2069 .0;
2070 device.set_root_line_height(root_style.effective_zoom.unzoom(line_height.px()));
2071 }
2072 drop(root_styles);
2073
2074 let origins = {
2075 let guard = &self.guard;
2076 let guards = StylesheetGuards {
2077 author: &guard.read(),
2078 ua_or_user: &guard.read(),
2079 };
2080 self.stylist.set_device(device, &guards)
2081 };
2082 self.stylist.force_stylesheet_origins_dirty(origins);
2083 }
2084
2085 pub fn stylist_device(&mut self) -> &Device {
2086 self.stylist.device()
2087 }
2088
2089 pub fn get_cursor(&self) -> Option<CursorIcon> {
2090 let node_id = self
2095 .hover_hit_node_id
2096 .filter(|&id| self.nodes.contains_key(id))
2097 .or(self.get_hover_node_id())?;
2098 let node = &self.nodes[node_id];
2099
2100 if let Some(subdoc) = node.subdoc().map(|doc| doc.inner()) {
2101 return subdoc.get_cursor();
2102 }
2103
2104 let style = node.primary_styles()?;
2105 let user_select = style.clone_user_select();
2106 let keyword = style.clone_cursor().keyword;
2107
2108 if keyword != CursorKind::Auto {
2110 return stylo_to_cursor_icon(keyword);
2111 }
2112
2113 if node
2115 .element_data()
2116 .is_some_and(|e| e.text_input_data().is_some())
2117 {
2118 return Some(CursorIcon::Text);
2119 }
2120
2121 let mut maybe_node = Some(node);
2123 while let Some(node) = maybe_node {
2124 if node.is_link() {
2125 return Some(CursorIcon::Pointer);
2126 }
2127
2128 maybe_node = node.layout_parent.get().map(|node_id| node.with(node_id));
2129 }
2130
2131 if self.hover_node_is_text {
2133 return Some(match user_select {
2134 UserSelect::Text | UserSelect::All | UserSelect::Auto => CursorIcon::Text,
2135 UserSelect::None => CursorIcon::Default,
2136 });
2137 }
2138
2139 Some(CursorIcon::Default)
2141 }
2142
2143 pub fn viewport_scroll(&self) -> crate::Point<f64> {
2144 self.viewport_scroll
2145 }
2146
2147 pub fn set_viewport_scroll(&mut self, scroll: crate::Point<f64>) {
2148 self.viewport_scroll = scroll;
2149 }
2150
2151 pub fn get_fragment_target(&self, fragment: &str) -> Option<NodeId> {
2156 if let Some(node_id) = self.get_element_by_id(fragment) {
2157 return Some(node_id);
2158 }
2159
2160 self.nodes.iter().find_map(|(id, node)| {
2162 let el = node.element_data()?;
2163 (el.name.local == local_name!("a") && el.attr(local_name!("name")) == Some(fragment))
2164 .then_some(id)
2165 })
2166 }
2167
2168 pub fn get_client_bounding_rect(&self, node_id: NodeId) -> Option<BoundingRect> {
2170 if let Some(rects) = self.inline_fragment_rects(node_id) {
2173 let x0 = rects.iter().map(|r| r.x).fold(f64::INFINITY, f64::min);
2174 let y0 = rects.iter().map(|r| r.y).fold(f64::INFINITY, f64::min);
2175 let x1 = rects
2176 .iter()
2177 .map(|r| r.x + r.width)
2178 .fold(f64::NEG_INFINITY, f64::max);
2179 let y1 = rects
2180 .iter()
2181 .map(|r| r.y + r.height)
2182 .fold(f64::NEG_INFINITY, f64::max);
2183 return match rects.is_empty() {
2184 true => None,
2185 false => Some(BoundingRect {
2186 x: x0,
2187 y: y0,
2188 width: x1 - x0,
2189 height: y1 - y0,
2190 }),
2191 };
2192 }
2193
2194 let node = self.get_node(node_id)?;
2195 let pos = node.absolute_position(0.0, 0.0);
2196
2197 Some(BoundingRect {
2198 x: pos.x as f64 - self.viewport_scroll.x,
2199 y: pos.y as f64 - self.viewport_scroll.y,
2200 width: node.unrounded_layout().size.width as f64,
2201 height: node.unrounded_layout().size.height as f64,
2202 })
2203 }
2204
2205 pub fn node_client_rects(&self, node_id: NodeId) -> Vec<BoundingRect> {
2210 match self.inline_fragment_rects(node_id) {
2211 Some(rects) => rects,
2212 None => self.get_client_bounding_rect(node_id).into_iter().collect(),
2213 }
2214 }
2215
2216 pub fn inline_fragment_rects(&self, node_id: NodeId) -> Option<Vec<BoundingRect>> {
2220 use parley::PositionedLayoutItem;
2221
2222 let node = self.get_node(node_id)?;
2223
2224 if !node.is_element() || node.flags.is_inline_root() {
2227 return None;
2228 }
2229 let display = node.primary_styles()?.clone_display();
2230 if !(display.outside() == DisplayOutside::Inline && display.inside() == DisplayInside::Flow)
2231 {
2232 return None;
2233 }
2234
2235 let inline_root = node.inline_root_ancestor()?;
2236 let inline_layout = inline_root.element_data()?.inline_layout_data.as_ref()?;
2237 let layout = &inline_layout.layout;
2238 let scale = layout.scale() as f64;
2239
2240 let is_in_target = |mut id: NodeId| -> bool {
2243 loop {
2244 if id == node_id {
2245 return true;
2246 }
2247 if id == inline_root.id {
2248 return false;
2249 }
2250 match self.get_node(id).and_then(|n| n.parent) {
2251 Some(parent) => id = parent,
2252 None => return false,
2253 }
2254 }
2255 };
2256
2257 let root_layout = inline_root.final_layout();
2259 let root_pos = inline_root.absolute_position(0.0, 0.0);
2260 let origin_x = root_pos.x as f64
2261 + (root_layout.padding.left + root_layout.border.left) as f64
2262 - self.viewport_scroll.x;
2263 let origin_y = root_pos.y as f64
2264 + (root_layout.padding.top + root_layout.border.top) as f64
2265 - self.viewport_scroll.y;
2266
2267 let mut rects: Vec<BoundingRect> = Vec::new();
2268 for line in layout.lines() {
2269 let line_metrics = line.metrics();
2270 let mut line_rect: Option<(f64, f64, f64, f64)> = None;
2272 let mut add = |x0: f64, y0: f64, x1: f64, y1: f64| {
2273 line_rect = Some(match line_rect {
2274 Some((lx0, ly0, lx1, ly1)) => {
2275 (lx0.min(x0), ly0.min(y0), lx1.max(x1), ly1.max(y1))
2276 }
2277 None => (x0, y0, x1, y1),
2278 });
2279 };
2280
2281 for item in line.items() {
2282 match item {
2283 PositionedLayoutItem::GlyphRun(glyph_run) => {
2284 if !is_in_target(glyph_run.style().brush.id) {
2285 continue;
2286 }
2287 let x0 = glyph_run.offset() as f64;
2288 let x1 = x0 + glyph_run.advance() as f64;
2289 let y0 = line_metrics.block_min_coord as f64;
2295 let y1 = line_metrics.block_max_coord as f64;
2296 add(x0, y0, x1, y1);
2297 }
2298 PositionedLayoutItem::InlineBox(inline_box) => {
2299 if !is_in_target(NodeId::from_u64(inline_box.id)) {
2300 continue;
2301 }
2302 let x0 = inline_box.x as f64;
2303 let y0 = inline_box.y as f64;
2304 add(
2305 x0,
2306 y0,
2307 x0 + inline_box.width as f64,
2308 y0 + inline_box.height as f64,
2309 );
2310 }
2311 }
2312 }
2313
2314 if let Some((x0, y0, x1, y1)) = line_rect {
2315 rects.push(BoundingRect {
2316 x: origin_x + x0 / scale,
2317 y: origin_y + y0 / scale,
2318 width: (x1 - x0) / scale,
2319 height: (y1 - y0) / scale,
2320 });
2321 }
2322 }
2323
2324 Some(rects)
2325 }
2326
2327 pub fn find_element_by_tag_name(&self, tag: &LocalName) -> Option<&Node> {
2331 let root = self.try_root_element()?;
2332 if root.data.is_element_with_tag_name(tag) {
2333 return Some(root);
2334 }
2335 root.children
2336 .iter()
2337 .copied()
2338 .find(|child_id| {
2339 self.get_node(*child_id)
2340 .is_some_and(|child| child.data.is_element_with_tag_name(tag))
2341 })
2342 .or_else(|| {
2343 TreeTraverser::new(self)
2344 .find(|node_id| self.nodes[*node_id].data.is_element_with_tag_name(tag))
2345 })
2346 .map(|node_id| &self.nodes[node_id])
2347 }
2348
2349 pub fn find_body_node(&self) -> Option<&Node> {
2351 self.find_element_by_tag_name(&local_name!("body"))
2352 }
2353
2354 pub fn find_head_node(&self) -> Option<&Node> {
2356 self.find_element_by_tag_name(&local_name!("head"))
2357 }
2358
2359 pub fn find_title_node(&self) -> Option<&Node> {
2361 self.find_element_by_tag_name(&local_name!("title"))
2362 }
2363
2364 pub fn with_text_input(
2365 &mut self,
2366 node_id: NodeId,
2367 cb: impl FnOnce(PlainEditorDriver<TextBrush>),
2368 ) {
2369 let Some(node) = self.nodes.get_mut(node_id) else {
2370 return;
2371 };
2372
2373 if let Some(text_input) = node
2374 .element_data_mut()
2375 .and_then(|el| el.text_input_data_mut())
2376 {
2377 let mut font_ctx = self.font_ctx.lock().unwrap();
2378 let layout_ctx = &mut self.layout_ctx;
2379 let driver = text_input.editor.driver(&mut font_ctx, layout_ctx);
2380 cb(driver)
2381 }
2382 }
2383
2384 pub(crate) fn clamp_text_input_scroll(&mut self, node_id: NodeId) {
2387 let Some(node) = self.nodes.get_mut(node_id) else {
2388 return;
2389 };
2390
2391 let content_box_width = node.final_layout().content_box_width();
2392 let content_box_height = node.final_layout().content_box_height();
2393
2394 if let Some(text_input) = node
2395 .element_data_mut()
2396 .and_then(|el| el.text_input_data_mut())
2397 {
2398 text_input.clamp_scroll_offset(content_box_width, content_box_height);
2399 }
2400 }
2401
2402 pub(crate) fn compute_has_canvas(&self) -> bool {
2403 TreeTraverser::new(self).any(|node_id| {
2404 let node = &self.nodes[node_id];
2405 let Some(element) = node.element_data() else {
2406 return false;
2407 };
2408 if element.name.local == local_name!("canvas") && element.has_attr(local_name!("src")) {
2409 return true;
2410 }
2411
2412 false
2413 })
2414 }
2415
2416 pub fn find_text_position(&self, x: f32, y: f32) -> Option<(NodeId, usize)> {
2422 let hit = self.hit(x, y)?;
2423 let hit_node = self.get_node(hit.node_id)?;
2424 let inline_root = hit_node.inline_root_ancestor()?;
2425 let byte_offset = inline_root.text_offset_at_point(hit.x, hit.y)?;
2426 Some((inline_root.id, byte_offset))
2427 }
2428
2429 pub fn set_text_selection(
2431 &mut self,
2432 anchor_node: NodeId,
2433 anchor_offset: usize,
2434 focus_node: NodeId,
2435 focus_offset: usize,
2436 ) {
2437 self.text_selection =
2438 TextSelection::new(anchor_node, anchor_offset, focus_node, focus_offset);
2439
2440 if let (Some(parent), Some(idx)) = self.anonymous_block_location(anchor_node) {
2442 self.text_selection
2443 .anchor
2444 .set_anonymous(parent, idx, anchor_offset);
2445 }
2446 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
2447 self.text_selection
2448 .focus
2449 .set_anonymous(parent, idx, focus_offset);
2450 }
2451 }
2452
2453 fn anonymous_block_location(&self, node_id: NodeId) -> (Option<NodeId>, Option<usize>) {
2456 let Some(node) = self.get_node(node_id) else {
2457 return (None, None);
2458 };
2459
2460 if !node.is_anonymous() {
2461 return (None, None);
2462 }
2463
2464 let Some(parent_id) = node.parent else {
2465 return (None, None);
2466 };
2467
2468 let Some(parent) = self.get_node(parent_id) else {
2469 return (Some(parent_id), None);
2470 };
2471
2472 let layout_children = parent.layout_children.borrow();
2473 let Some(children) = layout_children.as_ref() else {
2474 return (Some(parent_id), None);
2475 };
2476
2477 let mut anon_index = 0;
2479 for &child_id in children.iter() {
2480 if child_id == node_id {
2481 return (Some(parent_id), Some(anon_index));
2482 }
2483 if self.get_node(child_id).is_some_and(|n| n.is_anonymous()) {
2484 anon_index += 1;
2485 }
2486 }
2487
2488 (Some(parent_id), None)
2489 }
2490
2491 pub fn clear_text_selection(&mut self) {
2493 self.text_selection.clear();
2494 }
2495
2496 pub fn update_selection_focus(&mut self, focus_node: NodeId, focus_offset: usize) {
2498 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
2500 self.text_selection
2501 .focus
2502 .set_anonymous(parent, idx, focus_offset);
2503 } else {
2504 self.text_selection.set_focus(focus_node, focus_offset);
2505 }
2506 }
2507
2508 pub fn extend_text_selection_to_point(&mut self, x: f32, y: f32) -> bool {
2511 if !self.text_selection.anchor.is_some() {
2512 return false;
2513 }
2514
2515 if let Some((node, offset)) = self.find_text_position(x, y) {
2516 self.update_selection_focus(node, offset);
2517 self.shell_provider.request_redraw();
2518 true
2519 } else {
2520 false
2521 }
2522 }
2523
2524 fn find_anonymous_block_by_index(
2526 &self,
2527 parent_id: NodeId,
2528 target_index: usize,
2529 ) -> Option<NodeId> {
2530 let parent = self.get_node(parent_id)?;
2531 let layout_children = parent.layout_children.borrow();
2532 let children = layout_children.as_ref()?;
2533
2534 children
2535 .iter()
2536 .filter(|&&child_id| self.get_node(child_id).is_some_and(|n| n.is_anonymous()))
2537 .nth(target_index)
2538 .copied()
2539 }
2540
2541 pub fn has_text_selection(&self) -> bool {
2543 self.text_selection.is_active()
2544 }
2545
2546 pub fn get_selected_text(&self) -> Option<String> {
2548 let ranges = self.get_text_selection_ranges();
2549 if ranges.is_empty() {
2550 return None;
2551 }
2552
2553 let mut result = String::new();
2554 for (node_id, start, end) in &ranges {
2555 let node = self.get_node(*node_id)?;
2556 let element_data = node.element_data()?;
2557 let inline_layout = element_data.inline_layout_data.as_ref()?;
2558
2559 if *end > inline_layout.text.len() {
2560 continue;
2561 }
2562
2563 if !result.is_empty() {
2564 result.push(' ');
2565 }
2566 result.push_str(&inline_layout.text[*start..*end]);
2567 }
2568
2569 if result.is_empty() {
2570 None
2571 } else {
2572 Some(result)
2573 }
2574 }
2575
2576 pub fn get_text_selection_ranges(&self) -> Vec<(NodeId, usize, usize)> {
2579 let lookup = |parent_id, idx| self.find_anonymous_block_by_index(parent_id, idx);
2580
2581 let anchor_node = match self.text_selection.anchor.resolve_node_id(lookup) {
2582 Some(id) => id,
2583 None => return Vec::new(),
2584 };
2585 let focus_node = match self.text_selection.focus.resolve_node_id(lookup) {
2586 Some(id) => id,
2587 None => return Vec::new(),
2588 };
2589
2590 let node_is_in_doc = |node_id: NodeId| {
2593 self.nodes
2594 .get(node_id)
2595 .is_some_and(|node| node.flags.is_in_document())
2596 };
2597 if !node_is_in_doc(anchor_node) || !node_is_in_doc(focus_node) {
2598 return Vec::new();
2599 }
2600
2601 if anchor_node == focus_node {
2603 let start = self
2604 .text_selection
2605 .anchor
2606 .offset
2607 .min(self.text_selection.focus.offset);
2608 let end = self
2609 .text_selection
2610 .anchor
2611 .offset
2612 .max(self.text_selection.focus.offset);
2613
2614 if start == end {
2615 return Vec::new();
2616 }
2617 return vec![(anchor_node, start, end)];
2618 }
2619
2620 let inline_roots = self.collect_inline_roots_in_range(anchor_node, focus_node);
2622 if inline_roots.is_empty() {
2623 return Vec::new();
2624 }
2625
2626 let first_in_roots = inline_roots[0];
2629
2630 let (first_node, first_offset, last_node, last_offset) =
2631 if first_in_roots == anchor_node || (first_in_roots != focus_node) {
2632 (
2634 anchor_node,
2635 self.text_selection.anchor.offset,
2636 focus_node,
2637 self.text_selection.focus.offset,
2638 )
2639 } else {
2640 (
2642 focus_node,
2643 self.text_selection.focus.offset,
2644 anchor_node,
2645 self.text_selection.anchor.offset,
2646 )
2647 };
2648
2649 let mut ranges = Vec::with_capacity(inline_roots.len());
2650
2651 for &node_id in &inline_roots {
2652 let Some(node) = self.get_node(node_id) else {
2653 continue;
2654 };
2655 let Some(element_data) = node.element_data() else {
2656 continue;
2657 };
2658 let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
2659 continue;
2660 };
2661
2662 let text_len = inline_layout.text.len();
2663
2664 if node_id == first_node && node_id == last_node {
2665 let start = first_offset.min(last_offset);
2666 let end = first_offset.max(last_offset);
2667 if start < end && end <= text_len {
2668 ranges.push((node_id, start, end));
2669 }
2670 } else if node_id == first_node {
2671 if first_offset < text_len {
2672 ranges.push((node_id, first_offset, text_len));
2673 }
2674 } else if node_id == last_node {
2675 if last_offset > 0 && last_offset <= text_len {
2676 ranges.push((node_id, 0, last_offset));
2677 }
2678 } else if text_len > 0 {
2679 ranges.push((node_id, 0, text_len));
2680 }
2681 }
2682
2683 ranges
2684 }
2685}
2686
2687#[derive(Debug, Clone, Copy, PartialEq)]
2688pub struct BoundingRect {
2689 pub x: f64,
2690 pub y: f64,
2691 pub width: f64,
2692 pub height: f64,
2693}
2694
2695impl AsRef<BaseDocument> for BaseDocument {
2696 fn as_ref(&self) -> &BaseDocument {
2697 self
2698 }
2699}
2700
2701impl AsMut<BaseDocument> for BaseDocument {
2702 fn as_mut(&mut self) -> &mut BaseDocument {
2703 self
2704 }
2705}
2706
2707#[cfg(test)]
2708mod hover_state_tests {
2709 use super::*;
2710 use crate::{Attribute, qual_name};
2711 use blitz_traits::shell::ColorScheme;
2712
2713 fn make_doc() -> (BaseDocument, NodeId) {
2720 let mut doc = BaseDocument::new(DocumentConfig {
2721 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
2722 ..Default::default()
2723 });
2724 let root_id = doc.root_node().id;
2725 let style = |value: &str| Attribute {
2726 name: qual_name!("style"),
2727 value: value.to_string(),
2728 };
2729
2730 let mut mutator = doc.mutate();
2731 let html = mutator.create_element(qual_name!("html"), vec![]);
2732 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
2733 let container = mutator.create_element(qual_name!("div"), vec![style("width:300px")]);
2734 let text = mutator.create_text_node("some text");
2735 let block = mutator.create_element(qual_name!("div"), vec![style("height:50px")]);
2736 mutator.append_children(container, &[text, block]);
2737 mutator.append_children(body, &[container]);
2738 mutator.append_children(html, &[body]);
2739 mutator.append_children(root_id, &[html]);
2740 drop(mutator);
2741
2742 doc.resolve(0.0);
2743 (doc, container)
2744 }
2745
2746 fn text_has_size(doc: &BaseDocument, container: NodeId) -> bool {
2750 doc.nodes[container].final_layout().size.height > 50.0
2751 }
2752
2753 #[test]
2759 fn hovering_text_in_anonymous_block_reports_text_cursor() {
2760 let (mut doc, container) = make_doc();
2761 if !text_has_size(&doc, container) {
2762 eprintln!("skipping: no usable font (text measures 0x0)");
2763 return;
2764 }
2765
2766 doc.set_hover_to(5.0, 8.0);
2767 assert!(doc.hover_node_is_text, "expected a text hit");
2768 let hit_id = doc.hover_hit_node_id.expect("expected a hit node");
2769 assert!(
2770 doc.nodes[hit_id].is_anonymous(),
2771 "expected the hit node to be the anonymous inline root"
2772 );
2773 assert_eq!(
2774 doc.get_hover_node_id(),
2775 Some(container),
2776 "expected the stored hover target to be the containing element"
2777 );
2778 assert_eq!(doc.get_cursor(), Some(CursorIcon::Text));
2779 }
2780
2781 #[test]
2784 fn hovering_anonymous_block_whitespace_reports_default_cursor() {
2785 let (mut doc, container) = make_doc();
2786 if !text_has_size(&doc, container) {
2787 eprintln!("skipping: no usable font (text measures 0x0)");
2788 return;
2789 }
2790
2791 doc.set_hover_to(250.0, 8.0);
2792 assert!(!doc.hover_node_is_text);
2793 assert_eq!(doc.get_hover_node_id(), Some(container));
2794 assert_eq!(doc.get_cursor(), Some(CursorIcon::Default));
2795 }
2796}
2797
2798#[cfg(test)]
2799mod hover_invalidation_tests {
2800 use super::*;
2801 use crate::{Attribute, QualName, qual_name};
2802 use blitz_traits::shell::ColorScheme;
2803
2804 fn make_doc() -> (BaseDocument, NodeId, NodeId) {
2807 let mut doc = BaseDocument::new(DocumentConfig {
2808 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
2809 ..Default::default()
2810 });
2811 doc.add_user_agent_stylesheet(
2812 "div:hover { background-color: rgb(255, 0, 0); } div:hover span { color: rgb(0, 255, 0); }",
2813 );
2814 let root_id = doc.root_node().id;
2815 let style = |value: &str| Attribute {
2816 name: qual_name!("style"),
2817 value: value.to_string(),
2818 };
2819
2820 let mut mutator = doc.mutate();
2821 let html = mutator.create_element(qual_name!("html"), vec![]);
2822 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
2823 let div =
2824 mutator.create_element(qual_name!("div"), vec![style("width:300px;height:100px")]);
2825 let span = mutator.create_element(qual_name!("span"), vec![]);
2826 let text = mutator.create_text_node("some text");
2827 mutator.append_children(span, &[text]);
2828 mutator.append_children(div, &[span]);
2829 mutator.append_children(body, &[div]);
2830 mutator.append_children(html, &[body]);
2831 mutator.append_children(root_id, &[html]);
2832 drop(mutator);
2833
2834 doc.resolve(0.0);
2835 (doc, div, span)
2836 }
2837
2838 fn bg_color(doc: &BaseDocument, id: NodeId) -> String {
2839 format!(
2840 "{:?}",
2841 doc.nodes[id]
2842 .primary_styles()
2843 .unwrap()
2844 .get_background()
2845 .background_color
2846 )
2847 }
2848
2849 fn text_color(doc: &BaseDocument, id: NodeId) -> String {
2850 format!(
2851 "{:?}",
2852 doc.nodes[id].primary_styles().unwrap().clone_color()
2853 )
2854 }
2855
2856 #[test]
2857 fn hover_styles_apply_and_clear() {
2858 let (mut doc, div, span) = make_doc();
2859 let initial_bg = bg_color(&doc, div);
2860 let initial_color = text_color(&doc, span);
2861
2862 doc.set_hover_to(10.0, 10.0);
2864 assert!(doc.nodes[div].is_hovered());
2865 doc.resolve(0.0);
2866 let hovered_bg = bg_color(&doc, div);
2867 let hovered_color = text_color(&doc, span);
2868 assert_ne!(initial_bg, hovered_bg, "hover should change div background");
2869 assert_ne!(
2870 initial_color, hovered_color,
2871 "hover should change span color"
2872 );
2873
2874 doc.set_hover_to(10.0, 200.0);
2876 assert!(!doc.nodes[div].is_hovered());
2877 doc.resolve(0.0);
2878 assert_eq!(
2879 bg_color(&doc, div),
2880 initial_bg,
2881 "unhover should restore div background"
2882 );
2883 assert_eq!(
2884 text_color(&doc, span),
2885 initial_color,
2886 "unhover should restore span color"
2887 );
2888 }
2889
2890 #[test]
2893 fn ancestor_hover_with_link_state_updates_descendant() {
2894 let mut doc = BaseDocument::new(DocumentConfig {
2895 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
2896 ..Default::default()
2897 });
2898 doc.add_user_agent_stylesheet(
2899 ".promo:link:hover .headline, .promo:visited:hover .headline { color: rgb(184, 0, 0); text-decoration-line: underline; }",
2900 );
2901 let root_id = doc.root_node().id;
2902 let attr = |name: QualName, value: &str| Attribute {
2903 name,
2904 value: value.to_string(),
2905 };
2906
2907 let mut mutator = doc.mutate();
2908 let html = mutator.create_element(qual_name!("html"), vec![]);
2909 let body = mutator.create_element(
2910 qual_name!("body"),
2911 vec![attr(qual_name!("style"), "margin:0")],
2912 );
2913 let a = mutator.create_element(
2914 qual_name!("a"),
2915 vec![
2916 attr(qual_name!("href"), "https://example.com"),
2917 attr(qual_name!("class"), "promo"),
2918 attr(
2919 qual_name!("style"),
2920 "display:block;width:300px;height:100px",
2921 ),
2922 ],
2923 );
2924 let p = mutator.create_element(qual_name!("p"), vec![]);
2925 let span = mutator.create_element(
2926 qual_name!("span"),
2927 vec![attr(qual_name!("class"), "headline")],
2928 );
2929 let text = mutator.create_text_node("Headline text");
2930 mutator.append_children(span, &[text]);
2931 mutator.append_children(p, &[span]);
2932 mutator.append_children(a, &[p]);
2933 mutator.append_children(body, &[a]);
2934 mutator.append_children(html, &[body]);
2935 mutator.append_children(root_id, &[html]);
2936 drop(mutator);
2937
2938 doc.resolve(0.0);
2939 let initial_color = text_color(&doc, span);
2940
2941 doc.set_hover_to(10.0, 10.0);
2942 assert!(doc.nodes[a].is_hovered());
2943 doc.resolve(0.0);
2944 let hovered_color = text_color(&doc, span);
2945 assert_ne!(
2946 initial_color, hovered_color,
2947 "hovering the anchor should change the headline color"
2948 );
2949
2950 doc.set_hover_to(10.0, 200.0);
2951 assert!(!doc.nodes[a].is_hovered());
2952 doc.resolve(0.0);
2953 assert_eq!(
2954 text_color(&doc, span),
2955 initial_color,
2956 "unhovering the anchor should restore the headline color"
2957 );
2958 }
2959
2960 #[test]
2962 fn checkbox_toggle_updates_checked_styles() {
2963 let mut doc = BaseDocument::new(DocumentConfig {
2964 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
2965 ..Default::default()
2966 });
2967 doc.add_user_agent_stylesheet(
2968 "input:checked { color: rgb(184, 0, 0); } input:checked + label { color: rgb(0, 184, 0); }",
2969 );
2970 let root_id = doc.root_node().id;
2971 let attr = |name: QualName, value: &str| Attribute {
2972 name,
2973 value: value.to_string(),
2974 };
2975
2976 let mut mutator = doc.mutate();
2977 let html = mutator.create_element(qual_name!("html"), vec![]);
2978 let body = mutator.create_element(
2979 qual_name!("body"),
2980 vec![attr(qual_name!("style"), "margin:0")],
2981 );
2982 let input = mutator.create_element(
2983 qual_name!("input"),
2984 vec![attr(qual_name!("type"), "checkbox")],
2985 );
2986 let label = mutator.create_element(qual_name!("label"), vec![]);
2987 let text = mutator.create_text_node("label text");
2988 mutator.append_children(label, &[text]);
2989 mutator.append_children(body, &[input, label]);
2990 mutator.append_children(html, &[body]);
2991 mutator.append_children(root_id, &[html]);
2992 drop(mutator);
2993
2994 doc.resolve(0.0);
2995 let initial_input_color = text_color(&doc, input);
2996 let initial_label_color = text_color(&doc, label);
2997
2998 doc.snapshot_node_and(input, ElementState::CHECKED, |node| {
3000 if let Some(el) = node.element_data_mut() {
3001 BaseDocument::toggle_checkbox(el);
3002 }
3003 node.mark_ancestors_dirty();
3004 });
3005 doc.resolve(0.0);
3006 assert_ne!(
3007 text_color(&doc, input),
3008 initial_input_color,
3009 "checking should change the input color"
3010 );
3011 assert_ne!(
3012 text_color(&doc, label),
3013 initial_label_color,
3014 "checking should change the sibling label color"
3015 );
3016
3017 doc.snapshot_node_and(input, ElementState::CHECKED, |node| {
3019 if let Some(el) = node.element_data_mut() {
3020 BaseDocument::toggle_checkbox(el);
3021 }
3022 node.mark_ancestors_dirty();
3023 });
3024 doc.resolve(0.0);
3025 assert_eq!(
3026 text_color(&doc, input),
3027 initial_input_color,
3028 "unchecking should restore the input color"
3029 );
3030 assert_eq!(
3031 text_color(&doc, label),
3032 initial_label_color,
3033 "unchecking should restore the sibling label color"
3034 );
3035 }
3036
3037 #[test]
3041 fn hover_updates_existing_pseudo_element_style() {
3042 let mut doc = BaseDocument::new(DocumentConfig {
3043 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3044 ..Default::default()
3045 });
3046 doc.add_user_agent_stylesheet(
3047 "div::before { content: \"x\"; color: rgb(1, 2, 3); } \
3048 div:hover::before { color: rgb(0, 0, 255); }",
3049 );
3050 let root_id = doc.root_node().id;
3051 let style = |value: &str| Attribute {
3052 name: qual_name!("style"),
3053 value: value.to_string(),
3054 };
3055
3056 let mut mutator = doc.mutate();
3057 let html = mutator.create_element(qual_name!("html"), vec![]);
3058 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
3059 let div = mutator.create_element(
3060 qual_name!("div"),
3061 vec![style("display:block;width:300px;height:100px")],
3062 );
3063 let text = mutator.create_text_node("some text");
3064 mutator.append_children(div, &[text]);
3065 mutator.append_children(body, &[div]);
3066 mutator.append_children(html, &[body]);
3067 mutator.append_children(root_id, &[html]);
3068 drop(mutator);
3069
3070 doc.resolve(0.0);
3071 let before = doc.nodes[div].before().expect("::before node should exist");
3072 let initial_color = text_color(&doc, before);
3073
3074 doc.set_hover_to(10.0, 10.0);
3075 assert!(doc.nodes[div].is_hovered());
3076 doc.resolve(0.0);
3077 assert_ne!(
3078 text_color(&doc, before),
3079 initial_color,
3080 "hover should change the ::before color"
3081 );
3082
3083 doc.set_hover_to(10.0, 200.0);
3084 assert!(!doc.nodes[div].is_hovered());
3085 doc.resolve(0.0);
3086 assert_eq!(
3087 text_color(&doc, before),
3088 initial_color,
3089 "unhover should restore the ::before color"
3090 );
3091 }
3092
3093 #[test]
3097 fn hover_updates_background_image_layers() {
3098 let mut doc = BaseDocument::new(DocumentConfig {
3099 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3100 ..Default::default()
3101 });
3102 doc.add_user_agent_stylesheet(
3103 "div { background-image: url(\"https://example.com/a.png\"); } \
3104 div:hover { background-image: url(\"https://example.com/b.png\"); }",
3105 );
3106 let root_id = doc.root_node().id;
3107 let style = |value: &str| Attribute {
3108 name: qual_name!("style"),
3109 value: value.to_string(),
3110 };
3111
3112 let mut mutator = doc.mutate();
3113 let html = mutator.create_element(qual_name!("html"), vec![]);
3114 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
3115 let div = mutator.create_element(
3116 qual_name!("div"),
3117 vec![style("display:block;width:300px;height:100px")],
3118 );
3119 mutator.append_children(body, &[div]);
3120 mutator.append_children(html, &[body]);
3121 mutator.append_children(root_id, &[html]);
3122 drop(mutator);
3123
3124 let background_image_url = |doc: &BaseDocument, id: NodeId| -> Option<String> {
3125 let elem = doc.nodes[id].data.downcast_element().unwrap();
3126 elem.background_images
3127 .first()
3128 .and_then(|img| img.as_ref())
3129 .map(|img| img.url.as_str().to_string())
3130 };
3131
3132 doc.resolve(0.0);
3133 assert_eq!(
3134 background_image_url(&doc, div).as_deref(),
3135 Some("https://example.com/a.png"),
3136 "initial resolve should flush the background image"
3137 );
3138
3139 doc.set_hover_to(10.0, 10.0);
3140 assert!(doc.nodes[div].is_hovered());
3141 doc.resolve(0.0);
3142 assert_eq!(
3143 background_image_url(&doc, div).as_deref(),
3144 Some("https://example.com/b.png"),
3145 "hover should flush the changed background image"
3146 );
3147
3148 doc.set_hover_to(10.0, 200.0);
3149 assert!(!doc.nodes[div].is_hovered());
3150 doc.resolve(0.0);
3151 assert_eq!(
3152 background_image_url(&doc, div).as_deref(),
3153 Some("https://example.com/a.png"),
3154 "unhover should flush the restored background image"
3155 );
3156 }
3157}
3158
3159#[cfg(test)]
3160mod font_face_override_tests {
3161 use super::*;
3162 use crate::net::{FontFaceOverrides, Resource, ResourceLoadResponse};
3163
3164 #[test]
3180 fn font_face_overrides_alias_family_name() {
3181 const ALIAS: &str = "AliasedFamily";
3182
3183 let mut document = BaseDocument::new(DocumentConfig::default());
3184
3185 {
3187 let mut ctx = document.font_ctx.lock().unwrap();
3188 assert!(
3189 ctx.collection.family_id(ALIAS).is_none(),
3190 "alias must not exist before registration",
3191 );
3192 }
3193
3194 let response = ResourceLoadResponse {
3199 request_id: 0,
3200 node_id: None,
3201 resolved_url: Some(String::from("test://aliased-family")),
3202 result: Ok(Resource::Font(
3203 blitz_traits::net::Bytes::from_static(crate::BULLET_FONT),
3204 FontFaceOverrides {
3205 family_name: Some(String::from(ALIAS)),
3206 weight: Some(800.0),
3207 style: Some(parley::fontique::FontStyle::Italic),
3208 },
3209 )),
3210 };
3211 document.load_resource(response);
3212
3213 let mut ctx = document.font_ctx.lock().unwrap();
3216 let family_id = ctx
3217 .collection
3218 .family_id(ALIAS)
3219 .expect("CSS-declared family name should be registered as a family alias");
3220 let resolved_name = ctx
3221 .collection
3222 .family_name(family_id)
3223 .expect("family id should resolve back to a name");
3224 assert_eq!(
3225 resolved_name, ALIAS,
3226 "registered family should report the CSS-declared name, \
3227 not the font file's internal `name` table entry",
3228 );
3229 }
3230}