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::Image(_kind, width, height, image_data) => {
1526 let image = ImageData::Raster(RasterImageData::new(width, height, image_data));
1528
1529 let Some(url) = res.resolved_url.as_ref() else {
1530 return;
1531 };
1532
1533 self.apply_loaded_image(url, image);
1534 }
1535 #[cfg(feature = "svg")]
1536 Resource::Svg(_kind, svg) => {
1537 let image = ImageData::Svg(svg);
1539
1540 let Some(url) = res.resolved_url.as_ref() else {
1541 return;
1542 };
1543
1544 self.apply_loaded_image(url, image);
1545 }
1546 Resource::DocumentSrc(html) => {
1547 let Some(node_id) = res.node_id else {
1548 return;
1549 };
1550 self.apply_iframe_html(node_id, res.request_id, res.resolved_url, &html);
1551 }
1552 Resource::Font(bytes, overrides) => {
1553 let font = Blob::new(Arc::new(bytes));
1554
1555 let weight_override = overrides.weight.map(parley::fontique::FontWeight::new);
1561 let info_override = parley::fontique::FontInfoOverride {
1562 family_name: overrides.family_name.as_deref(),
1563 weight: weight_override,
1564 style: overrides.style,
1565 ..Default::default()
1566 };
1567
1568 let mut global_font_ctx = self.font_ctx.lock().unwrap();
1570 global_font_ctx
1571 .collection
1572 .register_fonts(font.clone(), Some(info_override));
1573
1574 #[cfg(feature = "parallel-construct")]
1575 {
1576 rayon::broadcast(|_ctx| {
1577 let mut font_ctx = self
1578 .thread_font_contexts
1579 .get_or(|| RefCell::new(Box::new(global_font_ctx.clone())))
1580 .borrow_mut();
1581 font_ctx
1582 .collection
1583 .register_fonts(font.clone(), Some(info_override));
1584 });
1585 }
1586 drop(global_font_ctx);
1587
1588 self.invalidate_inline_contexts();
1590 }
1591 Resource::None => {
1592 }
1594 }
1595 }
1596
1597 fn apply_loaded_image(&mut self, url: &str, image: ImageData) {
1600 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1602
1603 #[cfg(feature = "tracing")]
1604 tracing::info!(
1605 "Image {url} loaded, applying to {} nodes",
1606 waiting_nodes.len()
1607 );
1608
1609 self.image_cache.insert(url.to_string(), image.clone());
1611
1612 for (node_id, image_type) in waiting_nodes {
1614 let Some(node) = self.get_node_mut(node_id) else {
1615 continue;
1616 };
1617
1618 match image_type {
1619 ImageType::Image => {
1620 node.element_data_mut().unwrap().special_data =
1621 SpecialElementData::Image(Box::new(image.clone()));
1622
1623 node.cache_mut().clear();
1625 node.insert_damage(ALL_DAMAGE);
1626 }
1627 ImageType::Background(idx) | ImageType::Mask(idx) => {
1628 let layer_image = node.element_data_mut().and_then(|el| {
1629 let images = match image_type {
1630 ImageType::Background(_) => &mut el.background_images,
1631 ImageType::Mask(_) => &mut el.mask_images,
1632 ImageType::Image => unreachable!(),
1633 };
1634 images.get_mut(idx)
1635 });
1636 if let Some(Some(layer_image)) = layer_image {
1637 layer_image.status = Status::Ok;
1638 layer_image.image = image.clone();
1639 }
1640 }
1641 }
1642 }
1643 }
1644
1645 pub fn snapshot_node(&mut self, node_id: NodeId) {
1646 let node = &mut self.nodes[node_id];
1647
1648 let has_been_styled = node.primary_styles().is_some();
1653 if !has_been_styled {
1654 return;
1655 }
1656
1657 let opaque_node_id = TNode::opaque(&&*node);
1658 node.set_has_snapshot(true);
1659 node.snapshot_handled()
1660 .store(false, std::sync::atomic::Ordering::SeqCst);
1661
1662 if let Some(_existing_snapshot) = self.snapshots.get_mut(&opaque_node_id) {
1664 } else {
1667 let attrs: Option<Vec<_>> = node.attrs().map(|attrs| {
1668 attrs
1669 .iter()
1670 .map(|attr| {
1671 let ident = AttrIdentifier {
1672 local_name: GenericAtomIdent(attr.name.local.clone()),
1673 name: GenericAtomIdent(attr.name.local.clone()),
1674 namespace: GenericAtomIdent(attr.name.ns.clone()),
1675 prefix: None,
1676 };
1677
1678 let value = if attr.name.local == local_name!("id") {
1679 AttrValue::Atom(Atom::from(&*attr.value))
1680 } else if attr.name.local == local_name!("class") {
1681 let classes = attr
1682 .value
1683 .split_ascii_whitespace()
1684 .map(Atom::from)
1685 .collect();
1686 AttrValue::TokenList(OnceLock::from(attr.value.to_string()), classes)
1692 } else {
1693 AttrValue::String(attr.value.to_string())
1694 };
1695
1696 (ident, value)
1697 })
1698 .collect()
1699 });
1700
1701 let changed_attrs = attrs
1702 .as_ref()
1703 .map(|attrs| attrs.iter().map(|attr| attr.0.name.clone()).collect())
1704 .unwrap_or_default();
1705
1706 self.snapshots.insert(
1707 opaque_node_id,
1708 ServoElementSnapshot {
1709 state: Some(*node.element_state()),
1710 attrs,
1711 changed_attrs,
1712 class_changed: true,
1713 id_changed: true,
1714 other_attributes_changed: true,
1715 },
1716 );
1717 }
1718 }
1719
1720 pub fn snapshot_node_and(&mut self, node_id: NodeId, cb: impl FnOnce(&mut Node)) {
1727 if !self.nodes.contains_key(node_id) {
1728 return;
1729 }
1730 self.snapshot_node(node_id);
1731 cb(&mut self.nodes[node_id]);
1732 }
1733
1734 pub fn hit(&self, x: f32, y: f32) -> Option<HitResult> {
1736 self.hit_with_scrollbar(x, y).0
1737 }
1738
1739 pub fn nearest_non_anonymous_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
1752 let mut node = self.get_node(node_id)?;
1756 loop {
1757 let parent = match node.parent {
1758 Some(parent_id) => self.get_node(parent_id)?,
1759 None => return Some(node.id),
1760 };
1761 if !node.is_anonymous() && !parent.is_anonymous() {
1762 return Some(node.id);
1763 }
1764 node = parent;
1765 }
1766 }
1767
1768 pub fn focus_next_node(&mut self) -> Option<NodeId> {
1769 let focussed_node_id = self.get_focussed_node_id()?;
1770 let id = self.next_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
1771 self.set_focus_to(id);
1772 Some(id)
1773 }
1774
1775 pub fn focus_prev_node(&mut self) -> Option<NodeId> {
1777 let focussed_node_id = self.get_focussed_node_id()?;
1778 let id = self.prev_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
1779 self.set_focus_to(id);
1780 Some(id)
1781 }
1782
1783 pub fn clear_focus(&mut self) {
1785 if let Some(id) = self.focus_node_id {
1786 let shell_provider = self.shell_provider.clone();
1787 self.snapshot_node_and(id, |node| node.blur(shell_provider));
1788 self.focus_node_id = None;
1789 }
1790 }
1791
1792 pub fn set_mousedown_node_id(&mut self, node_id: Option<NodeId>) {
1793 self.mousedown_node_id = node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
1794 }
1795 pub fn set_focus_to(&mut self, focus_node_id: NodeId) -> bool {
1796 let Some(focus_node_id) = self.nearest_non_anonymous_ancestor(focus_node_id) else {
1797 return false;
1798 };
1799 if Some(focus_node_id) == self.focus_node_id {
1800 return false;
1801 }
1802
1803 #[cfg(feature = "tracing")]
1804 tracing::info!("Focussed node {focus_node_id}");
1805
1806 let shell_provider = self.shell_provider.clone();
1807
1808 if let Some(id) = self.focus_node_id {
1810 self.snapshot_node_and(id, |node| node.blur(shell_provider.clone()));
1811 }
1812
1813 self.snapshot_node_and(focus_node_id, |node| node.focus(shell_provider));
1815
1816 self.focus_node_id = Some(focus_node_id);
1817
1818 true
1819 }
1820
1821 pub fn active_node(&mut self) -> bool {
1822 let Some(hover_node_id) = self.get_hover_node_id() else {
1823 return false;
1824 };
1825
1826 if let Some(active_node_id) = self.active_node_id {
1827 if active_node_id == hover_node_id {
1828 return true;
1829 }
1830 self.unactive_node();
1831 }
1832
1833 debug_assert!(
1835 self.get_node(hover_node_id)
1836 .is_some_and(|node| !node.is_anonymous()),
1837 "interaction state must reference DOM nodes, not layout-generated nodes"
1838 );
1839 let active_node_id = Some(hover_node_id);
1840
1841 let node_path = self.maybe_node_layout_ancestors(active_node_id);
1842 for &id in node_path.iter() {
1843 self.snapshot_node_and(id, |node| node.active());
1844 }
1845
1846 self.active_node_id = active_node_id;
1847
1848 true
1849 }
1850
1851 pub fn unactive_node(&mut self) -> bool {
1852 let Some(active_node_id) = self.active_node_id.take() else {
1853 return false;
1854 };
1855
1856 let node_path = self.maybe_node_layout_ancestors(Some(active_node_id));
1857 for &id in node_path.iter() {
1858 self.snapshot_node_and(id, |node| node.unactive());
1859 }
1860
1861 true
1862 }
1863
1864 pub fn hovered_scrollbar(&self) -> Option<crate::node::ScrollbarRef> {
1866 self.hovered_scrollbar
1867 }
1868
1869 pub fn scrollbar_drag_target(&self) -> Option<crate::node::ScrollbarRef> {
1871 match &self.drag_mode {
1872 DragMode::ScrollbarDrag(state) => Some(state.scrollbar),
1873 _ => None,
1874 }
1875 }
1876
1877 pub fn scrollbar_opacity(&self, node_id: NodeId) -> f32 {
1882 let interacting = |scrollbar: &crate::node::ScrollbarRef| scrollbar.node_id == node_id;
1883 if self.hovered_scrollbar.as_ref().is_some_and(interacting)
1884 || self
1885 .scrollbar_drag_target()
1886 .as_ref()
1887 .is_some_and(interacting)
1888 {
1889 return 1.0;
1890 }
1891 self.scrollbar_activity.get(&node_id).map_or(1.0, |last| {
1892 crate::node::scrollbar::opacity_at(last.elapsed())
1893 })
1894 }
1895
1896 pub(crate) fn show_scrollbars(&mut self, node_id: NodeId) {
1899 if cfg!(feature = "scrollbars") {
1900 self.scrollbar_activity.insert(node_id, Instant::now());
1901 }
1902 }
1903
1904 fn scrollbars_animating(&self) -> bool {
1907 use crate::node::scrollbar::{FADE_DELAY, FADE_DURATION};
1908 self.scrollbar_activity
1909 .values()
1910 .any(|last| last.elapsed() < FADE_DELAY + FADE_DURATION)
1911 }
1912
1913 pub(crate) fn hit_with_scrollbar(
1917 &self,
1918 x: f32,
1919 y: f32,
1920 ) -> (Option<HitResult>, Option<crate::node::ScrollbarRef>) {
1921 if TDocument::as_node(&self.root_node())
1922 .first_element_child()
1923 .is_none()
1924 {
1925 #[cfg(feature = "tracing")]
1926 tracing::warn!("No DOM - not resolving hit test");
1927 return (None, None);
1928 }
1929 let mut scrollbar = None;
1930 let hit = self
1931 .root_element()
1932 .hit_inner(x, y, self.viewport().scale_f64(), &mut scrollbar);
1933 (hit, scrollbar)
1934 }
1935
1936 pub fn set_hover_to(&mut self, x: f32, y: f32) -> bool {
1937 self.semantic_hover_node_id = None;
1938 self.last_client_pointer_position = Some(taffy::Point {
1942 x: x - self.viewport_scroll.x as f32,
1943 y: y - self.viewport_scroll.y as f32,
1944 });
1945
1946 let (hit, hovered_scrollbar) = self.hit_with_scrollbar(x, y);
1947 let hovered_scrollbar =
1950 hovered_scrollbar.filter(|scrollbar| self.scrollbar_opacity(scrollbar.node_id) > 0.0);
1951 let scrollbar_changed = hovered_scrollbar != self.hovered_scrollbar;
1955 if scrollbar_changed {
1956 for scrollbar in [self.hovered_scrollbar, hovered_scrollbar]
1959 .into_iter()
1960 .flatten()
1961 {
1962 self.show_scrollbars(scrollbar.node_id);
1963 }
1964 }
1965 self.hovered_scrollbar = hovered_scrollbar;
1966
1967 let hit_node_id = hit.map(|hit| hit.node_id);
1972 let hover_node_id = hit_node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
1973 let new_is_text = hit.map(|hit| hit.is_text).unwrap_or(false);
1974
1975 self.apply_hover_target(hit_node_id, hover_node_id, new_is_text, scrollbar_changed)
1976 }
1977
1978 pub fn set_hover_to_node(&mut self, node_id: NodeId, x: f32, y: f32) -> bool {
1986 self.semantic_hover_node_id = Some(node_id);
1987 self.last_client_pointer_position = Some(taffy::Point {
1988 x: x - self.viewport_scroll.x as f32,
1989 y: y - self.viewport_scroll.y as f32,
1990 });
1991
1992 let hovered_scrollbar = self.hovered_scrollbar.take();
1993 let scrollbar_changed = hovered_scrollbar.is_some();
1994 if let Some(scrollbar) = hovered_scrollbar {
1995 self.show_scrollbars(scrollbar.node_id);
1996 }
1997 let hover_node_id = self.nearest_non_anonymous_ancestor(node_id);
1998 self.apply_hover_target(Some(node_id), hover_node_id, false, scrollbar_changed)
1999 }
2000
2001 fn apply_hover_target(
2002 &mut self,
2003 hit_node_id: Option<NodeId>,
2004 hover_node_id: Option<NodeId>,
2005 new_is_text: bool,
2006 scrollbar_changed: bool,
2007 ) -> bool {
2008 let hit_changed =
2009 hit_node_id != self.hover_hit_node_id || new_is_text != self.hover_node_is_text;
2010 self.hover_hit_node_id = hit_node_id;
2011 self.hover_node_is_text = new_is_text;
2012
2013 if hover_node_id == self.hover_node_id {
2015 if hit_changed {
2016 self.shell_provider.set_cursor(self.get_cursor());
2020 }
2021 return scrollbar_changed;
2022 }
2023
2024 let old_node_path = self.maybe_node_layout_ancestors(self.hover_node_id);
2025 let new_node_path = self.maybe_node_layout_ancestors(hover_node_id);
2026 let same_count = old_node_path
2027 .iter()
2028 .zip(&new_node_path)
2029 .take_while(|(o, n)| o == n)
2030 .count();
2031 for &id in old_node_path.iter().skip(same_count) {
2032 self.snapshot_node_and(id, |node| node.unhover());
2033 }
2034 for &id in new_node_path.iter().skip(same_count) {
2035 self.snapshot_node_and(id, |node| node.hover());
2036 }
2037
2038 self.hover_node_id = hover_node_id;
2039
2040 self.shell_provider.set_cursor(self.get_cursor());
2042
2043 self.shell_provider.request_redraw();
2045
2046 true
2047 }
2048
2049 pub fn clear_hover(&mut self) -> bool {
2050 self.last_client_pointer_position = None;
2053 self.semantic_hover_node_id = None;
2054 self.hover_hit_node_id = None;
2055
2056 let Some(hover_node_id) = self.hover_node_id else {
2057 return false;
2058 };
2059
2060 let old_node_path = self.maybe_node_layout_ancestors(Some(hover_node_id));
2061 for &id in old_node_path.iter() {
2062 self.snapshot_node_and(id, |node| node.unhover());
2063 }
2064
2065 self.hover_node_id = None;
2066 self.hover_node_is_text = false;
2067
2068 self.shell_provider.set_cursor(self.get_cursor());
2070
2071 self.shell_provider.request_redraw();
2073
2074 true
2075 }
2076
2077 pub fn refresh_hover(&mut self) -> bool {
2083 if let Some(node_id) = self.semantic_hover_node_id {
2084 if self.get_node(node_id).is_some() {
2085 let hover_node_id = self.nearest_non_anonymous_ancestor(node_id);
2086 return self.apply_hover_target(Some(node_id), hover_node_id, false, false);
2087 }
2088 self.semantic_hover_node_id = None;
2089 }
2090 let Some(pos) = self.last_client_pointer_position else {
2091 return false;
2092 };
2093 let x = pos.x + self.viewport_scroll.x as f32;
2094 let y = pos.y + self.viewport_scroll.y as f32;
2095 self.set_hover_to(x, y)
2096 }
2097
2098 pub fn get_hover_node_id(&self) -> Option<NodeId> {
2099 self.hover_node_id
2100 }
2101
2102 pub fn get_mousedown_node_id(&self) -> Option<NodeId> {
2103 self.mousedown_node_id
2104 }
2105
2106 pub fn set_viewport(&mut self, viewport: Viewport) {
2107 let scale_has_changed = viewport.scale_f64() != self.viewport.scale_f64();
2108 self.viewport = viewport;
2109 self.set_stylist_device(make_device(
2110 &self.viewport,
2111 self.media_type.clone(),
2112 self.font_ctx.clone(),
2113 ));
2114 self.scroll_viewport_by(0.0, 0.0); if scale_has_changed {
2117 self.invalidate_inline_contexts();
2118 self.shell_provider.request_redraw();
2119 }
2120 }
2121
2122 pub fn media_type(&self) -> &MediaType {
2124 &self.media_type
2125 }
2126
2127 pub fn set_media_type(&mut self, media_type: MediaType) {
2130 if self.media_type == media_type {
2131 return;
2132 }
2133 self.media_type = media_type;
2134 self.set_stylist_device(make_device(
2135 &self.viewport,
2136 self.media_type.clone(),
2137 self.font_ctx.clone(),
2138 ));
2139 }
2140
2141 pub fn viewport(&self) -> &Viewport {
2142 &self.viewport
2143 }
2144
2145 pub fn viewport_mut(&mut self) -> ViewportMut<'_> {
2146 ViewportMut::new(self)
2147 }
2148
2149 pub fn zoom_by(&mut self, increment: f32) {
2150 *self.viewport.zoom_mut() += increment;
2151 self.set_viewport(self.viewport.clone());
2152 }
2153
2154 pub fn zoom_to(&mut self, zoom: f32) {
2155 *self.viewport.zoom_mut() = zoom;
2156 self.set_viewport(self.viewport.clone());
2157 }
2158
2159 pub fn get_viewport(&self) -> Viewport {
2160 self.viewport.clone()
2161 }
2162
2163 pub fn incremental_layout(&self) -> bool {
2165 self.incremental_layout
2166 }
2167
2168 pub fn set_incremental_layout(&mut self, enabled: bool) {
2170 self.incremental_layout = enabled;
2171 }
2172
2173 pub fn devtools(&self) -> &DevtoolSettings {
2174 &self.devtool_settings
2175 }
2176
2177 pub fn devtools_mut(&mut self) -> &mut DevtoolSettings {
2178 &mut self.devtool_settings
2179 }
2180
2181 pub fn subdoc(&self, node_id: NodeId) -> Option<&dyn Document> {
2182 self.get_node(node_id)
2183 .and_then(|node| node.element_data())
2184 .and_then(|el| el.sub_doc_data())
2185 }
2186
2187 pub fn subdoc_mut(&mut self, node_id: NodeId) -> Option<&mut dyn Document> {
2188 self.get_node_mut(node_id)
2189 .and_then(|node| node.element_data_mut())
2190 .and_then(|el| el.sub_doc_data_mut())
2191 }
2192
2193 pub fn is_animating(&self) -> bool {
2194 #[cfg(feature = "custom-widget")]
2195 let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
2196 self.nodes[node_id]
2197 .element_data()
2198 .and_then(|el| el.custom_widget_data())
2199 .is_some_and(|data| data.widget.requires_redraw())
2200 });
2201 #[cfg(not(feature = "custom-widget"))]
2202 let custom_widget_is_animating = false;
2203
2204 let animating = self.has_canvas
2205 | self.has_active_animations
2206 | (self.subdoc_animation_pacing != AnimationPacing::Idle)
2207 | custom_widget_is_animating
2208 | (self.scroll_animation != ScrollAnimationState::None)
2209 | self.scrollbars_animating();
2210
2211 if animating && crate::debug::animation_reasons_enabled() {
2212 crate::debug::report_animation_reasons(
2213 self.id(),
2214 self.has_canvas,
2215 self.has_active_animations,
2216 self.subdoc_animation_pacing != AnimationPacing::Idle,
2217 custom_widget_is_animating,
2218 self.scroll_animation != ScrollAnimationState::None,
2219 self.scrollbars_animating(),
2220 self.animating_node_names().as_deref(),
2221 );
2222 }
2223
2224 animating
2225 }
2226
2227 pub fn animation_pacing(&self) -> AnimationPacing {
2232 let focused_text_input = self.focus_node_id.is_some_and(|node_id| {
2233 self.nodes
2234 .get(node_id)
2235 .and_then(|node| node.element_data())
2236 .is_some_and(|element| element.text_input_data().is_some())
2237 });
2238 #[cfg(feature = "custom-widget")]
2239 let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
2240 self.nodes[node_id]
2241 .element_data()
2242 .and_then(|el| el.custom_widget_data())
2243 .is_some_and(|data| data.widget.requires_redraw())
2244 });
2245 #[cfg(not(feature = "custom-widget"))]
2246 let custom_widget_is_animating = false;
2247
2248 if self.has_canvas
2249 || custom_widget_is_animating
2250 || self.scroll_animation != ScrollAnimationState::None
2251 || self.scrollbars_animating()
2252 {
2253 AnimationPacing::Interactive
2254 } else if self.has_active_animations {
2255 const SLOW_ANIMATION_SECONDS: f64 = 2.0;
2256 let sets = self.animations.sets.read();
2257 let has_fast_animation_or_transition = sets.values().any(|set| {
2258 set.transitions.iter().any(|transition| {
2259 matches!(
2260 transition.state,
2261 AnimationState::Pending | AnimationState::Running
2262 )
2263 }) || set.animations.iter().any(|animation| {
2264 matches!(
2265 animation.state,
2266 AnimationState::Pending | AnimationState::Running
2267 ) && animation.duration < SLOW_ANIMATION_SECONDS
2268 })
2269 });
2270 if has_fast_animation_or_transition {
2271 AnimationPacing::Interactive
2272 } else {
2273 AnimationPacing::SlowCss
2274 }
2275 } else if focused_text_input {
2276 AnimationPacing::Caret
2277 } else if self.subdoc_animation_pacing != AnimationPacing::Idle {
2278 self.subdoc_animation_pacing
2279 } else {
2280 AnimationPacing::Idle
2281 }
2282 }
2283
2284 fn animating_node_names(&self) -> Option<String> {
2291 if !self.has_active_animations {
2292 return None;
2293 }
2294 let sets = self.animations.sets.read();
2295 let mut described: Vec<String> = sets
2296 .iter()
2297 .filter(|(_, state)| state.needs_animation_ticks())
2298 .filter_map(|(key, state)| {
2299 let node_id = NodeId::from_u64(key.node.id() as u64);
2300 let node = self.nodes.get(node_id)?;
2301 let element = node.element_data()?;
2302 let name = element
2303 .attr(local_name!("id"))
2304 .map(|id| format!("#{id}"))
2305 .or_else(|| {
2306 element
2307 .attr(local_name!("class"))
2308 .and_then(|c| c.split_ascii_whitespace().next())
2309 .map(|c| format!(".{c}"))
2310 })
2311 .unwrap_or_else(|| element.name.local.to_string());
2312 Some(format!(
2313 "{name}(anim={},trans={},in_doc={})",
2314 state.animations.len(),
2315 state.transitions.len(),
2316 node.flags.is_in_document(),
2317 ))
2318 })
2319 .collect();
2320 described.sort();
2321 described.truncate(12);
2322 Some(described.join(" "))
2323 }
2324
2325 pub fn set_stylist_device(&mut self, device: Device) {
2327 let root_styles = self
2333 .try_root_element()
2334 .and_then(|root| root.primary_styles());
2335 if let Some(root_style) = root_styles.as_deref() {
2336 device.set_root_style(root_style);
2337
2338 let font = root_style.get_font();
2339 let font_size = font.clone_font_size().computed_size();
2340 device.set_root_font_size(root_style.effective_zoom.unzoom(font_size.px()));
2341
2342 let line_height = device
2343 .calc_line_height(font, root_style.writing_mode, None)
2344 .0;
2345 device.set_root_line_height(root_style.effective_zoom.unzoom(line_height.px()));
2346 }
2347 drop(root_styles);
2348
2349 let origins = {
2350 let guard = &self.guard;
2351 let guards = StylesheetGuards {
2352 author: &guard.read(),
2353 ua_or_user: &guard.read(),
2354 };
2355 self.stylist.set_device(device, &guards)
2356 };
2357 self.stylist.force_stylesheet_origins_dirty(origins);
2358 }
2359
2360 pub fn stylist_device(&mut self) -> &Device {
2361 self.stylist.device()
2362 }
2363
2364 pub fn get_cursor(&self) -> Option<CursorIcon> {
2372 let node_id = self
2377 .hover_hit_node_id
2378 .filter(|&id| self.nodes.contains_key(id))
2379 .or(self.get_hover_node_id());
2380 let Some(node_id) = node_id else {
2381 return Some(CursorIcon::Default);
2382 };
2383 let node = &self.nodes[node_id];
2384
2385 if let Some(subdoc) = node.subdoc().map(|doc| doc.inner()) {
2386 if subdoc.hover_hit_node_id.is_some() || subdoc.get_hover_node_id().is_some() {
2392 return subdoc.get_cursor();
2393 }
2394 return Some(CursorIcon::Default);
2395 }
2396
2397 let Some(style) = node.primary_styles() else {
2398 return Some(CursorIcon::Default);
2399 };
2400 let user_select = style.clone_user_select();
2401 let keyword = style.clone_cursor().keyword;
2402
2403 if keyword != CursorKind::Auto {
2405 return stylo_to_cursor_icon(keyword);
2406 }
2407
2408 if node
2410 .element_data()
2411 .is_some_and(|e| e.text_input_data().is_some())
2412 {
2413 return Some(CursorIcon::Text);
2414 }
2415
2416 let mut maybe_node = Some(node);
2418 while let Some(node) = maybe_node {
2419 if node.is_link() {
2420 return Some(CursorIcon::Pointer);
2421 }
2422
2423 maybe_node = node.layout_parent.get().map(|node_id| node.with(node_id));
2424 }
2425
2426 if self.hover_node_is_text {
2428 return Some(match user_select {
2429 UserSelect::Text | UserSelect::All | UserSelect::Auto => CursorIcon::Text,
2430 UserSelect::None => CursorIcon::Default,
2431 });
2432 }
2433
2434 Some(CursorIcon::Default)
2436 }
2437
2438 pub fn scroll_node_by<F: FnMut(DomEvent)>(
2439 &mut self,
2440 node_id: NodeId,
2441 x: f64,
2442 y: f64,
2443 dispatch_event: F,
2444 ) {
2445 self.scroll_node_by_has_changed(node_id, x, y, dispatch_event);
2446 }
2447
2448 pub fn scroll_node_by_has_changed<F: FnMut(DomEvent)>(
2452 &mut self,
2453 node_id: NodeId,
2454 x: f64,
2455 y: f64,
2456 mut dispatch_event: F,
2457 ) -> bool {
2458 if self.try_root_element().is_some_and(|el| el.id == node_id) {
2463 let has_changed = self.scroll_viewport_by_has_changed(x, y);
2464 if has_changed {
2465 let layout = *self.root_element().final_layout();
2466 let scale = self.viewport.scale() as f64;
2467 let event = BlitzScrollEvent {
2468 scroll_top: self.viewport_scroll.y,
2469 scroll_left: self.viewport_scroll.x,
2470 scroll_width: layout.size.width.max(layout.content_size.width) as i32,
2471 scroll_height: layout.size.height.max(layout.content_size.height) as i32,
2472 client_width: (self.viewport.window_size.0 as f64 / scale) as i32,
2473 client_height: (self.viewport.window_size.1 as f64 / scale) as i32,
2474 };
2475 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
2476 }
2477 return has_changed;
2478 }
2479
2480 let Some(node) = self.nodes.get_mut(node_id) else {
2481 return false;
2482 };
2483
2484 if node
2488 .element_data()
2489 .is_some_and(|el| el.text_input_data().is_some())
2490 {
2491 let parent = node.parent;
2492 let content_box_width = node.final_layout().content_box_width();
2493 let content_box_height = node.final_layout().content_box_height();
2494 let input = node
2495 .element_data_mut()
2496 .and_then(|el| el.text_input_data_mut())
2497 .unwrap();
2498
2499 let (bubble_x, bubble_y) = if input.is_multiline {
2500 (
2501 x,
2502 input.scroll_by(y as f32, content_box_width, content_box_height) as f64,
2503 )
2504 } else {
2505 (
2506 input.scroll_by(x as f32, content_box_width, content_box_height) as f64,
2507 y,
2508 )
2509 };
2510
2511 let has_changed = bubble_x != x || bubble_y != y;
2512
2513 if bubble_x != 0.0 || bubble_y != 0.0 {
2514 let bubbled = if let Some(parent) = parent {
2515 self.scroll_node_by_has_changed(parent, bubble_x, bubble_y, dispatch_event)
2516 } else {
2517 self.scroll_viewport_by_has_changed(bubble_x, bubble_y)
2518 };
2519 return bubbled | has_changed;
2520 }
2521
2522 return has_changed;
2523 }
2524
2525 let (can_x_scroll, can_y_scroll) = node
2526 .primary_styles()
2527 .map(|styles| {
2528 (
2529 matches!(styles.clone_overflow_x(), Overflow::Scroll | Overflow::Auto),
2530 matches!(styles.clone_overflow_y(), Overflow::Scroll | Overflow::Auto),
2531 )
2532 })
2533 .unwrap_or((false, false));
2534
2535 let initial = *node.scroll_offset();
2536 let new_x = node.scroll_offset().x - x;
2537 let new_y = node.scroll_offset().y - y;
2538
2539 let mut bubble_x = 0.0;
2540 let mut bubble_y = 0.0;
2541
2542 let scroll_width = node.final_layout().scroll_width() as f64;
2543 let scroll_height = node.final_layout().scroll_height() as f64;
2544
2545 if let Some(mut sub_doc) = node.subdoc_mut().map(|doc| doc.inner_mut()) {
2547 let has_changed = if let Some(hover_node_id) = sub_doc.get_hover_node_id() {
2548 sub_doc.scroll_node_by_has_changed(hover_node_id, x, y, dispatch_event)
2549 } else {
2550 sub_doc.scroll_viewport_by_has_changed(x, y)
2551 };
2552
2553 return has_changed;
2555 }
2556
2557 if !can_x_scroll {
2559 bubble_x = x
2560 } else if new_x < 0.0 {
2561 bubble_x = -new_x;
2562 node.scroll_offset_mut().x = 0.0;
2563 } else if new_x > scroll_width {
2564 bubble_x = scroll_width - new_x;
2565 node.scroll_offset_mut().x = scroll_width;
2566 } else {
2567 node.scroll_offset_mut().x = new_x;
2568 }
2569
2570 if !can_y_scroll {
2571 bubble_y = y
2572 } else if new_y < 0.0 {
2573 bubble_y = -new_y;
2574 node.scroll_offset_mut().y = 0.0;
2575 } else if new_y > scroll_height {
2576 bubble_y = scroll_height - new_y;
2577 node.scroll_offset_mut().y = scroll_height;
2578 } else {
2579 node.scroll_offset_mut().y = new_y;
2580 }
2581
2582 let has_changed = *node.scroll_offset() != initial;
2583
2584 if has_changed {
2585 let layout = *node.final_layout();
2586 let event = BlitzScrollEvent {
2587 scroll_top: node.scroll_offset().y,
2588 scroll_left: node.scroll_offset().x,
2589 scroll_width: layout.scroll_width() as i32,
2590 scroll_height: layout.scroll_height() as i32,
2591 client_width: layout.size.width as i32,
2592 client_height: layout.size.height as i32,
2593 };
2594
2595 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
2596 }
2597
2598 let parent = node.parent;
2599 if has_changed {
2600 self.show_scrollbars(node_id);
2601 }
2602
2603 if bubble_x != 0.0 || bubble_y != 0.0 {
2604 if let Some(parent) = parent {
2605 return self.scroll_node_by_has_changed(parent, bubble_x, bubble_y, dispatch_event)
2606 | has_changed;
2607 } else {
2608 return self.scroll_viewport_by_has_changed(bubble_x, bubble_y) | has_changed;
2609 }
2610 }
2611
2612 has_changed
2613 }
2614
2615 pub fn scroll_viewport_by(&mut self, x: f64, y: f64) {
2616 self.scroll_viewport_by_has_changed(x, y);
2617 }
2618
2619 pub fn scroll_viewport_by_has_changed(&mut self, x: f64, y: f64) -> bool {
2621 let (content_width, content_height) = match self.try_root_element() {
2626 Some(root) => {
2627 let root_layout = root.final_layout();
2628 (
2629 root_layout.size.width.max(root_layout.content_size.width) as f64,
2630 root_layout.size.height.max(root_layout.content_size.height) as f64,
2631 )
2632 }
2633 None => (0.0, 0.0),
2634 };
2635 let new_scroll = (self.viewport_scroll.x - x, self.viewport_scroll.y - y);
2636 let window_width = self.viewport.window_size.0 as f64 / self.viewport.scale() as f64;
2637 let window_height = self.viewport.window_size.1 as f64 / self.viewport.scale() as f64;
2638
2639 let initial = self.viewport_scroll;
2640 self.viewport_scroll.x =
2641 f64::max(0.0, f64::min(new_scroll.0, content_width - window_width));
2642 self.viewport_scroll.y =
2643 f64::max(0.0, f64::min(new_scroll.1, content_height - window_height));
2644
2645 self.viewport_scroll != initial
2646 }
2647
2648 pub fn scroll_by(
2649 &mut self,
2650 anchor_node_id: Option<NodeId>,
2651 scroll_x: f64,
2652 scroll_y: f64,
2653 dispatch_event: &mut dyn FnMut(DomEvent),
2654 ) -> bool {
2655 if let Some(anchor_node_id) = anchor_node_id {
2656 self.scroll_node_by_has_changed(anchor_node_id, scroll_x, scroll_y, dispatch_event)
2657 } else {
2658 self.scroll_viewport_by_has_changed(scroll_x, scroll_y)
2659 }
2660 }
2661
2662 pub fn viewport_scroll(&self) -> crate::Point<f64> {
2663 self.viewport_scroll
2664 }
2665
2666 pub fn set_viewport_scroll(&mut self, scroll: crate::Point<f64>) {
2667 self.viewport_scroll = scroll;
2668 }
2669
2670 pub fn get_fragment_target(&self, fragment: &str) -> Option<NodeId> {
2675 if let Some(node_id) = self.get_element_by_id(fragment) {
2676 return Some(node_id);
2677 }
2678
2679 self.nodes.iter().find_map(|(id, node)| {
2681 let el = node.element_data()?;
2682 (el.name.local == local_name!("a") && el.attr(local_name!("name")) == Some(fragment))
2683 .then_some(id)
2684 })
2685 }
2686
2687 pub fn nearest_scroll_container(&self, node_id: NodeId) -> Option<NodeId> {
2698 let mut current = Some(node_id);
2699 for _ in 0..64 {
2700 let id = current?;
2701 let node = self.nodes.get(id)?;
2702 if node.style().overflow.x.is_scroll_container()
2703 || node.style().overflow.y.is_scroll_container()
2704 {
2705 return Some(id);
2706 }
2707 current = node.parent;
2708 }
2709 None
2710 }
2711
2712 pub fn scroll_nearest_container_by(&mut self, node_id: NodeId, x: f64, y: f64) -> bool {
2713 self.scroll_nearest_container_by_with_events(node_id, x, y, |_| {})
2714 }
2715
2716 pub fn scroll_nearest_container_by_with_events<F: FnMut(DomEvent)>(
2717 &mut self,
2718 node_id: NodeId,
2719 x: f64,
2720 y: f64,
2721 mut dispatch_event: F,
2722 ) -> bool {
2723 let mut current = Some(node_id);
2724 for _ in 0..64 {
2725 let Some(id) = current else { break };
2726 let Some(node) = self.nodes.get(id) else {
2727 break;
2728 };
2729 let scrolls = node.style().overflow.x.is_scroll_container()
2730 || node.style().overflow.y.is_scroll_container();
2731 if scrolls {
2732 self.scroll_node_by(id, x, y, &mut dispatch_event);
2733 return true;
2734 }
2735 current = node.parent;
2736 }
2737 self.scroll_viewport_by(x, y);
2738 false
2739 }
2740
2741 pub fn scroll_to_node(&mut self, node_id: NodeId) {
2742 self.scroll_to_node_with_events(node_id, |_| {});
2743 }
2744
2745 pub fn scroll_to_node_with_events<F: FnMut(DomEvent)>(
2746 &mut self,
2747 node_id: NodeId,
2748 mut dispatch_event: F,
2749 ) {
2750 let mut chain = Vec::new();
2762 let mut current = self.nodes.get(node_id).and_then(|node| node.parent);
2763 while let Some(id) = current {
2764 let Some(node) = self.nodes.get(id) else {
2765 break;
2766 };
2767 let scrolls = node.style().overflow.x.is_scroll_container()
2768 || node.style().overflow.y.is_scroll_container();
2769 if scrolls {
2770 chain.push(id);
2771 }
2772 current = node.parent;
2773 }
2774
2775 for container in chain {
2779 let Some(node) = self.nodes.get(node_id) else {
2780 return;
2781 };
2782 let target = node.absolute_position(0.0, 0.0);
2783 let Some(scroller) = self.nodes.get(container) else {
2784 continue;
2785 };
2786 let box_ = scroller.absolute_position(0.0, 0.0);
2787 let layout = scroller.final_layout();
2788 let dx = f64::from(box_.x - target.x);
2792 let dy = f64::from(box_.y - target.y);
2793 let _ = layout;
2794 self.scroll_node_by(container, dx, dy, &mut dispatch_event);
2795 }
2796
2797 let Some(node) = self.nodes.get(node_id) else {
2800 return;
2801 };
2802 let target = node.absolute_position(0.0, 0.0);
2803 let current = self.viewport_scroll;
2804
2805 let dx = current.x - target.x as f64;
2808 let dy = current.y - target.y as f64;
2809 if let Some(root) = self.try_root_element().map(|element| element.id) {
2810 self.scroll_node_by(root, dx, dy, dispatch_event);
2811 } else {
2812 self.scroll_viewport_by(dx, dy);
2813 }
2814 }
2815
2816 pub fn scroll_to_fragment(&mut self, fragment: &str) -> bool {
2822 let decoded = percent_encoding::percent_decode_str(fragment)
2824 .decode_utf8_lossy()
2825 .into_owned();
2826
2827 if !decoded.is_empty() {
2828 if let Some(node_id) = self.get_fragment_target(&decoded) {
2829 self.scroll_to_node(node_id);
2830 return true;
2831 }
2832 }
2833
2834 if decoded.is_empty() || decoded.eq_ignore_ascii_case("top") {
2837 let current = self.viewport_scroll;
2838 self.scroll_viewport_by(current.x, current.y);
2839 return true;
2840 }
2841
2842 false
2843 }
2844
2845 pub fn get_client_bounding_rect(&self, node_id: NodeId) -> Option<BoundingRect> {
2847 if let Some(rects) = self.inline_fragment_rects(node_id) {
2850 let x0 = rects.iter().map(|r| r.x).fold(f64::INFINITY, f64::min);
2851 let y0 = rects.iter().map(|r| r.y).fold(f64::INFINITY, f64::min);
2852 let x1 = rects
2853 .iter()
2854 .map(|r| r.x + r.width)
2855 .fold(f64::NEG_INFINITY, f64::max);
2856 let y1 = rects
2857 .iter()
2858 .map(|r| r.y + r.height)
2859 .fold(f64::NEG_INFINITY, f64::max);
2860 return match rects.is_empty() {
2861 true => None,
2862 false => Some(BoundingRect {
2863 x: x0,
2864 y: y0,
2865 width: x1 - x0,
2866 height: y1 - y0,
2867 }),
2868 };
2869 }
2870
2871 let node = self.get_node(node_id)?;
2872 if !matches!(
2873 node.data,
2874 NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
2875 ) {
2876 return None;
2877 }
2878 let pos = node.absolute_position(0.0, 0.0);
2879
2880 Some(BoundingRect {
2881 x: pos.x as f64 - self.viewport_scroll.x,
2882 y: pos.y as f64 - self.viewport_scroll.y,
2883 width: node.unrounded_layout().size.width as f64,
2884 height: node.unrounded_layout().size.height as f64,
2885 })
2886 }
2887
2888 pub fn node_client_rects(&self, node_id: NodeId) -> Vec<BoundingRect> {
2893 match self.inline_fragment_rects(node_id) {
2894 Some(rects) => rects,
2895 None => self.get_client_bounding_rect(node_id).into_iter().collect(),
2896 }
2897 }
2898
2899 pub(crate) fn trace_escaped_inline_fragments(&self) {
2913 static TRACE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2914 if !*TRACE.get_or_init(|| std::env::var_os("BLITZ_TRACE_INLINE").is_some()) {
2915 return;
2916 }
2917 let mut reported = 0;
2918 for (id, node) in self.nodes.iter() {
2919 if !node.is_element() {
2920 continue;
2921 }
2922 let Some(rects) = self.inline_fragment_rects(id) else {
2923 continue;
2924 };
2925 let Some(root) = node.inline_root_ancestor() else {
2926 continue;
2927 };
2928 let root_layout = root.final_layout();
2929 let root_pos = root.absolute_position(0.0, 0.0);
2930 let root_right =
2931 root_pos.x as f64 + root_layout.size.width as f64 - self.viewport_scroll.x;
2932 for rect in &rects {
2933 if rect.x + rect.width > root_right + 1.0 {
2934 reported += 1;
2935 if reported <= 12 {
2936 eprintln!(
2937 "escaped-fragment node={id:?} rect=[{:.1},{:.1} {:.1}x{:.1}] \
2938root={:?} root_right={root_right:.1} root_w={:.1} lines={} layout_scale={:.2} vp_scale={:.2} layout_w={:.1}",
2939 rect.x,
2940 rect.y,
2941 rect.width,
2942 rect.height,
2943 root.id,
2944 root_layout.size.width,
2945 root.element_data()
2946 .and_then(|e| e.inline_layout_data.as_ref())
2947 .map(|i| i.layout.len())
2948 .unwrap_or(0),
2949 root.element_data()
2950 .and_then(|e| e.inline_layout_data.as_ref())
2951 .map(|i| i.layout.scale())
2952 .unwrap_or(0.0),
2953 self.viewport.scale(),
2954 root.element_data()
2955 .and_then(|e| e.inline_layout_data.as_ref())
2956 .map(|i| i.layout.width())
2957 .unwrap_or(0.0),
2958 );
2959 }
2960 break;
2961 }
2962 }
2963 }
2964 if reported > 0 {
2965 eprintln!("escaped-fragment total={reported}");
2966 }
2967
2968 let mut narrow = 0;
2973 for (id, node) in self.nodes.iter() {
2974 let Some(inline) = node
2975 .data
2976 .downcast_element()
2977 .and_then(|element| element.inline_layout_data.as_ref())
2978 else {
2979 continue;
2980 };
2981 let box_width = node.final_layout().size.width as f64 * self.viewport.scale() as f64;
2982 let broken_at = inline.layout.width() as f64;
2983 let full = inline.layout.calculate_content_widths().max as f64;
2986 if box_width > 40.0 && broken_at < box_width * 0.6 && full > box_width * 0.9 {
2987 narrow += 1;
2988 if narrow <= 12 {
2989 eprintln!(
2990 "narrow-break node={id:?} broken_at={broken_at:.1} box={box_width:.1} \
2991 max_content={full:.1} lines={} text={:?}",
2992 inline.layout.len(),
2993 inline.text.chars().take(40).collect::<String>(),
2994 );
2995 }
2996 }
2997 }
2998 if narrow > 0 {
2999 eprintln!("narrow-break total={narrow}");
3000 }
3001 }
3002
3003 pub fn inline_fragment_rects(&self, node_id: NodeId) -> Option<Vec<BoundingRect>> {
3004 use parley::PositionedLayoutItem;
3005
3006 let node = self.get_node(node_id)?;
3007
3008 if !node.is_element() || node.flags.is_inline_root() {
3011 return None;
3012 }
3013 let display = node.primary_styles()?.clone_display();
3014 if !(display.outside() == DisplayOutside::Inline && display.inside() == DisplayInside::Flow)
3015 {
3016 return None;
3017 }
3018
3019 let inline_root = node.inline_root_ancestor()?;
3020 let inline_layout = inline_root.element_data()?.inline_layout_data.as_ref()?;
3021 let layout = &inline_layout.layout;
3022 let scale = layout.scale() as f64;
3023
3024 let is_in_target = |mut id: NodeId| -> bool {
3027 loop {
3028 if id == node_id {
3029 return true;
3030 }
3031 if id == inline_root.id {
3032 return false;
3033 }
3034 match self.get_node(id).and_then(|n| n.parent) {
3035 Some(parent) => id = parent,
3036 None => return false,
3037 }
3038 }
3039 };
3040
3041 let root_layout = inline_root.final_layout();
3043 let root_pos = inline_root.absolute_position(0.0, 0.0);
3044 let origin_x = root_pos.x as f64
3045 + (root_layout.padding.left + root_layout.border.left) as f64
3046 - self.viewport_scroll.x;
3047 let origin_y = root_pos.y as f64
3048 + (root_layout.padding.top + root_layout.border.top) as f64
3049 - self.viewport_scroll.y;
3050
3051 let mut rects: Vec<BoundingRect> = Vec::new();
3052 for line in layout.lines() {
3053 let line_metrics = line.metrics();
3054 let mut line_rect: Option<(f64, f64, f64, f64)> = None;
3056 let mut add = |x0: f64, y0: f64, x1: f64, y1: f64| {
3057 line_rect = Some(match line_rect {
3058 Some((lx0, ly0, lx1, ly1)) => {
3059 (lx0.min(x0), ly0.min(y0), lx1.max(x1), ly1.max(y1))
3060 }
3061 None => (x0, y0, x1, y1),
3062 });
3063 };
3064
3065 for item in line.items() {
3066 match item {
3067 PositionedLayoutItem::GlyphRun(glyph_run) => {
3068 if !is_in_target(glyph_run.style().brush.id) {
3069 continue;
3070 }
3071 let x0 = glyph_run.offset() as f64;
3072 let x1 = x0 + glyph_run.advance() as f64;
3073 let y0 = line_metrics.block_min_coord as f64;
3079 let y1 = line_metrics.block_max_coord as f64;
3080 add(x0, y0, x1, y1);
3081 }
3082 PositionedLayoutItem::InlineBox(inline_box) => {
3083 if !is_in_target(NodeId::from_u64(inline_box.id)) {
3084 continue;
3085 }
3086 let x0 = inline_box.x as f64;
3087 let y0 = inline_box.y as f64;
3088 add(
3089 x0,
3090 y0,
3091 x0 + inline_box.width as f64,
3092 y0 + inline_box.height as f64,
3093 );
3094 }
3095 }
3096 }
3097
3098 if let Some((x0, y0, x1, y1)) = line_rect {
3099 rects.push(BoundingRect {
3100 x: origin_x + x0 / scale,
3101 y: origin_y + y0 / scale,
3102 width: (x1 - x0) / scale,
3103 height: (y1 - y0) / scale,
3104 });
3105 }
3106 }
3107
3108 Some(rects)
3109 }
3110
3111 pub fn find_title_node(&self) -> Option<&Node> {
3112 TreeTraverser::new(self)
3113 .find(|node_id| {
3114 let node = &self.nodes[*node_id];
3115 let Some(element) = node.element_data() else {
3116 return false;
3117 };
3118 if element.name.ns != ns!(html) || element.name.local != local_name!("title") {
3119 return false;
3120 }
3121 node.parent
3122 .and_then(|parent_id| self.nodes.get(parent_id))
3123 .and_then(Node::element_data)
3124 .is_some_and(|parent| {
3125 parent.name.ns == ns!(html) && parent.name.local == local_name!("head")
3126 })
3127 })
3128 .map(|node_id| &self.nodes[node_id])
3129 }
3130
3131 pub fn with_text_input(
3132 &mut self,
3133 node_id: NodeId,
3134 cb: impl FnOnce(PlainEditorDriver<TextBrush>),
3135 ) {
3136 let Some(node) = self.nodes.get_mut(node_id) else {
3137 return;
3138 };
3139
3140 if let Some(text_input) = node
3141 .element_data_mut()
3142 .and_then(|el| el.text_input_data_mut())
3143 {
3144 let mut font_ctx = self.font_ctx.lock().unwrap();
3145 let layout_ctx = &mut self.layout_ctx;
3146 let driver = text_input.editor.driver(&mut font_ctx, layout_ctx);
3147 cb(driver)
3148 }
3149 }
3150
3151 pub(crate) fn clamp_text_input_scroll(&mut self, node_id: NodeId) {
3154 let Some(node) = self.nodes.get_mut(node_id) else {
3155 return;
3156 };
3157
3158 let content_box_width = node.final_layout().content_box_width();
3159 let content_box_height = node.final_layout().content_box_height();
3160
3161 if let Some(text_input) = node
3162 .element_data_mut()
3163 .and_then(|el| el.text_input_data_mut())
3164 {
3165 text_input.clamp_scroll_offset(content_box_width, content_box_height);
3166 }
3167 }
3168
3169 pub(crate) fn compute_has_canvas(&self) -> bool {
3170 TreeTraverser::new(self).any(|node_id| {
3171 let node = &self.nodes[node_id];
3172 let Some(element) = node.element_data() else {
3173 return false;
3174 };
3175 if element.name.local == local_name!("canvas") && element.has_attr(local_name!("src")) {
3176 return true;
3177 }
3178
3179 false
3180 })
3181 }
3182
3183 pub fn find_text_position(&self, x: f32, y: f32) -> Option<(NodeId, usize)> {
3189 let hit = self.hit(x, y)?;
3190 let hit_node = self.get_node(hit.node_id)?;
3191 let inline_root = hit_node.inline_root_ancestor()?;
3192 let byte_offset = inline_root.text_offset_at_point(hit.x, hit.y)?;
3193 Some((inline_root.id, byte_offset))
3194 }
3195
3196 pub fn find_text_range(
3202 &self,
3203 x: f32,
3204 y: f32,
3205 granularity: TextGranularity,
3206 ) -> Option<(NodeId, usize, usize)> {
3207 let hit = self.hit(x, y)?;
3208 let hit_node = self.get_node(hit.node_id)?;
3209 let inline_root = hit_node.inline_root_ancestor()?;
3210 let range = inline_root.text_range_at_point(hit.x, hit.y, granularity)?;
3211 Some((inline_root.id, range.start, range.end))
3212 }
3213
3214 pub fn set_text_selection(
3216 &mut self,
3217 anchor_node: NodeId,
3218 anchor_offset: usize,
3219 focus_node: NodeId,
3220 focus_offset: usize,
3221 ) {
3222 self.text_selection =
3223 TextSelection::new(anchor_node, anchor_offset, focus_node, focus_offset);
3224
3225 if let (Some(parent), Some(idx)) = self.anonymous_block_location(anchor_node) {
3227 self.text_selection
3228 .anchor
3229 .set_anonymous(parent, idx, anchor_offset);
3230 }
3231 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
3232 self.text_selection
3233 .focus
3234 .set_anonymous(parent, idx, focus_offset);
3235 }
3236 }
3237
3238 fn anonymous_block_location(&self, node_id: NodeId) -> (Option<NodeId>, Option<usize>) {
3241 let Some(node) = self.get_node(node_id) else {
3242 return (None, None);
3243 };
3244
3245 if !node.is_anonymous() {
3246 return (None, None);
3247 }
3248
3249 let Some(parent_id) = node.parent else {
3250 return (None, None);
3251 };
3252
3253 let Some(parent) = self.get_node(parent_id) else {
3254 return (Some(parent_id), None);
3255 };
3256
3257 let layout_children = parent.layout_children.borrow();
3258 let Some(children) = layout_children.as_ref() else {
3259 return (Some(parent_id), None);
3260 };
3261
3262 let mut anon_index = 0;
3264 for &child_id in children.iter() {
3265 if child_id == node_id {
3266 return (Some(parent_id), Some(anon_index));
3267 }
3268 if self.get_node(child_id).is_some_and(|n| n.is_anonymous()) {
3269 anon_index += 1;
3270 }
3271 }
3272
3273 (Some(parent_id), None)
3274 }
3275
3276 pub fn clear_text_selection(&mut self) {
3278 self.text_selection.clear();
3279 }
3280
3281 pub fn update_selection_focus(&mut self, focus_node: NodeId, focus_offset: usize) {
3283 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
3285 self.text_selection
3286 .focus
3287 .set_anonymous(parent, idx, focus_offset);
3288 } else {
3289 self.text_selection.set_focus(focus_node, focus_offset);
3290 }
3291 }
3292
3293 pub fn extend_text_selection_to_point(&mut self, x: f32, y: f32) -> bool {
3296 if !self.text_selection.anchor.is_some() {
3297 return false;
3298 }
3299
3300 if let Some((node, offset)) = self.find_text_position(x, y) {
3301 self.update_selection_focus(node, offset);
3302 self.shell_provider.request_redraw();
3303 true
3304 } else {
3305 false
3306 }
3307 }
3308
3309 fn find_anonymous_block_by_index(
3311 &self,
3312 parent_id: NodeId,
3313 target_index: usize,
3314 ) -> Option<NodeId> {
3315 let parent = self.get_node(parent_id)?;
3316 let layout_children = parent.layout_children.borrow();
3317 let children = layout_children.as_ref()?;
3318
3319 children
3320 .iter()
3321 .filter(|&&child_id| self.get_node(child_id).is_some_and(|n| n.is_anonymous()))
3322 .nth(target_index)
3323 .copied()
3324 }
3325
3326 pub fn has_text_selection(&self) -> bool {
3328 self.text_selection.is_active()
3329 }
3330
3331 pub fn get_selected_text(&self) -> Option<String> {
3333 let ranges = self.get_text_selection_ranges();
3334 if ranges.is_empty() {
3335 return None;
3336 }
3337
3338 let mut result = String::new();
3339 for (node_id, start, end) in &ranges {
3340 let node = self.get_node(*node_id)?;
3341 let element_data = node.element_data()?;
3342 let inline_layout = element_data.inline_layout_data.as_ref()?;
3343
3344 if *end > inline_layout.text.len() {
3345 continue;
3346 }
3347
3348 if !result.is_empty() {
3349 result.push(' ');
3350 }
3351 result.push_str(&inline_layout.text[*start..*end]);
3352 }
3353
3354 if result.is_empty() {
3355 None
3356 } else {
3357 Some(result)
3358 }
3359 }
3360
3361 pub fn get_text_selection_ranges(&self) -> Vec<(NodeId, usize, usize)> {
3364 let lookup = |parent_id, idx| self.find_anonymous_block_by_index(parent_id, idx);
3365
3366 let anchor_node = match self.text_selection.anchor.resolve_node_id(lookup) {
3367 Some(id) => id,
3368 None => return Vec::new(),
3369 };
3370 let focus_node = match self.text_selection.focus.resolve_node_id(lookup) {
3371 Some(id) => id,
3372 None => return Vec::new(),
3373 };
3374
3375 let node_is_in_doc = |node_id: NodeId| {
3378 self.nodes
3379 .get(node_id)
3380 .is_some_and(|node| node.flags.is_in_document())
3381 };
3382 if !node_is_in_doc(anchor_node) || !node_is_in_doc(focus_node) {
3383 return Vec::new();
3384 }
3385
3386 if anchor_node == focus_node {
3388 let start = self
3389 .text_selection
3390 .anchor
3391 .offset
3392 .min(self.text_selection.focus.offset);
3393 let end = self
3394 .text_selection
3395 .anchor
3396 .offset
3397 .max(self.text_selection.focus.offset);
3398
3399 if start == end {
3400 return Vec::new();
3401 }
3402 return vec![(anchor_node, start, end)];
3403 }
3404
3405 let inline_roots = self.collect_inline_roots_in_range(anchor_node, focus_node);
3407 if inline_roots.is_empty() {
3408 return Vec::new();
3409 }
3410
3411 let first_in_roots = inline_roots[0];
3414
3415 let (first_node, first_offset, last_node, last_offset) =
3416 if first_in_roots == anchor_node || (first_in_roots != focus_node) {
3417 (
3419 anchor_node,
3420 self.text_selection.anchor.offset,
3421 focus_node,
3422 self.text_selection.focus.offset,
3423 )
3424 } else {
3425 (
3427 focus_node,
3428 self.text_selection.focus.offset,
3429 anchor_node,
3430 self.text_selection.anchor.offset,
3431 )
3432 };
3433
3434 let mut ranges = Vec::with_capacity(inline_roots.len());
3435
3436 for &node_id in &inline_roots {
3437 let Some(node) = self.get_node(node_id) else {
3438 continue;
3439 };
3440 let Some(element_data) = node.element_data() else {
3441 continue;
3442 };
3443 let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
3444 continue;
3445 };
3446
3447 let text_len = inline_layout.text.len();
3448
3449 if node_id == first_node && node_id == last_node {
3450 let start = first_offset.min(last_offset);
3451 let end = first_offset.max(last_offset);
3452 if start < end && end <= text_len {
3453 ranges.push((node_id, start, end));
3454 }
3455 } else if node_id == first_node {
3456 if first_offset < text_len {
3457 ranges.push((node_id, first_offset, text_len));
3458 }
3459 } else if node_id == last_node {
3460 if last_offset > 0 && last_offset <= text_len {
3461 ranges.push((node_id, 0, last_offset));
3462 }
3463 } else if text_len > 0 {
3464 ranges.push((node_id, 0, text_len));
3465 }
3466 }
3467
3468 ranges
3469 }
3470}
3471
3472#[derive(Debug, Clone, Copy, PartialEq)]
3473pub struct BoundingRect {
3474 pub x: f64,
3475 pub y: f64,
3476 pub width: f64,
3477 pub height: f64,
3478}
3479
3480impl AsRef<BaseDocument> for BaseDocument {
3481 fn as_ref(&self) -> &BaseDocument {
3482 self
3483 }
3484}
3485
3486impl AsMut<BaseDocument> for BaseDocument {
3487 fn as_mut(&mut self) -> &mut BaseDocument {
3488 self
3489 }
3490}
3491
3492#[cfg(test)]
3493mod hover_state_tests {
3494 use super::*;
3495 use crate::{Attribute, qual_name};
3496 use blitz_traits::shell::ColorScheme;
3497
3498 fn make_doc() -> (BaseDocument, NodeId) {
3505 let mut doc = BaseDocument::new(DocumentConfig {
3506 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3507 ..Default::default()
3508 });
3509 let root_id = doc.root_node().id;
3510 let style = |value: &str| Attribute {
3511 name: qual_name!("style"),
3512 value: value.into(),
3513 };
3514
3515 let mut mutator = doc.mutate();
3516 let html = mutator.create_element(qual_name!("html"), vec![]);
3517 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
3518 let container = mutator.create_element(qual_name!("div"), vec![style("width:300px")]);
3519 let text = mutator.create_text_node("some text");
3520 let block = mutator.create_element(qual_name!("div"), vec![style("height:50px")]);
3521 mutator.append_children(container, &[text, block]);
3522 mutator.append_children(body, &[container]);
3523 mutator.append_children(html, &[body]);
3524 mutator.append_children(root_id, &[html]);
3525 drop(mutator);
3526
3527 doc.resolve(0.0);
3528 (doc, container)
3529 }
3530
3531 fn text_has_size(doc: &BaseDocument, container: NodeId) -> bool {
3535 doc.nodes[container].final_layout().size.height > 50.0
3536 }
3537
3538 #[test]
3544 fn hovering_text_in_anonymous_block_reports_text_cursor() {
3545 let (mut doc, container) = make_doc();
3546 if !text_has_size(&doc, container) {
3547 eprintln!("skipping: no usable font (text measures 0x0)");
3548 return;
3549 }
3550
3551 doc.set_hover_to(5.0, 8.0);
3552 assert!(doc.hover_node_is_text, "expected a text hit");
3553 let hit_id = doc.hover_hit_node_id.expect("expected a hit node");
3554 assert!(
3555 doc.nodes[hit_id].is_anonymous(),
3556 "expected the hit node to be the anonymous inline root"
3557 );
3558 assert_eq!(
3559 doc.get_hover_node_id(),
3560 Some(container),
3561 "expected the stored hover target to be the containing element"
3562 );
3563 assert_eq!(doc.get_cursor(), Some(CursorIcon::Text));
3564 }
3565
3566 #[test]
3567 fn semantic_hover_keeps_the_resolved_node_instead_of_hit_testing_again() {
3568 let (mut doc, container) = make_doc();
3569
3570 doc.set_hover_to_node(container, 350.0, 250.0);
3574
3575 assert_eq!(doc.get_hover_node_id(), Some(container));
3576 assert_eq!(doc.hover_hit_node_id, Some(container));
3577
3578 doc.resolve(0.0);
3579 assert_eq!(
3580 doc.get_hover_node_id(),
3581 Some(container),
3582 "a resolve must not turn semantic identity back into a coordinate hit"
3583 );
3584 }
3585
3586 #[test]
3589 fn hovering_anonymous_block_whitespace_reports_default_cursor() {
3590 let (mut doc, container) = make_doc();
3591 if !text_has_size(&doc, container) {
3592 eprintln!("skipping: no usable font (text measures 0x0)");
3593 return;
3594 }
3595
3596 doc.set_hover_to(250.0, 8.0);
3597 assert!(!doc.hover_node_is_text);
3598 assert_eq!(doc.get_hover_node_id(), Some(container));
3599 assert_eq!(doc.get_cursor(), Some(CursorIcon::Default));
3600 }
3601}
3602
3603#[cfg(test)]
3604mod control_scroll_tests {
3605 use super::*;
3606 use crate::{Attribute, qual_name};
3607 use blitz_traits::shell::ColorScheme;
3608
3609 #[test]
3610 fn controlled_scroll_dispatches_the_dom_scroll_event() {
3611 let mut doc = BaseDocument::new(DocumentConfig {
3612 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3613 ..Default::default()
3614 });
3615 let root_id = doc.root_node().id;
3616 let style = |value: &str| Attribute {
3617 name: qual_name!("style"),
3618 value: value.into(),
3619 };
3620
3621 let mut mutator = doc.mutate();
3622 let html = mutator.create_element(qual_name!("html"), vec![]);
3623 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
3624 let scroller = mutator.create_element(
3625 qual_name!("div"),
3626 vec![style("width:200px;height:100px;overflow-y:scroll")],
3627 );
3628 let spacer = mutator.create_element(qual_name!("div"), vec![style("height:400px")]);
3629 let target = mutator.create_element(qual_name!("button"), vec![style("height:40px")]);
3630 mutator.append_children(scroller, &[spacer, target]);
3631 mutator.append_children(body, &[scroller]);
3632 mutator.append_children(html, &[body]);
3633 mutator.append_children(root_id, &[html]);
3634 drop(mutator);
3635 doc.resolve(0.0);
3636
3637 doc.nodes[html].final_layout_mut().size.height = 300.0;
3641 doc.nodes[html].final_layout_mut().content_size.height = 600.0;
3642 doc.nodes[target].final_layout_mut().location.y = 400.0;
3643
3644 let mut events = Vec::new();
3645 doc.scroll_to_node_with_events(target, |event| events.push(event));
3646
3647 assert!(doc.viewport_scroll.y > 0.0);
3648 assert!(
3649 events
3650 .iter()
3651 .any(|event| { event.target == html && event.name() == "scroll" })
3652 );
3653 }
3654}
3655
3656#[cfg(test)]
3657mod font_face_override_tests {
3658 use super::*;
3659 use crate::net::{FontFaceOverrides, Resource, ResourceLoadResponse};
3660
3661 #[test]
3677 fn font_face_overrides_alias_family_name() {
3678 const ALIAS: &str = "AliasedFamily";
3679
3680 let mut document = BaseDocument::new(DocumentConfig::default());
3681
3682 {
3684 let mut ctx = document.font_ctx.lock().unwrap();
3685 assert!(
3686 ctx.collection.family_id(ALIAS).is_none(),
3687 "alias must not exist before registration",
3688 );
3689 }
3690
3691 let response = ResourceLoadResponse {
3696 request_id: 0,
3697 node_id: None,
3698 resolved_url: Some(String::from("test://aliased-family")),
3699 result: Ok(Resource::Font(
3700 blitz_traits::net::Bytes::from_static(crate::BULLET_FONT),
3701 FontFaceOverrides {
3702 family_name: Some(String::from(ALIAS)),
3703 weight: Some(800.0),
3704 style: Some(parley::fontique::FontStyle::Italic),
3705 },
3706 )),
3707 };
3708 document.load_resource(response);
3709
3710 let mut ctx = document.font_ctx.lock().unwrap();
3713 let family_id = ctx
3714 .collection
3715 .family_id(ALIAS)
3716 .expect("CSS-declared family name should be registered as a family alias");
3717 let resolved_name = ctx
3718 .collection
3719 .family_name(family_id)
3720 .expect("family id should resolve back to a name");
3721 assert_eq!(
3722 resolved_name, ALIAS,
3723 "registered family should report the CSS-declared name, \
3724 not the font file's internal `name` table entry",
3725 );
3726 }
3727}