1use crate::NodeTree;
2use crate::events::{DragMode, ScrollAnimationState, 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::{
11 ImageData, NodeFlags, RasterImageData, SpecialElementData, Status, TextBrush, TextGranularity,
12};
13use crate::selection::TextSelection;
14use crate::stylo_to_cursor_icon::stylo_to_cursor_icon;
15use crate::traversal::TreeTraverser;
16use crate::url::DocumentUrl;
17use crate::util::ImageType;
18use crate::{
19 DEFAULT_CSS, DocumentConfig, DocumentMutator, DummyHtmlParserProvider, ElementData,
20 EventDriver, HtmlParserProvider, Node, NodeData, NoopEventHandler, StyleThreading,
21 TextNodeData,
22};
23use blitz_traits::devtools::DevtoolSettings;
24use blitz_traits::events::{BlitzScrollEvent, DomEvent, DomEventData, HitResult, UiEvent};
25use blitz_traits::navigation::{DummyNavigationProvider, NavigationProvider};
26use blitz_traits::net::{AbortSignal, DummyNetProvider, NetProvider, Request};
27use blitz_traits::node_id::NodeId;
28use blitz_traits::shell::{ColorScheme, DummyShellProvider, ShellProvider, Viewport};
29use cursor_icon::CursorIcon;
30use linebender_resource_handle::Blob;
31use markup5ever::{local_name, ns};
32use parley::{FontContext, PlainEditorDriver};
33use selectors::{Element, matching::QuirksMode};
34use smallvec::SmallVec;
35use std::any::Any;
36use std::cell::RefCell;
37use std::collections::{BTreeMap, Bound, HashMap, HashSet};
38use std::ops::{Deref, DerefMut};
39use std::rc::Rc;
40use std::str::FromStr;
41use std::sync::atomic::{AtomicUsize, Ordering};
42use std::sync::mpsc::{Receiver, Sender, channel};
43use std::sync::{Arc, Mutex, MutexGuard, OnceLock, RwLockReadGuard, RwLockWriteGuard};
44use std::task::{Context as TaskContext, Waker};
45use style::Atom;
46use style::animation::{AnimationState, DocumentAnimationSet};
47use style::attr::{AttrIdentifier, AttrValue};
48use style::data::{ElementData as StyloElementData, ElementStyles};
49use style::media_queries::MediaType;
50use style::properties::ComputedValues;
51use style::properties::style_structs::Font;
52use style::queries::values::PrefersColorScheme;
53use style::selector_parser::ServoElementSnapshot;
54use style::servo::media_features::PointerCapabilities;
55use style::servo_arc::Arc as ServoArc;
56use style::values::GenericAtomIdent;
57use style::values::computed::ui::CursorKind;
58use style::values::computed::{Overflow, UserSelect};
59use style::values::specified::box_::{DisplayInside, DisplayOutside};
60use style::{
61 device::Device,
62 dom::{TDocument, TNode},
63 media_queries::MediaList,
64 selector_parser::SnapshotMap,
65 shared_lock::{SharedRwLock, StylesheetGuards},
66 stylesheets::{AllowImportRules, DocumentStyleSheet, Origin, Stylesheet},
67 stylist::Stylist,
68};
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
193#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
195pub enum AnimationPacing {
196 Idle,
197 Caret,
198 SlowCss,
199 Interactive,
200}
201
202pub struct BaseDocument {
203 id: usize,
205
206 pub(crate) url: DocumentUrl,
209 pub(crate) devtool_settings: DevtoolSettings,
211 pub(crate) viewport: Viewport,
213 pub(crate) viewport_scroll: crate::Point<f64>,
215 pub(crate) media_type: MediaType,
217 pub(crate) style_threading: StyleThreading,
219 pub(crate) incremental_layout: bool,
221 pub(crate) subdocument_depth: usize,
224
225 pub(crate) tx: Sender<DocumentEvent>,
227 pub(crate) rx: Option<Receiver<DocumentEvent>>,
229
230 pub(crate) nodes: Box<NodeTree>,
235
236 pub(crate) root_node_id: NodeId,
238
239 pub(crate) hoisted_fixed_parents: HashMap<NodeId, NodeId>,
249
250 pub(crate) hoisted_clip_hosts: Vec<NodeId>,
256
257 pub(crate) stylist: Stylist,
260 pub(crate) animations: DocumentAnimationSet,
261 pub(crate) last_resolve_animation_time: f64,
268 pub(crate) guard: SharedRwLock,
270 pub(crate) snapshots: SnapshotMap,
272
273 pub(crate) font_ctx: Arc<Mutex<parley::FontContext>>,
276 #[cfg(feature = "parallel-construct")]
277 pub(crate) thread_font_contexts: ThreadLocal<RefCell<Box<FontContext>>>,
279 pub(crate) layout_ctx: parley::LayoutContext<TextBrush>,
281
282 pub(crate) hover_node_id: Option<NodeId>,
286 pub(crate) hover_hit_node_id: Option<NodeId>,
290 pub(crate) hover_node_is_text: bool,
292 pub(crate) last_client_pointer_position: Option<taffy::Point<f32>>,
294 pub(crate) semantic_hover_node_id: Option<NodeId>,
300 pub(crate) focus_node_id: Option<NodeId>,
302 pub(crate) active_node_id: Option<NodeId>,
304 pub(crate) mousedown_node_id: Option<NodeId>,
306 pub(crate) last_mousedown_time: Option<Instant>,
308 pub(crate) mousedown_position: taffy::Point<f32>,
310 pub(crate) click_count: u16,
312 pub(crate) drag_mode: DragMode,
314 pub(crate) hovered_scrollbar: Option<crate::node::ScrollbarRef>,
316 pub(crate) scrollbar_activity: HashMap<NodeId, Instant>,
319 pub(crate) scroll_animation: ScrollAnimationState,
321
322 pub(crate) text_selection: TextSelection,
324
325 pub(crate) has_active_animations: bool,
328 pub(crate) has_canvas: bool,
330 pub(crate) subdoc_animation_pacing: AnimationPacing,
332
333 pub(crate) nodes_to_id: HashMap<String, SmallVec<[NodeId; 1]>>,
337 pub(crate) nodes_to_stylesheet: BTreeMap<NodeId, DocumentStyleSheet>,
339 pub(crate) ua_stylesheets: HashMap<String, DocumentStyleSheet>,
342 pub(crate) controls_to_form: HashMap<NodeId, NodeId>,
344 pub(crate) sub_document_nodes: HashSet<NodeId>,
346 pub(crate) iframe_loads: HashMap<NodeId, crate::iframe::IframeLoad>,
349 pub(crate) deferred_construction_nodes: Vec<ConstructionTask>,
351 pub(crate) paint_damage: crate::paint_damage::PaintDamageTracker,
357
358 #[cfg(feature = "custom-widget")]
360 pub(crate) custom_widget_nodes: HashSet<NodeId>,
361 #[cfg(feature = "custom-widget")]
363 pub(crate) pending_resource_deallocations: Vec<anyrender::ResourceId>,
364
365 #[cfg(feature = "shadow-dom")]
367 pub(crate) custom_element_registry: crate::node::CustomElementRegistry,
368 #[cfg(feature = "shadow-dom")]
370 pub(crate) shadow_host_nodes: HashSet<NodeId>,
371 #[cfg(feature = "shadow-dom")]
373 pub(crate) custom_element_nodes: HashSet<NodeId>,
374
375 pub(crate) image_cache: HashMap<String, ImageData>,
378
379 pub(crate) pending_images: HashMap<String, Vec<(NodeId, ImageType)>>,
383
384 pub(crate) pending_critical_resources: HashSet<usize>,
387
388 pub net_provider: Arc<dyn NetProvider>,
391 pub navigation_provider: Arc<dyn NavigationProvider>,
394 pub shell_provider: Arc<dyn ShellProvider>,
396 pub html_parser_provider: Arc<dyn HtmlParserProvider>,
398 pub(crate) abort_signal: Option<AbortSignal>,
402}
403
404pub(crate) fn make_device(
405 viewport: &Viewport,
406 media_type: MediaType,
407 font_ctx: Arc<Mutex<FontContext>>,
408) -> Device {
409 let width = viewport.window_size.0 as f32 / viewport.scale();
410 let height = viewport.window_size.1 as f32 / viewport.scale();
411 let viewport_size = euclid::Size2D::new(width, height);
412 let device_size = euclid::Size2D::new(width, height) * viewport.scale();
413 let device_pixel_ratio = euclid::Scale::new(viewport.scale());
414
415 Device::new(
416 media_type,
417 selectors::matching::QuirksMode::NoQuirks,
418 viewport_size,
419 device_size,
420 device_pixel_ratio,
421 Box::new(BlitzFontMetricsProvider { font_ctx }),
422 ComputedValues::initial_values_with_font_override(Font::initial_values()),
423 match viewport.color_scheme {
424 ColorScheme::Light => PrefersColorScheme::Light,
425 ColorScheme::Dark => PrefersColorScheme::Dark,
426 },
427 PointerCapabilities::default(),
428 PointerCapabilities::default(),
429 )
430}
431
432fn incremental_layout_default() -> bool {
447 !matches!(
448 std::env::var("BLITZ_INCREMENTAL").ok().as_deref(),
449 Some("0" | "false" | "off")
450 )
451}
452
453impl BaseDocument {
454 pub fn new(config: DocumentConfig) -> Self {
456 static ID_GENERATOR: AtomicUsize = AtomicUsize::new(1);
457
458 let id = ID_GENERATOR.fetch_add(1, Ordering::SeqCst);
459
460 let font_ctx = config
461 .font_ctx
462 .map(|mut font_ctx| {
463 font_ctx.source_cache.make_shared();
464 font_ctx
466 })
467 .unwrap_or_else(|| {
468 use parley::fontique::{Collection, CollectionOptions, SourceCache};
469 let mut font_ctx = FontContext {
470 source_cache: SourceCache::new_shared(),
471 collection: Collection::new(CollectionOptions {
472 shared: false,
473 system_fonts: cfg!(all(
474 feature = "system-fonts",
475 not(target_arch = "wasm32")
476 )),
477 }),
478 };
479 font_ctx
480 .collection
481 .register_fonts(Blob::new(Arc::new(crate::BULLET_FONT) as _), None);
482 font_ctx
483 });
484 let font_ctx = Arc::new(Mutex::new(font_ctx));
485
486 style_config::set_pref!("layout.grid.enabled", true);
488 style_config::set_pref!("layout.unimplemented", true);
489 style_config::set_pref!("layout.columns.enabled", true);
490 style_config::set_pref!("layout.css.basic-shape-shape.enabled", true);
491 style_config::set_pref!("layout.threads", -1);
492
493 let viewport = config.viewport.unwrap_or_default();
494 let media_type = config.media_type.unwrap_or_else(MediaType::screen);
495 let device = make_device(&viewport, media_type.clone(), font_ctx.clone());
496 let stylist = Stylist::new(device, QuirksMode::NoQuirks);
497 let snapshots = SnapshotMap::new();
498 let nodes = Box::new(NodeTree::new());
499 let guard = SharedRwLock::new();
500 let nodes_to_id = HashMap::new();
501
502 let base_url = config
503 .base_url
504 .and_then(|url| DocumentUrl::from_str(&url).ok())
505 .unwrap_or_default();
506
507 let net_provider = config
508 .net_provider
509 .unwrap_or_else(|| Arc::new(DummyNetProvider));
510 let navigation_provider = config
511 .navigation_provider
512 .unwrap_or_else(|| Arc::new(DummyNavigationProvider));
513 let shell_provider = config
514 .shell_provider
515 .unwrap_or_else(|| Arc::new(DummyShellProvider));
516 let html_parser_provider = config
517 .html_parser_provider
518 .unwrap_or_else(|| Arc::new(DummyHtmlParserProvider));
519
520 let (tx, rx) = channel();
521
522 let mut doc = Self {
523 hoisted_fixed_parents: HashMap::new(),
524 hoisted_clip_hosts: Vec::new(),
525 id,
526 tx,
527 rx: Some(rx),
528
529 guard,
530 nodes,
531 root_node_id: NodeId::default(),
532 stylist,
533 animations: DocumentAnimationSet::default(),
534 last_resolve_animation_time: 0.0,
535 snapshots,
536 nodes_to_id,
537 viewport,
538 media_type,
539 style_threading: config.style_threading,
540 incremental_layout: config
541 .incremental
542 .unwrap_or_else(incremental_layout_default),
543 subdocument_depth: config.subdocument_depth,
544 devtool_settings: DevtoolSettings::default(),
545 viewport_scroll: crate::Point::ZERO,
546 url: base_url,
547 ua_stylesheets: HashMap::new(),
548 nodes_to_stylesheet: BTreeMap::new(),
549 font_ctx,
550 #[cfg(feature = "parallel-construct")]
551 thread_font_contexts: ThreadLocal::new(),
552 layout_ctx: parley::LayoutContext::new(),
553
554 hover_node_id: None,
555 hover_hit_node_id: None,
556 hover_node_is_text: false,
557 last_client_pointer_position: None,
558 semantic_hover_node_id: None,
559 focus_node_id: None,
560 active_node_id: None,
561 mousedown_node_id: None,
562 has_active_animations: false,
563 subdoc_animation_pacing: AnimationPacing::Idle,
564 has_canvas: false,
565 sub_document_nodes: HashSet::new(),
566 iframe_loads: HashMap::new(),
567
568 #[cfg(feature = "custom-widget")]
569 custom_widget_nodes: HashSet::new(),
570 #[cfg(feature = "custom-widget")]
571 pending_resource_deallocations: Vec::new(),
572
573 #[cfg(feature = "shadow-dom")]
574 custom_element_registry: crate::node::CustomElementRegistry::new(),
575 #[cfg(feature = "shadow-dom")]
576 shadow_host_nodes: HashSet::new(),
577 #[cfg(feature = "shadow-dom")]
578 custom_element_nodes: HashSet::new(),
579
580 deferred_construction_nodes: Vec::new(),
581 paint_damage: Default::default(),
582 image_cache: HashMap::new(),
583 pending_images: HashMap::new(),
584 pending_critical_resources: HashSet::new(),
585 controls_to_form: HashMap::new(),
586 net_provider,
587 navigation_provider,
588 shell_provider,
589 html_parser_provider,
590 abort_signal: config.abort_signal,
591 last_mousedown_time: None,
592 mousedown_position: taffy::Point::ZERO,
593 click_count: 0,
594 drag_mode: DragMode::None,
595 hovered_scrollbar: None,
596 scrollbar_activity: HashMap::new(),
597 scroll_animation: ScrollAnimationState::None,
598 text_selection: TextSelection::default(),
599 };
600
601 doc.root_node_id = doc.create_node(NodeData::Document(Box::default()));
603 doc.root_node_mut().flags.insert(NodeFlags::IS_IN_DOCUMENT);
604
605 match config.ua_stylesheets {
606 Some(stylesheets) => {
607 for ss in &stylesheets {
608 doc.add_user_agent_stylesheet(ss);
609 }
610 }
611 None => doc.add_user_agent_stylesheet(DEFAULT_CSS),
612 }
613
614 let stylo_element_data = StyloElementData {
616 styles: ElementStyles {
617 primary: Some(
618 ComputedValues::initial_values_with_font_override(Font::initial_values())
619 .to_arc(),
620 ),
621 ..Default::default()
622 },
623 ..Default::default()
624 };
625 let stylo_data = doc.root_node_mut().stylo_element_data_mut();
626 *stylo_data.ensure_init_mut() = stylo_element_data;
627
628 doc
629 }
630
631 pub fn set_net_provider(&mut self, net_provider: Arc<dyn NetProvider>) {
633 self.net_provider = net_provider;
634 }
635
636 pub fn set_navigation_provider(&mut self, navigation_provider: Arc<dyn NavigationProvider>) {
638 self.navigation_provider = navigation_provider;
639 }
640
641 pub fn set_shell_provider(&mut self, shell_provider: Arc<dyn ShellProvider>) {
643 self.shell_provider = shell_provider;
644 }
645
646 pub fn set_html_parser_provider(&mut self, html_parser_provider: Arc<dyn HtmlParserProvider>) {
648 self.html_parser_provider = html_parser_provider;
649 }
650
651 pub fn set_base_url(&mut self, url: &str) {
653 self.url = DocumentUrl::from(Url::parse(url).unwrap());
654 }
655
656 pub fn guard(&self) -> &SharedRwLock {
657 &self.guard
658 }
659
660 pub fn tree(&self) -> &NodeTree {
661 &self.nodes
662 }
663
664 pub fn id(&self) -> usize {
665 self.id
666 }
667
668 pub(crate) fn build_request(&self, url: url::Url) -> Request {
671 crate::net::stamped_request(url, self.abort_signal.as_ref())
672 }
673
674 pub fn favicon_url(&self) -> Option<String> {
675 self.tree().iter().find_map(|(_, node)| {
676 let data = &node.data;
677 if !data.is_element_with_tag_name(&local_name!("link")) {
678 return None;
679 }
680 let rel = data.attr(local_name!("rel"))?;
681 if !rel
682 .split_ascii_whitespace()
683 .any(|v| v.eq_ignore_ascii_case("icon"))
684 {
685 return None;
686 }
687 data.attr(local_name!("href")).map(|s| s.to_string())
688 })
689 }
690
691 pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
692 self.nodes.get(node_id)
693 }
694
695 pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
696 self.nodes.get_mut(node_id)
697 }
698
699 pub fn get_focussed_node_id(&self) -> Option<NodeId> {
700 self.focus_node_id
701 .or(self.try_root_element().map(|el| el.id))
702 }
703
704 pub fn mutate<'doc>(&'doc mut self) -> DocumentMutator<'doc> {
705 DocumentMutator::new(self)
706 }
707
708 pub fn handle_dom_event<F: FnMut(DomEvent)>(
709 &mut self,
710 event: &mut DomEvent,
711 dispatch_event: F,
712 ) {
713 handle_dom_event(self, event, dispatch_event)
714 }
715
716 pub fn as_any_mut(&mut self) -> &mut dyn Any {
717 self
718 }
719
720 pub fn label_bound_input_element(&self, label_node_id: NodeId) -> Option<&Node> {
727 let label_element = self.nodes[label_node_id].element_data()?;
728 if let Some(target_element_dom_id) = label_element.attr(local_name!("for")) {
729 TreeTraverser::new(self)
730 .filter_map(|id| {
731 let node = self.get_node(id)?;
732 let element_data = node.element_data()?;
733 if element_data.name.local != local_name!("input") {
734 return None;
735 }
736 let id = element_data.id.as_ref()?;
737 if *id == *target_element_dom_id {
738 Some(node)
739 } else {
740 None
741 }
742 })
743 .next()
744 } else {
745 TreeTraverser::new_with_root(self, label_node_id)
746 .filter_map(|child_id| {
747 let node = self.get_node(child_id)?;
748 let element_data = node.element_data()?;
749 if element_data.name.local == local_name!("input") {
750 Some(node)
751 } else {
752 None
753 }
754 })
755 .next()
756 }
757 }
758
759 pub fn toggle_checkbox(el: &mut ElementData) -> bool {
760 let Some(is_checked) = el.checkbox_input_checked_mut() else {
761 return false;
762 };
763 *is_checked = !*is_checked;
764
765 *is_checked
766 }
767
768 pub fn toggle_radio(&mut self, radio_set_name: String, target_radio_id: NodeId) {
769 for (i, node) in self.nodes.iter_mut() {
770 if let Some(node_data) = node.data.downcast_element_mut() {
771 if node_data.attr(local_name!("name")) == Some(&radio_set_name) {
772 let was_clicked = i == target_radio_id;
773 let Some(is_checked) = node_data.checkbox_input_checked_mut() else {
774 continue;
775 };
776 *is_checked = was_clicked;
777 }
778 }
779 }
780 }
781
782 pub fn toggle_details_open(&mut self, details_id: NodeId) {
786 use crate::qual_name;
787
788 let node = &self.nodes[details_id];
789 if !node.data.is_element_with_tag_name(&local_name!("details")) {
790 return;
791 }
792 let is_open = node.data.has_attr(local_name!("open"));
793
794 let mut mutator = self.mutate();
798 if is_open {
799 mutator.clear_attribute(details_id, qual_name!("open"));
800 } else {
801 mutator.set_attribute(details_id, qual_name!("open"), "");
802 }
803 drop(mutator);
804
805 self.shell_provider.request_redraw();
806 }
807
808 pub fn set_style_property(&mut self, node_id: NodeId, name: &str, value: &str) {
809 let node = &mut self.nodes[node_id];
810 let did_change = node.element_data_mut().unwrap().set_style_property(
811 name,
812 value,
813 &self.guard,
814 self.url.url_extra_data(),
815 );
816 if did_change {
817 node.mark_style_attr_updated();
818 }
819 }
820
821 pub fn remove_style_property(&mut self, node_id: NodeId, name: &str) {
822 let node = &mut self.nodes[node_id];
823 let did_change = node.element_data_mut().unwrap().remove_style_property(
824 name,
825 &self.guard,
826 self.url.url_extra_data(),
827 );
828 if did_change {
829 node.mark_style_attr_updated();
830 }
831 }
832
833 pub fn sub_document_node_ids(&self) -> Vec<NodeId> {
834 self.sub_document_nodes.iter().copied().collect()
835 }
836
837 pub fn set_sub_document(&mut self, node_id: NodeId, sub_document: Box<dyn Document>) {
838 self.nodes[node_id]
839 .element_data_mut()
840 .unwrap()
841 .set_sub_document(sub_document);
842 self.sub_document_nodes.insert(node_id);
843 }
844
845 pub fn remove_sub_document(&mut self, node_id: NodeId) {
846 self.nodes[node_id]
847 .element_data_mut()
848 .unwrap()
849 .remove_sub_document();
850 self.sub_document_nodes.remove(&node_id);
851 if let Some(load) = self.iframe_loads.remove(&node_id) {
852 load.abort_controller.abort();
853 }
854 }
855
856 pub fn poll_subdocuments(&mut self, waker: Option<&Waker>) -> bool {
862 let mut has_changes = false;
863 let node_ids: Vec<NodeId> = self.sub_document_nodes.iter().copied().collect();
864 for node_id in node_ids {
865 let Some(sub_doc) = self
866 .nodes
867 .get_mut(node_id)
868 .and_then(|node| node.subdoc_mut())
869 else {
870 continue;
871 };
872 let task_context = waker.map(TaskContext::from_waker);
873 has_changes |= sub_doc.poll(task_context);
874 }
875 has_changes
876 }
877
878 #[cfg(feature = "custom-widget")]
879 pub fn custom_widget_node_ids(&self) -> Vec<NodeId> {
880 self.custom_widget_nodes.iter().copied().collect()
881 }
882
883 #[cfg(feature = "custom-widget")]
884 pub fn take_pending_resource_deallocations(&mut self) -> Vec<anyrender::ResourceId> {
885 std::mem::take(&mut self.pending_resource_deallocations)
886 }
887
888 #[cfg(feature = "custom-widget")]
889 pub fn set_custom_widget(&mut self, node_id: NodeId, widget: Box<dyn crate::Widget>) {
890 self.nodes[node_id]
891 .element_data_mut()
892 .unwrap()
893 .set_custom_widget(widget);
894 self.custom_widget_nodes.insert(node_id);
895 }
896
897 #[cfg(feature = "custom-widget")]
898 pub fn remove_custom_widget(&mut self, node_id: NodeId) {
899 let resources_to_deallocate = self.nodes[node_id]
900 .element_data_mut()
901 .unwrap()
902 .remove_custom_widget();
903 self.pending_resource_deallocations
904 .extend_from_slice(&resources_to_deallocate);
905 self.custom_widget_nodes.remove(&node_id);
906 }
907
908 #[cfg(feature = "shadow-dom")]
912 pub fn custom_elements_mut(&mut self) -> &mut crate::node::CustomElementRegistry {
913 &mut self.custom_element_registry
914 }
915
916 #[cfg(feature = "shadow-dom")]
919 pub fn define_custom_element(
920 &mut self,
921 name: markup5ever::LocalName,
922 definition: crate::node::CustomElementDefinition,
923 ) {
924 self.custom_element_registry.define(name, definition);
925 }
926
927 #[cfg(feature = "shadow-dom")]
929 pub fn shadow_host_node_ids(&self) -> Vec<NodeId> {
930 self.shadow_host_nodes.iter().copied().collect()
931 }
932
933 #[cfg(feature = "shadow-dom")]
935 pub fn shadow_root_id(&self, host_id: NodeId) -> Option<NodeId> {
936 self.get_node(host_id)
937 .and_then(|node| node.shadow_root_id())
938 }
939
940 #[cfg(feature = "shadow-dom")]
944 pub fn attach_shadow(&mut self, host_id: NodeId, mode: crate::node::ShadowRootMode) -> NodeId {
945 if let Some(existing) = self.nodes[host_id].shadow_root_id() {
946 return existing;
947 }
948
949 let shadow_root_id = self.create_node(NodeData::ShadowRoot(
950 crate::node::ShadowRootData::new(host_id, mode),
951 ));
952
953 self.nodes[shadow_root_id].parent = Some(host_id);
957 if self.nodes[host_id].flags.is_in_document() {
958 self.nodes[shadow_root_id]
959 .flags
960 .insert(NodeFlags::IS_IN_DOCUMENT);
961 }
962
963 self.nodes[host_id]
964 .element_data_mut()
965 .expect("Shadow host must be an element")
966 .shadow_root = Some(shadow_root_id);
967 self.shadow_host_nodes.insert(host_id);
968
969 self.nodes[host_id].insert_damage(ALL_DAMAGE);
971 self.nodes[host_id].mark_ancestors_dirty();
972
973 shadow_root_id
974 }
975
976 #[cfg(feature = "shadow-dom")]
978 pub fn detach_shadow(&mut self, host_id: NodeId) {
979 let shadow_root_id = self.nodes[host_id]
980 .element_data_mut()
981 .and_then(|el| el.shadow_root.take());
982 if let Some(shadow_root_id) = shadow_root_id {
983 self.drop_node_ignoring_parent(shadow_root_id);
984 self.shadow_host_nodes.remove(&host_id);
985 self.nodes[host_id].insert_damage(ALL_DAMAGE);
986 self.nodes[host_id].mark_ancestors_dirty();
987 }
988 }
989
990 #[cfg(feature = "shadow-dom")]
992 pub fn set_custom_element(
993 &mut self,
994 node_id: NodeId,
995 controller: Box<dyn crate::node::CustomElement>,
996 ) {
997 use crate::node::{CustomElementData, SpecialElementData};
998 self.nodes[node_id]
999 .element_data_mut()
1000 .expect("Custom element host must be an element")
1001 .special_data = SpecialElementData::CustomElement(CustomElementData::new(controller));
1002 self.custom_element_nodes.insert(node_id);
1003 }
1004
1005 #[cfg(feature = "shadow-dom")]
1008 pub fn take_custom_element(
1009 &mut self,
1010 node_id: NodeId,
1011 ) -> Option<Box<dyn crate::node::CustomElement>> {
1012 use crate::node::SpecialElementData;
1013 self.custom_element_nodes.remove(&node_id);
1014 let element = self.nodes[node_id].element_data_mut()?;
1015 if matches!(element.special_data, SpecialElementData::CustomElement(_)) {
1016 if let SpecialElementData::CustomElement(mut data) = element.special_data.take() {
1017 return data.controller.take();
1018 }
1019 }
1020 None
1021 }
1022
1023 pub fn root_node(&self) -> &Node {
1024 &self.nodes[self.root_node_id]
1025 }
1026
1027 pub fn root_node_mut(&mut self) -> &mut Node {
1028 &mut self.nodes[self.root_node_id]
1029 }
1030
1031 pub fn set_paint_damage_tracking(&mut self, enabled: bool) {
1048 self.paint_damage.set_enabled(enabled);
1049 }
1050
1051 pub fn paint_damage_tracking(&self) -> bool {
1053 self.paint_damage.is_enabled()
1054 }
1055
1056 pub fn paint_damage(&self) -> &crate::paint_damage::PaintDamage {
1064 self.paint_damage.damage()
1065 }
1066
1067 pub fn try_root_element(&self) -> Option<&Node> {
1068 TDocument::as_node(&self.root_node()).first_element_child()
1069 }
1070
1071 pub fn root_element(&self) -> &Node {
1072 TDocument::as_node(&self.root_node())
1073 .first_element_child()
1074 .unwrap()
1075 .as_element()
1076 .unwrap()
1077 }
1078
1079 pub fn create_node(&mut self, node_data: NodeData) -> NodeId {
1080 let tree_ptr = self.nodes.as_mut() as *mut NodeTree;
1081 let guard = self.guard.clone();
1082
1083 self.nodes
1084 .insert_with_key(|id| Node::new(tree_ptr, id, guard, node_data))
1085 }
1086
1087 pub(crate) fn remove_node_from_tree(&mut self, node_id: NodeId) -> Option<Node> {
1091 self.clear_interaction_state_for_removed_node(node_id);
1092 self.nodes.remove(node_id)
1093 }
1094
1095 fn nearest_surviving_element_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
1100 let mut current = self.get_node(node_id)?.parent;
1101 while let Some(id) = current {
1102 let node = self.get_node(id)?;
1103 if node.is_element() && node.flags.is_in_document() {
1104 return Some(id);
1105 }
1106 current = node.parent;
1107 }
1108 None
1109 }
1110
1111 pub(crate) fn clear_interaction_state_for_removed_node(&mut self, node_id: NodeId) {
1132 if !self.nodes.contains_key(node_id) {
1133 return;
1134 }
1135
1136 if self.hover_node_id == Some(node_id) {
1137 self.hover_node_id = self.nearest_surviving_element_ancestor(node_id);
1138 self.hover_node_is_text = false;
1139 }
1140 if self.hover_hit_node_id == Some(node_id) {
1141 self.hover_hit_node_id = None;
1142 }
1143 if self.active_node_id == Some(node_id) {
1144 self.active_node_id = self.nearest_surviving_element_ancestor(node_id);
1145 }
1146 if self.focus_node_id == Some(node_id) {
1147 let shell_provider = self.shell_provider.clone();
1148 self.nodes[node_id].blur(shell_provider);
1149 self.focus_node_id = None;
1150 }
1151 if self.mousedown_node_id == Some(node_id) {
1152 self.mousedown_node_id = None;
1153 }
1154 if self.text_selection.anchor.node_or_parent == Some(node_id)
1155 || self.text_selection.focus.node_or_parent == Some(node_id)
1156 {
1157 self.text_selection.clear();
1158 }
1159 if self
1160 .hovered_scrollbar
1161 .is_some_and(|scrollbar| scrollbar.node_id == node_id)
1162 {
1163 self.hovered_scrollbar = None;
1164 }
1165 let drag_references_node = match &self.drag_mode {
1166 DragMode::Panning(state) => state.target == node_id,
1167 DragMode::ScrollbarDrag(state) => state.scrollbar.node_id == node_id,
1168 DragMode::Selecting | DragMode::None => false,
1169 };
1170 if drag_references_node {
1171 self.drag_mode = DragMode::None;
1172 }
1173 self.scrollbar_activity.remove(&node_id);
1174 }
1175
1176 pub(crate) fn drop_node_ignoring_parent(&mut self, node_id: NodeId) -> Option<Node> {
1177 self.drop_node_ignoring_parent_with(node_id, &mut |_| {})
1178 }
1179
1180 pub(crate) fn drop_node_ignoring_parent_with(
1183 &mut self,
1184 node_id: NodeId,
1185 on_drop: &mut dyn FnMut(NodeId),
1186 ) -> Option<Node> {
1187 let mut node = self.remove_node_from_tree(node_id);
1188 if let Some(node) = &mut node {
1189 on_drop(node_id);
1190 if let Some(before) = node.before() {
1191 self.drop_node_ignoring_parent_with(before, on_drop);
1192 }
1193 if let Some(after) = node.after() {
1194 self.drop_node_ignoring_parent_with(after, on_drop);
1195 }
1196
1197 for &child in &node.children {
1198 self.drop_node_ignoring_parent_with(child, on_drop);
1199 }
1200
1201 for &anon_id in &node.anonymous_blocks {
1204 self.deallocate_anonymous_block(anon_id);
1205 }
1206
1207 #[cfg(feature = "shadow-dom")]
1210 if let Some(shadow_root_id) = node.shadow_root_id() {
1211 self.shadow_host_nodes.remove(&node_id);
1212 self.custom_element_nodes.remove(&node_id);
1213 self.drop_node_ignoring_parent(shadow_root_id);
1214 }
1215 }
1216 node
1217 }
1218
1219 pub(crate) fn deallocate_anonymous_block(&mut self, anon_id: NodeId) {
1222 if !self.nodes.contains_key(anon_id) {
1225 return;
1226 }
1227
1228 let nested = std::mem::take(&mut self.nodes[anon_id].anonymous_blocks);
1230 for nested_id in nested {
1231 self.deallocate_anonymous_block(nested_id);
1232 }
1233
1234 self.remove_node_from_tree(anon_id);
1235 }
1236
1237 pub fn create_text_node(&mut self, text: &str) -> NodeId {
1238 let content = text.to_string();
1239 let data = NodeData::Text(TextNodeData::new(content));
1240 self.create_node(data)
1241 }
1242
1243 pub fn deep_clone_node(&mut self, node_id: NodeId) -> NodeId {
1244 let node = &self.nodes[node_id];
1246 let mut data = node.data.clone();
1247
1248 match &mut data {
1249 NodeData::Element(elem) | NodeData::AnonymousBlock(elem) => {
1250 if let Some(arc) = elem.style_attribute.as_mut() {
1251 let read_guard = self.guard().read();
1252 let block = arc.read_with(&read_guard);
1253 *arc = ServoArc::new(self.guard().wrap(block.clone()));
1254 }
1255 }
1256 _ => {}
1257 }
1258
1259 let children = node.children.clone();
1260
1261 let new_node_id = self.create_node(data);
1263
1264 let new_children: ThinVec<NodeId> = children
1266 .into_iter()
1267 .map(|child_id| self.deep_clone_node(child_id))
1268 .collect();
1269 for &child_id in &new_children {
1270 self.nodes[child_id].parent = Some(new_node_id);
1271 }
1272 self.nodes[new_node_id].children = new_children;
1273
1274 new_node_id
1275 }
1276
1277 pub(crate) fn remove_and_drop_pe(&mut self, node_id: NodeId) -> Option<Node> {
1278 fn remove_pe_ignoring_parent(doc: &mut BaseDocument, node_id: NodeId) -> Option<Node> {
1279 let mut node = doc.remove_node_from_tree(node_id);
1280 if let Some(node) = &mut node {
1281 for &child in &node.children {
1282 remove_pe_ignoring_parent(doc, child);
1283 }
1284 for &anon_id in &node.anonymous_blocks {
1285 doc.deallocate_anonymous_block(anon_id);
1286 }
1287 }
1288 node
1289 }
1290
1291 let node = remove_pe_ignoring_parent(self, node_id);
1292
1293 if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
1295 let parent = &mut self.nodes[parent_id];
1296 parent.children.retain(|id| *id != node_id);
1297 }
1298
1299 node
1300 }
1301
1302 pub(crate) fn resolve_url(&self, raw: &str) -> url::Url {
1303 self.url.resolve_relative(raw).unwrap_or_else(|| {
1304 panic!(
1305 "to be able to resolve {raw} with the base_url: {:?}",
1306 *self.url
1307 )
1308 })
1309 }
1310
1311 pub fn print_tree(&self) {
1312 crate::util::walk_tree(0, self.root_node());
1313 }
1314
1315 pub fn print_subtree(&self, node_id: NodeId) {
1316 crate::util::walk_tree(0, &self.nodes[node_id]);
1317 }
1318
1319 pub fn reload_resource_by_href(&mut self, href_to_reload: &str) {
1320 for &node_id in self.nodes_to_stylesheet.keys() {
1321 let node = &self.nodes[node_id];
1322 let Some(element) = node.element_data() else {
1323 continue;
1324 };
1325
1326 if element.name.local == local_name!("link") {
1327 if let Some(href) = element.attr(local_name!("href")) {
1328 if href == href_to_reload {
1330 let resolved_href = self.resolve_url(href);
1331 self.net_provider.fetch(
1332 self.id(),
1333 self.build_request(resolved_href.clone()),
1334 ResourceHandler::boxed(
1335 self.tx.clone(),
1336 self.id,
1337 Some(node_id),
1338 self.shell_provider.clone(),
1339 StylesheetHandler {
1340 source_url: resolved_href,
1341 guard: self.guard.clone(),
1342 net_provider: self.net_provider.clone(),
1343 abort_signal: self.abort_signal.clone(),
1344 },
1345 ),
1346 );
1347 }
1348 }
1349 }
1350 }
1351 }
1352
1353 pub fn process_style_element(&mut self, target_id: NodeId) {
1354 let css = self.nodes[target_id].text_content();
1355 let css = html_escape::decode_html_entities(&css);
1356 let sheet = self.make_stylesheet(&css, Origin::Author);
1357 self.add_stylesheet_for_node(sheet, target_id);
1358 }
1359
1360 pub fn remove_user_agent_stylesheet(&mut self, contents: &str) {
1361 if let Some(sheet) = self.ua_stylesheets.remove(contents) {
1362 self.stylist.remove_stylesheet(sheet, &self.guard.read());
1363 }
1364 }
1365
1366 pub fn url(&self) -> &url::Url {
1368 &self.url
1369 }
1370
1371 pub fn author_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
1374 self.nodes_to_stylesheet.values()
1375 }
1376
1377 pub fn useragent_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
1379 self.ua_stylesheets.values()
1380 }
1381
1382 pub fn add_user_agent_stylesheet(&mut self, css: &str) {
1383 let sheet = self.make_stylesheet(css, Origin::UserAgent);
1384 self.ua_stylesheets.insert(css.to_string(), sheet.clone());
1385 self.stylist.append_stylesheet(sheet, &self.guard.read());
1386 }
1387
1388 pub fn make_stylesheet(&self, css: impl AsRef<str>, origin: Origin) -> DocumentStyleSheet {
1389 let data = Stylesheet::from_str(
1390 css.as_ref(),
1391 self.url.url_extra_data(),
1392 origin,
1393 ServoArc::new(self.guard.wrap(MediaList::empty())),
1394 self.guard.clone(),
1395 Some(&StylesheetLoader {
1396 tx: self.tx.clone(),
1397 doc_id: self.id,
1398 net_provider: self.net_provider.clone(),
1399 shell_provider: self.shell_provider.clone(),
1400 abort_signal: self.abort_signal.clone(),
1401 }),
1402 None,
1403 QuirksMode::NoQuirks,
1404 AllowImportRules::Yes,
1405 );
1406
1407 DocumentStyleSheet(ServoArc::new(data))
1408 }
1409
1410 pub fn upsert_stylesheet_for_node(&mut self, node_id: NodeId) {
1411 let raw_styles = self.nodes[node_id].text_content();
1412 let sheet = self.make_stylesheet(raw_styles, Origin::Author);
1413 self.add_stylesheet_for_node(sheet, node_id);
1414 }
1415
1416 pub fn add_stylesheet_for_node(&mut self, stylesheet: DocumentStyleSheet, node_id: NodeId) {
1417 let old = self.nodes_to_stylesheet.insert(node_id, stylesheet.clone());
1418
1419 if let Some(old) = old {
1420 self.stylist.remove_stylesheet(old, &self.guard.read())
1421 }
1422
1423 crate::net::fetch_font_face(
1425 self.tx.clone(),
1426 self.id,
1427 Some(node_id),
1428 &stylesheet.0,
1429 &self.net_provider,
1430 &self.shell_provider,
1431 &self.guard.read(),
1432 self.abort_signal.as_ref(),
1433 );
1434
1435 let element = &mut self.nodes[node_id].element_data_mut().unwrap();
1437 element.special_data = SpecialElementData::Stylesheet(stylesheet.clone());
1438
1439 let insertion_point = self
1441 .nodes_to_stylesheet
1442 .range((Bound::Excluded(node_id), Bound::Unbounded))
1443 .next()
1444 .map(|(_, sheet)| sheet);
1445
1446 if let Some(insertion_point) = insertion_point {
1447 self.stylist.insert_stylesheet_before(
1448 stylesheet,
1449 insertion_point.clone(),
1450 &self.guard.read(),
1451 )
1452 } else {
1453 self.stylist
1454 .append_stylesheet(stylesheet, &self.guard.read())
1455 }
1456 }
1457
1458 pub fn handle_messages(&mut self) {
1459 let rx = self.rx.take().unwrap();
1462
1463 while let Ok(msg) = rx.try_recv() {
1464 self.handle_message(msg);
1465 }
1466
1467 self.rx = Some(rx);
1469 }
1470
1471 pub fn handle_message(&mut self, msg: DocumentEvent) {
1472 match msg {
1473 DocumentEvent::ResourceLoad(resource) => self.load_resource(resource),
1474 DocumentEvent::NavigateIframe { node_id, url } => self.navigate_iframe(node_id, url),
1475 }
1476 }
1477
1478 pub fn has_pending_critical_resources(&self) -> bool {
1480 !self.pending_critical_resources.is_empty()
1481 }
1482
1483 pub fn pending_image_count(&self) -> usize {
1490 self.pending_images.len()
1491 }
1492
1493 pub fn load_resource(&mut self, res: ResourceLoadResponse) {
1494 self.pending_critical_resources.remove(&res.request_id);
1495
1496 let resource = match res.result {
1497 Ok(resource) => resource,
1498 Err(err) => {
1499 if let Some(url) = res.resolved_url.as_ref() {
1500 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1501 #[cfg(feature = "tracing")]
1502 tracing::warn!(
1503 url = url.as_str(),
1504 waiting_nodes = waiting_nodes.len(),
1505 error = err.as_str(),
1506 "Resource load failed"
1507 );
1508 #[cfg(not(feature = "tracing"))]
1509 let _ = (waiting_nodes, err);
1510 } else {
1511 #[cfg(feature = "tracing")]
1512 tracing::warn!(error = err.as_str(), "Resource load failed (no url)");
1513 #[cfg(not(feature = "tracing"))]
1514 let _ = err;
1515 }
1516 return;
1517 }
1518 };
1519
1520 match resource {
1521 Resource::Css(css) => {
1522 let node_id = res.node_id.unwrap();
1523 self.add_stylesheet_for_node(css, node_id);
1524 }
1525 Resource::ImportSheet(import_rule, sheet) => {
1526 {
1535 let mut guard = self.guard.write();
1536 import_rule.write_with(&mut guard).stylesheet =
1537 style::stylesheets::import_rule::ImportSheet::Sheet(sheet.clone());
1538 }
1539
1540 crate::net::fetch_font_face(
1544 self.tx.clone(),
1545 self.id,
1546 res.node_id,
1547 &sheet,
1548 &self.net_provider,
1549 &self.shell_provider,
1550 &self.guard.read(),
1551 self.abort_signal.as_ref(),
1552 );
1553 }
1554 Resource::Image(_kind, width, height, image_data) => {
1555 let image = ImageData::Raster(RasterImageData::new(width, height, image_data));
1557
1558 let Some(url) = res.resolved_url.as_ref() else {
1559 return;
1560 };
1561
1562 self.apply_loaded_image(url, image);
1563 }
1564 #[cfg(feature = "svg")]
1565 Resource::Svg(_kind, svg) => {
1566 let image = ImageData::Svg(svg);
1568
1569 let Some(url) = res.resolved_url.as_ref() else {
1570 return;
1571 };
1572
1573 self.apply_loaded_image(url, image);
1574 }
1575 Resource::DocumentSrc(html) => {
1576 let Some(node_id) = res.node_id else {
1577 return;
1578 };
1579 self.apply_iframe_html(node_id, res.request_id, res.resolved_url, &html);
1580 }
1581 Resource::Font(bytes, overrides) => {
1582 let font = Blob::new(Arc::new(bytes));
1583
1584 let weight_override = overrides.weight.map(parley::fontique::FontWeight::new);
1590 let info_override = parley::fontique::FontInfoOverride {
1591 family_name: overrides.family_name.as_deref(),
1592 weight: weight_override,
1593 style: overrides.style,
1594 ..Default::default()
1595 };
1596
1597 let mut global_font_ctx = self.font_ctx.lock().unwrap();
1599 global_font_ctx
1600 .collection
1601 .register_fonts(font.clone(), Some(info_override));
1602
1603 #[cfg(feature = "parallel-construct")]
1604 {
1605 rayon::broadcast(|_ctx| {
1606 let mut font_ctx = self
1607 .thread_font_contexts
1608 .get_or(|| RefCell::new(Box::new(global_font_ctx.clone())))
1609 .borrow_mut();
1610 font_ctx
1611 .collection
1612 .register_fonts(font.clone(), Some(info_override));
1613 });
1614 }
1615 drop(global_font_ctx);
1616
1617 self.invalidate_inline_contexts();
1619 }
1620 Resource::None => {
1621 }
1623 }
1624 }
1625
1626 fn apply_loaded_image(&mut self, url: &str, image: ImageData) {
1629 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1631
1632 #[cfg(feature = "tracing")]
1633 tracing::info!(
1634 "Image {url} loaded, applying to {} nodes",
1635 waiting_nodes.len()
1636 );
1637
1638 self.image_cache.insert(url.to_string(), image.clone());
1640
1641 for (node_id, image_type) in waiting_nodes {
1643 let Some(node) = self.get_node_mut(node_id) else {
1644 continue;
1645 };
1646
1647 match image_type {
1648 ImageType::Image => {
1649 node.element_data_mut().unwrap().special_data =
1650 SpecialElementData::Image(Box::new(image.clone()));
1651
1652 node.cache_mut().clear();
1654 node.insert_damage(ALL_DAMAGE);
1655 }
1656 ImageType::Background(idx) | ImageType::Mask(idx) => {
1657 let layer_image = node.element_data_mut().and_then(|el| {
1658 let images = match image_type {
1659 ImageType::Background(_) => &mut el.background_images,
1660 ImageType::Mask(_) => &mut el.mask_images,
1661 ImageType::Image => unreachable!(),
1662 };
1663 images.get_mut(idx)
1664 });
1665 if let Some(Some(layer_image)) = layer_image {
1666 layer_image.status = Status::Ok;
1667 layer_image.image = image.clone();
1668 }
1669 }
1670 }
1671 }
1672 }
1673
1674 pub fn snapshot_node(&mut self, node_id: NodeId) {
1675 let node = &mut self.nodes[node_id];
1676
1677 let has_been_styled = node.primary_styles().is_some();
1682 if !has_been_styled {
1683 return;
1684 }
1685
1686 let opaque_node_id = TNode::opaque(&&*node);
1687 node.set_has_snapshot(true);
1688 node.snapshot_handled()
1689 .store(false, std::sync::atomic::Ordering::SeqCst);
1690
1691 if let Some(_existing_snapshot) = self.snapshots.get_mut(&opaque_node_id) {
1693 } else {
1696 let attrs: Option<Vec<_>> = node.attrs().map(|attrs| {
1697 attrs
1698 .iter()
1699 .map(|attr| {
1700 let ident = AttrIdentifier {
1701 local_name: GenericAtomIdent(attr.name.local.clone()),
1702 name: GenericAtomIdent(attr.name.local.clone()),
1703 namespace: GenericAtomIdent(attr.name.ns.clone()),
1704 prefix: None,
1705 };
1706
1707 let value = if attr.name.local == local_name!("id") {
1708 AttrValue::Atom(Atom::from(&*attr.value))
1709 } else if attr.name.local == local_name!("class") {
1710 let classes = attr
1711 .value
1712 .split_ascii_whitespace()
1713 .map(Atom::from)
1714 .collect();
1715 AttrValue::TokenList(OnceLock::from(attr.value.to_string()), classes)
1721 } else {
1722 AttrValue::String(attr.value.to_string())
1723 };
1724
1725 (ident, value)
1726 })
1727 .collect()
1728 });
1729
1730 let changed_attrs = attrs
1731 .as_ref()
1732 .map(|attrs| attrs.iter().map(|attr| attr.0.name.clone()).collect())
1733 .unwrap_or_default();
1734
1735 self.snapshots.insert(
1736 opaque_node_id,
1737 ServoElementSnapshot {
1738 state: Some(*node.element_state()),
1739 attrs,
1740 changed_attrs,
1741 class_changed: true,
1742 id_changed: true,
1743 other_attributes_changed: true,
1744 },
1745 );
1746 }
1747 }
1748
1749 pub fn snapshot_node_and(&mut self, node_id: NodeId, cb: impl FnOnce(&mut Node)) {
1756 if !self.nodes.contains_key(node_id) {
1757 return;
1758 }
1759 self.snapshot_node(node_id);
1760 cb(&mut self.nodes[node_id]);
1761 }
1762
1763 pub fn hit(&self, x: f32, y: f32) -> Option<HitResult> {
1765 self.hit_with_scrollbar(x, y).0
1766 }
1767
1768 pub fn nearest_non_anonymous_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
1781 let mut node = self.get_node(node_id)?;
1785 loop {
1786 let parent = match node.parent {
1787 Some(parent_id) => self.get_node(parent_id)?,
1788 None => return Some(node.id),
1789 };
1790 if !node.is_anonymous() && !parent.is_anonymous() {
1791 return Some(node.id);
1792 }
1793 node = parent;
1794 }
1795 }
1796
1797 pub fn focus_next_node(&mut self) -> Option<NodeId> {
1798 let focussed_node_id = self.get_focussed_node_id()?;
1799 let id = self.next_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
1800 self.set_focus_to(id);
1801 Some(id)
1802 }
1803
1804 pub fn focus_prev_node(&mut self) -> Option<NodeId> {
1806 let focussed_node_id = self.get_focussed_node_id()?;
1807 let id = self.prev_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
1808 self.set_focus_to(id);
1809 Some(id)
1810 }
1811
1812 pub fn clear_focus(&mut self) {
1814 if let Some(id) = self.focus_node_id {
1815 let shell_provider = self.shell_provider.clone();
1816 self.snapshot_node_and(id, |node| node.blur(shell_provider));
1817 self.focus_node_id = None;
1818 }
1819 }
1820
1821 pub fn set_mousedown_node_id(&mut self, node_id: Option<NodeId>) {
1822 self.mousedown_node_id = node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
1823 }
1824 pub fn set_focus_to(&mut self, focus_node_id: NodeId) -> bool {
1825 let Some(focus_node_id) = self.nearest_non_anonymous_ancestor(focus_node_id) else {
1826 return false;
1827 };
1828 if Some(focus_node_id) == self.focus_node_id {
1829 return false;
1830 }
1831
1832 #[cfg(feature = "tracing")]
1833 tracing::info!("Focussed node {focus_node_id}");
1834
1835 let shell_provider = self.shell_provider.clone();
1836
1837 if let Some(id) = self.focus_node_id {
1839 self.snapshot_node_and(id, |node| node.blur(shell_provider.clone()));
1840 }
1841
1842 self.snapshot_node_and(focus_node_id, |node| node.focus(shell_provider));
1844
1845 self.focus_node_id = Some(focus_node_id);
1846
1847 true
1848 }
1849
1850 pub fn active_node(&mut self) -> bool {
1851 let Some(hover_node_id) = self.get_hover_node_id() else {
1852 return false;
1853 };
1854
1855 if let Some(active_node_id) = self.active_node_id {
1856 if active_node_id == hover_node_id {
1857 return true;
1858 }
1859 self.unactive_node();
1860 }
1861
1862 debug_assert!(
1864 self.get_node(hover_node_id)
1865 .is_some_and(|node| !node.is_anonymous()),
1866 "interaction state must reference DOM nodes, not layout-generated nodes"
1867 );
1868 let active_node_id = Some(hover_node_id);
1869
1870 let node_path = self.maybe_node_layout_ancestors(active_node_id);
1871 for &id in node_path.iter() {
1872 self.snapshot_node_and(id, |node| node.active());
1873 }
1874
1875 self.active_node_id = active_node_id;
1876
1877 true
1878 }
1879
1880 pub fn unactive_node(&mut self) -> bool {
1881 let Some(active_node_id) = self.active_node_id.take() else {
1882 return false;
1883 };
1884
1885 let node_path = self.maybe_node_layout_ancestors(Some(active_node_id));
1886 for &id in node_path.iter() {
1887 self.snapshot_node_and(id, |node| node.unactive());
1888 }
1889
1890 true
1891 }
1892
1893 pub fn hovered_scrollbar(&self) -> Option<crate::node::ScrollbarRef> {
1895 self.hovered_scrollbar
1896 }
1897
1898 pub fn scrollbar_drag_target(&self) -> Option<crate::node::ScrollbarRef> {
1900 match &self.drag_mode {
1901 DragMode::ScrollbarDrag(state) => Some(state.scrollbar),
1902 _ => None,
1903 }
1904 }
1905
1906 pub fn scrollbar_opacity(&self, node_id: NodeId) -> f32 {
1911 let interacting = |scrollbar: &crate::node::ScrollbarRef| scrollbar.node_id == node_id;
1912 if self.hovered_scrollbar.as_ref().is_some_and(interacting)
1913 || self
1914 .scrollbar_drag_target()
1915 .as_ref()
1916 .is_some_and(interacting)
1917 {
1918 return 1.0;
1919 }
1920 self.scrollbar_activity.get(&node_id).map_or(1.0, |last| {
1921 crate::node::scrollbar::opacity_at(last.elapsed())
1922 })
1923 }
1924
1925 pub(crate) fn show_scrollbars(&mut self, node_id: NodeId) {
1928 if cfg!(feature = "scrollbars") {
1929 self.scrollbar_activity.insert(node_id, Instant::now());
1930 }
1931 }
1932
1933 fn scrollbars_animating(&self) -> bool {
1936 use crate::node::scrollbar::{FADE_DELAY, FADE_DURATION};
1937 self.scrollbar_activity
1938 .values()
1939 .any(|last| last.elapsed() < FADE_DELAY + FADE_DURATION)
1940 }
1941
1942 pub(crate) fn hit_with_scrollbar(
1946 &self,
1947 x: f32,
1948 y: f32,
1949 ) -> (Option<HitResult>, Option<crate::node::ScrollbarRef>) {
1950 if TDocument::as_node(&self.root_node())
1951 .first_element_child()
1952 .is_none()
1953 {
1954 #[cfg(feature = "tracing")]
1955 tracing::warn!("No DOM - not resolving hit test");
1956 return (None, None);
1957 }
1958 let mut scrollbar = None;
1959 let hit = self
1960 .root_element()
1961 .hit_inner(x, y, self.viewport().scale_f64(), &mut scrollbar);
1962 (hit, scrollbar)
1963 }
1964
1965 pub fn set_hover_to(&mut self, x: f32, y: f32) -> bool {
1966 self.semantic_hover_node_id = None;
1967 self.last_client_pointer_position = Some(taffy::Point {
1971 x: x - self.viewport_scroll.x as f32,
1972 y: y - self.viewport_scroll.y as f32,
1973 });
1974
1975 let (hit, hovered_scrollbar) = self.hit_with_scrollbar(x, y);
1976 let hovered_scrollbar =
1979 hovered_scrollbar.filter(|scrollbar| self.scrollbar_opacity(scrollbar.node_id) > 0.0);
1980 let scrollbar_changed = hovered_scrollbar != self.hovered_scrollbar;
1984 if scrollbar_changed {
1985 for scrollbar in [self.hovered_scrollbar, hovered_scrollbar]
1988 .into_iter()
1989 .flatten()
1990 {
1991 self.show_scrollbars(scrollbar.node_id);
1992 }
1993 }
1994 self.hovered_scrollbar = hovered_scrollbar;
1995
1996 let hit_node_id = hit.map(|hit| hit.node_id);
2001 let hover_node_id = hit_node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
2002 let new_is_text = hit.map(|hit| hit.is_text).unwrap_or(false);
2003
2004 self.apply_hover_target(hit_node_id, hover_node_id, new_is_text, scrollbar_changed)
2005 }
2006
2007 pub fn set_hover_to_node(&mut self, node_id: NodeId, x: f32, y: f32) -> bool {
2015 self.semantic_hover_node_id = Some(node_id);
2016 self.last_client_pointer_position = Some(taffy::Point {
2017 x: x - self.viewport_scroll.x as f32,
2018 y: y - self.viewport_scroll.y as f32,
2019 });
2020
2021 let hovered_scrollbar = self.hovered_scrollbar.take();
2022 let scrollbar_changed = hovered_scrollbar.is_some();
2023 if let Some(scrollbar) = hovered_scrollbar {
2024 self.show_scrollbars(scrollbar.node_id);
2025 }
2026 let hover_node_id = self.nearest_non_anonymous_ancestor(node_id);
2027 self.apply_hover_target(Some(node_id), hover_node_id, false, scrollbar_changed)
2028 }
2029
2030 fn apply_hover_target(
2031 &mut self,
2032 hit_node_id: Option<NodeId>,
2033 hover_node_id: Option<NodeId>,
2034 new_is_text: bool,
2035 scrollbar_changed: bool,
2036 ) -> bool {
2037 let hit_changed =
2038 hit_node_id != self.hover_hit_node_id || new_is_text != self.hover_node_is_text;
2039 self.hover_hit_node_id = hit_node_id;
2040 self.hover_node_is_text = new_is_text;
2041
2042 if hover_node_id == self.hover_node_id {
2044 if hit_changed {
2045 self.shell_provider.set_cursor(self.get_cursor());
2049 }
2050 return scrollbar_changed;
2051 }
2052
2053 let old_node_path = self.maybe_node_layout_ancestors(self.hover_node_id);
2054 let new_node_path = self.maybe_node_layout_ancestors(hover_node_id);
2055 let same_count = old_node_path
2056 .iter()
2057 .zip(&new_node_path)
2058 .take_while(|(o, n)| o == n)
2059 .count();
2060 for &id in old_node_path.iter().skip(same_count) {
2061 self.snapshot_node_and(id, |node| node.unhover());
2062 }
2063 for &id in new_node_path.iter().skip(same_count) {
2064 self.snapshot_node_and(id, |node| node.hover());
2065 }
2066
2067 self.hover_node_id = hover_node_id;
2068
2069 self.shell_provider.set_cursor(self.get_cursor());
2071
2072 self.shell_provider.request_redraw();
2074
2075 true
2076 }
2077
2078 pub fn clear_hover(&mut self) -> bool {
2079 self.last_client_pointer_position = None;
2082 self.semantic_hover_node_id = None;
2083 self.hover_hit_node_id = None;
2084
2085 let Some(hover_node_id) = self.hover_node_id else {
2086 return false;
2087 };
2088
2089 let old_node_path = self.maybe_node_layout_ancestors(Some(hover_node_id));
2090 for &id in old_node_path.iter() {
2091 self.snapshot_node_and(id, |node| node.unhover());
2092 }
2093
2094 self.hover_node_id = None;
2095 self.hover_node_is_text = false;
2096
2097 self.shell_provider.set_cursor(self.get_cursor());
2099
2100 self.shell_provider.request_redraw();
2102
2103 true
2104 }
2105
2106 pub fn refresh_hover(&mut self) -> bool {
2112 if let Some(node_id) = self.semantic_hover_node_id {
2113 if self.get_node(node_id).is_some() {
2114 let hover_node_id = self.nearest_non_anonymous_ancestor(node_id);
2115 return self.apply_hover_target(Some(node_id), hover_node_id, false, false);
2116 }
2117 self.semantic_hover_node_id = None;
2118 }
2119 let Some(pos) = self.last_client_pointer_position else {
2120 return false;
2121 };
2122 let x = pos.x + self.viewport_scroll.x as f32;
2123 let y = pos.y + self.viewport_scroll.y as f32;
2124 self.set_hover_to(x, y)
2125 }
2126
2127 pub fn get_hover_node_id(&self) -> Option<NodeId> {
2128 self.hover_node_id
2129 }
2130
2131 pub fn get_mousedown_node_id(&self) -> Option<NodeId> {
2132 self.mousedown_node_id
2133 }
2134
2135 pub fn set_viewport(&mut self, viewport: Viewport) {
2136 let scale_has_changed = viewport.scale_f64() != self.viewport.scale_f64();
2137 self.viewport = viewport;
2138 self.set_stylist_device(make_device(
2139 &self.viewport,
2140 self.media_type.clone(),
2141 self.font_ctx.clone(),
2142 ));
2143 self.scroll_viewport_by(0.0, 0.0); if scale_has_changed {
2146 self.invalidate_inline_contexts();
2147 self.shell_provider.request_redraw();
2148 }
2149 }
2150
2151 pub fn media_type(&self) -> &MediaType {
2153 &self.media_type
2154 }
2155
2156 pub fn set_media_type(&mut self, media_type: MediaType) {
2159 if self.media_type == media_type {
2160 return;
2161 }
2162 self.media_type = media_type;
2163 self.set_stylist_device(make_device(
2164 &self.viewport,
2165 self.media_type.clone(),
2166 self.font_ctx.clone(),
2167 ));
2168 }
2169
2170 pub fn viewport(&self) -> &Viewport {
2171 &self.viewport
2172 }
2173
2174 pub fn viewport_mut(&mut self) -> ViewportMut<'_> {
2175 ViewportMut::new(self)
2176 }
2177
2178 pub fn zoom_by(&mut self, increment: f32) {
2179 *self.viewport.zoom_mut() += increment;
2180 self.set_viewport(self.viewport.clone());
2181 }
2182
2183 pub fn zoom_to(&mut self, zoom: f32) {
2184 *self.viewport.zoom_mut() = zoom;
2185 self.set_viewport(self.viewport.clone());
2186 }
2187
2188 pub fn get_viewport(&self) -> Viewport {
2189 self.viewport.clone()
2190 }
2191
2192 pub fn incremental_layout(&self) -> bool {
2194 self.incremental_layout
2195 }
2196
2197 pub fn set_incremental_layout(&mut self, enabled: bool) {
2199 self.incremental_layout = enabled;
2200 }
2201
2202 pub fn devtools(&self) -> &DevtoolSettings {
2203 &self.devtool_settings
2204 }
2205
2206 pub fn devtools_mut(&mut self) -> &mut DevtoolSettings {
2207 &mut self.devtool_settings
2208 }
2209
2210 pub fn subdoc(&self, node_id: NodeId) -> Option<&dyn Document> {
2211 self.get_node(node_id)
2212 .and_then(|node| node.element_data())
2213 .and_then(|el| el.sub_doc_data())
2214 }
2215
2216 pub fn subdoc_mut(&mut self, node_id: NodeId) -> Option<&mut dyn Document> {
2217 self.get_node_mut(node_id)
2218 .and_then(|node| node.element_data_mut())
2219 .and_then(|el| el.sub_doc_data_mut())
2220 }
2221
2222 pub fn is_animating(&self) -> bool {
2223 #[cfg(feature = "custom-widget")]
2224 let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
2225 self.nodes[node_id]
2226 .element_data()
2227 .and_then(|el| el.custom_widget_data())
2228 .is_some_and(|data| data.widget.requires_redraw())
2229 });
2230 #[cfg(not(feature = "custom-widget"))]
2231 let custom_widget_is_animating = false;
2232
2233 let animating = self.has_canvas
2234 | self.has_active_animations
2235 | (self.subdoc_animation_pacing != AnimationPacing::Idle)
2236 | custom_widget_is_animating
2237 | (self.scroll_animation != ScrollAnimationState::None)
2238 | self.scrollbars_animating();
2239
2240 if animating && crate::debug::animation_reasons_enabled() {
2241 crate::debug::report_animation_reasons(
2242 self.id(),
2243 self.has_canvas,
2244 self.has_active_animations,
2245 self.subdoc_animation_pacing != AnimationPacing::Idle,
2246 custom_widget_is_animating,
2247 self.scroll_animation != ScrollAnimationState::None,
2248 self.scrollbars_animating(),
2249 self.animating_node_names().as_deref(),
2250 );
2251 }
2252
2253 animating
2254 }
2255
2256 pub fn animation_pacing(&self) -> AnimationPacing {
2261 let focused_text_input = self.focus_node_id.is_some_and(|node_id| {
2262 self.nodes
2263 .get(node_id)
2264 .and_then(|node| node.element_data())
2265 .is_some_and(|element| element.text_input_data().is_some())
2266 });
2267 #[cfg(feature = "custom-widget")]
2268 let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
2269 self.nodes[node_id]
2270 .element_data()
2271 .and_then(|el| el.custom_widget_data())
2272 .is_some_and(|data| data.widget.requires_redraw())
2273 });
2274 #[cfg(not(feature = "custom-widget"))]
2275 let custom_widget_is_animating = false;
2276
2277 if self.has_canvas
2278 || custom_widget_is_animating
2279 || self.scroll_animation != ScrollAnimationState::None
2280 || self.scrollbars_animating()
2281 {
2282 AnimationPacing::Interactive
2283 } else if self.has_active_animations {
2284 const SLOW_ANIMATION_SECONDS: f64 = 2.0;
2285 let sets = self.animations.sets.read();
2286 let has_fast_animation_or_transition = sets.values().any(|set| {
2287 set.transitions.iter().any(|transition| {
2288 matches!(
2289 transition.state,
2290 AnimationState::Pending | AnimationState::Running
2291 )
2292 }) || set.animations.iter().any(|animation| {
2293 matches!(
2294 animation.state,
2295 AnimationState::Pending | AnimationState::Running
2296 ) && animation.duration < SLOW_ANIMATION_SECONDS
2297 })
2298 });
2299 if has_fast_animation_or_transition {
2300 AnimationPacing::Interactive
2301 } else {
2302 AnimationPacing::SlowCss
2303 }
2304 } else if focused_text_input {
2305 AnimationPacing::Caret
2306 } else if self.subdoc_animation_pacing != AnimationPacing::Idle {
2307 self.subdoc_animation_pacing
2308 } else {
2309 AnimationPacing::Idle
2310 }
2311 }
2312
2313 fn animating_node_names(&self) -> Option<String> {
2320 if !self.has_active_animations {
2321 return None;
2322 }
2323 let sets = self.animations.sets.read();
2324 let mut described: Vec<String> = sets
2325 .iter()
2326 .filter(|(_, state)| state.needs_animation_ticks())
2327 .filter_map(|(key, state)| {
2328 let node_id = NodeId::from_u64(key.node.id() as u64);
2329 let node = self.nodes.get(node_id)?;
2330 let element = node.element_data()?;
2331 let name = element
2332 .attr(local_name!("id"))
2333 .map(|id| format!("#{id}"))
2334 .or_else(|| {
2335 element
2336 .attr(local_name!("class"))
2337 .and_then(|c| c.split_ascii_whitespace().next())
2338 .map(|c| format!(".{c}"))
2339 })
2340 .unwrap_or_else(|| element.name.local.to_string());
2341 Some(format!(
2342 "{name}(anim={},trans={},in_doc={})",
2343 state.animations.len(),
2344 state.transitions.len(),
2345 node.flags.is_in_document(),
2346 ))
2347 })
2348 .collect();
2349 described.sort();
2350 described.truncate(12);
2351 Some(described.join(" "))
2352 }
2353
2354 pub fn set_stylist_device(&mut self, device: Device) {
2356 let root_styles = self
2362 .try_root_element()
2363 .and_then(|root| root.primary_styles());
2364 if let Some(root_style) = root_styles.as_deref() {
2365 device.set_root_style(root_style);
2366
2367 let font = root_style.get_font();
2368 let font_size = font.clone_font_size().computed_size();
2369 device.set_root_font_size(root_style.effective_zoom.unzoom(font_size.px()));
2370
2371 let line_height = device
2372 .calc_line_height(font, root_style.writing_mode, None)
2373 .0;
2374 device.set_root_line_height(root_style.effective_zoom.unzoom(line_height.px()));
2375 }
2376 drop(root_styles);
2377
2378 let origins = {
2379 let guard = &self.guard;
2380 let guards = StylesheetGuards {
2381 author: &guard.read(),
2382 ua_or_user: &guard.read(),
2383 };
2384 self.stylist.set_device(device, &guards)
2385 };
2386 self.stylist.force_stylesheet_origins_dirty(origins);
2387 }
2388
2389 pub fn stylist_device(&mut self) -> &Device {
2390 self.stylist.device()
2391 }
2392
2393 pub fn get_cursor(&self) -> Option<CursorIcon> {
2401 let node_id = self
2406 .hover_hit_node_id
2407 .filter(|&id| self.nodes.contains_key(id))
2408 .or(self.get_hover_node_id());
2409 let Some(node_id) = node_id else {
2410 return Some(CursorIcon::Default);
2411 };
2412 let node = &self.nodes[node_id];
2413
2414 if let Some(subdoc) = node.subdoc().map(|doc| doc.inner()) {
2415 if subdoc.hover_hit_node_id.is_some() || subdoc.get_hover_node_id().is_some() {
2421 return subdoc.get_cursor();
2422 }
2423 return Some(CursorIcon::Default);
2424 }
2425
2426 let Some(style) = node.primary_styles() else {
2427 return Some(CursorIcon::Default);
2428 };
2429 let user_select = style.clone_user_select();
2430 let keyword = style.clone_cursor().keyword;
2431
2432 if keyword != CursorKind::Auto {
2434 return stylo_to_cursor_icon(keyword);
2435 }
2436
2437 if node
2439 .element_data()
2440 .is_some_and(|e| e.text_input_data().is_some())
2441 {
2442 return Some(CursorIcon::Text);
2443 }
2444
2445 let mut maybe_node = Some(node);
2447 while let Some(node) = maybe_node {
2448 if node.is_link() {
2449 return Some(CursorIcon::Pointer);
2450 }
2451
2452 maybe_node = node.layout_parent.get().map(|node_id| node.with(node_id));
2453 }
2454
2455 if self.hover_node_is_text {
2457 return Some(match user_select {
2458 UserSelect::Text | UserSelect::All | UserSelect::Auto => CursorIcon::Text,
2459 UserSelect::None => CursorIcon::Default,
2460 });
2461 }
2462
2463 Some(CursorIcon::Default)
2465 }
2466
2467 pub fn scroll_node_by<F: FnMut(DomEvent)>(
2468 &mut self,
2469 node_id: NodeId,
2470 x: f64,
2471 y: f64,
2472 dispatch_event: F,
2473 ) {
2474 self.scroll_node_by_has_changed(node_id, x, y, dispatch_event);
2475 }
2476
2477 pub fn scroll_node_by_has_changed<F: FnMut(DomEvent)>(
2481 &mut self,
2482 node_id: NodeId,
2483 x: f64,
2484 y: f64,
2485 mut dispatch_event: F,
2486 ) -> bool {
2487 if self.try_root_element().is_some_and(|el| el.id == node_id) {
2492 let has_changed = self.scroll_viewport_by_has_changed(x, y);
2493 if has_changed {
2494 let layout = *self.root_element().final_layout();
2495 let scale = self.viewport.scale() as f64;
2496 let event = BlitzScrollEvent {
2497 scroll_top: self.viewport_scroll.y,
2498 scroll_left: self.viewport_scroll.x,
2499 scroll_width: layout.size.width.max(layout.content_size.width) as i32,
2500 scroll_height: layout.size.height.max(layout.content_size.height) as i32,
2501 client_width: (self.viewport.window_size.0 as f64 / scale) as i32,
2502 client_height: (self.viewport.window_size.1 as f64 / scale) as i32,
2503 };
2504 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
2505 }
2506 return has_changed;
2507 }
2508
2509 let Some(node) = self.nodes.get_mut(node_id) else {
2510 return false;
2511 };
2512
2513 if node
2517 .element_data()
2518 .is_some_and(|el| el.text_input_data().is_some())
2519 {
2520 let parent = node.parent;
2521 let content_box_width = node.final_layout().content_box_width();
2522 let content_box_height = node.final_layout().content_box_height();
2523 let input = node
2524 .element_data_mut()
2525 .and_then(|el| el.text_input_data_mut())
2526 .unwrap();
2527
2528 let (bubble_x, bubble_y) = if input.is_multiline {
2529 (
2530 x,
2531 input.scroll_by(y as f32, content_box_width, content_box_height) as f64,
2532 )
2533 } else {
2534 (
2535 input.scroll_by(x as f32, content_box_width, content_box_height) as f64,
2536 y,
2537 )
2538 };
2539
2540 let has_changed = bubble_x != x || bubble_y != y;
2541
2542 if bubble_x != 0.0 || bubble_y != 0.0 {
2543 let bubbled = if let Some(parent) = parent {
2544 self.scroll_node_by_has_changed(parent, bubble_x, bubble_y, dispatch_event)
2545 } else {
2546 self.scroll_viewport_by_has_changed(bubble_x, bubble_y)
2547 };
2548 return bubbled | has_changed;
2549 }
2550
2551 return has_changed;
2552 }
2553
2554 let (can_x_scroll, can_y_scroll) = node
2555 .primary_styles()
2556 .map(|styles| {
2557 (
2558 matches!(styles.clone_overflow_x(), Overflow::Scroll | Overflow::Auto),
2559 matches!(styles.clone_overflow_y(), Overflow::Scroll | Overflow::Auto),
2560 )
2561 })
2562 .unwrap_or((false, false));
2563
2564 let initial = *node.scroll_offset();
2565 let new_x = node.scroll_offset().x - x;
2566 let new_y = node.scroll_offset().y - y;
2567
2568 let mut bubble_x = 0.0;
2569 let mut bubble_y = 0.0;
2570
2571 let scroll_width = node.final_layout().scroll_width() as f64;
2572 let scroll_height = node.final_layout().scroll_height() as f64;
2573
2574 if let Some(mut sub_doc) = node.subdoc_mut().map(|doc| doc.inner_mut()) {
2576 let has_changed = if let Some(hover_node_id) = sub_doc.get_hover_node_id() {
2577 sub_doc.scroll_node_by_has_changed(hover_node_id, x, y, dispatch_event)
2578 } else {
2579 sub_doc.scroll_viewport_by_has_changed(x, y)
2580 };
2581
2582 return has_changed;
2584 }
2585
2586 if !can_x_scroll {
2588 bubble_x = x
2589 } else if new_x < 0.0 {
2590 bubble_x = -new_x;
2591 node.scroll_offset_mut().x = 0.0;
2592 } else if new_x > scroll_width {
2593 bubble_x = scroll_width - new_x;
2594 node.scroll_offset_mut().x = scroll_width;
2595 } else {
2596 node.scroll_offset_mut().x = new_x;
2597 }
2598
2599 if !can_y_scroll {
2600 bubble_y = y
2601 } else if new_y < 0.0 {
2602 bubble_y = -new_y;
2603 node.scroll_offset_mut().y = 0.0;
2604 } else if new_y > scroll_height {
2605 bubble_y = scroll_height - new_y;
2606 node.scroll_offset_mut().y = scroll_height;
2607 } else {
2608 node.scroll_offset_mut().y = new_y;
2609 }
2610
2611 let has_changed = *node.scroll_offset() != initial;
2612
2613 if has_changed {
2614 let layout = *node.final_layout();
2615 let event = BlitzScrollEvent {
2616 scroll_top: node.scroll_offset().y,
2617 scroll_left: node.scroll_offset().x,
2618 scroll_width: layout.scroll_width() as i32,
2619 scroll_height: layout.scroll_height() as i32,
2620 client_width: layout.size.width as i32,
2621 client_height: layout.size.height as i32,
2622 };
2623
2624 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
2625 }
2626
2627 let parent = node.parent;
2628 if has_changed {
2629 self.show_scrollbars(node_id);
2630 }
2631
2632 if bubble_x != 0.0 || bubble_y != 0.0 {
2633 if let Some(parent) = parent {
2634 return self.scroll_node_by_has_changed(parent, bubble_x, bubble_y, dispatch_event)
2635 | has_changed;
2636 } else {
2637 return self.scroll_viewport_by_has_changed(bubble_x, bubble_y) | has_changed;
2638 }
2639 }
2640
2641 has_changed
2642 }
2643
2644 pub fn scroll_viewport_by(&mut self, x: f64, y: f64) {
2645 self.scroll_viewport_by_has_changed(x, y);
2646 }
2647
2648 pub fn scroll_viewport_by_has_changed(&mut self, x: f64, y: f64) -> bool {
2650 let (content_width, content_height) = match self.try_root_element() {
2655 Some(root) => {
2656 let root_layout = root.final_layout();
2657 (
2658 root_layout.size.width.max(root_layout.content_size.width) as f64,
2659 root_layout.size.height.max(root_layout.content_size.height) as f64,
2660 )
2661 }
2662 None => (0.0, 0.0),
2663 };
2664 let new_scroll = (self.viewport_scroll.x - x, self.viewport_scroll.y - y);
2665 let window_width = self.viewport.window_size.0 as f64 / self.viewport.scale() as f64;
2666 let window_height = self.viewport.window_size.1 as f64 / self.viewport.scale() as f64;
2667
2668 let initial = self.viewport_scroll;
2669 self.viewport_scroll.x =
2670 f64::max(0.0, f64::min(new_scroll.0, content_width - window_width));
2671 self.viewport_scroll.y =
2672 f64::max(0.0, f64::min(new_scroll.1, content_height - window_height));
2673
2674 self.viewport_scroll != initial
2675 }
2676
2677 pub fn scroll_by(
2678 &mut self,
2679 anchor_node_id: Option<NodeId>,
2680 scroll_x: f64,
2681 scroll_y: f64,
2682 dispatch_event: &mut dyn FnMut(DomEvent),
2683 ) -> bool {
2684 if let Some(anchor_node_id) = anchor_node_id {
2685 self.scroll_node_by_has_changed(anchor_node_id, scroll_x, scroll_y, dispatch_event)
2686 } else {
2687 self.scroll_viewport_by_has_changed(scroll_x, scroll_y)
2688 }
2689 }
2690
2691 pub fn viewport_scroll(&self) -> crate::Point<f64> {
2692 self.viewport_scroll
2693 }
2694
2695 pub fn set_viewport_scroll(&mut self, scroll: crate::Point<f64>) {
2696 self.viewport_scroll = scroll;
2697 }
2698
2699 pub fn get_fragment_target(&self, fragment: &str) -> Option<NodeId> {
2704 if let Some(node_id) = self.get_element_by_id(fragment) {
2705 return Some(node_id);
2706 }
2707
2708 self.nodes.iter().find_map(|(id, node)| {
2710 let el = node.element_data()?;
2711 (el.name.local == local_name!("a") && el.attr(local_name!("name")) == Some(fragment))
2712 .then_some(id)
2713 })
2714 }
2715
2716 pub fn nearest_scroll_container(&self, node_id: NodeId) -> Option<NodeId> {
2727 let mut current = Some(node_id);
2728 for _ in 0..64 {
2729 let id = current?;
2730 let node = self.nodes.get(id)?;
2731 if node.style().overflow.x.is_scroll_container()
2732 || node.style().overflow.y.is_scroll_container()
2733 {
2734 return Some(id);
2735 }
2736 current = node.parent;
2737 }
2738 None
2739 }
2740
2741 pub fn scroll_nearest_container_by(&mut self, node_id: NodeId, x: f64, y: f64) -> bool {
2742 self.scroll_nearest_container_by_with_events(node_id, x, y, |_| {})
2743 }
2744
2745 pub fn scroll_nearest_container_by_with_events<F: FnMut(DomEvent)>(
2746 &mut self,
2747 node_id: NodeId,
2748 x: f64,
2749 y: f64,
2750 mut dispatch_event: F,
2751 ) -> bool {
2752 let mut current = Some(node_id);
2753 for _ in 0..64 {
2754 let Some(id) = current else { break };
2755 let Some(node) = self.nodes.get(id) else {
2756 break;
2757 };
2758 let scrolls = node.style().overflow.x.is_scroll_container()
2759 || node.style().overflow.y.is_scroll_container();
2760 if scrolls {
2761 self.scroll_node_by(id, x, y, &mut dispatch_event);
2762 return true;
2763 }
2764 current = node.parent;
2765 }
2766 self.scroll_viewport_by(x, y);
2767 false
2768 }
2769
2770 pub fn scroll_to_node(&mut self, node_id: NodeId) {
2771 self.scroll_to_node_with_events(node_id, |_| {});
2772 }
2773
2774 pub fn scroll_to_node_with_events<F: FnMut(DomEvent)>(
2775 &mut self,
2776 node_id: NodeId,
2777 mut dispatch_event: F,
2778 ) {
2779 let mut chain = Vec::new();
2791 let mut current = self.nodes.get(node_id).and_then(|node| node.parent);
2792 while let Some(id) = current {
2793 let Some(node) = self.nodes.get(id) else {
2794 break;
2795 };
2796 let scrolls = node.style().overflow.x.is_scroll_container()
2797 || node.style().overflow.y.is_scroll_container();
2798 if scrolls {
2799 chain.push(id);
2800 }
2801 current = node.parent;
2802 }
2803
2804 for container in chain {
2808 let Some(node) = self.nodes.get(node_id) else {
2809 return;
2810 };
2811 let target = node.absolute_position(0.0, 0.0);
2812 let Some(scroller) = self.nodes.get(container) else {
2813 continue;
2814 };
2815 let box_ = scroller.absolute_position(0.0, 0.0);
2816 let layout = scroller.final_layout();
2817 let dx = f64::from(box_.x - target.x);
2821 let dy = f64::from(box_.y - target.y);
2822 let _ = layout;
2823 self.scroll_node_by(container, dx, dy, &mut dispatch_event);
2824 }
2825
2826 let Some(node) = self.nodes.get(node_id) else {
2829 return;
2830 };
2831 let target = node.absolute_position(0.0, 0.0);
2832 let current = self.viewport_scroll;
2833
2834 let dx = current.x - target.x as f64;
2837 let dy = current.y - target.y as f64;
2838 if let Some(root) = self.try_root_element().map(|element| element.id) {
2839 self.scroll_node_by(root, dx, dy, dispatch_event);
2840 } else {
2841 self.scroll_viewport_by(dx, dy);
2842 }
2843 }
2844
2845 pub fn scroll_to_fragment(&mut self, fragment: &str) -> bool {
2851 let decoded = percent_encoding::percent_decode_str(fragment)
2853 .decode_utf8_lossy()
2854 .into_owned();
2855
2856 if !decoded.is_empty() {
2857 if let Some(node_id) = self.get_fragment_target(&decoded) {
2858 self.scroll_to_node(node_id);
2859 return true;
2860 }
2861 }
2862
2863 if decoded.is_empty() || decoded.eq_ignore_ascii_case("top") {
2866 let current = self.viewport_scroll;
2867 self.scroll_viewport_by(current.x, current.y);
2868 return true;
2869 }
2870
2871 false
2872 }
2873
2874 pub fn get_client_bounding_rect(&self, node_id: NodeId) -> Option<BoundingRect> {
2876 if let Some(rects) = self.inline_fragment_rects(node_id) {
2879 let x0 = rects.iter().map(|r| r.x).fold(f64::INFINITY, f64::min);
2880 let y0 = rects.iter().map(|r| r.y).fold(f64::INFINITY, f64::min);
2881 let x1 = rects
2882 .iter()
2883 .map(|r| r.x + r.width)
2884 .fold(f64::NEG_INFINITY, f64::max);
2885 let y1 = rects
2886 .iter()
2887 .map(|r| r.y + r.height)
2888 .fold(f64::NEG_INFINITY, f64::max);
2889 return match rects.is_empty() {
2890 true => None,
2891 false => Some(BoundingRect {
2892 x: x0,
2893 y: y0,
2894 width: x1 - x0,
2895 height: y1 - y0,
2896 }),
2897 };
2898 }
2899
2900 let node = self.get_node(node_id)?;
2901 if !matches!(
2902 node.data,
2903 NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
2904 ) {
2905 return None;
2906 }
2907 let pos = node.absolute_position(0.0, 0.0);
2908
2909 Some(BoundingRect {
2910 x: pos.x as f64 - self.viewport_scroll.x,
2911 y: pos.y as f64 - self.viewport_scroll.y,
2912 width: node.unrounded_layout().size.width as f64,
2913 height: node.unrounded_layout().size.height as f64,
2914 })
2915 }
2916
2917 pub fn node_client_rects(&self, node_id: NodeId) -> Vec<BoundingRect> {
2922 match self.inline_fragment_rects(node_id) {
2923 Some(rects) => rects,
2924 None => self.get_client_bounding_rect(node_id).into_iter().collect(),
2925 }
2926 }
2927
2928 pub(crate) fn trace_escaped_inline_fragments(&self) {
2942 static TRACE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2943 if !*TRACE.get_or_init(|| std::env::var_os("BLITZ_TRACE_INLINE").is_some()) {
2944 return;
2945 }
2946 let mut reported = 0;
2947 for (id, node) in self.nodes.iter() {
2948 if !node.is_element() {
2949 continue;
2950 }
2951 let Some(rects) = self.inline_fragment_rects(id) else {
2952 continue;
2953 };
2954 let Some(root) = node.inline_root_ancestor() else {
2955 continue;
2956 };
2957 let root_layout = root.final_layout();
2958 let root_pos = root.absolute_position(0.0, 0.0);
2959 let root_right =
2960 root_pos.x as f64 + root_layout.size.width as f64 - self.viewport_scroll.x;
2961 for rect in &rects {
2962 if rect.x + rect.width > root_right + 1.0 {
2963 reported += 1;
2964 if reported <= 12 {
2965 eprintln!(
2966 "escaped-fragment node={id:?} rect=[{:.1},{:.1} {:.1}x{:.1}] \
2967root={:?} root_right={root_right:.1} root_w={:.1} lines={} layout_scale={:.2} vp_scale={:.2} layout_w={:.1}",
2968 rect.x,
2969 rect.y,
2970 rect.width,
2971 rect.height,
2972 root.id,
2973 root_layout.size.width,
2974 root.element_data()
2975 .and_then(|e| e.inline_layout_data.as_ref())
2976 .map(|i| i.layout.len())
2977 .unwrap_or(0),
2978 root.element_data()
2979 .and_then(|e| e.inline_layout_data.as_ref())
2980 .map(|i| i.layout.scale())
2981 .unwrap_or(0.0),
2982 self.viewport.scale(),
2983 root.element_data()
2984 .and_then(|e| e.inline_layout_data.as_ref())
2985 .map(|i| i.layout.width())
2986 .unwrap_or(0.0),
2987 );
2988 }
2989 break;
2990 }
2991 }
2992 }
2993 if reported > 0 {
2994 eprintln!("escaped-fragment total={reported}");
2995 }
2996
2997 let mut narrow = 0;
3002 for (id, node) in self.nodes.iter() {
3003 let Some(inline) = node
3004 .data
3005 .downcast_element()
3006 .and_then(|element| element.inline_layout_data.as_ref())
3007 else {
3008 continue;
3009 };
3010 let box_width = node.final_layout().size.width as f64 * self.viewport.scale() as f64;
3011 let broken_at = inline.layout.width() as f64;
3012 let full = inline.layout.calculate_content_widths().max as f64;
3015 if box_width > 40.0 && broken_at < box_width * 0.6 && full > box_width * 0.9 {
3016 narrow += 1;
3017 if narrow <= 12 {
3018 eprintln!(
3019 "narrow-break node={id:?} broken_at={broken_at:.1} box={box_width:.1} \
3020 max_content={full:.1} lines={} text={:?}",
3021 inline.layout.len(),
3022 inline.text.chars().take(40).collect::<String>(),
3023 );
3024 }
3025 }
3026 }
3027 if narrow > 0 {
3028 eprintln!("narrow-break total={narrow}");
3029 }
3030 }
3031
3032 pub fn inline_fragment_rects(&self, node_id: NodeId) -> Option<Vec<BoundingRect>> {
3033 use parley::PositionedLayoutItem;
3034
3035 let node = self.get_node(node_id)?;
3036
3037 if !node.is_element() || node.flags.is_inline_root() {
3040 return None;
3041 }
3042 let display = node.primary_styles()?.clone_display();
3043 if !(display.outside() == DisplayOutside::Inline && display.inside() == DisplayInside::Flow)
3044 {
3045 return None;
3046 }
3047
3048 let inline_root = node.inline_root_ancestor()?;
3049 let inline_layout = inline_root.element_data()?.inline_layout_data.as_ref()?;
3050 let layout = &inline_layout.layout;
3051 let scale = layout.scale() as f64;
3052
3053 let is_in_target = |mut id: NodeId| -> bool {
3056 loop {
3057 if id == node_id {
3058 return true;
3059 }
3060 if id == inline_root.id {
3061 return false;
3062 }
3063 match self.get_node(id).and_then(|n| n.parent) {
3064 Some(parent) => id = parent,
3065 None => return false,
3066 }
3067 }
3068 };
3069
3070 let root_layout = inline_root.final_layout();
3072 let root_pos = inline_root.absolute_position(0.0, 0.0);
3073 let origin_x = root_pos.x as f64
3074 + (root_layout.padding.left + root_layout.border.left) as f64
3075 - self.viewport_scroll.x;
3076 let origin_y = root_pos.y as f64
3077 + (root_layout.padding.top + root_layout.border.top) as f64
3078 - self.viewport_scroll.y;
3079
3080 let mut rects: Vec<BoundingRect> = Vec::new();
3081 for line in layout.lines() {
3082 let line_metrics = line.metrics();
3083 let mut line_rect: Option<(f64, f64, f64, f64)> = None;
3085 let mut add = |x0: f64, y0: f64, x1: f64, y1: f64| {
3086 line_rect = Some(match line_rect {
3087 Some((lx0, ly0, lx1, ly1)) => {
3088 (lx0.min(x0), ly0.min(y0), lx1.max(x1), ly1.max(y1))
3089 }
3090 None => (x0, y0, x1, y1),
3091 });
3092 };
3093
3094 for item in line.items() {
3095 match item {
3096 PositionedLayoutItem::GlyphRun(glyph_run) => {
3097 if !is_in_target(glyph_run.style().brush.id) {
3098 continue;
3099 }
3100 let x0 = glyph_run.offset() as f64;
3101 let x1 = x0 + glyph_run.advance() as f64;
3102 let y0 = line_metrics.block_min_coord as f64;
3108 let y1 = line_metrics.block_max_coord as f64;
3109 add(x0, y0, x1, y1);
3110 }
3111 PositionedLayoutItem::InlineBox(inline_box) => {
3112 if !is_in_target(NodeId::from_u64(inline_box.id)) {
3113 continue;
3114 }
3115 let x0 = inline_box.x as f64;
3116 let y0 = inline_box.y as f64;
3117 add(
3118 x0,
3119 y0,
3120 x0 + inline_box.width as f64,
3121 y0 + inline_box.height as f64,
3122 );
3123 }
3124 }
3125 }
3126
3127 if let Some((x0, y0, x1, y1)) = line_rect {
3128 rects.push(BoundingRect {
3129 x: origin_x + x0 / scale,
3130 y: origin_y + y0 / scale,
3131 width: (x1 - x0) / scale,
3132 height: (y1 - y0) / scale,
3133 });
3134 }
3135 }
3136
3137 Some(rects)
3138 }
3139
3140 pub fn find_title_node(&self) -> Option<&Node> {
3141 TreeTraverser::new(self)
3142 .find(|node_id| {
3143 let node = &self.nodes[*node_id];
3144 let Some(element) = node.element_data() else {
3145 return false;
3146 };
3147 if element.name.ns != ns!(html) || element.name.local != local_name!("title") {
3148 return false;
3149 }
3150 node.parent
3151 .and_then(|parent_id| self.nodes.get(parent_id))
3152 .and_then(Node::element_data)
3153 .is_some_and(|parent| {
3154 parent.name.ns == ns!(html) && parent.name.local == local_name!("head")
3155 })
3156 })
3157 .map(|node_id| &self.nodes[node_id])
3158 }
3159
3160 pub fn with_text_input(
3161 &mut self,
3162 node_id: NodeId,
3163 cb: impl FnOnce(PlainEditorDriver<TextBrush>),
3164 ) {
3165 let Some(node) = self.nodes.get_mut(node_id) else {
3166 return;
3167 };
3168
3169 if let Some(text_input) = node
3170 .element_data_mut()
3171 .and_then(|el| el.text_input_data_mut())
3172 {
3173 let mut font_ctx = self.font_ctx.lock().unwrap();
3174 let layout_ctx = &mut self.layout_ctx;
3175 let driver = text_input.editor.driver(&mut font_ctx, layout_ctx);
3176 cb(driver)
3177 }
3178 }
3179
3180 pub(crate) fn clamp_text_input_scroll(&mut self, node_id: NodeId) {
3183 let Some(node) = self.nodes.get_mut(node_id) else {
3184 return;
3185 };
3186
3187 let content_box_width = node.final_layout().content_box_width();
3188 let content_box_height = node.final_layout().content_box_height();
3189
3190 if let Some(text_input) = node
3191 .element_data_mut()
3192 .and_then(|el| el.text_input_data_mut())
3193 {
3194 text_input.clamp_scroll_offset(content_box_width, content_box_height);
3195 }
3196 }
3197
3198 pub(crate) fn compute_has_canvas(&self) -> bool {
3199 TreeTraverser::new(self).any(|node_id| {
3200 let node = &self.nodes[node_id];
3201 let Some(element) = node.element_data() else {
3202 return false;
3203 };
3204 if element.name.local == local_name!("canvas") && element.has_attr(local_name!("src")) {
3205 return true;
3206 }
3207
3208 false
3209 })
3210 }
3211
3212 pub fn find_text_position(&self, x: f32, y: f32) -> Option<(NodeId, usize)> {
3218 let hit = self.hit(x, y)?;
3219 let hit_node = self.get_node(hit.node_id)?;
3220 let inline_root = hit_node.inline_root_ancestor()?;
3221 let byte_offset = inline_root.text_offset_at_point(hit.x, hit.y)?;
3222 Some((inline_root.id, byte_offset))
3223 }
3224
3225 pub fn find_text_range(
3231 &self,
3232 x: f32,
3233 y: f32,
3234 granularity: TextGranularity,
3235 ) -> Option<(NodeId, usize, usize)> {
3236 let hit = self.hit(x, y)?;
3237 let hit_node = self.get_node(hit.node_id)?;
3238 let inline_root = hit_node.inline_root_ancestor()?;
3239 let range = inline_root.text_range_at_point(hit.x, hit.y, granularity)?;
3240 Some((inline_root.id, range.start, range.end))
3241 }
3242
3243 pub fn set_text_selection(
3245 &mut self,
3246 anchor_node: NodeId,
3247 anchor_offset: usize,
3248 focus_node: NodeId,
3249 focus_offset: usize,
3250 ) {
3251 self.text_selection =
3252 TextSelection::new(anchor_node, anchor_offset, focus_node, focus_offset);
3253
3254 if let (Some(parent), Some(idx)) = self.anonymous_block_location(anchor_node) {
3256 self.text_selection
3257 .anchor
3258 .set_anonymous(parent, idx, anchor_offset);
3259 }
3260 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
3261 self.text_selection
3262 .focus
3263 .set_anonymous(parent, idx, focus_offset);
3264 }
3265 }
3266
3267 fn anonymous_block_location(&self, node_id: NodeId) -> (Option<NodeId>, Option<usize>) {
3270 let Some(node) = self.get_node(node_id) else {
3271 return (None, None);
3272 };
3273
3274 if !node.is_anonymous() {
3275 return (None, None);
3276 }
3277
3278 let Some(parent_id) = node.parent else {
3279 return (None, None);
3280 };
3281
3282 let Some(parent) = self.get_node(parent_id) else {
3283 return (Some(parent_id), None);
3284 };
3285
3286 let layout_children = parent.layout_children.borrow();
3287 let Some(children) = layout_children.as_ref() else {
3288 return (Some(parent_id), None);
3289 };
3290
3291 let mut anon_index = 0;
3293 for &child_id in children.iter() {
3294 if child_id == node_id {
3295 return (Some(parent_id), Some(anon_index));
3296 }
3297 if self.get_node(child_id).is_some_and(|n| n.is_anonymous()) {
3298 anon_index += 1;
3299 }
3300 }
3301
3302 (Some(parent_id), None)
3303 }
3304
3305 pub fn clear_text_selection(&mut self) {
3307 self.text_selection.clear();
3308 }
3309
3310 pub fn update_selection_focus(&mut self, focus_node: NodeId, focus_offset: usize) {
3312 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
3314 self.text_selection
3315 .focus
3316 .set_anonymous(parent, idx, focus_offset);
3317 } else {
3318 self.text_selection.set_focus(focus_node, focus_offset);
3319 }
3320 }
3321
3322 pub fn extend_text_selection_to_point(&mut self, x: f32, y: f32) -> bool {
3325 if !self.text_selection.anchor.is_some() {
3326 return false;
3327 }
3328
3329 if let Some((node, offset)) = self.find_text_position(x, y) {
3330 self.update_selection_focus(node, offset);
3331 self.shell_provider.request_redraw();
3332 true
3333 } else {
3334 false
3335 }
3336 }
3337
3338 fn find_anonymous_block_by_index(
3340 &self,
3341 parent_id: NodeId,
3342 target_index: usize,
3343 ) -> Option<NodeId> {
3344 let parent = self.get_node(parent_id)?;
3345 let layout_children = parent.layout_children.borrow();
3346 let children = layout_children.as_ref()?;
3347
3348 children
3349 .iter()
3350 .filter(|&&child_id| self.get_node(child_id).is_some_and(|n| n.is_anonymous()))
3351 .nth(target_index)
3352 .copied()
3353 }
3354
3355 pub fn has_text_selection(&self) -> bool {
3357 self.text_selection.is_active()
3358 }
3359
3360 pub fn get_selected_text(&self) -> Option<String> {
3362 let ranges = self.get_text_selection_ranges();
3363 if ranges.is_empty() {
3364 return None;
3365 }
3366
3367 let mut result = String::new();
3368 for (node_id, start, end) in &ranges {
3369 let node = self.get_node(*node_id)?;
3370 let element_data = node.element_data()?;
3371 let inline_layout = element_data.inline_layout_data.as_ref()?;
3372
3373 if *end > inline_layout.text.len() {
3374 continue;
3375 }
3376
3377 if !result.is_empty() {
3378 result.push(' ');
3379 }
3380 result.push_str(&inline_layout.text[*start..*end]);
3381 }
3382
3383 if result.is_empty() {
3384 None
3385 } else {
3386 Some(result)
3387 }
3388 }
3389
3390 pub fn get_text_selection_ranges(&self) -> Vec<(NodeId, usize, usize)> {
3393 let lookup = |parent_id, idx| self.find_anonymous_block_by_index(parent_id, idx);
3394
3395 let anchor_node = match self.text_selection.anchor.resolve_node_id(lookup) {
3396 Some(id) => id,
3397 None => return Vec::new(),
3398 };
3399 let focus_node = match self.text_selection.focus.resolve_node_id(lookup) {
3400 Some(id) => id,
3401 None => return Vec::new(),
3402 };
3403
3404 let node_is_in_doc = |node_id: NodeId| {
3407 self.nodes
3408 .get(node_id)
3409 .is_some_and(|node| node.flags.is_in_document())
3410 };
3411 if !node_is_in_doc(anchor_node) || !node_is_in_doc(focus_node) {
3412 return Vec::new();
3413 }
3414
3415 if anchor_node == focus_node {
3417 let start = self
3418 .text_selection
3419 .anchor
3420 .offset
3421 .min(self.text_selection.focus.offset);
3422 let end = self
3423 .text_selection
3424 .anchor
3425 .offset
3426 .max(self.text_selection.focus.offset);
3427
3428 if start == end {
3429 return Vec::new();
3430 }
3431 return vec![(anchor_node, start, end)];
3432 }
3433
3434 let inline_roots = self.collect_inline_roots_in_range(anchor_node, focus_node);
3436 if inline_roots.is_empty() {
3437 return Vec::new();
3438 }
3439
3440 let first_in_roots = inline_roots[0];
3443
3444 let (first_node, first_offset, last_node, last_offset) =
3445 if first_in_roots == anchor_node || (first_in_roots != focus_node) {
3446 (
3448 anchor_node,
3449 self.text_selection.anchor.offset,
3450 focus_node,
3451 self.text_selection.focus.offset,
3452 )
3453 } else {
3454 (
3456 focus_node,
3457 self.text_selection.focus.offset,
3458 anchor_node,
3459 self.text_selection.anchor.offset,
3460 )
3461 };
3462
3463 let mut ranges = Vec::with_capacity(inline_roots.len());
3464
3465 for &node_id in &inline_roots {
3466 let Some(node) = self.get_node(node_id) else {
3467 continue;
3468 };
3469 let Some(element_data) = node.element_data() else {
3470 continue;
3471 };
3472 let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
3473 continue;
3474 };
3475
3476 let text_len = inline_layout.text.len();
3477
3478 if node_id == first_node && node_id == last_node {
3479 let start = first_offset.min(last_offset);
3480 let end = first_offset.max(last_offset);
3481 if start < end && end <= text_len {
3482 ranges.push((node_id, start, end));
3483 }
3484 } else if node_id == first_node {
3485 if first_offset < text_len {
3486 ranges.push((node_id, first_offset, text_len));
3487 }
3488 } else if node_id == last_node {
3489 if last_offset > 0 && last_offset <= text_len {
3490 ranges.push((node_id, 0, last_offset));
3491 }
3492 } else if text_len > 0 {
3493 ranges.push((node_id, 0, text_len));
3494 }
3495 }
3496
3497 ranges
3498 }
3499}
3500
3501#[derive(Debug, Clone, Copy, PartialEq)]
3502pub struct BoundingRect {
3503 pub x: f64,
3504 pub y: f64,
3505 pub width: f64,
3506 pub height: f64,
3507}
3508
3509impl AsRef<BaseDocument> for BaseDocument {
3510 fn as_ref(&self) -> &BaseDocument {
3511 self
3512 }
3513}
3514
3515impl AsMut<BaseDocument> for BaseDocument {
3516 fn as_mut(&mut self) -> &mut BaseDocument {
3517 self
3518 }
3519}
3520
3521#[cfg(test)]
3522mod hover_state_tests {
3523 use super::*;
3524 use crate::{Attribute, qual_name};
3525 use blitz_traits::shell::ColorScheme;
3526
3527 fn make_doc() -> (BaseDocument, NodeId) {
3534 let mut doc = BaseDocument::new(DocumentConfig {
3535 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3536 ..Default::default()
3537 });
3538 let root_id = doc.root_node().id;
3539 let style = |value: &str| Attribute {
3540 name: qual_name!("style"),
3541 value: value.into(),
3542 };
3543
3544 let mut mutator = doc.mutate();
3545 let html = mutator.create_element(qual_name!("html"), vec![]);
3546 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
3547 let container = mutator.create_element(qual_name!("div"), vec![style("width:300px")]);
3548 let text = mutator.create_text_node("some text");
3549 let block = mutator.create_element(qual_name!("div"), vec![style("height:50px")]);
3550 mutator.append_children(container, &[text, block]);
3551 mutator.append_children(body, &[container]);
3552 mutator.append_children(html, &[body]);
3553 mutator.append_children(root_id, &[html]);
3554 drop(mutator);
3555
3556 doc.resolve(0.0);
3557 (doc, container)
3558 }
3559
3560 fn text_has_size(doc: &BaseDocument, container: NodeId) -> bool {
3564 doc.nodes[container].final_layout().size.height > 50.0
3565 }
3566
3567 #[test]
3573 fn hovering_text_in_anonymous_block_reports_text_cursor() {
3574 let (mut doc, container) = make_doc();
3575 if !text_has_size(&doc, container) {
3576 eprintln!("skipping: no usable font (text measures 0x0)");
3577 return;
3578 }
3579
3580 doc.set_hover_to(5.0, 8.0);
3581 assert!(doc.hover_node_is_text, "expected a text hit");
3582 let hit_id = doc.hover_hit_node_id.expect("expected a hit node");
3583 assert!(
3584 doc.nodes[hit_id].is_anonymous(),
3585 "expected the hit node to be the anonymous inline root"
3586 );
3587 assert_eq!(
3588 doc.get_hover_node_id(),
3589 Some(container),
3590 "expected the stored hover target to be the containing element"
3591 );
3592 assert_eq!(doc.get_cursor(), Some(CursorIcon::Text));
3593 }
3594
3595 #[test]
3596 fn semantic_hover_keeps_the_resolved_node_instead_of_hit_testing_again() {
3597 let (mut doc, container) = make_doc();
3598
3599 doc.set_hover_to_node(container, 350.0, 250.0);
3603
3604 assert_eq!(doc.get_hover_node_id(), Some(container));
3605 assert_eq!(doc.hover_hit_node_id, Some(container));
3606
3607 doc.resolve(0.0);
3608 assert_eq!(
3609 doc.get_hover_node_id(),
3610 Some(container),
3611 "a resolve must not turn semantic identity back into a coordinate hit"
3612 );
3613 }
3614
3615 #[test]
3618 fn hovering_anonymous_block_whitespace_reports_default_cursor() {
3619 let (mut doc, container) = make_doc();
3620 if !text_has_size(&doc, container) {
3621 eprintln!("skipping: no usable font (text measures 0x0)");
3622 return;
3623 }
3624
3625 doc.set_hover_to(250.0, 8.0);
3626 assert!(!doc.hover_node_is_text);
3627 assert_eq!(doc.get_hover_node_id(), Some(container));
3628 assert_eq!(doc.get_cursor(), Some(CursorIcon::Default));
3629 }
3630}
3631
3632#[cfg(test)]
3633mod control_scroll_tests {
3634 use super::*;
3635 use crate::{Attribute, qual_name};
3636 use blitz_traits::shell::ColorScheme;
3637
3638 #[test]
3639 fn controlled_scroll_dispatches_the_dom_scroll_event() {
3640 let mut doc = BaseDocument::new(DocumentConfig {
3641 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3642 ..Default::default()
3643 });
3644 let root_id = doc.root_node().id;
3645 let style = |value: &str| Attribute {
3646 name: qual_name!("style"),
3647 value: value.into(),
3648 };
3649
3650 let mut mutator = doc.mutate();
3651 let html = mutator.create_element(qual_name!("html"), vec![]);
3652 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
3653 let scroller = mutator.create_element(
3654 qual_name!("div"),
3655 vec![style("width:200px;height:100px;overflow-y:scroll")],
3656 );
3657 let spacer = mutator.create_element(qual_name!("div"), vec![style("height:400px")]);
3658 let target = mutator.create_element(qual_name!("button"), vec![style("height:40px")]);
3659 mutator.append_children(scroller, &[spacer, target]);
3660 mutator.append_children(body, &[scroller]);
3661 mutator.append_children(html, &[body]);
3662 mutator.append_children(root_id, &[html]);
3663 drop(mutator);
3664 doc.resolve(0.0);
3665
3666 doc.nodes[html].final_layout_mut().size.height = 300.0;
3670 doc.nodes[html].final_layout_mut().content_size.height = 600.0;
3671 doc.nodes[target].final_layout_mut().location.y = 400.0;
3672
3673 let mut events = Vec::new();
3674 doc.scroll_to_node_with_events(target, |event| events.push(event));
3675
3676 assert!(doc.viewport_scroll.y > 0.0);
3677 assert!(
3678 events
3679 .iter()
3680 .any(|event| { event.target == html && event.name() == "scroll" })
3681 );
3682 }
3683}
3684
3685#[cfg(test)]
3686mod font_face_override_tests {
3687 use super::*;
3688 use crate::net::{FontFaceOverrides, Resource, ResourceLoadResponse};
3689
3690 #[test]
3706 fn font_face_overrides_alias_family_name() {
3707 const ALIAS: &str = "AliasedFamily";
3708
3709 let mut document = BaseDocument::new(DocumentConfig::default());
3710
3711 {
3713 let mut ctx = document.font_ctx.lock().unwrap();
3714 assert!(
3715 ctx.collection.family_id(ALIAS).is_none(),
3716 "alias must not exist before registration",
3717 );
3718 }
3719
3720 let response = ResourceLoadResponse {
3725 request_id: 0,
3726 node_id: None,
3727 resolved_url: Some(String::from("test://aliased-family")),
3728 result: Ok(Resource::Font(
3729 blitz_traits::net::Bytes::from_static(crate::BULLET_FONT),
3730 FontFaceOverrides {
3731 family_name: Some(String::from(ALIAS)),
3732 weight: Some(800.0),
3733 style: Some(parley::fontique::FontStyle::Italic),
3734 },
3735 )),
3736 };
3737 document.load_resource(response);
3738
3739 let mut ctx = document.font_ctx.lock().unwrap();
3742 let family_id = ctx
3743 .collection
3744 .family_id(ALIAS)
3745 .expect("CSS-declared family name should be registered as a family alias");
3746 let resolved_name = ctx
3747 .collection
3748 .family_name(family_id)
3749 .expect("family id should resolve back to a name");
3750 assert_eq!(
3751 resolved_name, ALIAS,
3752 "registered family should report the CSS-declared name, \
3753 not the font file's internal `name` table entry",
3754 );
3755 }
3756}