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