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