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) guard: SharedRwLock,
263 pub(crate) snapshots: SnapshotMap,
265
266 pub(crate) font_ctx: Arc<Mutex<parley::FontContext>>,
269 #[cfg(feature = "parallel-construct")]
270 pub(crate) thread_font_contexts: ThreadLocal<RefCell<Box<FontContext>>>,
272 pub(crate) layout_ctx: parley::LayoutContext<TextBrush>,
274
275 pub(crate) hover_node_id: Option<NodeId>,
279 pub(crate) hover_hit_node_id: Option<NodeId>,
283 pub(crate) hover_node_is_text: bool,
285 pub(crate) last_client_pointer_position: Option<taffy::Point<f32>>,
287 pub(crate) focus_node_id: Option<NodeId>,
289 pub(crate) active_node_id: Option<NodeId>,
291 pub(crate) mousedown_node_id: Option<NodeId>,
293 pub(crate) last_mousedown_time: Option<Instant>,
295 pub(crate) mousedown_position: taffy::Point<f32>,
297 pub(crate) click_count: u16,
299 pub(crate) drag_mode: DragMode,
301 pub(crate) hovered_scrollbar: Option<crate::node::ScrollbarRef>,
303 pub(crate) scrollbar_activity: HashMap<NodeId, Instant>,
306 pub(crate) scroll_animation: ScrollAnimationState,
308
309 pub(crate) text_selection: TextSelection,
311
312 pub(crate) has_active_animations: bool,
315 pub(crate) has_canvas: bool,
317 pub(crate) subdoc_animation_pacing: AnimationPacing,
319
320 pub(crate) nodes_to_id: HashMap<String, SmallVec<[NodeId; 1]>>,
324 pub(crate) nodes_to_stylesheet: BTreeMap<NodeId, DocumentStyleSheet>,
326 pub(crate) ua_stylesheets: HashMap<String, DocumentStyleSheet>,
329 pub(crate) controls_to_form: HashMap<NodeId, NodeId>,
331 pub(crate) sub_document_nodes: HashSet<NodeId>,
333 pub(crate) iframe_loads: HashMap<NodeId, crate::iframe::IframeLoad>,
336 pub(crate) changed_nodes: HashSet<NodeId>,
338 pub(crate) deferred_construction_nodes: Vec<ConstructionTask>,
340 pub(crate) paint_damage: crate::paint_damage::PaintDamageTracker,
346
347 #[cfg(feature = "custom-widget")]
349 pub(crate) custom_widget_nodes: HashSet<NodeId>,
350 #[cfg(feature = "custom-widget")]
352 pub(crate) pending_resource_deallocations: Vec<anyrender::ResourceId>,
353
354 #[cfg(feature = "shadow-dom")]
356 pub(crate) custom_element_registry: crate::node::CustomElementRegistry,
357 #[cfg(feature = "shadow-dom")]
359 pub(crate) shadow_host_nodes: HashSet<NodeId>,
360 #[cfg(feature = "shadow-dom")]
362 pub(crate) custom_element_nodes: HashSet<NodeId>,
363
364 pub(crate) image_cache: HashMap<String, ImageData>,
367
368 pub(crate) pending_images: HashMap<String, Vec<(NodeId, ImageType)>>,
372
373 pub(crate) pending_critical_resources: HashSet<usize>,
376
377 pub net_provider: Arc<dyn NetProvider>,
380 pub navigation_provider: Arc<dyn NavigationProvider>,
383 pub shell_provider: Arc<dyn ShellProvider>,
385 pub html_parser_provider: Arc<dyn HtmlParserProvider>,
387 pub(crate) abort_signal: Option<AbortSignal>,
391}
392
393pub(crate) fn make_device(
394 viewport: &Viewport,
395 media_type: MediaType,
396 font_ctx: Arc<Mutex<FontContext>>,
397) -> Device {
398 let width = viewport.window_size.0 as f32 / viewport.scale();
399 let height = viewport.window_size.1 as f32 / viewport.scale();
400 let viewport_size = euclid::Size2D::new(width, height);
401 let device_size = euclid::Size2D::new(width, height) * viewport.scale();
402 let device_pixel_ratio = euclid::Scale::new(viewport.scale());
403
404 Device::new(
405 media_type,
406 selectors::matching::QuirksMode::NoQuirks,
407 viewport_size,
408 device_size,
409 device_pixel_ratio,
410 Box::new(BlitzFontMetricsProvider { font_ctx }),
411 ComputedValues::initial_values_with_font_override(Font::initial_values()),
412 match viewport.color_scheme {
413 ColorScheme::Light => PrefersColorScheme::Light,
414 ColorScheme::Dark => PrefersColorScheme::Dark,
415 },
416 PointerCapabilities::default(),
417 PointerCapabilities::default(),
418 )
419}
420
421fn incremental_layout_default() -> bool {
436 !matches!(
437 std::env::var("BLITZ_INCREMENTAL").ok().as_deref(),
438 Some("0" | "false" | "off")
439 )
440}
441
442impl BaseDocument {
443 pub fn new(config: DocumentConfig) -> Self {
445 static ID_GENERATOR: AtomicUsize = AtomicUsize::new(1);
446
447 let id = ID_GENERATOR.fetch_add(1, Ordering::SeqCst);
448
449 let font_ctx = config
450 .font_ctx
451 .map(|mut font_ctx| {
452 font_ctx.source_cache.make_shared();
453 font_ctx
455 })
456 .unwrap_or_else(|| {
457 use parley::fontique::{Collection, CollectionOptions, SourceCache};
458 let mut font_ctx = FontContext {
459 source_cache: SourceCache::new_shared(),
460 collection: Collection::new(CollectionOptions {
461 shared: false,
462 system_fonts: cfg!(all(
463 feature = "system-fonts",
464 not(target_arch = "wasm32")
465 )),
466 }),
467 };
468 font_ctx
469 .collection
470 .register_fonts(Blob::new(Arc::new(crate::BULLET_FONT) as _), None);
471 font_ctx
472 });
473 let font_ctx = Arc::new(Mutex::new(font_ctx));
474
475 style_config::set_pref!("layout.grid.enabled", true);
477 style_config::set_pref!("layout.unimplemented", true);
478 style_config::set_pref!("layout.columns.enabled", true);
479 style_config::set_pref!("layout.css.basic-shape-shape.enabled", true);
480 style_config::set_pref!("layout.threads", -1);
481
482 let viewport = config.viewport.unwrap_or_default();
483 let media_type = config.media_type.unwrap_or_else(MediaType::screen);
484 let device = make_device(&viewport, media_type.clone(), font_ctx.clone());
485 let stylist = Stylist::new(device, QuirksMode::NoQuirks);
486 let snapshots = SnapshotMap::new();
487 let nodes = Box::new(NodeTree::new());
488 let guard = SharedRwLock::new();
489 let nodes_to_id = HashMap::new();
490
491 let base_url = config
492 .base_url
493 .and_then(|url| DocumentUrl::from_str(&url).ok())
494 .unwrap_or_default();
495
496 let net_provider = config
497 .net_provider
498 .unwrap_or_else(|| Arc::new(DummyNetProvider));
499 let navigation_provider = config
500 .navigation_provider
501 .unwrap_or_else(|| Arc::new(DummyNavigationProvider));
502 let shell_provider = config
503 .shell_provider
504 .unwrap_or_else(|| Arc::new(DummyShellProvider));
505 let html_parser_provider = config
506 .html_parser_provider
507 .unwrap_or_else(|| Arc::new(DummyHtmlParserProvider));
508
509 let (tx, rx) = channel();
510
511 let mut doc = Self {
512 hoisted_fixed_parents: HashMap::new(),
513 hoisted_clip_hosts: Vec::new(),
514 id,
515 tx,
516 rx: Some(rx),
517
518 guard,
519 nodes,
520 root_node_id: NodeId::default(),
521 stylist,
522 animations: DocumentAnimationSet::default(),
523 snapshots,
524 nodes_to_id,
525 viewport,
526 media_type,
527 style_threading: config.style_threading,
528 incremental_layout: config
529 .incremental
530 .unwrap_or_else(incremental_layout_default),
531 subdocument_depth: config.subdocument_depth,
532 devtool_settings: DevtoolSettings::default(),
533 viewport_scroll: crate::Point::ZERO,
534 url: base_url,
535 ua_stylesheets: HashMap::new(),
536 nodes_to_stylesheet: BTreeMap::new(),
537 font_ctx,
538 #[cfg(feature = "parallel-construct")]
539 thread_font_contexts: ThreadLocal::new(),
540 layout_ctx: parley::LayoutContext::new(),
541
542 hover_node_id: None,
543 hover_hit_node_id: None,
544 hover_node_is_text: false,
545 last_client_pointer_position: None,
546 focus_node_id: None,
547 active_node_id: None,
548 mousedown_node_id: None,
549 has_active_animations: false,
550 subdoc_animation_pacing: AnimationPacing::Idle,
551 has_canvas: false,
552 sub_document_nodes: HashSet::new(),
553 iframe_loads: HashMap::new(),
554
555 #[cfg(feature = "custom-widget")]
556 custom_widget_nodes: HashSet::new(),
557 #[cfg(feature = "custom-widget")]
558 pending_resource_deallocations: Vec::new(),
559
560 #[cfg(feature = "shadow-dom")]
561 custom_element_registry: crate::node::CustomElementRegistry::new(),
562 #[cfg(feature = "shadow-dom")]
563 shadow_host_nodes: HashSet::new(),
564 #[cfg(feature = "shadow-dom")]
565 custom_element_nodes: HashSet::new(),
566
567 changed_nodes: HashSet::new(),
568 deferred_construction_nodes: Vec::new(),
569 paint_damage: Default::default(),
570 image_cache: HashMap::new(),
571 pending_images: HashMap::new(),
572 pending_critical_resources: HashSet::new(),
573 controls_to_form: HashMap::new(),
574 net_provider,
575 navigation_provider,
576 shell_provider,
577 html_parser_provider,
578 abort_signal: config.abort_signal,
579 last_mousedown_time: None,
580 mousedown_position: taffy::Point::ZERO,
581 click_count: 0,
582 drag_mode: DragMode::None,
583 hovered_scrollbar: None,
584 scrollbar_activity: HashMap::new(),
585 scroll_animation: ScrollAnimationState::None,
586 text_selection: TextSelection::default(),
587 };
588
589 doc.root_node_id = doc.create_node(NodeData::Document(Box::default()));
591 doc.root_node_mut().flags.insert(NodeFlags::IS_IN_DOCUMENT);
592
593 match config.ua_stylesheets {
594 Some(stylesheets) => {
595 for ss in &stylesheets {
596 doc.add_user_agent_stylesheet(ss);
597 }
598 }
599 None => doc.add_user_agent_stylesheet(DEFAULT_CSS),
600 }
601
602 let stylo_element_data = StyloElementData {
604 styles: ElementStyles {
605 primary: Some(
606 ComputedValues::initial_values_with_font_override(Font::initial_values())
607 .to_arc(),
608 ),
609 ..Default::default()
610 },
611 ..Default::default()
612 };
613 let stylo_data = doc.root_node_mut().stylo_element_data_mut();
614 *stylo_data.ensure_init_mut() = stylo_element_data;
615
616 doc
617 }
618
619 pub fn set_net_provider(&mut self, net_provider: Arc<dyn NetProvider>) {
621 self.net_provider = net_provider;
622 }
623
624 pub fn set_navigation_provider(&mut self, navigation_provider: Arc<dyn NavigationProvider>) {
626 self.navigation_provider = navigation_provider;
627 }
628
629 pub fn set_shell_provider(&mut self, shell_provider: Arc<dyn ShellProvider>) {
631 self.shell_provider = shell_provider;
632 }
633
634 pub fn set_html_parser_provider(&mut self, html_parser_provider: Arc<dyn HtmlParserProvider>) {
636 self.html_parser_provider = html_parser_provider;
637 }
638
639 pub fn set_base_url(&mut self, url: &str) {
641 self.url = DocumentUrl::from(Url::parse(url).unwrap());
642 }
643
644 pub fn guard(&self) -> &SharedRwLock {
645 &self.guard
646 }
647
648 pub fn tree(&self) -> &NodeTree {
649 &self.nodes
650 }
651
652 pub fn id(&self) -> usize {
653 self.id
654 }
655
656 pub(crate) fn build_request(&self, url: url::Url) -> Request {
659 crate::net::stamped_request(url, self.abort_signal.as_ref())
660 }
661
662 pub fn favicon_url(&self) -> Option<String> {
663 self.tree().iter().find_map(|(_, node)| {
664 let data = &node.data;
665 if !data.is_element_with_tag_name(&local_name!("link")) {
666 return None;
667 }
668 let rel = data.attr(local_name!("rel"))?;
669 if !rel
670 .split_ascii_whitespace()
671 .any(|v| v.eq_ignore_ascii_case("icon"))
672 {
673 return None;
674 }
675 data.attr(local_name!("href")).map(|s| s.to_string())
676 })
677 }
678
679 pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
680 self.nodes.get(node_id)
681 }
682
683 pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
684 self.nodes.get_mut(node_id)
685 }
686
687 pub fn get_focussed_node_id(&self) -> Option<NodeId> {
688 self.focus_node_id
689 .or(self.try_root_element().map(|el| el.id))
690 }
691
692 pub fn mutate<'doc>(&'doc mut self) -> DocumentMutator<'doc> {
693 DocumentMutator::new(self)
694 }
695
696 pub fn handle_dom_event<F: FnMut(DomEvent)>(
697 &mut self,
698 event: &mut DomEvent,
699 dispatch_event: F,
700 ) {
701 handle_dom_event(self, event, dispatch_event)
702 }
703
704 pub fn as_any_mut(&mut self) -> &mut dyn Any {
705 self
706 }
707
708 pub fn label_bound_input_element(&self, label_node_id: NodeId) -> Option<&Node> {
715 let label_element = self.nodes[label_node_id].element_data()?;
716 if let Some(target_element_dom_id) = label_element.attr(local_name!("for")) {
717 TreeTraverser::new(self)
718 .filter_map(|id| {
719 let node = self.get_node(id)?;
720 let element_data = node.element_data()?;
721 if element_data.name.local != local_name!("input") {
722 return None;
723 }
724 let id = element_data.id.as_ref()?;
725 if *id == *target_element_dom_id {
726 Some(node)
727 } else {
728 None
729 }
730 })
731 .next()
732 } else {
733 TreeTraverser::new_with_root(self, label_node_id)
734 .filter_map(|child_id| {
735 let node = self.get_node(child_id)?;
736 let element_data = node.element_data()?;
737 if element_data.name.local == local_name!("input") {
738 Some(node)
739 } else {
740 None
741 }
742 })
743 .next()
744 }
745 }
746
747 pub fn toggle_checkbox(el: &mut ElementData) -> bool {
748 let Some(is_checked) = el.checkbox_input_checked_mut() else {
749 return false;
750 };
751 *is_checked = !*is_checked;
752
753 *is_checked
754 }
755
756 pub fn toggle_radio(&mut self, radio_set_name: String, target_radio_id: NodeId) {
757 for (i, node) in self.nodes.iter_mut() {
758 if let Some(node_data) = node.data.downcast_element_mut() {
759 if node_data.attr(local_name!("name")) == Some(&radio_set_name) {
760 let was_clicked = i == target_radio_id;
761 let Some(is_checked) = node_data.checkbox_input_checked_mut() else {
762 continue;
763 };
764 *is_checked = was_clicked;
765 }
766 }
767 }
768 }
769
770 pub fn toggle_details_open(&mut self, details_id: NodeId) {
774 use crate::qual_name;
775
776 let node = &self.nodes[details_id];
777 if !node.data.is_element_with_tag_name(&local_name!("details")) {
778 return;
779 }
780 let is_open = node.data.has_attr(local_name!("open"));
781
782 let mut mutator = self.mutate();
786 if is_open {
787 mutator.clear_attribute(details_id, qual_name!("open"));
788 } else {
789 mutator.set_attribute(details_id, qual_name!("open"), "");
790 }
791 drop(mutator);
792
793 self.shell_provider.request_redraw();
794 }
795
796 pub fn set_style_property(&mut self, node_id: NodeId, name: &str, value: &str) {
797 let node = &mut self.nodes[node_id];
798 let did_change = node.element_data_mut().unwrap().set_style_property(
799 name,
800 value,
801 &self.guard,
802 self.url.url_extra_data(),
803 );
804 if did_change {
805 node.mark_style_attr_updated();
806 }
807 }
808
809 pub fn remove_style_property(&mut self, node_id: NodeId, name: &str) {
810 let node = &mut self.nodes[node_id];
811 let did_change = node.element_data_mut().unwrap().remove_style_property(
812 name,
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 sub_document_node_ids(&self) -> Vec<NodeId> {
822 self.sub_document_nodes.iter().copied().collect()
823 }
824
825 pub fn set_sub_document(&mut self, node_id: NodeId, sub_document: Box<dyn Document>) {
826 self.nodes[node_id]
827 .element_data_mut()
828 .unwrap()
829 .set_sub_document(sub_document);
830 self.sub_document_nodes.insert(node_id);
831 }
832
833 pub fn remove_sub_document(&mut self, node_id: NodeId) {
834 self.nodes[node_id]
835 .element_data_mut()
836 .unwrap()
837 .remove_sub_document();
838 self.sub_document_nodes.remove(&node_id);
839 if let Some(load) = self.iframe_loads.remove(&node_id) {
840 load.abort_controller.abort();
841 }
842 }
843
844 pub fn poll_subdocuments(&mut self, waker: Option<&Waker>) -> bool {
850 let mut has_changes = false;
851 let node_ids: Vec<NodeId> = self.sub_document_nodes.iter().copied().collect();
852 for node_id in node_ids {
853 let Some(sub_doc) = self
854 .nodes
855 .get_mut(node_id)
856 .and_then(|node| node.subdoc_mut())
857 else {
858 continue;
859 };
860 let task_context = waker.map(TaskContext::from_waker);
861 has_changes |= sub_doc.poll(task_context);
862 }
863 has_changes
864 }
865
866 #[cfg(feature = "custom-widget")]
867 pub fn custom_widget_node_ids(&self) -> Vec<NodeId> {
868 self.custom_widget_nodes.iter().copied().collect()
869 }
870
871 #[cfg(feature = "custom-widget")]
872 pub fn take_pending_resource_deallocations(&mut self) -> Vec<anyrender::ResourceId> {
873 std::mem::take(&mut self.pending_resource_deallocations)
874 }
875
876 #[cfg(feature = "custom-widget")]
877 pub fn set_custom_widget(&mut self, node_id: NodeId, widget: Box<dyn crate::Widget>) {
878 self.nodes[node_id]
879 .element_data_mut()
880 .unwrap()
881 .set_custom_widget(widget);
882 self.custom_widget_nodes.insert(node_id);
883 }
884
885 #[cfg(feature = "custom-widget")]
886 pub fn remove_custom_widget(&mut self, node_id: NodeId) {
887 let resources_to_deallocate = self.nodes[node_id]
888 .element_data_mut()
889 .unwrap()
890 .remove_custom_widget();
891 self.pending_resource_deallocations
892 .extend_from_slice(&resources_to_deallocate);
893 self.custom_widget_nodes.remove(&node_id);
894 }
895
896 #[cfg(feature = "shadow-dom")]
900 pub fn custom_elements_mut(&mut self) -> &mut crate::node::CustomElementRegistry {
901 &mut self.custom_element_registry
902 }
903
904 #[cfg(feature = "shadow-dom")]
907 pub fn define_custom_element(
908 &mut self,
909 name: markup5ever::LocalName,
910 definition: crate::node::CustomElementDefinition,
911 ) {
912 self.custom_element_registry.define(name, definition);
913 }
914
915 #[cfg(feature = "shadow-dom")]
917 pub fn shadow_host_node_ids(&self) -> Vec<NodeId> {
918 self.shadow_host_nodes.iter().copied().collect()
919 }
920
921 #[cfg(feature = "shadow-dom")]
923 pub fn shadow_root_id(&self, host_id: NodeId) -> Option<NodeId> {
924 self.get_node(host_id)
925 .and_then(|node| node.shadow_root_id())
926 }
927
928 #[cfg(feature = "shadow-dom")]
932 pub fn attach_shadow(&mut self, host_id: NodeId, mode: crate::node::ShadowRootMode) -> NodeId {
933 if let Some(existing) = self.nodes[host_id].shadow_root_id() {
934 return existing;
935 }
936
937 let shadow_root_id = self.create_node(NodeData::ShadowRoot(
938 crate::node::ShadowRootData::new(host_id, mode),
939 ));
940
941 self.nodes[shadow_root_id].parent = Some(host_id);
945 if self.nodes[host_id].flags.is_in_document() {
946 self.nodes[shadow_root_id]
947 .flags
948 .insert(NodeFlags::IS_IN_DOCUMENT);
949 }
950
951 self.nodes[host_id]
952 .element_data_mut()
953 .expect("Shadow host must be an element")
954 .shadow_root = Some(shadow_root_id);
955 self.shadow_host_nodes.insert(host_id);
956
957 self.nodes[host_id].insert_damage(ALL_DAMAGE);
959 self.nodes[host_id].mark_ancestors_dirty();
960
961 shadow_root_id
962 }
963
964 #[cfg(feature = "shadow-dom")]
966 pub fn detach_shadow(&mut self, host_id: NodeId) {
967 let shadow_root_id = self.nodes[host_id]
968 .element_data_mut()
969 .and_then(|el| el.shadow_root.take());
970 if let Some(shadow_root_id) = shadow_root_id {
971 self.drop_node_ignoring_parent(shadow_root_id);
972 self.shadow_host_nodes.remove(&host_id);
973 self.nodes[host_id].insert_damage(ALL_DAMAGE);
974 self.nodes[host_id].mark_ancestors_dirty();
975 }
976 }
977
978 #[cfg(feature = "shadow-dom")]
980 pub fn set_custom_element(
981 &mut self,
982 node_id: NodeId,
983 controller: Box<dyn crate::node::CustomElement>,
984 ) {
985 use crate::node::{CustomElementData, SpecialElementData};
986 self.nodes[node_id]
987 .element_data_mut()
988 .expect("Custom element host must be an element")
989 .special_data = SpecialElementData::CustomElement(CustomElementData::new(controller));
990 self.custom_element_nodes.insert(node_id);
991 }
992
993 #[cfg(feature = "shadow-dom")]
996 pub fn take_custom_element(
997 &mut self,
998 node_id: NodeId,
999 ) -> Option<Box<dyn crate::node::CustomElement>> {
1000 use crate::node::SpecialElementData;
1001 self.custom_element_nodes.remove(&node_id);
1002 let element = self.nodes[node_id].element_data_mut()?;
1003 if matches!(element.special_data, SpecialElementData::CustomElement(_)) {
1004 if let SpecialElementData::CustomElement(mut data) = element.special_data.take() {
1005 return data.controller.take();
1006 }
1007 }
1008 None
1009 }
1010
1011 pub fn root_node(&self) -> &Node {
1012 &self.nodes[self.root_node_id]
1013 }
1014
1015 pub fn root_node_mut(&mut self) -> &mut Node {
1016 &mut self.nodes[self.root_node_id]
1017 }
1018
1019 pub fn set_paint_damage_tracking(&mut self, enabled: bool) {
1036 self.paint_damage.set_enabled(enabled);
1037 }
1038
1039 pub fn paint_damage_tracking(&self) -> bool {
1041 self.paint_damage.is_enabled()
1042 }
1043
1044 pub fn paint_damage(&self) -> &crate::paint_damage::PaintDamage {
1052 self.paint_damage.damage()
1053 }
1054
1055 pub fn try_root_element(&self) -> Option<&Node> {
1056 TDocument::as_node(&self.root_node()).first_element_child()
1057 }
1058
1059 pub fn root_element(&self) -> &Node {
1060 TDocument::as_node(&self.root_node())
1061 .first_element_child()
1062 .unwrap()
1063 .as_element()
1064 .unwrap()
1065 }
1066
1067 pub fn create_node(&mut self, node_data: NodeData) -> NodeId {
1068 let tree_ptr = self.nodes.as_mut() as *mut NodeTree;
1069 let guard = self.guard.clone();
1070
1071 let id = self
1072 .nodes
1073 .insert_with_key(|id| Node::new(tree_ptr, id, guard, node_data));
1074
1075 self.changed_nodes.insert(id);
1077 id
1078 }
1079
1080 pub(crate) fn remove_node_from_tree(&mut self, node_id: NodeId) -> Option<Node> {
1084 self.clear_interaction_state_for_removed_node(node_id);
1085 self.nodes.remove(node_id)
1086 }
1087
1088 fn nearest_surviving_element_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
1093 let mut current = self.get_node(node_id)?.parent;
1094 while let Some(id) = current {
1095 let node = self.get_node(id)?;
1096 if node.is_element() && node.flags.is_in_document() {
1097 return Some(id);
1098 }
1099 current = node.parent;
1100 }
1101 None
1102 }
1103
1104 pub(crate) fn clear_interaction_state_for_removed_node(&mut self, node_id: NodeId) {
1125 if !self.nodes.contains_key(node_id) {
1126 return;
1127 }
1128
1129 if self.hover_node_id == Some(node_id) {
1130 self.hover_node_id = self.nearest_surviving_element_ancestor(node_id);
1131 self.hover_node_is_text = false;
1132 }
1133 if self.hover_hit_node_id == Some(node_id) {
1134 self.hover_hit_node_id = None;
1135 }
1136 if self.active_node_id == Some(node_id) {
1137 self.active_node_id = self.nearest_surviving_element_ancestor(node_id);
1138 }
1139 if self.focus_node_id == Some(node_id) {
1140 let shell_provider = self.shell_provider.clone();
1141 self.nodes[node_id].blur(shell_provider);
1142 self.focus_node_id = None;
1143 }
1144 if self.mousedown_node_id == Some(node_id) {
1145 self.mousedown_node_id = None;
1146 }
1147 if self.text_selection.anchor.node_or_parent == Some(node_id)
1148 || self.text_selection.focus.node_or_parent == Some(node_id)
1149 {
1150 self.text_selection.clear();
1151 }
1152 if self
1153 .hovered_scrollbar
1154 .is_some_and(|scrollbar| scrollbar.node_id == node_id)
1155 {
1156 self.hovered_scrollbar = None;
1157 }
1158 let drag_references_node = match &self.drag_mode {
1159 DragMode::Panning(state) => state.target == node_id,
1160 DragMode::ScrollbarDrag(state) => state.scrollbar.node_id == node_id,
1161 DragMode::Selecting | DragMode::None => false,
1162 };
1163 if drag_references_node {
1164 self.drag_mode = DragMode::None;
1165 }
1166 self.scrollbar_activity.remove(&node_id);
1167 }
1168
1169 pub(crate) fn drop_node_ignoring_parent(&mut self, node_id: NodeId) -> Option<Node> {
1170 self.drop_node_ignoring_parent_with(node_id, &mut |_| {})
1171 }
1172
1173 pub(crate) fn drop_node_ignoring_parent_with(
1176 &mut self,
1177 node_id: NodeId,
1178 on_drop: &mut dyn FnMut(NodeId),
1179 ) -> Option<Node> {
1180 let mut node = self.remove_node_from_tree(node_id);
1181 if let Some(node) = &mut node {
1182 on_drop(node_id);
1183 if let Some(before) = node.before() {
1184 self.drop_node_ignoring_parent_with(before, on_drop);
1185 }
1186 if let Some(after) = node.after() {
1187 self.drop_node_ignoring_parent_with(after, on_drop);
1188 }
1189
1190 for &child in &node.children {
1191 self.drop_node_ignoring_parent_with(child, on_drop);
1192 }
1193
1194 for &anon_id in &node.anonymous_blocks {
1197 self.deallocate_anonymous_block(anon_id);
1198 }
1199
1200 #[cfg(feature = "shadow-dom")]
1203 if let Some(shadow_root_id) = node.shadow_root_id() {
1204 self.shadow_host_nodes.remove(&node_id);
1205 self.custom_element_nodes.remove(&node_id);
1206 self.drop_node_ignoring_parent(shadow_root_id);
1207 }
1208 }
1209 node
1210 }
1211
1212 pub(crate) fn deallocate_anonymous_block(&mut self, anon_id: NodeId) {
1215 if !self.nodes.contains_key(anon_id) {
1218 return;
1219 }
1220
1221 let nested = std::mem::take(&mut self.nodes[anon_id].anonymous_blocks);
1223 for nested_id in nested {
1224 self.deallocate_anonymous_block(nested_id);
1225 }
1226
1227 self.remove_node_from_tree(anon_id);
1228 }
1229
1230 pub fn has_changes(&self) -> bool {
1232 self.changed_nodes.is_empty()
1233 }
1234
1235 pub fn create_text_node(&mut self, text: &str) -> NodeId {
1236 let content = text.to_string();
1237 let data = NodeData::Text(TextNodeData::new(content));
1238 self.create_node(data)
1239 }
1240
1241 pub fn deep_clone_node(&mut self, node_id: NodeId) -> NodeId {
1242 let node = &self.nodes[node_id];
1244 let mut data = node.data.clone();
1245
1246 match &mut data {
1247 NodeData::Element(elem) | NodeData::AnonymousBlock(elem) => {
1248 if let Some(arc) = elem.style_attribute.as_mut() {
1249 let read_guard = self.guard().read();
1250 let block = arc.read_with(&read_guard);
1251 *arc = ServoArc::new(self.guard().wrap(block.clone()));
1252 }
1253 }
1254 _ => {}
1255 }
1256
1257 let children = node.children.clone();
1258
1259 let new_node_id = self.create_node(data);
1261
1262 let new_children: ThinVec<NodeId> = children
1264 .into_iter()
1265 .map(|child_id| self.deep_clone_node(child_id))
1266 .collect();
1267 for &child_id in &new_children {
1268 self.nodes[child_id].parent = Some(new_node_id);
1269 }
1270 self.nodes[new_node_id].children = new_children;
1271
1272 new_node_id
1273 }
1274
1275 pub(crate) fn remove_and_drop_pe(&mut self, node_id: NodeId) -> Option<Node> {
1276 fn remove_pe_ignoring_parent(doc: &mut BaseDocument, node_id: NodeId) -> Option<Node> {
1277 let mut node = doc.remove_node_from_tree(node_id);
1278 if let Some(node) = &mut node {
1279 for &child in &node.children {
1280 remove_pe_ignoring_parent(doc, child);
1281 }
1282 for &anon_id in &node.anonymous_blocks {
1283 doc.deallocate_anonymous_block(anon_id);
1284 }
1285 }
1286 node
1287 }
1288
1289 let node = remove_pe_ignoring_parent(self, node_id);
1290
1291 if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
1293 let parent = &mut self.nodes[parent_id];
1294 parent.children.retain(|id| *id != node_id);
1295 }
1296
1297 node
1298 }
1299
1300 pub(crate) fn resolve_url(&self, raw: &str) -> url::Url {
1301 self.url.resolve_relative(raw).unwrap_or_else(|| {
1302 panic!(
1303 "to be able to resolve {raw} with the base_url: {:?}",
1304 *self.url
1305 )
1306 })
1307 }
1308
1309 pub fn print_tree(&self) {
1310 crate::util::walk_tree(0, self.root_node());
1311 }
1312
1313 pub fn print_subtree(&self, node_id: NodeId) {
1314 crate::util::walk_tree(0, &self.nodes[node_id]);
1315 }
1316
1317 pub fn reload_resource_by_href(&mut self, href_to_reload: &str) {
1318 for &node_id in self.nodes_to_stylesheet.keys() {
1319 let node = &self.nodes[node_id];
1320 let Some(element) = node.element_data() else {
1321 continue;
1322 };
1323
1324 if element.name.local == local_name!("link") {
1325 if let Some(href) = element.attr(local_name!("href")) {
1326 if href == href_to_reload {
1328 let resolved_href = self.resolve_url(href);
1329 self.net_provider.fetch(
1330 self.id(),
1331 self.build_request(resolved_href.clone()),
1332 ResourceHandler::boxed(
1333 self.tx.clone(),
1334 self.id,
1335 Some(node_id),
1336 self.shell_provider.clone(),
1337 StylesheetHandler {
1338 source_url: resolved_href,
1339 guard: self.guard.clone(),
1340 net_provider: self.net_provider.clone(),
1341 abort_signal: self.abort_signal.clone(),
1342 },
1343 ),
1344 );
1345 }
1346 }
1347 }
1348 }
1349 }
1350
1351 pub fn process_style_element(&mut self, target_id: NodeId) {
1352 let css = self.nodes[target_id].text_content();
1353 let css = html_escape::decode_html_entities(&css);
1354 let sheet = self.make_stylesheet(&css, Origin::Author);
1355 self.add_stylesheet_for_node(sheet, target_id);
1356 }
1357
1358 pub fn remove_user_agent_stylesheet(&mut self, contents: &str) {
1359 if let Some(sheet) = self.ua_stylesheets.remove(contents) {
1360 self.stylist.remove_stylesheet(sheet, &self.guard.read());
1361 }
1362 }
1363
1364 pub fn url(&self) -> &url::Url {
1366 &self.url
1367 }
1368
1369 pub fn author_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
1372 self.nodes_to_stylesheet.values()
1373 }
1374
1375 pub fn useragent_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
1377 self.ua_stylesheets.values()
1378 }
1379
1380 pub fn add_user_agent_stylesheet(&mut self, css: &str) {
1381 let sheet = self.make_stylesheet(css, Origin::UserAgent);
1382 self.ua_stylesheets.insert(css.to_string(), sheet.clone());
1383 self.stylist.append_stylesheet(sheet, &self.guard.read());
1384 }
1385
1386 pub fn make_stylesheet(&self, css: impl AsRef<str>, origin: Origin) -> DocumentStyleSheet {
1387 let data = Stylesheet::from_str(
1388 css.as_ref(),
1389 self.url.url_extra_data(),
1390 origin,
1391 ServoArc::new(self.guard.wrap(MediaList::empty())),
1392 self.guard.clone(),
1393 Some(&StylesheetLoader {
1394 tx: self.tx.clone(),
1395 doc_id: self.id,
1396 net_provider: self.net_provider.clone(),
1397 shell_provider: self.shell_provider.clone(),
1398 abort_signal: self.abort_signal.clone(),
1399 }),
1400 None,
1401 QuirksMode::NoQuirks,
1402 AllowImportRules::Yes,
1403 );
1404
1405 DocumentStyleSheet(ServoArc::new(data))
1406 }
1407
1408 pub fn upsert_stylesheet_for_node(&mut self, node_id: NodeId) {
1409 let raw_styles = self.nodes[node_id].text_content();
1410 let sheet = self.make_stylesheet(raw_styles, Origin::Author);
1411 self.add_stylesheet_for_node(sheet, node_id);
1412 }
1413
1414 pub fn add_stylesheet_for_node(&mut self, stylesheet: DocumentStyleSheet, node_id: NodeId) {
1415 let old = self.nodes_to_stylesheet.insert(node_id, stylesheet.clone());
1416
1417 if let Some(old) = old {
1418 self.stylist.remove_stylesheet(old, &self.guard.read())
1419 }
1420
1421 crate::net::fetch_font_face(
1423 self.tx.clone(),
1424 self.id,
1425 Some(node_id),
1426 &stylesheet.0,
1427 &self.net_provider,
1428 &self.shell_provider,
1429 &self.guard.read(),
1430 self.abort_signal.as_ref(),
1431 );
1432
1433 let element = &mut self.nodes[node_id].element_data_mut().unwrap();
1435 element.special_data = SpecialElementData::Stylesheet(stylesheet.clone());
1436
1437 let insertion_point = self
1439 .nodes_to_stylesheet
1440 .range((Bound::Excluded(node_id), Bound::Unbounded))
1441 .next()
1442 .map(|(_, sheet)| sheet);
1443
1444 if let Some(insertion_point) = insertion_point {
1445 self.stylist.insert_stylesheet_before(
1446 stylesheet,
1447 insertion_point.clone(),
1448 &self.guard.read(),
1449 )
1450 } else {
1451 self.stylist
1452 .append_stylesheet(stylesheet, &self.guard.read())
1453 }
1454 }
1455
1456 pub fn handle_messages(&mut self) {
1457 let rx = self.rx.take().unwrap();
1460
1461 while let Ok(msg) = rx.try_recv() {
1462 self.handle_message(msg);
1463 }
1464
1465 self.rx = Some(rx);
1467 }
1468
1469 pub fn handle_message(&mut self, msg: DocumentEvent) {
1470 match msg {
1471 DocumentEvent::ResourceLoad(resource) => self.load_resource(resource),
1472 DocumentEvent::NavigateIframe { node_id, url } => self.navigate_iframe(node_id, url),
1473 }
1474 }
1475
1476 pub fn has_pending_critical_resources(&self) -> bool {
1478 !self.pending_critical_resources.is_empty()
1479 }
1480
1481 pub fn pending_image_count(&self) -> usize {
1488 self.pending_images.len()
1489 }
1490
1491 pub fn load_resource(&mut self, res: ResourceLoadResponse) {
1492 self.pending_critical_resources.remove(&res.request_id);
1493
1494 let resource = match res.result {
1495 Ok(resource) => resource,
1496 Err(err) => {
1497 if let Some(url) = res.resolved_url.as_ref() {
1498 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1499 #[cfg(feature = "tracing")]
1500 tracing::warn!(
1501 url = url.as_str(),
1502 waiting_nodes = waiting_nodes.len(),
1503 error = err.as_str(),
1504 "Resource load failed"
1505 );
1506 #[cfg(not(feature = "tracing"))]
1507 let _ = (waiting_nodes, err);
1508 } else {
1509 #[cfg(feature = "tracing")]
1510 tracing::warn!(error = err.as_str(), "Resource load failed (no url)");
1511 #[cfg(not(feature = "tracing"))]
1512 let _ = err;
1513 }
1514 return;
1515 }
1516 };
1517
1518 match resource {
1519 Resource::Css(css) => {
1520 let node_id = res.node_id.unwrap();
1521 self.add_stylesheet_for_node(css, node_id);
1522 }
1523 Resource::Image(_kind, width, height, image_data) => {
1524 let image = ImageData::Raster(RasterImageData::new(width, height, image_data));
1526
1527 let Some(url) = res.resolved_url.as_ref() else {
1528 return;
1529 };
1530
1531 self.apply_loaded_image(url, image);
1532 }
1533 #[cfg(feature = "svg")]
1534 Resource::Svg(_kind, svg) => {
1535 let image = ImageData::Svg(svg);
1537
1538 let Some(url) = res.resolved_url.as_ref() else {
1539 return;
1540 };
1541
1542 self.apply_loaded_image(url, image);
1543 }
1544 Resource::DocumentSrc(html) => {
1545 let Some(node_id) = res.node_id else {
1546 return;
1547 };
1548 self.apply_iframe_html(node_id, res.request_id, res.resolved_url, &html);
1549 }
1550 Resource::Font(bytes, overrides) => {
1551 let font = Blob::new(Arc::new(bytes));
1552
1553 let weight_override = overrides.weight.map(parley::fontique::FontWeight::new);
1559 let info_override = parley::fontique::FontInfoOverride {
1560 family_name: overrides.family_name.as_deref(),
1561 weight: weight_override,
1562 style: overrides.style,
1563 ..Default::default()
1564 };
1565
1566 let mut global_font_ctx = self.font_ctx.lock().unwrap();
1568 global_font_ctx
1569 .collection
1570 .register_fonts(font.clone(), Some(info_override));
1571
1572 #[cfg(feature = "parallel-construct")]
1573 {
1574 rayon::broadcast(|_ctx| {
1575 let mut font_ctx = self
1576 .thread_font_contexts
1577 .get_or(|| RefCell::new(Box::new(global_font_ctx.clone())))
1578 .borrow_mut();
1579 font_ctx
1580 .collection
1581 .register_fonts(font.clone(), Some(info_override));
1582 });
1583 }
1584 drop(global_font_ctx);
1585
1586 self.invalidate_inline_contexts();
1588 }
1589 Resource::None => {
1590 }
1592 }
1593 }
1594
1595 fn apply_loaded_image(&mut self, url: &str, image: ImageData) {
1598 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1600
1601 #[cfg(feature = "tracing")]
1602 tracing::info!(
1603 "Image {url} loaded, applying to {} nodes",
1604 waiting_nodes.len()
1605 );
1606
1607 self.image_cache.insert(url.to_string(), image.clone());
1609
1610 for (node_id, image_type) in waiting_nodes {
1612 let Some(node) = self.get_node_mut(node_id) else {
1613 continue;
1614 };
1615
1616 match image_type {
1617 ImageType::Image => {
1618 node.element_data_mut().unwrap().special_data =
1619 SpecialElementData::Image(Box::new(image.clone()));
1620
1621 node.cache_mut().clear();
1623 node.insert_damage(ALL_DAMAGE);
1624 }
1625 ImageType::Background(idx) | ImageType::Mask(idx) => {
1626 let layer_image = node.element_data_mut().and_then(|el| {
1627 let images = match image_type {
1628 ImageType::Background(_) => &mut el.background_images,
1629 ImageType::Mask(_) => &mut el.mask_images,
1630 ImageType::Image => unreachable!(),
1631 };
1632 images.get_mut(idx)
1633 });
1634 if let Some(Some(layer_image)) = layer_image {
1635 layer_image.status = Status::Ok;
1636 layer_image.image = image.clone();
1637 }
1638 }
1639 }
1640 }
1641 }
1642
1643 pub fn snapshot_node(&mut self, node_id: NodeId) {
1644 let node = &mut self.nodes[node_id];
1645
1646 let has_been_styled = node.primary_styles().is_some();
1651 if !has_been_styled {
1652 return;
1653 }
1654
1655 let opaque_node_id = TNode::opaque(&&*node);
1656 node.set_has_snapshot(true);
1657 node.snapshot_handled()
1658 .store(false, std::sync::atomic::Ordering::SeqCst);
1659
1660 if let Some(_existing_snapshot) = self.snapshots.get_mut(&opaque_node_id) {
1662 } else {
1665 let attrs: Option<Vec<_>> = node.attrs().map(|attrs| {
1666 attrs
1667 .iter()
1668 .map(|attr| {
1669 let ident = AttrIdentifier {
1670 local_name: GenericAtomIdent(attr.name.local.clone()),
1671 name: GenericAtomIdent(attr.name.local.clone()),
1672 namespace: GenericAtomIdent(attr.name.ns.clone()),
1673 prefix: None,
1674 };
1675
1676 let value = if attr.name.local == local_name!("id") {
1677 AttrValue::Atom(Atom::from(&*attr.value))
1678 } else if attr.name.local == local_name!("class") {
1679 let classes = attr
1680 .value
1681 .split_ascii_whitespace()
1682 .map(Atom::from)
1683 .collect();
1684 AttrValue::TokenList(OnceLock::from(attr.value.to_string()), classes)
1690 } else {
1691 AttrValue::String(attr.value.to_string())
1692 };
1693
1694 (ident, value)
1695 })
1696 .collect()
1697 });
1698
1699 let changed_attrs = attrs
1700 .as_ref()
1701 .map(|attrs| attrs.iter().map(|attr| attr.0.name.clone()).collect())
1702 .unwrap_or_default();
1703
1704 self.snapshots.insert(
1705 opaque_node_id,
1706 ServoElementSnapshot {
1707 state: Some(*node.element_state()),
1708 attrs,
1709 changed_attrs,
1710 class_changed: true,
1711 id_changed: true,
1712 other_attributes_changed: true,
1713 },
1714 );
1715 }
1716 }
1717
1718 pub fn snapshot_node_and(&mut self, node_id: NodeId, cb: impl FnOnce(&mut Node)) {
1725 if !self.nodes.contains_key(node_id) {
1726 return;
1727 }
1728 self.snapshot_node(node_id);
1729 cb(&mut self.nodes[node_id]);
1730 }
1731
1732 pub fn hit(&self, x: f32, y: f32) -> Option<HitResult> {
1734 self.hit_with_scrollbar(x, y).0
1735 }
1736
1737 pub fn nearest_non_anonymous_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
1750 let mut node = self.get_node(node_id)?;
1754 loop {
1755 let parent = match node.parent {
1756 Some(parent_id) => self.get_node(parent_id)?,
1757 None => return Some(node.id),
1758 };
1759 if !node.is_anonymous() && !parent.is_anonymous() {
1760 return Some(node.id);
1761 }
1762 node = parent;
1763 }
1764 }
1765
1766 pub fn focus_next_node(&mut self) -> Option<NodeId> {
1767 let focussed_node_id = self.get_focussed_node_id()?;
1768 let id = self.next_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
1769 self.set_focus_to(id);
1770 Some(id)
1771 }
1772
1773 pub fn focus_prev_node(&mut self) -> Option<NodeId> {
1775 let focussed_node_id = self.get_focussed_node_id()?;
1776 let id = self.prev_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
1777 self.set_focus_to(id);
1778 Some(id)
1779 }
1780
1781 pub fn clear_focus(&mut self) {
1783 if let Some(id) = self.focus_node_id {
1784 let shell_provider = self.shell_provider.clone();
1785 self.snapshot_node_and(id, |node| node.blur(shell_provider));
1786 self.focus_node_id = None;
1787 }
1788 }
1789
1790 pub fn set_mousedown_node_id(&mut self, node_id: Option<NodeId>) {
1791 self.mousedown_node_id = node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
1792 }
1793 pub fn set_focus_to(&mut self, focus_node_id: NodeId) -> bool {
1794 let Some(focus_node_id) = self.nearest_non_anonymous_ancestor(focus_node_id) else {
1795 return false;
1796 };
1797 if Some(focus_node_id) == self.focus_node_id {
1798 return false;
1799 }
1800
1801 #[cfg(feature = "tracing")]
1802 tracing::info!("Focussed node {focus_node_id}");
1803
1804 let shell_provider = self.shell_provider.clone();
1805
1806 if let Some(id) = self.focus_node_id {
1808 self.snapshot_node_and(id, |node| node.blur(shell_provider.clone()));
1809 }
1810
1811 self.snapshot_node_and(focus_node_id, |node| node.focus(shell_provider));
1813
1814 self.focus_node_id = Some(focus_node_id);
1815
1816 true
1817 }
1818
1819 pub fn active_node(&mut self) -> bool {
1820 let Some(hover_node_id) = self.get_hover_node_id() else {
1821 return false;
1822 };
1823
1824 if let Some(active_node_id) = self.active_node_id {
1825 if active_node_id == hover_node_id {
1826 return true;
1827 }
1828 self.unactive_node();
1829 }
1830
1831 debug_assert!(
1833 self.get_node(hover_node_id)
1834 .is_some_and(|node| !node.is_anonymous()),
1835 "interaction state must reference DOM nodes, not layout-generated nodes"
1836 );
1837 let active_node_id = Some(hover_node_id);
1838
1839 let node_path = self.maybe_node_layout_ancestors(active_node_id);
1840 for &id in node_path.iter() {
1841 self.snapshot_node_and(id, |node| node.active());
1842 }
1843
1844 self.active_node_id = active_node_id;
1845
1846 true
1847 }
1848
1849 pub fn unactive_node(&mut self) -> bool {
1850 let Some(active_node_id) = self.active_node_id.take() else {
1851 return false;
1852 };
1853
1854 let node_path = self.maybe_node_layout_ancestors(Some(active_node_id));
1855 for &id in node_path.iter() {
1856 self.snapshot_node_and(id, |node| node.unactive());
1857 }
1858
1859 true
1860 }
1861
1862 pub fn hovered_scrollbar(&self) -> Option<crate::node::ScrollbarRef> {
1864 self.hovered_scrollbar
1865 }
1866
1867 pub fn scrollbar_drag_target(&self) -> Option<crate::node::ScrollbarRef> {
1869 match &self.drag_mode {
1870 DragMode::ScrollbarDrag(state) => Some(state.scrollbar),
1871 _ => None,
1872 }
1873 }
1874
1875 pub fn scrollbar_opacity(&self, node_id: NodeId) -> f32 {
1880 let interacting = |scrollbar: &crate::node::ScrollbarRef| scrollbar.node_id == node_id;
1881 if self.hovered_scrollbar.as_ref().is_some_and(interacting)
1882 || self
1883 .scrollbar_drag_target()
1884 .as_ref()
1885 .is_some_and(interacting)
1886 {
1887 return 1.0;
1888 }
1889 self.scrollbar_activity.get(&node_id).map_or(1.0, |last| {
1890 crate::node::scrollbar::opacity_at(last.elapsed())
1891 })
1892 }
1893
1894 pub(crate) fn show_scrollbars(&mut self, node_id: NodeId) {
1897 if cfg!(feature = "scrollbars") {
1898 self.scrollbar_activity.insert(node_id, Instant::now());
1899 }
1900 }
1901
1902 fn scrollbars_animating(&self) -> bool {
1905 use crate::node::scrollbar::{FADE_DELAY, FADE_DURATION};
1906 self.scrollbar_activity
1907 .values()
1908 .any(|last| last.elapsed() < FADE_DELAY + FADE_DURATION)
1909 }
1910
1911 pub(crate) fn hit_with_scrollbar(
1915 &self,
1916 x: f32,
1917 y: f32,
1918 ) -> (Option<HitResult>, Option<crate::node::ScrollbarRef>) {
1919 if TDocument::as_node(&self.root_node())
1920 .first_element_child()
1921 .is_none()
1922 {
1923 #[cfg(feature = "tracing")]
1924 tracing::warn!("No DOM - not resolving hit test");
1925 return (None, None);
1926 }
1927 let mut scrollbar = None;
1928 let hit = self
1929 .root_element()
1930 .hit_inner(x, y, self.viewport().scale_f64(), &mut scrollbar);
1931 (hit, scrollbar)
1932 }
1933
1934 pub fn set_hover_to(&mut self, x: f32, y: f32) -> bool {
1935 self.last_client_pointer_position = Some(taffy::Point {
1939 x: x - self.viewport_scroll.x as f32,
1940 y: y - self.viewport_scroll.y as f32,
1941 });
1942
1943 let (hit, hovered_scrollbar) = self.hit_with_scrollbar(x, y);
1944 let hovered_scrollbar =
1947 hovered_scrollbar.filter(|scrollbar| self.scrollbar_opacity(scrollbar.node_id) > 0.0);
1948 let scrollbar_changed = hovered_scrollbar != self.hovered_scrollbar;
1952 if scrollbar_changed {
1953 for scrollbar in [self.hovered_scrollbar, hovered_scrollbar]
1956 .into_iter()
1957 .flatten()
1958 {
1959 self.show_scrollbars(scrollbar.node_id);
1960 }
1961 }
1962 self.hovered_scrollbar = hovered_scrollbar;
1963
1964 let hit_node_id = hit.map(|hit| hit.node_id);
1969 let hover_node_id = hit_node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
1970 let new_is_text = hit.map(|hit| hit.is_text).unwrap_or(false);
1971
1972 let hit_changed =
1973 hit_node_id != self.hover_hit_node_id || new_is_text != self.hover_node_is_text;
1974 self.hover_hit_node_id = hit_node_id;
1975 self.hover_node_is_text = new_is_text;
1976
1977 if hover_node_id == self.hover_node_id {
1979 if hit_changed {
1980 self.shell_provider.set_cursor(self.get_cursor());
1984 }
1985 return scrollbar_changed;
1986 }
1987
1988 let old_node_path = self.maybe_node_layout_ancestors(self.hover_node_id);
1989 let new_node_path = self.maybe_node_layout_ancestors(hover_node_id);
1990 let same_count = old_node_path
1991 .iter()
1992 .zip(&new_node_path)
1993 .take_while(|(o, n)| o == n)
1994 .count();
1995 for &id in old_node_path.iter().skip(same_count) {
1996 self.snapshot_node_and(id, |node| node.unhover());
1997 }
1998 for &id in new_node_path.iter().skip(same_count) {
1999 self.snapshot_node_and(id, |node| node.hover());
2000 }
2001
2002 self.hover_node_id = hover_node_id;
2003
2004 self.shell_provider.set_cursor(self.get_cursor());
2006
2007 self.shell_provider.request_redraw();
2009
2010 true
2011 }
2012
2013 pub fn clear_hover(&mut self) -> bool {
2014 self.last_client_pointer_position = None;
2017 self.hover_hit_node_id = None;
2018
2019 let Some(hover_node_id) = self.hover_node_id else {
2020 return false;
2021 };
2022
2023 let old_node_path = self.maybe_node_layout_ancestors(Some(hover_node_id));
2024 for &id in old_node_path.iter() {
2025 self.snapshot_node_and(id, |node| node.unhover());
2026 }
2027
2028 self.hover_node_id = None;
2029 self.hover_node_is_text = false;
2030
2031 self.shell_provider.set_cursor(self.get_cursor());
2033
2034 self.shell_provider.request_redraw();
2036
2037 true
2038 }
2039
2040 pub fn refresh_hover(&mut self) -> bool {
2046 let Some(pos) = self.last_client_pointer_position else {
2047 return false;
2048 };
2049 let x = pos.x + self.viewport_scroll.x as f32;
2050 let y = pos.y + self.viewport_scroll.y as f32;
2051 self.set_hover_to(x, y)
2052 }
2053
2054 pub fn get_hover_node_id(&self) -> Option<NodeId> {
2055 self.hover_node_id
2056 }
2057
2058 pub fn get_mousedown_node_id(&self) -> Option<NodeId> {
2059 self.mousedown_node_id
2060 }
2061
2062 pub fn set_viewport(&mut self, viewport: Viewport) {
2063 let scale_has_changed = viewport.scale_f64() != self.viewport.scale_f64();
2064 self.viewport = viewport;
2065 self.set_stylist_device(make_device(
2066 &self.viewport,
2067 self.media_type.clone(),
2068 self.font_ctx.clone(),
2069 ));
2070 self.scroll_viewport_by(0.0, 0.0); if scale_has_changed {
2073 self.invalidate_inline_contexts();
2074 self.shell_provider.request_redraw();
2075 }
2076 }
2077
2078 pub fn media_type(&self) -> &MediaType {
2080 &self.media_type
2081 }
2082
2083 pub fn set_media_type(&mut self, media_type: MediaType) {
2086 if self.media_type == media_type {
2087 return;
2088 }
2089 self.media_type = media_type;
2090 self.set_stylist_device(make_device(
2091 &self.viewport,
2092 self.media_type.clone(),
2093 self.font_ctx.clone(),
2094 ));
2095 }
2096
2097 pub fn viewport(&self) -> &Viewport {
2098 &self.viewport
2099 }
2100
2101 pub fn viewport_mut(&mut self) -> ViewportMut<'_> {
2102 ViewportMut::new(self)
2103 }
2104
2105 pub fn zoom_by(&mut self, increment: f32) {
2106 *self.viewport.zoom_mut() += increment;
2107 self.set_viewport(self.viewport.clone());
2108 }
2109
2110 pub fn zoom_to(&mut self, zoom: f32) {
2111 *self.viewport.zoom_mut() = zoom;
2112 self.set_viewport(self.viewport.clone());
2113 }
2114
2115 pub fn get_viewport(&self) -> Viewport {
2116 self.viewport.clone()
2117 }
2118
2119 pub fn incremental_layout(&self) -> bool {
2121 self.incremental_layout
2122 }
2123
2124 pub fn set_incremental_layout(&mut self, enabled: bool) {
2126 self.incremental_layout = enabled;
2127 }
2128
2129 pub fn devtools(&self) -> &DevtoolSettings {
2130 &self.devtool_settings
2131 }
2132
2133 pub fn devtools_mut(&mut self) -> &mut DevtoolSettings {
2134 &mut self.devtool_settings
2135 }
2136
2137 pub fn subdoc(&self, node_id: NodeId) -> Option<&dyn Document> {
2138 self.get_node(node_id)
2139 .and_then(|node| node.element_data())
2140 .and_then(|el| el.sub_doc_data())
2141 }
2142
2143 pub fn subdoc_mut(&mut self, node_id: NodeId) -> Option<&mut dyn Document> {
2144 self.get_node_mut(node_id)
2145 .and_then(|node| node.element_data_mut())
2146 .and_then(|el| el.sub_doc_data_mut())
2147 }
2148
2149 pub fn is_animating(&self) -> bool {
2150 #[cfg(feature = "custom-widget")]
2151 let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
2152 self.nodes[node_id]
2153 .element_data()
2154 .and_then(|el| el.custom_widget_data())
2155 .is_some_and(|data| data.widget.requires_redraw())
2156 });
2157 #[cfg(not(feature = "custom-widget"))]
2158 let custom_widget_is_animating = false;
2159
2160 let animating = self.has_canvas
2161 | self.has_active_animations
2162 | (self.subdoc_animation_pacing != AnimationPacing::Idle)
2163 | custom_widget_is_animating
2164 | (self.scroll_animation != ScrollAnimationState::None)
2165 | self.scrollbars_animating();
2166
2167 if animating && crate::debug::animation_reasons_enabled() {
2168 crate::debug::report_animation_reasons(
2169 self.id(),
2170 self.has_canvas,
2171 self.has_active_animations,
2172 self.subdoc_animation_pacing != AnimationPacing::Idle,
2173 custom_widget_is_animating,
2174 self.scroll_animation != ScrollAnimationState::None,
2175 self.scrollbars_animating(),
2176 self.animating_node_names().as_deref(),
2177 );
2178 }
2179
2180 animating
2181 }
2182
2183 pub fn animation_pacing(&self) -> AnimationPacing {
2188 let focused_text_input = self.focus_node_id.is_some_and(|node_id| {
2189 self.nodes
2190 .get(node_id)
2191 .and_then(|node| node.element_data())
2192 .is_some_and(|element| element.text_input_data().is_some())
2193 });
2194 #[cfg(feature = "custom-widget")]
2195 let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
2196 self.nodes[node_id]
2197 .element_data()
2198 .and_then(|el| el.custom_widget_data())
2199 .is_some_and(|data| data.widget.requires_redraw())
2200 });
2201 #[cfg(not(feature = "custom-widget"))]
2202 let custom_widget_is_animating = false;
2203
2204 if self.has_canvas
2205 || custom_widget_is_animating
2206 || self.scroll_animation != ScrollAnimationState::None
2207 || self.scrollbars_animating()
2208 {
2209 AnimationPacing::Interactive
2210 } else if self.has_active_animations {
2211 const SLOW_ANIMATION_SECONDS: f64 = 2.0;
2212 let sets = self.animations.sets.read();
2213 let has_fast_animation_or_transition = sets.values().any(|set| {
2214 set.transitions.iter().any(|transition| {
2215 matches!(
2216 transition.state,
2217 AnimationState::Pending | AnimationState::Running
2218 )
2219 }) || set.animations.iter().any(|animation| {
2220 matches!(
2221 animation.state,
2222 AnimationState::Pending | AnimationState::Running
2223 ) && animation.duration < SLOW_ANIMATION_SECONDS
2224 })
2225 });
2226 if has_fast_animation_or_transition {
2227 AnimationPacing::Interactive
2228 } else {
2229 AnimationPacing::SlowCss
2230 }
2231 } else if focused_text_input {
2232 AnimationPacing::Caret
2233 } else if self.subdoc_animation_pacing != AnimationPacing::Idle {
2234 self.subdoc_animation_pacing
2235 } else {
2236 AnimationPacing::Idle
2237 }
2238 }
2239
2240 fn animating_node_names(&self) -> Option<String> {
2247 if !self.has_active_animations {
2248 return None;
2249 }
2250 let sets = self.animations.sets.read();
2251 let mut described: Vec<String> = sets
2252 .iter()
2253 .filter(|(_, state)| state.needs_animation_ticks())
2254 .filter_map(|(key, state)| {
2255 let node_id = NodeId::from_u64(key.node.id() as u64);
2256 let node = self.nodes.get(node_id)?;
2257 let element = node.element_data()?;
2258 let name = element
2259 .attr(local_name!("id"))
2260 .map(|id| format!("#{id}"))
2261 .or_else(|| {
2262 element
2263 .attr(local_name!("class"))
2264 .and_then(|c| c.split_ascii_whitespace().next())
2265 .map(|c| format!(".{c}"))
2266 })
2267 .unwrap_or_else(|| element.name.local.to_string());
2268 Some(format!(
2269 "{name}(anim={},trans={},in_doc={})",
2270 state.animations.len(),
2271 state.transitions.len(),
2272 node.flags.is_in_document(),
2273 ))
2274 })
2275 .collect();
2276 described.sort();
2277 described.truncate(12);
2278 Some(described.join(" "))
2279 }
2280
2281 pub fn set_stylist_device(&mut self, device: Device) {
2283 let root_styles = self
2289 .try_root_element()
2290 .and_then(|root| root.primary_styles());
2291 if let Some(root_style) = root_styles.as_deref() {
2292 device.set_root_style(root_style);
2293
2294 let font = root_style.get_font();
2295 let font_size = font.clone_font_size().computed_size();
2296 device.set_root_font_size(root_style.effective_zoom.unzoom(font_size.px()));
2297
2298 let line_height = device
2299 .calc_line_height(font, root_style.writing_mode, None)
2300 .0;
2301 device.set_root_line_height(root_style.effective_zoom.unzoom(line_height.px()));
2302 }
2303 drop(root_styles);
2304
2305 let origins = {
2306 let guard = &self.guard;
2307 let guards = StylesheetGuards {
2308 author: &guard.read(),
2309 ua_or_user: &guard.read(),
2310 };
2311 self.stylist.set_device(device, &guards)
2312 };
2313 self.stylist.force_stylesheet_origins_dirty(origins);
2314 }
2315
2316 pub fn stylist_device(&mut self) -> &Device {
2317 self.stylist.device()
2318 }
2319
2320 pub fn get_cursor(&self) -> Option<CursorIcon> {
2328 let node_id = self
2333 .hover_hit_node_id
2334 .filter(|&id| self.nodes.contains_key(id))
2335 .or(self.get_hover_node_id());
2336 let Some(node_id) = node_id else {
2337 return Some(CursorIcon::Default);
2338 };
2339 let node = &self.nodes[node_id];
2340
2341 if let Some(subdoc) = node.subdoc().map(|doc| doc.inner()) {
2342 if subdoc.hover_hit_node_id.is_some() || subdoc.get_hover_node_id().is_some() {
2348 return subdoc.get_cursor();
2349 }
2350 return Some(CursorIcon::Default);
2351 }
2352
2353 let Some(style) = node.primary_styles() else {
2354 return Some(CursorIcon::Default);
2355 };
2356 let user_select = style.clone_user_select();
2357 let keyword = style.clone_cursor().keyword;
2358
2359 if keyword != CursorKind::Auto {
2361 return stylo_to_cursor_icon(keyword);
2362 }
2363
2364 if node
2366 .element_data()
2367 .is_some_and(|e| e.text_input_data().is_some())
2368 {
2369 return Some(CursorIcon::Text);
2370 }
2371
2372 let mut maybe_node = Some(node);
2374 while let Some(node) = maybe_node {
2375 if node.is_link() {
2376 return Some(CursorIcon::Pointer);
2377 }
2378
2379 maybe_node = node.layout_parent.get().map(|node_id| node.with(node_id));
2380 }
2381
2382 if self.hover_node_is_text {
2384 return Some(match user_select {
2385 UserSelect::Text | UserSelect::All | UserSelect::Auto => CursorIcon::Text,
2386 UserSelect::None => CursorIcon::Default,
2387 });
2388 }
2389
2390 Some(CursorIcon::Default)
2392 }
2393
2394 pub fn scroll_node_by<F: FnMut(DomEvent)>(
2395 &mut self,
2396 node_id: NodeId,
2397 x: f64,
2398 y: f64,
2399 dispatch_event: F,
2400 ) {
2401 self.scroll_node_by_has_changed(node_id, x, y, dispatch_event);
2402 }
2403
2404 pub fn scroll_node_by_has_changed<F: FnMut(DomEvent)>(
2408 &mut self,
2409 node_id: NodeId,
2410 x: f64,
2411 y: f64,
2412 mut dispatch_event: F,
2413 ) -> bool {
2414 if self.try_root_element().is_some_and(|el| el.id == node_id) {
2419 let has_changed = self.scroll_viewport_by_has_changed(x, y);
2420 if has_changed {
2421 let layout = *self.root_element().final_layout();
2422 let scale = self.viewport.scale() as f64;
2423 let event = BlitzScrollEvent {
2424 scroll_top: self.viewport_scroll.y,
2425 scroll_left: self.viewport_scroll.x,
2426 scroll_width: layout.size.width.max(layout.content_size.width) as i32,
2427 scroll_height: layout.size.height.max(layout.content_size.height) as i32,
2428 client_width: (self.viewport.window_size.0 as f64 / scale) as i32,
2429 client_height: (self.viewport.window_size.1 as f64 / scale) as i32,
2430 };
2431 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
2432 }
2433 return has_changed;
2434 }
2435
2436 let Some(node) = self.nodes.get_mut(node_id) else {
2437 return false;
2438 };
2439
2440 if node
2444 .element_data()
2445 .is_some_and(|el| el.text_input_data().is_some())
2446 {
2447 let parent = node.parent;
2448 let content_box_width = node.final_layout().content_box_width();
2449 let content_box_height = node.final_layout().content_box_height();
2450 let input = node
2451 .element_data_mut()
2452 .and_then(|el| el.text_input_data_mut())
2453 .unwrap();
2454
2455 let (bubble_x, bubble_y) = if input.is_multiline {
2456 (
2457 x,
2458 input.scroll_by(y as f32, content_box_width, content_box_height) as f64,
2459 )
2460 } else {
2461 (
2462 input.scroll_by(x as f32, content_box_width, content_box_height) as f64,
2463 y,
2464 )
2465 };
2466
2467 let has_changed = bubble_x != x || bubble_y != y;
2468
2469 if bubble_x != 0.0 || bubble_y != 0.0 {
2470 let bubbled = if let Some(parent) = parent {
2471 self.scroll_node_by_has_changed(parent, bubble_x, bubble_y, dispatch_event)
2472 } else {
2473 self.scroll_viewport_by_has_changed(bubble_x, bubble_y)
2474 };
2475 return bubbled | has_changed;
2476 }
2477
2478 return has_changed;
2479 }
2480
2481 let (can_x_scroll, can_y_scroll) = node
2482 .primary_styles()
2483 .map(|styles| {
2484 (
2485 matches!(styles.clone_overflow_x(), Overflow::Scroll | Overflow::Auto),
2486 matches!(styles.clone_overflow_y(), Overflow::Scroll | Overflow::Auto),
2487 )
2488 })
2489 .unwrap_or((false, false));
2490
2491 let initial = *node.scroll_offset();
2492 let new_x = node.scroll_offset().x - x;
2493 let new_y = node.scroll_offset().y - y;
2494
2495 let mut bubble_x = 0.0;
2496 let mut bubble_y = 0.0;
2497
2498 let scroll_width = node.final_layout().scroll_width() as f64;
2499 let scroll_height = node.final_layout().scroll_height() as f64;
2500
2501 if let Some(mut sub_doc) = node.subdoc_mut().map(|doc| doc.inner_mut()) {
2503 let has_changed = if let Some(hover_node_id) = sub_doc.get_hover_node_id() {
2504 sub_doc.scroll_node_by_has_changed(hover_node_id, x, y, dispatch_event)
2505 } else {
2506 sub_doc.scroll_viewport_by_has_changed(x, y)
2507 };
2508
2509 return has_changed;
2511 }
2512
2513 if !can_x_scroll {
2515 bubble_x = x
2516 } else if new_x < 0.0 {
2517 bubble_x = -new_x;
2518 node.scroll_offset_mut().x = 0.0;
2519 } else if new_x > scroll_width {
2520 bubble_x = scroll_width - new_x;
2521 node.scroll_offset_mut().x = scroll_width;
2522 } else {
2523 node.scroll_offset_mut().x = new_x;
2524 }
2525
2526 if !can_y_scroll {
2527 bubble_y = y
2528 } else if new_y < 0.0 {
2529 bubble_y = -new_y;
2530 node.scroll_offset_mut().y = 0.0;
2531 } else if new_y > scroll_height {
2532 bubble_y = scroll_height - new_y;
2533 node.scroll_offset_mut().y = scroll_height;
2534 } else {
2535 node.scroll_offset_mut().y = new_y;
2536 }
2537
2538 let has_changed = *node.scroll_offset() != initial;
2539
2540 if has_changed {
2541 let layout = *node.final_layout();
2542 let event = BlitzScrollEvent {
2543 scroll_top: node.scroll_offset().y,
2544 scroll_left: node.scroll_offset().x,
2545 scroll_width: layout.scroll_width() as i32,
2546 scroll_height: layout.scroll_height() as i32,
2547 client_width: layout.size.width as i32,
2548 client_height: layout.size.height as i32,
2549 };
2550
2551 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
2552 }
2553
2554 let parent = node.parent;
2555 if has_changed {
2556 self.show_scrollbars(node_id);
2557 }
2558
2559 if bubble_x != 0.0 || bubble_y != 0.0 {
2560 if let Some(parent) = parent {
2561 return self.scroll_node_by_has_changed(parent, bubble_x, bubble_y, dispatch_event)
2562 | has_changed;
2563 } else {
2564 return self.scroll_viewport_by_has_changed(bubble_x, bubble_y) | has_changed;
2565 }
2566 }
2567
2568 has_changed
2569 }
2570
2571 pub fn scroll_viewport_by(&mut self, x: f64, y: f64) {
2572 self.scroll_viewport_by_has_changed(x, y);
2573 }
2574
2575 pub fn scroll_viewport_by_has_changed(&mut self, x: f64, y: f64) -> bool {
2577 let (content_width, content_height) = match self.try_root_element() {
2582 Some(root) => {
2583 let root_layout = root.final_layout();
2584 (
2585 root_layout.size.width.max(root_layout.content_size.width) as f64,
2586 root_layout.size.height.max(root_layout.content_size.height) as f64,
2587 )
2588 }
2589 None => (0.0, 0.0),
2590 };
2591 let new_scroll = (self.viewport_scroll.x - x, self.viewport_scroll.y - y);
2592 let window_width = self.viewport.window_size.0 as f64 / self.viewport.scale() as f64;
2593 let window_height = self.viewport.window_size.1 as f64 / self.viewport.scale() as f64;
2594
2595 let initial = self.viewport_scroll;
2596 self.viewport_scroll.x =
2597 f64::max(0.0, f64::min(new_scroll.0, content_width - window_width));
2598 self.viewport_scroll.y =
2599 f64::max(0.0, f64::min(new_scroll.1, content_height - window_height));
2600
2601 self.viewport_scroll != initial
2602 }
2603
2604 pub fn scroll_by(
2605 &mut self,
2606 anchor_node_id: Option<NodeId>,
2607 scroll_x: f64,
2608 scroll_y: f64,
2609 dispatch_event: &mut dyn FnMut(DomEvent),
2610 ) -> bool {
2611 if let Some(anchor_node_id) = anchor_node_id {
2612 self.scroll_node_by_has_changed(anchor_node_id, scroll_x, scroll_y, dispatch_event)
2613 } else {
2614 self.scroll_viewport_by_has_changed(scroll_x, scroll_y)
2615 }
2616 }
2617
2618 pub fn viewport_scroll(&self) -> crate::Point<f64> {
2619 self.viewport_scroll
2620 }
2621
2622 pub fn set_viewport_scroll(&mut self, scroll: crate::Point<f64>) {
2623 self.viewport_scroll = scroll;
2624 }
2625
2626 pub fn get_fragment_target(&self, fragment: &str) -> Option<NodeId> {
2631 if let Some(node_id) = self.get_element_by_id(fragment) {
2632 return Some(node_id);
2633 }
2634
2635 self.nodes.iter().find_map(|(id, node)| {
2637 let el = node.element_data()?;
2638 (el.name.local == local_name!("a") && el.attr(local_name!("name")) == Some(fragment))
2639 .then_some(id)
2640 })
2641 }
2642
2643 pub fn nearest_scroll_container(&self, node_id: NodeId) -> Option<NodeId> {
2654 let mut current = Some(node_id);
2655 for _ in 0..64 {
2656 let id = current?;
2657 let node = self.nodes.get(id)?;
2658 if node.style().overflow.x.is_scroll_container()
2659 || node.style().overflow.y.is_scroll_container()
2660 {
2661 return Some(id);
2662 }
2663 current = node.parent;
2664 }
2665 None
2666 }
2667
2668 pub fn scroll_nearest_container_by(&mut self, node_id: NodeId, x: f64, y: f64) -> bool {
2669 let mut current = Some(node_id);
2670 for _ in 0..64 {
2671 let Some(id) = current else { break };
2672 let Some(node) = self.nodes.get(id) else {
2673 break;
2674 };
2675 let scrolls = node.style().overflow.x.is_scroll_container()
2676 || node.style().overflow.y.is_scroll_container();
2677 if scrolls {
2678 self.scroll_node_by(id, x, y, |_| {});
2679 return true;
2680 }
2681 current = node.parent;
2682 }
2683 self.scroll_viewport_by(x, y);
2684 false
2685 }
2686
2687 pub fn scroll_to_node(&mut self, node_id: NodeId) {
2688 let mut chain = Vec::new();
2700 let mut current = self.nodes.get(node_id).and_then(|node| node.parent);
2701 while let Some(id) = current {
2702 let Some(node) = self.nodes.get(id) else {
2703 break;
2704 };
2705 let scrolls = node.style().overflow.x.is_scroll_container()
2706 || node.style().overflow.y.is_scroll_container();
2707 if scrolls {
2708 chain.push(id);
2709 }
2710 current = node.parent;
2711 }
2712
2713 for container in chain {
2717 let Some(node) = self.nodes.get(node_id) else {
2718 return;
2719 };
2720 let target = node.absolute_position(0.0, 0.0);
2721 let Some(scroller) = self.nodes.get(container) else {
2722 continue;
2723 };
2724 let box_ = scroller.absolute_position(0.0, 0.0);
2725 let layout = scroller.final_layout();
2726 let dx = f64::from(box_.x - target.x);
2730 let dy = f64::from(box_.y - target.y);
2731 let _ = layout;
2732 self.scroll_node_by(container, dx, dy, |_| {});
2733 }
2734
2735 let Some(node) = self.nodes.get(node_id) else {
2738 return;
2739 };
2740 let target = node.absolute_position(0.0, 0.0);
2741 let current = self.viewport_scroll;
2742
2743 self.scroll_viewport_by(current.x - target.x as f64, current.y - target.y as f64);
2746 }
2747
2748 pub fn scroll_to_fragment(&mut self, fragment: &str) -> bool {
2754 let decoded = percent_encoding::percent_decode_str(fragment)
2756 .decode_utf8_lossy()
2757 .into_owned();
2758
2759 if !decoded.is_empty() {
2760 if let Some(node_id) = self.get_fragment_target(&decoded) {
2761 self.scroll_to_node(node_id);
2762 return true;
2763 }
2764 }
2765
2766 if decoded.is_empty() || decoded.eq_ignore_ascii_case("top") {
2769 let current = self.viewport_scroll;
2770 self.scroll_viewport_by(current.x, current.y);
2771 return true;
2772 }
2773
2774 false
2775 }
2776
2777 pub fn get_client_bounding_rect(&self, node_id: NodeId) -> Option<BoundingRect> {
2779 if let Some(rects) = self.inline_fragment_rects(node_id) {
2782 let x0 = rects.iter().map(|r| r.x).fold(f64::INFINITY, f64::min);
2783 let y0 = rects.iter().map(|r| r.y).fold(f64::INFINITY, f64::min);
2784 let x1 = rects
2785 .iter()
2786 .map(|r| r.x + r.width)
2787 .fold(f64::NEG_INFINITY, f64::max);
2788 let y1 = rects
2789 .iter()
2790 .map(|r| r.y + r.height)
2791 .fold(f64::NEG_INFINITY, f64::max);
2792 return match rects.is_empty() {
2793 true => None,
2794 false => Some(BoundingRect {
2795 x: x0,
2796 y: y0,
2797 width: x1 - x0,
2798 height: y1 - y0,
2799 }),
2800 };
2801 }
2802
2803 let node = self.get_node(node_id)?;
2804 let pos = node.absolute_position(0.0, 0.0);
2805
2806 Some(BoundingRect {
2807 x: pos.x as f64 - self.viewport_scroll.x,
2808 y: pos.y as f64 - self.viewport_scroll.y,
2809 width: node.unrounded_layout().size.width as f64,
2810 height: node.unrounded_layout().size.height as f64,
2811 })
2812 }
2813
2814 pub fn node_client_rects(&self, node_id: NodeId) -> Vec<BoundingRect> {
2819 match self.inline_fragment_rects(node_id) {
2820 Some(rects) => rects,
2821 None => self.get_client_bounding_rect(node_id).into_iter().collect(),
2822 }
2823 }
2824
2825 pub(crate) fn trace_escaped_inline_fragments(&self) {
2839 static TRACE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2840 if !*TRACE.get_or_init(|| std::env::var_os("BLITZ_TRACE_INLINE").is_some()) {
2841 return;
2842 }
2843 let mut reported = 0;
2844 for (id, node) in self.nodes.iter() {
2845 if !node.is_element() {
2846 continue;
2847 }
2848 let Some(rects) = self.inline_fragment_rects(id) else {
2849 continue;
2850 };
2851 let Some(root) = node.inline_root_ancestor() else {
2852 continue;
2853 };
2854 let root_layout = root.final_layout();
2855 let root_pos = root.absolute_position(0.0, 0.0);
2856 let root_right =
2857 root_pos.x as f64 + root_layout.size.width as f64 - self.viewport_scroll.x;
2858 for rect in &rects {
2859 if rect.x + rect.width > root_right + 1.0 {
2860 reported += 1;
2861 if reported <= 12 {
2862 eprintln!(
2863 "escaped-fragment node={id:?} rect=[{:.1},{:.1} {:.1}x{:.1}] \
2864root={:?} root_right={root_right:.1} root_w={:.1} lines={} layout_scale={:.2} vp_scale={:.2} layout_w={:.1}",
2865 rect.x,
2866 rect.y,
2867 rect.width,
2868 rect.height,
2869 root.id,
2870 root_layout.size.width,
2871 root.element_data()
2872 .and_then(|e| e.inline_layout_data.as_ref())
2873 .map(|i| i.layout.len())
2874 .unwrap_or(0),
2875 root.element_data()
2876 .and_then(|e| e.inline_layout_data.as_ref())
2877 .map(|i| i.layout.scale())
2878 .unwrap_or(0.0),
2879 self.viewport.scale(),
2880 root.element_data()
2881 .and_then(|e| e.inline_layout_data.as_ref())
2882 .map(|i| i.layout.width())
2883 .unwrap_or(0.0),
2884 );
2885 }
2886 break;
2887 }
2888 }
2889 }
2890 if reported > 0 {
2891 eprintln!("escaped-fragment total={reported}");
2892 }
2893
2894 let mut narrow = 0;
2899 for (id, node) in self.nodes.iter() {
2900 let Some(inline) = node
2901 .data
2902 .downcast_element()
2903 .and_then(|element| element.inline_layout_data.as_ref())
2904 else {
2905 continue;
2906 };
2907 let box_width = node.final_layout().size.width as f64 * self.viewport.scale() as f64;
2908 let broken_at = inline.layout.width() as f64;
2909 let full = inline.layout.calculate_content_widths().max as f64;
2912 if box_width > 40.0 && broken_at < box_width * 0.6 && full > box_width * 0.9 {
2913 narrow += 1;
2914 if narrow <= 12 {
2915 eprintln!(
2916 "narrow-break node={id:?} broken_at={broken_at:.1} box={box_width:.1} \
2917 max_content={full:.1} lines={} text={:?}",
2918 inline.layout.len(),
2919 inline.text.chars().take(40).collect::<String>(),
2920 );
2921 }
2922 }
2923 }
2924 if narrow > 0 {
2925 eprintln!("narrow-break total={narrow}");
2926 }
2927 }
2928
2929 pub fn inline_fragment_rects(&self, node_id: NodeId) -> Option<Vec<BoundingRect>> {
2930 use parley::PositionedLayoutItem;
2931
2932 let node = self.get_node(node_id)?;
2933
2934 if !node.is_element() || node.flags.is_inline_root() {
2937 return None;
2938 }
2939 let display = node.primary_styles()?.clone_display();
2940 if !(display.outside() == DisplayOutside::Inline && display.inside() == DisplayInside::Flow)
2941 {
2942 return None;
2943 }
2944
2945 let inline_root = node.inline_root_ancestor()?;
2946 let inline_layout = inline_root.element_data()?.inline_layout_data.as_ref()?;
2947 let layout = &inline_layout.layout;
2948 let scale = layout.scale() as f64;
2949
2950 let is_in_target = |mut id: NodeId| -> bool {
2953 loop {
2954 if id == node_id {
2955 return true;
2956 }
2957 if id == inline_root.id {
2958 return false;
2959 }
2960 match self.get_node(id).and_then(|n| n.parent) {
2961 Some(parent) => id = parent,
2962 None => return false,
2963 }
2964 }
2965 };
2966
2967 let root_layout = inline_root.final_layout();
2969 let root_pos = inline_root.absolute_position(0.0, 0.0);
2970 let origin_x = root_pos.x as f64
2971 + (root_layout.padding.left + root_layout.border.left) as f64
2972 - self.viewport_scroll.x;
2973 let origin_y = root_pos.y as f64
2974 + (root_layout.padding.top + root_layout.border.top) as f64
2975 - self.viewport_scroll.y;
2976
2977 let mut rects: Vec<BoundingRect> = Vec::new();
2978 for line in layout.lines() {
2979 let line_metrics = line.metrics();
2980 let mut line_rect: Option<(f64, f64, f64, f64)> = None;
2982 let mut add = |x0: f64, y0: f64, x1: f64, y1: f64| {
2983 line_rect = Some(match line_rect {
2984 Some((lx0, ly0, lx1, ly1)) => {
2985 (lx0.min(x0), ly0.min(y0), lx1.max(x1), ly1.max(y1))
2986 }
2987 None => (x0, y0, x1, y1),
2988 });
2989 };
2990
2991 for item in line.items() {
2992 match item {
2993 PositionedLayoutItem::GlyphRun(glyph_run) => {
2994 if !is_in_target(glyph_run.style().brush.id) {
2995 continue;
2996 }
2997 let x0 = glyph_run.offset() as f64;
2998 let x1 = x0 + glyph_run.advance() as f64;
2999 let y0 = line_metrics.block_min_coord as f64;
3005 let y1 = line_metrics.block_max_coord as f64;
3006 add(x0, y0, x1, y1);
3007 }
3008 PositionedLayoutItem::InlineBox(inline_box) => {
3009 if !is_in_target(NodeId::from_u64(inline_box.id)) {
3010 continue;
3011 }
3012 let x0 = inline_box.x as f64;
3013 let y0 = inline_box.y as f64;
3014 add(
3015 x0,
3016 y0,
3017 x0 + inline_box.width as f64,
3018 y0 + inline_box.height as f64,
3019 );
3020 }
3021 }
3022 }
3023
3024 if let Some((x0, y0, x1, y1)) = line_rect {
3025 rects.push(BoundingRect {
3026 x: origin_x + x0 / scale,
3027 y: origin_y + y0 / scale,
3028 width: (x1 - x0) / scale,
3029 height: (y1 - y0) / scale,
3030 });
3031 }
3032 }
3033
3034 Some(rects)
3035 }
3036
3037 pub fn find_title_node(&self) -> Option<&Node> {
3038 TreeTraverser::new(self)
3039 .find(|node_id| {
3040 let node = &self.nodes[*node_id];
3041 let Some(element) = node.element_data() else {
3042 return false;
3043 };
3044 if element.name.ns != ns!(html) || element.name.local != local_name!("title") {
3045 return false;
3046 }
3047 node.parent
3048 .and_then(|parent_id| self.nodes.get(parent_id))
3049 .and_then(Node::element_data)
3050 .is_some_and(|parent| {
3051 parent.name.ns == ns!(html) && parent.name.local == local_name!("head")
3052 })
3053 })
3054 .map(|node_id| &self.nodes[node_id])
3055 }
3056
3057 pub fn with_text_input(
3058 &mut self,
3059 node_id: NodeId,
3060 cb: impl FnOnce(PlainEditorDriver<TextBrush>),
3061 ) {
3062 let Some(node) = self.nodes.get_mut(node_id) else {
3063 return;
3064 };
3065
3066 if let Some(text_input) = node
3067 .element_data_mut()
3068 .and_then(|el| el.text_input_data_mut())
3069 {
3070 let mut font_ctx = self.font_ctx.lock().unwrap();
3071 let layout_ctx = &mut self.layout_ctx;
3072 let driver = text_input.editor.driver(&mut font_ctx, layout_ctx);
3073 cb(driver)
3074 }
3075 }
3076
3077 pub(crate) fn clamp_text_input_scroll(&mut self, node_id: NodeId) {
3080 let Some(node) = self.nodes.get_mut(node_id) else {
3081 return;
3082 };
3083
3084 let content_box_width = node.final_layout().content_box_width();
3085 let content_box_height = node.final_layout().content_box_height();
3086
3087 if let Some(text_input) = node
3088 .element_data_mut()
3089 .and_then(|el| el.text_input_data_mut())
3090 {
3091 text_input.clamp_scroll_offset(content_box_width, content_box_height);
3092 }
3093 }
3094
3095 pub(crate) fn compute_has_canvas(&self) -> bool {
3096 TreeTraverser::new(self).any(|node_id| {
3097 let node = &self.nodes[node_id];
3098 let Some(element) = node.element_data() else {
3099 return false;
3100 };
3101 if element.name.local == local_name!("canvas") && element.has_attr(local_name!("src")) {
3102 return true;
3103 }
3104
3105 false
3106 })
3107 }
3108
3109 pub fn find_text_position(&self, x: f32, y: f32) -> Option<(NodeId, usize)> {
3115 let hit = self.hit(x, y)?;
3116 let hit_node = self.get_node(hit.node_id)?;
3117 let inline_root = hit_node.inline_root_ancestor()?;
3118 let byte_offset = inline_root.text_offset_at_point(hit.x, hit.y)?;
3119 Some((inline_root.id, byte_offset))
3120 }
3121
3122 pub fn find_text_range(
3128 &self,
3129 x: f32,
3130 y: f32,
3131 granularity: TextGranularity,
3132 ) -> Option<(NodeId, usize, usize)> {
3133 let hit = self.hit(x, y)?;
3134 let hit_node = self.get_node(hit.node_id)?;
3135 let inline_root = hit_node.inline_root_ancestor()?;
3136 let range = inline_root.text_range_at_point(hit.x, hit.y, granularity)?;
3137 Some((inline_root.id, range.start, range.end))
3138 }
3139
3140 pub fn set_text_selection(
3142 &mut self,
3143 anchor_node: NodeId,
3144 anchor_offset: usize,
3145 focus_node: NodeId,
3146 focus_offset: usize,
3147 ) {
3148 self.text_selection =
3149 TextSelection::new(anchor_node, anchor_offset, focus_node, focus_offset);
3150
3151 if let (Some(parent), Some(idx)) = self.anonymous_block_location(anchor_node) {
3153 self.text_selection
3154 .anchor
3155 .set_anonymous(parent, idx, anchor_offset);
3156 }
3157 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
3158 self.text_selection
3159 .focus
3160 .set_anonymous(parent, idx, focus_offset);
3161 }
3162 }
3163
3164 fn anonymous_block_location(&self, node_id: NodeId) -> (Option<NodeId>, Option<usize>) {
3167 let Some(node) = self.get_node(node_id) else {
3168 return (None, None);
3169 };
3170
3171 if !node.is_anonymous() {
3172 return (None, None);
3173 }
3174
3175 let Some(parent_id) = node.parent else {
3176 return (None, None);
3177 };
3178
3179 let Some(parent) = self.get_node(parent_id) else {
3180 return (Some(parent_id), None);
3181 };
3182
3183 let layout_children = parent.layout_children.borrow();
3184 let Some(children) = layout_children.as_ref() else {
3185 return (Some(parent_id), None);
3186 };
3187
3188 let mut anon_index = 0;
3190 for &child_id in children.iter() {
3191 if child_id == node_id {
3192 return (Some(parent_id), Some(anon_index));
3193 }
3194 if self.get_node(child_id).is_some_and(|n| n.is_anonymous()) {
3195 anon_index += 1;
3196 }
3197 }
3198
3199 (Some(parent_id), None)
3200 }
3201
3202 pub fn clear_text_selection(&mut self) {
3204 self.text_selection.clear();
3205 }
3206
3207 pub fn update_selection_focus(&mut self, focus_node: NodeId, focus_offset: usize) {
3209 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
3211 self.text_selection
3212 .focus
3213 .set_anonymous(parent, idx, focus_offset);
3214 } else {
3215 self.text_selection.set_focus(focus_node, focus_offset);
3216 }
3217 }
3218
3219 pub fn extend_text_selection_to_point(&mut self, x: f32, y: f32) -> bool {
3222 if !self.text_selection.anchor.is_some() {
3223 return false;
3224 }
3225
3226 if let Some((node, offset)) = self.find_text_position(x, y) {
3227 self.update_selection_focus(node, offset);
3228 self.shell_provider.request_redraw();
3229 true
3230 } else {
3231 false
3232 }
3233 }
3234
3235 fn find_anonymous_block_by_index(
3237 &self,
3238 parent_id: NodeId,
3239 target_index: usize,
3240 ) -> Option<NodeId> {
3241 let parent = self.get_node(parent_id)?;
3242 let layout_children = parent.layout_children.borrow();
3243 let children = layout_children.as_ref()?;
3244
3245 children
3246 .iter()
3247 .filter(|&&child_id| self.get_node(child_id).is_some_and(|n| n.is_anonymous()))
3248 .nth(target_index)
3249 .copied()
3250 }
3251
3252 pub fn has_text_selection(&self) -> bool {
3254 self.text_selection.is_active()
3255 }
3256
3257 pub fn get_selected_text(&self) -> Option<String> {
3259 let ranges = self.get_text_selection_ranges();
3260 if ranges.is_empty() {
3261 return None;
3262 }
3263
3264 let mut result = String::new();
3265 for (node_id, start, end) in &ranges {
3266 let node = self.get_node(*node_id)?;
3267 let element_data = node.element_data()?;
3268 let inline_layout = element_data.inline_layout_data.as_ref()?;
3269
3270 if *end > inline_layout.text.len() {
3271 continue;
3272 }
3273
3274 if !result.is_empty() {
3275 result.push(' ');
3276 }
3277 result.push_str(&inline_layout.text[*start..*end]);
3278 }
3279
3280 if result.is_empty() {
3281 None
3282 } else {
3283 Some(result)
3284 }
3285 }
3286
3287 pub fn get_text_selection_ranges(&self) -> Vec<(NodeId, usize, usize)> {
3290 let lookup = |parent_id, idx| self.find_anonymous_block_by_index(parent_id, idx);
3291
3292 let anchor_node = match self.text_selection.anchor.resolve_node_id(lookup) {
3293 Some(id) => id,
3294 None => return Vec::new(),
3295 };
3296 let focus_node = match self.text_selection.focus.resolve_node_id(lookup) {
3297 Some(id) => id,
3298 None => return Vec::new(),
3299 };
3300
3301 let node_is_in_doc = |node_id: NodeId| {
3304 self.nodes
3305 .get(node_id)
3306 .is_some_and(|node| node.flags.is_in_document())
3307 };
3308 if !node_is_in_doc(anchor_node) || !node_is_in_doc(focus_node) {
3309 return Vec::new();
3310 }
3311
3312 if anchor_node == focus_node {
3314 let start = self
3315 .text_selection
3316 .anchor
3317 .offset
3318 .min(self.text_selection.focus.offset);
3319 let end = self
3320 .text_selection
3321 .anchor
3322 .offset
3323 .max(self.text_selection.focus.offset);
3324
3325 if start == end {
3326 return Vec::new();
3327 }
3328 return vec![(anchor_node, start, end)];
3329 }
3330
3331 let inline_roots = self.collect_inline_roots_in_range(anchor_node, focus_node);
3333 if inline_roots.is_empty() {
3334 return Vec::new();
3335 }
3336
3337 let first_in_roots = inline_roots[0];
3340
3341 let (first_node, first_offset, last_node, last_offset) =
3342 if first_in_roots == anchor_node || (first_in_roots != focus_node) {
3343 (
3345 anchor_node,
3346 self.text_selection.anchor.offset,
3347 focus_node,
3348 self.text_selection.focus.offset,
3349 )
3350 } else {
3351 (
3353 focus_node,
3354 self.text_selection.focus.offset,
3355 anchor_node,
3356 self.text_selection.anchor.offset,
3357 )
3358 };
3359
3360 let mut ranges = Vec::with_capacity(inline_roots.len());
3361
3362 for &node_id in &inline_roots {
3363 let Some(node) = self.get_node(node_id) else {
3364 continue;
3365 };
3366 let Some(element_data) = node.element_data() else {
3367 continue;
3368 };
3369 let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
3370 continue;
3371 };
3372
3373 let text_len = inline_layout.text.len();
3374
3375 if node_id == first_node && node_id == last_node {
3376 let start = first_offset.min(last_offset);
3377 let end = first_offset.max(last_offset);
3378 if start < end && end <= text_len {
3379 ranges.push((node_id, start, end));
3380 }
3381 } else if node_id == first_node {
3382 if first_offset < text_len {
3383 ranges.push((node_id, first_offset, text_len));
3384 }
3385 } else if node_id == last_node {
3386 if last_offset > 0 && last_offset <= text_len {
3387 ranges.push((node_id, 0, last_offset));
3388 }
3389 } else if text_len > 0 {
3390 ranges.push((node_id, 0, text_len));
3391 }
3392 }
3393
3394 ranges
3395 }
3396}
3397
3398#[derive(Debug, Clone, Copy, PartialEq)]
3399pub struct BoundingRect {
3400 pub x: f64,
3401 pub y: f64,
3402 pub width: f64,
3403 pub height: f64,
3404}
3405
3406impl AsRef<BaseDocument> for BaseDocument {
3407 fn as_ref(&self) -> &BaseDocument {
3408 self
3409 }
3410}
3411
3412impl AsMut<BaseDocument> for BaseDocument {
3413 fn as_mut(&mut self) -> &mut BaseDocument {
3414 self
3415 }
3416}
3417
3418#[cfg(test)]
3419mod hover_state_tests {
3420 use super::*;
3421 use crate::{Attribute, qual_name};
3422 use blitz_traits::shell::ColorScheme;
3423
3424 fn make_doc() -> (BaseDocument, NodeId) {
3431 let mut doc = BaseDocument::new(DocumentConfig {
3432 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3433 ..Default::default()
3434 });
3435 let root_id = doc.root_node().id;
3436 let style = |value: &str| Attribute {
3437 name: qual_name!("style"),
3438 value: value.into(),
3439 };
3440
3441 let mut mutator = doc.mutate();
3442 let html = mutator.create_element(qual_name!("html"), vec![]);
3443 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
3444 let container = mutator.create_element(qual_name!("div"), vec![style("width:300px")]);
3445 let text = mutator.create_text_node("some text");
3446 let block = mutator.create_element(qual_name!("div"), vec![style("height:50px")]);
3447 mutator.append_children(container, &[text, block]);
3448 mutator.append_children(body, &[container]);
3449 mutator.append_children(html, &[body]);
3450 mutator.append_children(root_id, &[html]);
3451 drop(mutator);
3452
3453 doc.resolve(0.0);
3454 (doc, container)
3455 }
3456
3457 fn text_has_size(doc: &BaseDocument, container: NodeId) -> bool {
3461 doc.nodes[container].final_layout().size.height > 50.0
3462 }
3463
3464 #[test]
3470 fn hovering_text_in_anonymous_block_reports_text_cursor() {
3471 let (mut doc, container) = make_doc();
3472 if !text_has_size(&doc, container) {
3473 eprintln!("skipping: no usable font (text measures 0x0)");
3474 return;
3475 }
3476
3477 doc.set_hover_to(5.0, 8.0);
3478 assert!(doc.hover_node_is_text, "expected a text hit");
3479 let hit_id = doc.hover_hit_node_id.expect("expected a hit node");
3480 assert!(
3481 doc.nodes[hit_id].is_anonymous(),
3482 "expected the hit node to be the anonymous inline root"
3483 );
3484 assert_eq!(
3485 doc.get_hover_node_id(),
3486 Some(container),
3487 "expected the stored hover target to be the containing element"
3488 );
3489 assert_eq!(doc.get_cursor(), Some(CursorIcon::Text));
3490 }
3491
3492 #[test]
3495 fn hovering_anonymous_block_whitespace_reports_default_cursor() {
3496 let (mut doc, container) = make_doc();
3497 if !text_has_size(&doc, container) {
3498 eprintln!("skipping: no usable font (text measures 0x0)");
3499 return;
3500 }
3501
3502 doc.set_hover_to(250.0, 8.0);
3503 assert!(!doc.hover_node_is_text);
3504 assert_eq!(doc.get_hover_node_id(), Some(container));
3505 assert_eq!(doc.get_cursor(), Some(CursorIcon::Default));
3506 }
3507}
3508
3509#[cfg(test)]
3510mod font_face_override_tests {
3511 use super::*;
3512 use crate::net::{FontFaceOverrides, Resource, ResourceLoadResponse};
3513
3514 #[test]
3530 fn font_face_overrides_alias_family_name() {
3531 const ALIAS: &str = "AliasedFamily";
3532
3533 let mut document = BaseDocument::new(DocumentConfig::default());
3534
3535 {
3537 let mut ctx = document.font_ctx.lock().unwrap();
3538 assert!(
3539 ctx.collection.family_id(ALIAS).is_none(),
3540 "alias must not exist before registration",
3541 );
3542 }
3543
3544 let response = ResourceLoadResponse {
3549 request_id: 0,
3550 node_id: None,
3551 resolved_url: Some(String::from("test://aliased-family")),
3552 result: Ok(Resource::Font(
3553 blitz_traits::net::Bytes::from_static(crate::BULLET_FONT),
3554 FontFaceOverrides {
3555 family_name: Some(String::from(ALIAS)),
3556 weight: Some(800.0),
3557 style: Some(parley::fontique::FontStyle::Italic),
3558 },
3559 )),
3560 };
3561 document.load_resource(response);
3562
3563 let mut ctx = document.font_ctx.lock().unwrap();
3566 let family_id = ctx
3567 .collection
3568 .family_id(ALIAS)
3569 .expect("CSS-declared family name should be registered as a family alias");
3570 let resolved_name = ctx
3571 .collection
3572 .family_name(family_id)
3573 .expect("family id should resolve back to a name");
3574 assert_eq!(
3575 resolved_name, ALIAS,
3576 "registered family should report the CSS-declared name, \
3577 not the font file's internal `name` table entry",
3578 );
3579 }
3580}