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, RequestDestination};
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 rustc_hash::FxHashSet;
34use selectors::{Element, matching::QuirksMode};
35use smallvec::SmallVec;
36use std::any::Any;
37use std::cell::RefCell;
38use std::collections::{BTreeMap, Bound, HashMap, HashSet};
39use std::ops::{Deref, DerefMut};
40use std::rc::Rc;
41use std::str::FromStr;
42use std::sync::atomic::{AtomicUsize, Ordering};
43use std::sync::mpsc::{Receiver, Sender, channel};
44use std::sync::{Arc, Mutex, MutexGuard, OnceLock, RwLockReadGuard, RwLockWriteGuard};
45use std::task::{Context as TaskContext, Waker};
46use style::Atom;
47use style::animation::{AnimationState, DocumentAnimationSet};
48use style::attr::{AttrIdentifier, AttrValue};
49use style::data::{ElementData as StyloElementData, ElementStyles};
50use style::media_queries::MediaType;
51use style::properties::ComputedValues;
52use style::properties::style_structs::Font;
53use style::queries::values::PrefersColorScheme;
54use style::selector_parser::ServoElementSnapshot;
55use style::servo::media_features::PointerCapabilities;
56use style::servo_arc::Arc as ServoArc;
57use style::values::GenericAtomIdent;
58use style::values::computed::ui::CursorKind;
59use style::values::computed::{Overflow, UserSelect};
60use style::values::specified::box_::{DisplayInside, DisplayOutside};
61use style::{
62 device::Device,
63 dom::{TDocument, TNode},
64 media_queries::MediaList,
65 selector_parser::SnapshotMap,
66 shared_lock::{SharedRwLock, StylesheetGuards},
67 stylesheets::{AllowImportRules, DocumentStyleSheet, Origin, Stylesheet},
68 stylist::Stylist,
69};
70use thin_vec::ThinVec;
71use url::Url;
72use web_time::Instant;
73
74#[cfg(feature = "parallel-construct")]
75use thread_local::ThreadLocal;
76
77pub enum DocGuard<'a> {
78 Ref(&'a BaseDocument),
79 RefCell(std::cell::Ref<'a, BaseDocument>),
80 RwLock(RwLockReadGuard<'a, BaseDocument>),
81 Mutex(MutexGuard<'a, BaseDocument>),
82}
83
84impl Deref for DocGuard<'_> {
85 type Target = BaseDocument;
86 #[inline(always)]
87 fn deref(&self) -> &Self::Target {
88 match self {
89 Self::Ref(base_document) => base_document,
90 Self::RefCell(refcell_guard) => refcell_guard,
91 Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
92 Self::Mutex(mutex_guard) => mutex_guard,
93 }
94 }
95}
96
97pub enum DocGuardMut<'a> {
98 Ref(&'a mut BaseDocument),
99 RefCell(std::cell::RefMut<'a, BaseDocument>),
100 RwLock(RwLockWriteGuard<'a, BaseDocument>),
101 Mutex(MutexGuard<'a, BaseDocument>),
102}
103
104impl Deref for DocGuardMut<'_> {
105 type Target = BaseDocument;
106 #[inline(always)]
107 fn deref(&self) -> &Self::Target {
108 match self {
109 Self::Ref(base_document) => base_document,
110 Self::RefCell(refcell_guard) => refcell_guard,
111 Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
112 Self::Mutex(mutex_guard) => mutex_guard,
113 }
114 }
115}
116
117impl DerefMut for DocGuardMut<'_> {
118 #[inline(always)]
119 fn deref_mut(&mut self) -> &mut Self::Target {
120 match self {
121 Self::Ref(base_document) => base_document,
122 Self::RefCell(refcell_guard) => &mut *refcell_guard,
123 Self::RwLock(rw_lock_read_guard) => &mut *rw_lock_read_guard,
124 Self::Mutex(mutex_guard) => &mut *mutex_guard,
125 }
126 }
127}
128
129pub trait Document: Any + 'static {
132 fn inner(&self) -> DocGuard<'_>;
133 fn inner_mut(&mut self) -> DocGuardMut<'_>;
134
135 fn handle_ui_event(&mut self, event: UiEvent) {
137 let mut doc = self.inner_mut();
138 let mut driver = EventDriver::new(&mut *doc, NoopEventHandler);
139 driver.handle_ui_event(event);
140 }
141
142 fn poll(&mut self, task_context: Option<TaskContext>) -> bool {
144 let _ = task_context;
146 false
147 }
148
149 fn poll_for_settle(&mut self, task_context: Option<TaskContext>, run_timers: bool) -> bool {
158 let _ = run_timers;
159 self.poll(task_context)
160 }
161
162 fn id(&self) -> usize {
164 self.inner().id
165 }
166}
167
168#[derive(Debug, Clone, PartialEq)]
171pub struct PreClickActivation {
172 previous: Vec<(NodeId, bool)>,
174}
175
176fn escape_html(text: &str) -> String {
182 let mut out = String::with_capacity(text.len());
183 for ch in text.chars() {
184 match ch {
185 '&' => out.push_str("&"),
186 '<' => out.push_str("<"),
187 '>' => out.push_str(">"),
188 '"' => out.push_str("""),
189 _ => out.push(ch),
190 }
191 }
192 out
193}
194
195pub struct PlainDocument(pub BaseDocument);
196impl Document for PlainDocument {
197 fn inner(&self) -> DocGuard<'_> {
198 DocGuard::Ref(&self.0)
199 }
200 fn inner_mut(&mut self) -> DocGuardMut<'_> {
201 DocGuardMut::Ref(&mut self.0)
202 }
203}
204
205impl Document for BaseDocument {
206 fn inner(&self) -> DocGuard<'_> {
207 DocGuard::Ref(self)
208 }
209 fn inner_mut(&mut self) -> DocGuardMut<'_> {
210 DocGuardMut::Ref(self)
211 }
212}
213
214impl Document for Rc<RefCell<BaseDocument>> {
215 fn inner(&self) -> DocGuard<'_> {
216 DocGuard::RefCell(self.borrow())
217 }
218
219 fn inner_mut(&mut self) -> DocGuardMut<'_> {
220 DocGuardMut::RefCell(self.borrow_mut())
221 }
222}
223
224pub enum DocumentEvent {
225 ResourceLoad(ResourceLoadResponse),
226 NavigateIframe {
229 node_id: NodeId,
230 url: Url,
231 },
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
236pub enum AnimationPacing {
237 Idle,
238 Caret,
239 SlowCss,
240 Interactive,
241}
242
243pub struct BaseDocument {
244 id: usize,
246
247 pub(crate) url: DocumentUrl,
250 pub(crate) devtool_settings: DevtoolSettings,
252 pub(crate) viewport: Viewport,
254 pub(crate) viewport_scroll: crate::Point<f64>,
256 pub(crate) mutation_log: Option<Vec<crate::DomMutation>>,
259 inline_handler_changes: Vec<(NodeId, crate::QualName, Option<crate::node::AttrAtom>)>,
261 pub(crate) live_ranges: Vec<crate::range::WeakRange>,
263 pub dom_selection: Option<crate::LiveRange>,
265 pub(crate) media_type: MediaType,
267 pub(crate) style_threading: StyleThreading,
269 pub(crate) incremental_layout: bool,
271 pub(crate) subdocument_depth: usize,
274
275 pub(crate) tx: Sender<DocumentEvent>,
277 pub(crate) rx: Option<Receiver<DocumentEvent>>,
279
280 pub(crate) nodes: Box<NodeTree>,
285
286 node_drop_depth: usize,
288 pub(crate) mutator_depth: usize,
290 pub(crate) mutator_redraw_pending: bool,
291 details_toggles: Vec<(NodeId, bool)>,
295 viewport_generation: u64,
298 template_contents_owner: Option<NodeId>,
301 dropped_node_ids: FxHashSet<NodeId>,
302 dropped_node_parents: FxHashSet<NodeId>,
303 dropped_reference_owners: FxHashSet<NodeId>,
309
310 pub(crate) root_node_id: NodeId,
312
313 pub(crate) hoisted_fixed_parents: HashMap<NodeId, NodeId>,
323
324 pub(crate) hoisted_fixed_indices: HashMap<NodeId, usize>,
326
327 pub(crate) abspos_placements: Vec<crate::layout::abspos::AbsposPlacement>,
329
330 pub(crate) abspos_candidates: Vec<crate::layout::abspos::AbsposCandidate>,
332 pub(crate) abspos_candidate_ids: HashSet<NodeId>,
333 pub(crate) abspos_inputs: HashMap<NodeId, crate::layout::abspos::AbsposInputs>,
334 pub(crate) abspos_written: HashSet<NodeId>,
336 pub(crate) abspos_candidates_dirty: bool,
337 pub(crate) abspos_layout_dirty: bool,
338 pub(crate) abspos_normal_layout: bool,
340
341 pub(crate) fixed_nodes: Vec<NodeId>,
347
348 pub(crate) fixed_scroll_offset: crate::Point<f64>,
355
356 pub(crate) sticky_nodes: Vec<NodeId>,
364
365 pub(crate) sticky_offsets: HashMap<NodeId, taffy::Point<f32>>,
375
376 pub(crate) hoisted_clip_hosts: HashSet<NodeId>,
382
383 pub(crate) hoisted_position_hosts: HashSet<NodeId>,
385
386 pub(crate) stylist: Stylist,
389 pub(crate) animations: DocumentAnimationSet,
390 pub(crate) last_resolve_animation_time: f64,
397 pub(crate) guard: SharedRwLock,
399 pub(crate) snapshots: SnapshotMap,
401
402 pub(crate) font_ctx: Arc<Mutex<parley::FontContext>>,
405 #[cfg(feature = "parallel-construct")]
406 pub(crate) thread_font_contexts: ThreadLocal<RefCell<Box<FontContext>>>,
408 pub(crate) layout_ctx: parley::LayoutContext<TextBrush>,
410
411 pub(crate) hover_node_id: Option<NodeId>,
415 pub(crate) hover_hit_node_id: Option<NodeId>,
419 pub(crate) hover_node_is_text: bool,
421 pub(crate) last_client_pointer_position: Option<taffy::Point<f32>>,
423 pub(crate) semantic_hover_node_id: Option<NodeId>,
429 pub(crate) focus_node_id: Option<NodeId>,
431 pub(crate) active_node_id: Option<NodeId>,
433 pub(crate) mousedown_node_id: Option<NodeId>,
435 pub(crate) last_mousedown_time: Option<Instant>,
437 pub(crate) mousedown_position: taffy::Point<f32>,
439 pub(crate) click_count: u16,
441 pub(crate) drag_mode: DragMode,
443 pub(crate) hovered_scrollbar: Option<crate::node::ScrollbarRef>,
445 pub(crate) scrollbar_activity: HashMap<NodeId, Instant>,
448 pub(crate) scroll_animation: ScrollAnimationState,
450
451 pub(crate) text_selection: TextSelection,
453
454 pub(crate) has_active_animations: bool,
457 pub(crate) has_canvas: bool,
459 pub(crate) subdoc_animation_pacing: AnimationPacing,
461
462 pub(crate) nodes_to_id: HashMap<String, SmallVec<[NodeId; 1]>>,
466 pub(crate) nodes_to_stylesheet: BTreeMap<NodeId, DocumentStyleSheet>,
468 pub(crate) document_sheet_nodes: HashSet<NodeId>,
472 #[cfg(feature = "shadow-dom")]
476 pub(crate) shadow_sheet_roots: HashMap<NodeId, NodeId>,
477 pub(crate) ua_stylesheets: HashMap<String, DocumentStyleSheet>,
480 pub(crate) controls_to_form: HashMap<NodeId, NodeId>,
482 pub(crate) sub_document_nodes: HashSet<NodeId>,
484 pub(crate) iframe_loads: HashMap<NodeId, crate::iframe::IframeLoad>,
487 pub(crate) deferred_construction_nodes: Vec<ConstructionTask>,
489 pub(crate) paint_damage: crate::paint_damage::PaintDamageTracker,
495
496 #[cfg(feature = "custom-widget")]
498 pub(crate) custom_widget_nodes: HashSet<NodeId>,
499 #[cfg(feature = "custom-widget")]
501 pub(crate) pending_resource_deallocations: Vec<anyrender::ResourceId>,
502
503 #[cfg(feature = "shadow-dom")]
505 pub(crate) custom_element_registry: crate::node::CustomElementRegistry,
506 #[cfg(feature = "shadow-dom")]
508 pub(crate) shadow_host_nodes: HashSet<NodeId>,
509 #[cfg(feature = "shadow-dom")]
510 pub(crate) dirty_shadow_hosts: HashSet<NodeId>,
511 #[cfg(feature = "shadow-dom")]
512 pub(crate) pending_slot_changes: Vec<NodeId>,
513 #[cfg(feature = "shadow-dom")]
514 pub(crate) signaled_slots: HashSet<NodeId>,
515 #[cfg(feature = "shadow-dom")]
516 pub(crate) adopted_stylesheets: HashMap<NodeId, Vec<DocumentStyleSheet>>,
517 #[cfg(feature = "shadow-dom")]
519 pub(crate) custom_element_nodes: HashSet<NodeId>,
520 #[cfg(feature = "shadow-dom")]
524 pub(crate) declarative_template_shadows: HashMap<NodeId, NodeId>,
525
526 pub(crate) script_custom_elements: crate::node::script_custom_element::Registry,
528
529 pub(crate) image_cache: HashMap<String, ImageData>,
532
533 pub(crate) pending_images: HashMap<String, Vec<(NodeId, ImageType)>>,
537
538 pub(crate) pending_critical_resources: HashSet<usize>,
541
542 pub(crate) web_fonts: Vec<Arc<crate::net::web_fonts::WebFont>>,
543 pub(crate) font_epoch: usize,
544 document_referrer: String,
545 document_content_type: String,
546 document_window_focused: bool,
547
548 pub net_provider: Arc<dyn NetProvider>,
551 pub navigation_provider: Arc<dyn NavigationProvider>,
554 pub shell_provider: Arc<dyn ShellProvider>,
556 pub html_parser_provider: Arc<dyn HtmlParserProvider>,
558 pub(crate) abort_signal: Option<AbortSignal>,
562}
563
564pub(crate) fn make_device(
565 viewport: &Viewport,
566 media_type: MediaType,
567 font_ctx: Arc<Mutex<FontContext>>,
568) -> Device {
569 let width = viewport.window_size.0 as f32 / viewport.scale();
570 let height = viewport.window_size.1 as f32 / viewport.scale();
571 let viewport_size = euclid::Size2D::new(width, height);
572 let device_size = euclid::Size2D::new(width, height) * viewport.scale();
573 let device_pixel_ratio = euclid::Scale::new(viewport.scale());
574
575 Device::new(
576 media_type,
577 selectors::matching::QuirksMode::NoQuirks,
578 viewport_size,
579 device_size,
580 device_pixel_ratio,
581 Box::new(BlitzFontMetricsProvider { font_ctx }),
582 ComputedValues::initial_values_with_font_override(Font::initial_values()),
583 match viewport.color_scheme {
584 ColorScheme::Light => PrefersColorScheme::Light,
585 ColorScheme::Dark => PrefersColorScheme::Dark,
586 },
587 PointerCapabilities::default(),
588 PointerCapabilities::default(),
589 )
590}
591
592fn incremental_layout_default() -> bool {
607 !matches!(
608 std::env::var("BLITZ_INCREMENTAL").ok().as_deref(),
609 Some("0" | "false" | "off")
610 )
611}
612
613impl BaseDocument {
614 pub fn new(config: DocumentConfig) -> Self {
616 static ID_GENERATOR: AtomicUsize = AtomicUsize::new(1);
617
618 let id = ID_GENERATOR.fetch_add(1, Ordering::SeqCst);
619
620 let font_ctx = config
621 .font_ctx
622 .map(|mut font_ctx| {
623 font_ctx.source_cache.make_shared();
624 font_ctx
626 })
627 .unwrap_or_else(|| {
628 use parley::fontique::{Collection, CollectionOptions, SourceCache};
629 let mut font_ctx = FontContext {
630 source_cache: SourceCache::new_shared(),
631 collection: Collection::new(CollectionOptions {
632 shared: false,
633 system_fonts: cfg!(all(
634 feature = "system-fonts",
635 not(target_arch = "wasm32")
636 )),
637 }),
638 };
639 font_ctx
640 .collection
641 .register_fonts(Blob::new(Arc::new(crate::BULLET_FONT) as _), None);
642 font_ctx
643 });
644 let font_ctx = Arc::new(Mutex::new(font_ctx));
645 if let Some(fallback) = config.family_fallback {
646 crate::font_fallback::set(fallback);
647 }
648
649 style_config::set_pref!("layout.grid.enabled", true);
651 style_config::set_pref!("layout.unimplemented", true);
652 style_config::set_pref!("layout.columns.enabled", true);
653 style_config::set_pref!("layout.css.basic-shape-shape.enabled", true);
654 style_config::set_pref!("layout.threads", -1);
655
656 let viewport = config.viewport.unwrap_or_default();
657 let media_type = config.media_type.unwrap_or_else(MediaType::screen);
658 let device = make_device(&viewport, media_type.clone(), font_ctx.clone());
659 let stylist = Stylist::new(device, QuirksMode::NoQuirks);
660 let snapshots = SnapshotMap::new();
661 let nodes = Box::new(NodeTree::new());
662 let guard = SharedRwLock::new();
663 let nodes_to_id = HashMap::new();
664
665 let base_url = config
666 .base_url
667 .and_then(|url| DocumentUrl::from_str(&url).ok())
668 .unwrap_or_default();
669
670 let net_provider = config
671 .net_provider
672 .unwrap_or_else(|| Arc::new(DummyNetProvider));
673 let navigation_provider = config
674 .navigation_provider
675 .unwrap_or_else(|| Arc::new(DummyNavigationProvider));
676 let shell_provider = config
677 .shell_provider
678 .unwrap_or_else(|| Arc::new(DummyShellProvider));
679 let html_parser_provider = config
680 .html_parser_provider
681 .unwrap_or_else(|| Arc::new(DummyHtmlParserProvider));
682
683 let (tx, rx) = channel();
684
685 let mut doc = Self {
686 node_drop_depth: 0,
687 mutator_depth: 0,
688 mutator_redraw_pending: false,
689 details_toggles: Vec::new(),
690 viewport_generation: 0,
691 template_contents_owner: None,
692 dropped_node_ids: FxHashSet::default(),
693 dropped_node_parents: FxHashSet::default(),
694 dropped_reference_owners: FxHashSet::default(),
695 hoisted_fixed_parents: HashMap::new(),
696 hoisted_fixed_indices: HashMap::new(),
697 abspos_placements: Vec::new(),
698 abspos_candidates: Vec::new(),
699 abspos_candidate_ids: HashSet::new(),
700 abspos_inputs: HashMap::new(),
701 abspos_written: HashSet::new(),
702 abspos_candidates_dirty: true,
703 abspos_layout_dirty: true,
704 abspos_normal_layout: false,
705 fixed_nodes: Vec::new(),
706 fixed_scroll_offset: crate::Point::ZERO,
707 sticky_nodes: Vec::new(),
708 sticky_offsets: HashMap::new(),
709 hoisted_clip_hosts: HashSet::new(),
710 hoisted_position_hosts: HashSet::new(),
711 id,
712 tx,
713 rx: Some(rx),
714
715 guard,
716 nodes,
717 root_node_id: NodeId::default(),
718 stylist,
719 animations: DocumentAnimationSet::default(),
720 last_resolve_animation_time: 0.0,
721 snapshots,
722 nodes_to_id,
723 viewport,
724 media_type,
725 style_threading: config.style_threading,
726 incremental_layout: config
727 .incremental
728 .unwrap_or_else(incremental_layout_default),
729 subdocument_depth: config.subdocument_depth,
730 devtool_settings: DevtoolSettings::default(),
731 viewport_scroll: crate::Point::ZERO,
732 mutation_log: None,
733 inline_handler_changes: Vec::new(),
734 live_ranges: Vec::new(),
735 dom_selection: None,
736 url: base_url,
737 ua_stylesheets: HashMap::new(),
738 nodes_to_stylesheet: BTreeMap::new(),
739 document_sheet_nodes: HashSet::new(),
740 #[cfg(feature = "shadow-dom")]
741 shadow_sheet_roots: HashMap::new(),
742 font_ctx,
743 #[cfg(feature = "parallel-construct")]
744 thread_font_contexts: ThreadLocal::new(),
745 layout_ctx: parley::LayoutContext::new(),
746
747 hover_node_id: None,
748 hover_hit_node_id: None,
749 hover_node_is_text: false,
750 last_client_pointer_position: None,
751 semantic_hover_node_id: None,
752 focus_node_id: None,
753 active_node_id: None,
754 mousedown_node_id: None,
755 has_active_animations: false,
756 subdoc_animation_pacing: AnimationPacing::Idle,
757 has_canvas: false,
758 sub_document_nodes: HashSet::new(),
759 iframe_loads: HashMap::new(),
760
761 #[cfg(feature = "custom-widget")]
762 custom_widget_nodes: HashSet::new(),
763 #[cfg(feature = "custom-widget")]
764 pending_resource_deallocations: Vec::new(),
765
766 #[cfg(feature = "shadow-dom")]
767 custom_element_registry: crate::node::CustomElementRegistry::new(),
768 #[cfg(feature = "shadow-dom")]
769 shadow_host_nodes: HashSet::new(),
770 #[cfg(feature = "shadow-dom")]
771 dirty_shadow_hosts: HashSet::new(),
772 #[cfg(feature = "shadow-dom")]
773 pending_slot_changes: Vec::new(),
774 #[cfg(feature = "shadow-dom")]
775 signaled_slots: HashSet::new(),
776 #[cfg(feature = "shadow-dom")]
777 adopted_stylesheets: HashMap::new(),
778 #[cfg(feature = "shadow-dom")]
779 custom_element_nodes: HashSet::new(),
780 #[cfg(feature = "shadow-dom")]
781 declarative_template_shadows: HashMap::new(),
782
783 deferred_construction_nodes: Vec::new(),
784 paint_damage: Default::default(),
785 script_custom_elements: Default::default(),
786 image_cache: HashMap::new(),
787 pending_images: HashMap::new(),
788 pending_critical_resources: HashSet::new(),
789 web_fonts: Vec::new(),
790 font_epoch: 0,
791 document_referrer: String::new(),
792 document_content_type: String::from("text/html"),
793 document_window_focused: false,
794 controls_to_form: HashMap::new(),
795 net_provider,
796 navigation_provider,
797 shell_provider,
798 html_parser_provider,
799 abort_signal: config.abort_signal,
800 last_mousedown_time: None,
801 mousedown_position: taffy::Point::ZERO,
802 click_count: 0,
803 drag_mode: DragMode::None,
804 hovered_scrollbar: None,
805 scrollbar_activity: HashMap::new(),
806 scroll_animation: ScrollAnimationState::None,
807 text_selection: TextSelection::default(),
808 };
809
810 doc.root_node_id = doc.create_node(NodeData::Document(Box::default()));
812 doc.root_node_mut().flags.insert(NodeFlags::IS_IN_DOCUMENT);
813
814 match config.ua_stylesheets {
815 Some(stylesheets) => {
816 for ss in &stylesheets {
817 doc.add_user_agent_stylesheet(ss);
818 }
819 }
820 None => doc.add_user_agent_stylesheet(DEFAULT_CSS),
821 }
822
823 let stylo_element_data = StyloElementData {
825 styles: ElementStyles {
826 primary: Some(
827 ComputedValues::initial_values_with_font_override(Font::initial_values())
828 .to_arc(),
829 ),
830 ..Default::default()
831 },
832 ..Default::default()
833 };
834 let stylo_data = doc.root_node_mut().stylo_element_data_mut();
835 *stylo_data.ensure_init_mut() = stylo_element_data;
836
837 doc
838 }
839
840 pub fn set_net_provider(&mut self, net_provider: Arc<dyn NetProvider>) {
842 self.net_provider = net_provider;
843 }
844
845 pub fn set_navigation_provider(&mut self, navigation_provider: Arc<dyn NavigationProvider>) {
847 self.navigation_provider = navigation_provider;
848 }
849
850 pub fn set_shell_provider(&mut self, shell_provider: Arc<dyn ShellProvider>) {
852 self.shell_provider = shell_provider;
853 }
854
855 pub fn set_html_parser_provider(&mut self, html_parser_provider: Arc<dyn HtmlParserProvider>) {
857 self.html_parser_provider = html_parser_provider;
858 }
859
860 pub fn set_base_url(&mut self, url: &str) {
862 self.url = DocumentUrl::from(Url::parse(url).unwrap());
863 }
864
865 pub fn guard(&self) -> &SharedRwLock {
866 &self.guard
867 }
868
869 pub fn tree(&self) -> &NodeTree {
870 &self.nodes
871 }
872
873 pub fn id(&self) -> usize {
874 self.id
875 }
876
877 pub(crate) fn build_request(&self, url: url::Url) -> Request {
880 crate::net::with_referrer(
881 crate::net::stamped_request(url, self.abort_signal.as_ref()),
882 self.url(),
883 )
884 }
885
886 pub fn favicon_url(&self) -> Option<String> {
887 self.tree().iter().find_map(|(_, node)| {
888 let data = &node.data;
889 if !data.is_element_with_tag_name(&local_name!("link")) {
890 return None;
891 }
892 let rel = data.attr(local_name!("rel"))?;
893 if !rel
894 .split_ascii_whitespace()
895 .any(|v| v.eq_ignore_ascii_case("icon"))
896 {
897 return None;
898 }
899 data.attr(local_name!("href")).map(|s| s.to_string())
900 })
901 }
902
903 pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
904 self.nodes.get(node_id)
905 }
906
907 pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
908 self.nodes.get_mut(node_id)
909 }
910
911 pub fn get_focussed_node_id(&self) -> Option<NodeId> {
912 self.focus_node_id
913 .or(self.try_root_element().map(|el| el.id))
914 }
915
916 pub fn mutate<'doc>(&'doc mut self) -> DocumentMutator<'doc> {
917 DocumentMutator::new(self)
918 }
919
920 pub fn handle_dom_event<F: FnMut(DomEvent)>(
921 &mut self,
922 event: &mut DomEvent,
923 dispatch_event: F,
924 ) {
925 handle_dom_event(self, event, dispatch_event)
926 }
927
928 pub fn as_any_mut(&mut self) -> &mut dyn Any {
929 self
930 }
931
932 pub fn label_bound_input_element(&self, label_node_id: NodeId) -> Option<&Node> {
939 let label_element = self.nodes[label_node_id].element_data()?;
940 if let Some(target_element_dom_id) = label_element.attr(local_name!("for")) {
941 TreeTraverser::new(self)
942 .filter_map(|id| {
943 let node = self.get_node(id)?;
944 let element_data = node.element_data()?;
945 if element_data.name.local != local_name!("input") {
946 return None;
947 }
948 let id = element_data.id.as_ref()?;
949 if *id == *target_element_dom_id {
950 Some(node)
951 } else {
952 None
953 }
954 })
955 .next()
956 } else {
957 TreeTraverser::new_with_root(self, label_node_id)
958 .filter_map(|child_id| {
959 let node = self.get_node(child_id)?;
960 let element_data = node.element_data()?;
961 if element_data.name.local == local_name!("input") {
962 Some(node)
963 } else {
964 None
965 }
966 })
967 .next()
968 }
969 }
970
971 pub fn run_pre_click_activation(&mut self, target: NodeId) -> Option<PreClickActivation> {
977 let node_id = crate::events::pointer::checkable_activation_target(self, target)?;
978 let el = self.get_node(node_id)?.data.downcast_element()?;
979 let is_radio = el.attr(local_name!("type")) == Some("radio");
980
981 if !is_radio {
982 let previous = el.checkbox_input_checked()?;
983 let el = self.get_node_mut(node_id)?.data.downcast_element_mut()?;
984 Self::toggle_checkbox(el);
985 return Some(PreClickActivation {
986 previous: vec![(node_id, previous)],
987 });
988 }
989
990 let radio_set = el.attr(local_name!("name")).map(str::to_string);
991 let Some(radio_set) = radio_set else {
992 let previous = el.checkbox_input_checked()?;
993 let el = self.get_node_mut(node_id)?.data.downcast_element_mut()?;
994 *el.checkbox_input_checked_mut()? = true;
995 return Some(PreClickActivation {
996 previous: vec![(node_id, previous)],
997 });
998 };
999
1000 let mut previous: Vec<(NodeId, bool)> = Vec::new();
1012 for (id, node) in self.nodes.iter_mut() {
1013 let Some(el) = node.data.downcast_element_mut() else {
1014 continue;
1015 };
1016 if el.attr(local_name!("name")) != Some(&*radio_set) {
1017 continue;
1018 }
1019 let Some(is_checked) = el.checkbox_input_checked_mut() else {
1020 continue;
1021 };
1022 previous.push((id, *is_checked));
1023 *is_checked = id == node_id;
1024 }
1025 Some(PreClickActivation { previous })
1026 }
1027
1028 pub fn undo_pre_click_activation(&mut self, activation: PreClickActivation) {
1031 for (node_id, was_checked) in activation.previous {
1032 let Some(node) = self.get_node_mut(node_id) else {
1033 continue;
1034 };
1035 let Some(el) = node.data.downcast_element_mut() else {
1036 continue;
1037 };
1038 if let Some(is_checked) = el.checkbox_input_checked_mut() {
1039 *is_checked = was_checked;
1040 }
1041 }
1042 }
1043
1044 pub fn toggle_checkbox(el: &mut ElementData) -> bool {
1045 let Some(is_checked) = el.checkbox_input_checked_mut() else {
1046 return false;
1047 };
1048 *is_checked = !*is_checked;
1049
1050 *is_checked
1051 }
1052
1053 pub fn toggle_radio(&mut self, radio_set_name: String, target_radio_id: NodeId) {
1054 for (i, node) in self.nodes.iter_mut() {
1055 if let Some(node_data) = node.data.downcast_element_mut() {
1056 if node_data.attr(local_name!("name")) == Some(&radio_set_name) {
1057 let was_clicked = i == target_radio_id;
1058 let Some(is_checked) = node_data.checkbox_input_checked_mut() else {
1059 continue;
1060 };
1061 *is_checked = was_clicked;
1062 }
1063 }
1064 }
1065 }
1066
1067 pub fn toggle_details_open(&mut self, details_id: NodeId) {
1071 use crate::qual_name;
1072
1073 let node = &self.nodes[details_id];
1074 if !node.data.is_element_with_tag_name(&local_name!("details")) {
1075 return;
1076 }
1077 let is_open = node.data.has_attr(local_name!("open"));
1078
1079 let mut mutator = self.mutate();
1083 if is_open {
1084 mutator.clear_attribute(details_id, qual_name!("open"));
1085 } else {
1086 mutator.set_attribute(details_id, qual_name!("open"), "");
1087 }
1088 drop(mutator);
1089
1090 self.shell_provider.request_redraw();
1091 }
1092
1093 pub fn set_style_property(&mut self, node_id: NodeId, name: &str, value: &str) {
1094 let node = &mut self.nodes[node_id];
1095 let did_change = node.element_data_mut().unwrap().set_style_property(
1096 name,
1097 value,
1098 &self.guard,
1099 self.url.url_extra_data(),
1100 );
1101 if did_change {
1102 node.mark_style_attr_updated();
1103 }
1104 }
1105
1106 pub fn remove_style_property(&mut self, node_id: NodeId, name: &str) {
1107 let node = &mut self.nodes[node_id];
1108 let did_change = node.element_data_mut().unwrap().remove_style_property(
1109 name,
1110 &self.guard,
1111 self.url.url_extra_data(),
1112 );
1113 if did_change {
1114 node.mark_style_attr_updated();
1115 }
1116 }
1117
1118 pub fn sub_document_node_ids(&self) -> Vec<NodeId> {
1119 self.sub_document_nodes.iter().copied().collect()
1120 }
1121
1122 pub fn set_navigation_metadata(&mut self, request: &Request, content_type: Option<&str>) {
1124 self.document_referrer = request
1125 .headers
1126 .get(blitz_traits::net::http::header::REFERER)
1127 .and_then(|value| value.to_str().ok())
1128 .and_then(|value| Url::parse(value).ok())
1129 .map(|mut url| {
1130 let _ = url.set_username("");
1131 let _ = url.set_password(None);
1132 url.set_fragment(None);
1133 url.to_string()
1134 })
1135 .unwrap_or_default();
1136 self.document_content_type = content_type
1137 .and_then(|value| value.split(';').next())
1138 .map(str::trim)
1139 .filter(|value| !value.is_empty())
1140 .unwrap_or("text/html")
1141 .to_ascii_lowercase();
1142 }
1143
1144 pub fn referrer(&self) -> &str {
1145 &self.document_referrer
1146 }
1147
1148 pub fn content_type(&self) -> &str {
1149 &self.document_content_type
1150 }
1151
1152 pub fn window_focused(&self) -> bool {
1153 self.document_window_focused
1154 }
1155
1156 pub fn set_window_focused(&mut self, focused: bool) {
1158 self.document_window_focused = focused;
1159 for id in self.sub_document_node_ids() {
1160 if let Some(child) = self
1161 .get_node_mut(id)
1162 .and_then(|node| node.element_data_mut())
1163 .and_then(|element| element.sub_doc_data_mut())
1164 {
1165 child.inner_mut().set_window_focused(focused);
1166 }
1167 }
1168 }
1169
1170 pub fn set_document_quirks_mode(&mut self, mode: u8) {
1172 if let NodeData::Document(data) = &mut self.root_node_mut().data {
1173 data.quirks_mode = mode;
1174 }
1175 }
1176
1177 pub fn compat_mode(&self) -> &'static str {
1178 if matches!(&self.root_node().data, NodeData::Document(data) if data.quirks_mode == 2) {
1179 "BackCompat"
1180 } else {
1181 "CSS1Compat"
1182 }
1183 }
1184
1185 pub fn begin_document_stream(&mut self) {
1186 self.reset_web_fonts();
1187 self.pending_critical_resources.clear();
1188 self.pending_images.clear();
1189 self.set_document_quirks_mode(0);
1190 }
1191
1192 pub fn set_sub_document(&mut self, node_id: NodeId, mut sub_document: Box<dyn Document>) {
1193 sub_document
1194 .inner_mut()
1195 .set_window_focused(self.document_window_focused);
1196 self.nodes[node_id]
1197 .element_data_mut()
1198 .unwrap()
1199 .set_sub_document(sub_document);
1200 self.sub_document_nodes.insert(node_id);
1201 }
1202
1203 pub fn remove_sub_document(&mut self, node_id: NodeId) {
1204 self.nodes[node_id]
1205 .element_data_mut()
1206 .unwrap()
1207 .remove_sub_document();
1208 self.sub_document_nodes.remove(&node_id);
1209 if let Some(load) = self.iframe_loads.remove(&node_id) {
1210 load.abort_controller.abort();
1211 }
1212 }
1213
1214 pub fn poll_subdocuments(&mut self, waker: Option<&Waker>) -> bool {
1220 self.poll_subdocuments_inner(waker, None)
1221 }
1222
1223 pub fn poll_subdocuments_for_settle(
1225 &mut self,
1226 waker: Option<&Waker>,
1227 run_timers: bool,
1228 ) -> bool {
1229 self.poll_subdocuments_inner(waker, Some(run_timers))
1230 }
1231
1232 fn poll_subdocuments_inner(
1233 &mut self,
1234 waker: Option<&Waker>,
1235 settle_timers: Option<bool>,
1236 ) -> bool {
1237 let mut has_changes = false;
1238 let node_ids: Vec<NodeId> = self.sub_document_nodes.iter().copied().collect();
1239 for node_id in node_ids {
1240 let Some(sub_doc) = self
1241 .nodes
1242 .get_mut(node_id)
1243 .and_then(|node| node.subdoc_mut())
1244 else {
1245 continue;
1246 };
1247 let task_context = waker.map(TaskContext::from_waker);
1248 has_changes |= match settle_timers {
1249 Some(run_timers) => sub_doc.poll_for_settle(task_context, run_timers),
1250 None => sub_doc.poll(task_context),
1251 };
1252 }
1253 has_changes
1254 }
1255
1256 #[cfg(feature = "custom-widget")]
1257 pub fn custom_widget_node_ids(&self) -> Vec<NodeId> {
1258 self.custom_widget_nodes.iter().copied().collect()
1259 }
1260
1261 #[cfg(feature = "custom-widget")]
1262 pub fn take_pending_resource_deallocations(&mut self) -> Vec<anyrender::ResourceId> {
1263 std::mem::take(&mut self.pending_resource_deallocations)
1264 }
1265
1266 #[cfg(feature = "custom-widget")]
1267 pub fn set_custom_widget(&mut self, node_id: NodeId, widget: Box<dyn crate::Widget>) {
1268 self.nodes[node_id]
1269 .element_data_mut()
1270 .unwrap()
1271 .set_custom_widget(widget);
1272 self.custom_widget_nodes.insert(node_id);
1273 }
1274
1275 #[cfg(feature = "custom-widget")]
1276 pub fn remove_custom_widget(&mut self, node_id: NodeId) {
1277 let resources_to_deallocate = self.nodes[node_id]
1278 .element_data_mut()
1279 .unwrap()
1280 .remove_custom_widget();
1281 self.pending_resource_deallocations
1282 .extend_from_slice(&resources_to_deallocate);
1283 self.custom_widget_nodes.remove(&node_id);
1284 }
1285
1286 #[cfg(feature = "shadow-dom")]
1290 pub fn custom_elements_mut(&mut self) -> &mut crate::node::CustomElementRegistry {
1291 &mut self.custom_element_registry
1292 }
1293
1294 #[cfg(feature = "shadow-dom")]
1297 pub fn define_custom_element(
1298 &mut self,
1299 name: markup5ever::LocalName,
1300 definition: crate::node::CustomElementDefinition,
1301 ) {
1302 self.custom_element_registry.define(name, definition);
1303 }
1304
1305 #[cfg(feature = "shadow-dom")]
1307 pub fn shadow_host_node_ids(&self) -> Vec<NodeId> {
1308 self.shadow_host_nodes.iter().copied().collect()
1309 }
1310
1311 #[cfg(feature = "shadow-dom")]
1313 pub fn shadow_root_id(&self, host_id: NodeId) -> Option<NodeId> {
1314 self.get_node(host_id)
1315 .and_then(|node| node.shadow_root_id())
1316 }
1317
1318 #[cfg(feature = "shadow-dom")]
1322 pub fn attach_shadow(&mut self, host_id: NodeId, mode: crate::node::ShadowRootMode) -> NodeId {
1323 if let Some(existing) = self.nodes[host_id].shadow_root_id() {
1324 return existing;
1325 }
1326
1327 let shadow_root_id = self.create_node(NodeData::ShadowRoot(
1328 crate::node::ShadowRootData::new(host_id, mode),
1329 ));
1330
1331 self.nodes[shadow_root_id].parent = Some(host_id);
1335 self.nodes[shadow_root_id].owner_document = self.nodes[host_id].owner_document;
1336 if self.nodes[host_id].flags.is_in_document() {
1337 self.nodes[shadow_root_id]
1338 .flags
1339 .insert(NodeFlags::IS_IN_DOCUMENT);
1340 }
1341
1342 self.nodes[host_id]
1343 .element_data_mut()
1344 .expect("Shadow host must be an element")
1345 .shadow_root = Some(shadow_root_id);
1346 self.shadow_host_nodes.insert(host_id);
1347 self.dirty_shadow_hosts.insert(host_id);
1348 self.nodes[host_id].set_restyle_hint(
1349 style::invalidation::element::restyle_hints::RestyleHint::restyle_subtree(),
1350 );
1351
1352 self.nodes[host_id].insert_damage(ALL_DAMAGE);
1354 self.nodes[host_id].mark_ancestors_dirty();
1355
1356 shadow_root_id
1357 }
1358
1359 #[cfg(feature = "shadow-dom")]
1364 pub fn element_can_be_shadow_host(element: &ElementData) -> bool {
1365 let tag = element.name.local.as_ref();
1366 element.name.ns == ns!(html)
1367 && (tag.contains('-')
1368 || matches!(
1369 tag,
1370 "article"
1371 | "aside"
1372 | "blockquote"
1373 | "body"
1374 | "div"
1375 | "footer"
1376 | "h1"
1377 | "h2"
1378 | "h3"
1379 | "h4"
1380 | "h5"
1381 | "h6"
1382 | "header"
1383 | "main"
1384 | "nav"
1385 | "p"
1386 | "section"
1387 | "span"
1388 ))
1389 }
1390
1391 #[cfg(feature = "shadow-dom")]
1397 pub fn try_attach_declarative_shadow(
1398 &mut self,
1399 host_id: NodeId,
1400 template_id: NodeId,
1401 mode: crate::node::ShadowRootMode,
1402 clonable: bool,
1403 serializable: bool,
1404 delegates_focus: bool,
1405 ) -> bool {
1406 let can_attach = self.nodes.get(host_id).is_some_and(|node| {
1407 node.element_data().is_some_and(|element| {
1408 Self::element_can_be_shadow_host(element) && element.shadow_root.is_none()
1409 })
1410 });
1411 if !can_attach {
1412 return false;
1413 }
1414 let root_id = self.attach_shadow(host_id, mode);
1415 if let Some(root) = self.nodes[root_id].shadow_root_data_mut() {
1416 root.declarative = true;
1417 root.clonable = clonable;
1418 root.serializable = serializable;
1419 root.delegates_focus = delegates_focus;
1420 }
1421 self.declarative_template_shadows
1422 .insert(template_id, root_id);
1423 true
1424 }
1425
1426 #[cfg(feature = "shadow-dom")]
1428 pub fn detach_shadow(&mut self, host_id: NodeId) {
1429 let shadow_root_id = self.nodes[host_id]
1430 .element_data_mut()
1431 .and_then(|el| el.shadow_root.take());
1432 if let Some(shadow_root_id) = shadow_root_id {
1433 self.drop_node_ignoring_parent(shadow_root_id);
1434 self.shadow_host_nodes.remove(&host_id);
1435 self.dirty_shadow_hosts.remove(&host_id);
1436 self.nodes[host_id].flattened_children = None;
1437 self.nodes[host_id].set_restyle_hint(
1438 style::invalidation::element::restyle_hints::RestyleHint::restyle_subtree(),
1439 );
1440 self.nodes[host_id].insert_damage(ALL_DAMAGE);
1441 self.nodes[host_id].mark_ancestors_dirty();
1442 }
1443 }
1444
1445 #[cfg(feature = "shadow-dom")]
1447 pub fn set_custom_element(
1448 &mut self,
1449 node_id: NodeId,
1450 controller: Box<dyn crate::node::CustomElement>,
1451 ) {
1452 use crate::node::{CustomElementData, SpecialElementData};
1453 self.nodes[node_id]
1454 .element_data_mut()
1455 .expect("Custom element host must be an element")
1456 .special_data = SpecialElementData::CustomElement(CustomElementData::new(controller));
1457 self.custom_element_nodes.insert(node_id);
1458 }
1459
1460 #[cfg(feature = "shadow-dom")]
1463 pub fn take_custom_element(
1464 &mut self,
1465 node_id: NodeId,
1466 ) -> Option<Box<dyn crate::node::CustomElement>> {
1467 use crate::node::SpecialElementData;
1468 self.custom_element_nodes.remove(&node_id);
1469 let element = self.nodes[node_id].element_data_mut()?;
1470 if matches!(element.special_data, SpecialElementData::CustomElement(_)) {
1471 if let SpecialElementData::CustomElement(mut data) = element.special_data.take() {
1472 return data.controller.take();
1473 }
1474 }
1475 None
1476 }
1477
1478 pub fn root_node(&self) -> &Node {
1479 &self.nodes[self.root_node_id]
1480 }
1481
1482 pub fn root_node_mut(&mut self) -> &mut Node {
1483 &mut self.nodes[self.root_node_id]
1484 }
1485
1486 pub fn set_paint_damage_tracking(&mut self, enabled: bool) {
1503 self.paint_damage.set_enabled(enabled);
1504 }
1505
1506 pub fn paint_damage_tracking(&self) -> bool {
1508 self.paint_damage.is_enabled()
1509 }
1510
1511 pub fn paint_damage(&self) -> &crate::paint_damage::PaintDamage {
1519 self.paint_damage.damage()
1520 }
1521
1522 pub fn try_root_element(&self) -> Option<&Node> {
1523 TDocument::as_node(&self.root_node()).first_element_child()
1524 }
1525
1526 pub fn root_element(&self) -> &Node {
1527 TDocument::as_node(&self.root_node())
1528 .first_element_child()
1529 .unwrap()
1530 .as_element()
1531 .unwrap()
1532 }
1533
1534 pub fn create_node(&mut self, node_data: NodeData) -> NodeId {
1535 let tree_ptr = self.nodes.as_mut() as *mut NodeTree;
1536 let guard = self.guard.clone();
1537 let is_document = matches!(node_data, NodeData::Document(_));
1538 let id = self
1539 .nodes
1540 .insert_with_key(|id| Node::new(tree_ptr, id, guard, node_data));
1541 if !is_document {
1542 self.nodes[id].owner_document = Some(self.root_node_id);
1543 }
1544 self.register_script_custom_element(id);
1545 if let Some(attributes) = self.nodes[id]
1546 .element_data()
1547 .and_then(|element| element.inline_event_attributes.as_deref())
1548 {
1549 self.inline_handler_changes.extend(
1550 attributes
1551 .iter()
1552 .map(|attr| (id, attr.name.clone(), Some(attr.value.clone()))),
1553 );
1554 }
1555 id
1556 }
1557
1558 pub fn add_to_document_id_map(&mut self, document_id: NodeId, id: &str, node_id: NodeId) {
1559 if document_id == self.root_node_id || id.is_empty() {
1560 return;
1561 }
1562 let Some(node) = self.get_node_mut(document_id) else {
1563 return;
1564 };
1565 let NodeData::Document(data) = &mut node.data else {
1566 return;
1567 };
1568 let candidates = data.ids.entry(id.to_owned()).or_default();
1569 if !candidates.contains(&node_id) {
1570 candidates.push(node_id);
1571 }
1572 }
1573
1574 pub fn remove_from_document_id_map(&mut self, document_id: NodeId, id: &str, node_id: NodeId) {
1575 if document_id == self.root_node_id {
1576 return;
1577 }
1578 let Some(node) = self.get_node_mut(document_id) else {
1579 return;
1580 };
1581 let NodeData::Document(data) = &mut node.data else {
1582 return;
1583 };
1584 if let Some(candidates) = data.ids.get_mut(id) {
1585 candidates.retain(|candidate| *candidate != node_id);
1586 if candidates.is_empty() {
1587 data.ids.remove(id);
1588 }
1589 }
1590 }
1591
1592 pub fn get_element_by_id_in(&self, document_id: NodeId, id: &str) -> Option<NodeId> {
1595 if document_id == self.root_node_id {
1596 return self.get_element_by_id(id);
1597 }
1598 if id.is_empty() {
1599 return None;
1600 }
1601 let NodeData::Document(data) = &self.get_node(document_id)?.data else {
1602 return None;
1603 };
1604 data.ids
1605 .get(id)?
1606 .iter()
1607 .filter_map(|candidate| {
1608 let mut current = *candidate;
1609 let mut path = Vec::new();
1610 while current != document_id {
1611 let node = self.get_node(current)?;
1612 let parent_id = node.parent?;
1613 let parent = self.get_node(parent_id)?;
1614 path.push(parent.index_of_child(current)?);
1615 current = parent_id;
1616 }
1617 path.reverse();
1618 Some((path, *candidate))
1619 })
1620 .min_by(|left, right| left.0.cmp(&right.0))
1621 .map(|(_, node_id)| node_id)
1622 }
1623
1624 fn collect_live_reference_owners(
1636 nodes: &NodeTree,
1637 hoisted_fixed_parents: &HashMap<NodeId, NodeId>,
1638 removed: &FxHashSet<NodeId>,
1639 node_id: NodeId,
1640 candidates: &mut FxHashSet<NodeId>,
1641 ) {
1642 fn push_survivor(
1643 removed: &FxHashSet<NodeId>,
1644 candidates: &mut FxHashSet<NodeId>,
1645 id: NodeId,
1646 ) {
1647 if !removed.contains(&id) {
1648 candidates.insert(id);
1649 }
1650 }
1651
1652 let (removed_is_text, dom_parent, layout_parent, seeds) = {
1653 let Some(node) = nodes.get(node_id) else {
1654 return;
1655 };
1656 let mut seeds: SmallVec<[NodeId; 8]> = SmallVec::new();
1657 {
1658 let borrowed = node.layout_children.borrow();
1659 if let Some(children) = borrowed.as_ref() {
1660 seeds.extend(children.iter().copied());
1661 }
1662 }
1663 seeds.extend(node.children.iter().copied());
1664 seeds.extend(node.anonymous_blocks.iter().copied());
1665 seeds.extend(node.before());
1666 seeds.extend(node.after());
1667 #[cfg(feature = "shadow-dom")]
1668 {
1669 seeds.extend(node.assigned_slot);
1670 seeds.extend(
1671 node.element_data()
1672 .and_then(|element| element.assigned_slot),
1673 );
1674 if let Some(assigned) = node.assigned_nodes.as_ref() {
1675 seeds.extend(assigned.iter().copied());
1676 }
1677 if let Some(root) = node.shadow_root_data() {
1678 seeds.push(root.host);
1679 }
1680 }
1681 (
1682 node.is_text_node(),
1683 node.parent,
1684 node.layout_parent.get(),
1685 seeds,
1686 )
1687 };
1688
1689 for parent_id in [dom_parent, layout_parent].into_iter().flatten() {
1690 let Some(parent) = nodes.get(parent_id) else {
1691 continue;
1692 };
1693 let anons: SmallVec<[NodeId; 4]> = parent.anonymous_blocks.iter().copied().collect();
1694 for anon in anons {
1695 let Some(anon_node) = nodes.get(anon) else {
1696 continue;
1697 };
1698 let listed = anon_node
1699 .children
1700 .iter()
1701 .any(|child| removed.contains(child))
1702 || anon_node
1703 .layout_children
1704 .borrow()
1705 .as_ref()
1706 .is_some_and(|children| {
1707 children.iter().any(|child| removed.contains(child))
1708 });
1709 if listed {
1710 push_survivor(removed, candidates, anon);
1711 }
1712 }
1713 }
1714 for id in seeds {
1715 push_survivor(removed, candidates, id);
1716 }
1717
1718 let mut stack: SmallVec<[NodeId; 16]> = SmallVec::new();
1719 if let Some(id) = dom_parent {
1720 if !removed.contains(&id) {
1721 stack.push(id);
1722 }
1723 }
1724 if let Some(id) = layout_parent {
1725 if !removed.contains(&id) {
1726 stack.push(id);
1727 }
1728 }
1729 if let Some(origin) = hoisted_fixed_parents.get(&node_id).copied() {
1730 push_survivor(removed, candidates, origin);
1733 if !removed.contains(&origin) {
1734 stack.push(origin);
1735 }
1736 }
1737
1738 let mut seen: SmallVec<[NodeId; 16]> = SmallVec::new();
1739 seen.push(node_id);
1740 while let Some(id) = stack.pop() {
1741 if seen.contains(&id) {
1742 continue;
1743 }
1744 seen.push(id);
1745 let Some(ancestor) = nodes.get(id) else {
1746 continue;
1747 };
1748 let stacking = ancestor.stacking_context.as_ref().is_some_and(|context| {
1749 context.children.iter().any(|child| {
1750 removed.contains(&child.node_id)
1751 || child
1752 .clip_ancestors
1753 .iter()
1754 .any(|clip| removed.contains(clip))
1755 })
1756 });
1757 let inline_or_list = ancestor.element_data().is_some_and(|element| {
1758 element.inline_layout_data.is_some() || element.list_item_data.is_some()
1759 });
1760 let table_root = !removed_is_text && ancestor.flags.is_table_root();
1762 #[cfg(feature = "shadow-dom")]
1763 let shadow_hit = {
1764 let mut hit = ancestor
1765 .flattened_children
1766 .as_ref()
1767 .is_some_and(|children| children.iter().any(|child| removed.contains(child)))
1768 || ancestor.assigned_nodes.as_ref().is_some_and(|assigned| {
1769 assigned.iter().any(|child| removed.contains(child))
1770 });
1771 if let Some(root) = ancestor.shadow_root_data() {
1772 let host = root.host;
1773 hit = hit
1774 || root.slots.iter().any(|slot| removed.contains(slot))
1775 || root
1776 .slottables
1777 .iter()
1778 .any(|slottable| removed.contains(slottable))
1779 || root
1780 .stylesheet_nodes
1781 .iter()
1782 .any(|sheet| removed.contains(sheet));
1783 stack.push(host);
1784 }
1785 hit
1786 };
1787 #[cfg(not(feature = "shadow-dom"))]
1788 let shadow_hit = false;
1789 if stacking || inline_or_list || table_root || shadow_hit {
1790 push_survivor(removed, candidates, id);
1791 }
1792 if let Some(parent) = ancestor.parent {
1793 stack.push(parent);
1794 }
1795 if let Some(parent) = ancestor.layout_parent.get() {
1796 stack.push(parent);
1797 }
1798 }
1799 }
1800
1801 fn collect_stacking_context_owners(
1805 nodes: &NodeTree,
1806 position_hosts: &HashSet<NodeId>,
1807 clip_hosts: &HashSet<NodeId>,
1808 removed: &FxHashSet<NodeId>,
1809 candidates: &mut FxHashSet<NodeId>,
1810 ) {
1811 for host in position_hosts.iter().chain(clip_hosts.iter()).copied() {
1812 if removed.contains(&host) {
1813 continue;
1814 }
1815 let Some(node) = nodes.get(host) else {
1816 continue;
1817 };
1818 let Some(context) = node.stacking_context.as_ref() else {
1819 continue;
1820 };
1821 let references = context.children.iter().any(|child| {
1822 removed.contains(&child.node_id)
1823 || child
1824 .clip_ancestors
1825 .iter()
1826 .any(|clip| removed.contains(clip))
1827 });
1828 if references {
1829 candidates.insert(host);
1830 }
1831 }
1832 }
1833
1834 pub(crate) fn invalidate_node_references(
1845 &mut self,
1846 removed: &FxHashSet<NodeId>,
1847 mut parents: FxHashSet<NodeId>,
1848 extra_owners: FxHashSet<NodeId>,
1849 ) {
1850 if removed.is_empty() {
1851 return;
1852 }
1853
1854 fn layout_references_removed(
1855 layout: &parley::Layout<TextBrush>,
1856 removed: &FxHashSet<NodeId>,
1857 ) -> bool {
1858 layout
1859 .inline_boxes()
1860 .iter()
1861 .any(|ibox| removed.contains(&NodeId::from_u64(ibox.id)))
1862 || layout.styles().iter().any(|style| {
1863 removed.contains(&style.brush.id)
1864 || style
1865 .brush
1866 .text_node
1867 .is_some_and(|id| removed.contains(&id))
1868 })
1869 }
1870
1871 for (&id, &origin) in &self.hoisted_fixed_parents {
1875 if removed.contains(&id) || removed.contains(&origin) {
1876 if let Some(node) = self.nodes.get(id) {
1877 parents.extend(node.layout_parent.get());
1878 }
1879 }
1880 }
1881
1882 let mut candidates: FxHashSet<NodeId> = FxHashSet::with_capacity_and_hasher(
1883 parents.len() + extra_owners.len() + removed.len(),
1884 Default::default(),
1885 );
1886 for &id in &parents {
1887 if !removed.contains(&id) {
1888 candidates.insert(id);
1889 }
1890 }
1891 for id in extra_owners {
1892 if !removed.contains(&id) {
1893 candidates.insert(id);
1894 }
1895 }
1896 for &id in removed {
1897 let detach = self
1898 .nodes
1899 .get(id)
1900 .and_then(|node| node.layout_parent.get())
1901 .is_some_and(|parent| !removed.contains(&parent));
1902 if detach {
1903 candidates.insert(id);
1904 }
1905 if self.nodes.contains_key(id) {
1906 Self::collect_live_reference_owners(
1907 &self.nodes,
1908 &self.hoisted_fixed_parents,
1909 removed,
1910 id,
1911 &mut candidates,
1912 );
1913 }
1914 }
1915 Self::collect_stacking_context_owners(
1916 &self.nodes,
1917 &self.hoisted_position_hosts,
1918 &self.hoisted_clip_hosts,
1919 removed,
1920 &mut candidates,
1921 );
1922
1923 self.fixed_nodes.retain(|id| !removed.contains(id));
1924 self.sticky_nodes.retain(|id| !removed.contains(id));
1925 self.sticky_offsets.retain(|id, _| !removed.contains(id));
1926 self.hoisted_fixed_parents
1927 .retain(|id, origin| !removed.contains(id) && !removed.contains(origin));
1928 let fixed_parents = &self.hoisted_fixed_parents;
1929 self.hoisted_fixed_indices
1930 .retain(|id, _| fixed_parents.contains_key(id));
1931 self.hoisted_clip_hosts.retain(|id| !removed.contains(id));
1932 self.hoisted_position_hosts
1933 .retain(|id| !removed.contains(id));
1934
1935 self.abspos_placements.clear();
1939 self.abspos_candidates.clear();
1940 self.abspos_candidate_ids.clear();
1941 self.abspos_inputs.clear();
1942 self.abspos_written.clear();
1943 self.abspos_candidates_dirty = true;
1944 self.abspos_layout_dirty = true;
1945
1946 let mut dirty = FxHashSet::default();
1947 for id in candidates {
1948 let Some(node) = self.nodes.get_mut(id) else {
1949 continue;
1950 };
1951 if removed.contains(&id) {
1954 if node
1955 .layout_parent
1956 .get()
1957 .is_some_and(|parent| !removed.contains(&parent))
1958 {
1959 node.layout_parent.set(None);
1960 }
1961 continue;
1962 }
1963
1964 let mut reconstruct = parents.contains(&id);
1965 reconstruct |= node
1966 .layout_children
1967 .get_mut()
1968 .as_ref()
1969 .is_some_and(|children| children.iter().any(|id| removed.contains(id)));
1970 let mut repaint = node
1971 .paint_children
1972 .get_mut()
1973 .as_ref()
1974 .is_some_and(|children| children.iter().any(|id| removed.contains(id)));
1975
1976 let old_anonymous_count = node.anonymous_blocks.len();
1977 node.anonymous_blocks.retain(|id| !removed.contains(id));
1978 reconstruct |= old_anonymous_count != node.anonymous_blocks.len();
1979 if node.is_anonymous() {
1980 let old_child_count = node.children.len();
1981 node.children.retain(|id| !removed.contains(id));
1982 reconstruct |= old_child_count != node.children.len();
1983 }
1984 if node
1985 .layout_parent
1986 .get()
1987 .is_some_and(|parent| removed.contains(&parent))
1988 {
1989 node.layout_parent.set(None);
1990 reconstruct = true;
1991 }
1992
1993 if let Some(context) = node.stacking_context.as_mut() {
1994 let old_count = context.children.len();
1995 context.children.retain(|child| {
1996 !removed.contains(&child.node_id)
1997 && !child.clip_ancestors.iter().any(|id| removed.contains(id))
1998 });
1999 if old_count != context.children.len() {
2000 context.sort();
2003 context.content_area = taffy::Rect::ZERO;
2004 repaint = true;
2005 }
2006 }
2007
2008 #[cfg(feature = "shadow-dom")]
2009 {
2010 for children in [
2011 node.flattened_children.as_mut(),
2012 node.assigned_nodes.as_mut(),
2013 ]
2014 .into_iter()
2015 .flatten()
2016 {
2017 let old_count = children.len();
2018 children.retain(|id| !removed.contains(id));
2019 reconstruct |= old_count != children.len();
2020 }
2021 if node.assigned_slot.is_some_and(|id| removed.contains(&id)) {
2022 node.assigned_slot = None;
2023 reconstruct = true;
2024 }
2025 if let Some(root) = node.shadow_root_data_mut() {
2026 let old_count = root.slots.len() + root.slottables.len();
2027 root.slots.retain(|id| !removed.contains(id));
2028 root.slottables.retain(|id| !removed.contains(id));
2029 root.stylesheet_nodes.retain(|id| !removed.contains(id));
2030 reconstruct |= old_count != root.slots.len() + root.slottables.len();
2031 }
2032 }
2033
2034 let mut table_removed = false;
2035 if let Some(element) = node.data.downcast_element_mut() {
2036 for pseudo in [&mut element.before, &mut element.after] {
2037 if pseudo.is_some_and(|id| removed.contains(&id)) {
2038 *pseudo = None;
2039 reconstruct = true;
2040 }
2041 }
2042 if element
2043 .inline_layout_data
2044 .as_ref()
2045 .is_some_and(|inline| layout_references_removed(&inline.layout, removed))
2046 {
2047 element.inline_layout_data = None;
2048 reconstruct = true;
2049 }
2050 if element
2051 .list_item_data
2052 .as_ref()
2053 .is_some_and(|item| match &item.position {
2054 crate::node::ListItemLayoutPosition::Outside(layout) => {
2055 layout_references_removed(layout, removed)
2056 }
2057 crate::node::ListItemLayoutPosition::Inside => false,
2058 })
2059 {
2060 element.list_item_data = None;
2061 reconstruct = true;
2062 }
2063 if let SpecialElementData::TableRoot(context) = &element.special_data {
2064 table_removed = context.references_removed_nodes(removed);
2065 }
2066 if table_removed {
2067 element.special_data = SpecialElementData::None;
2068 reconstruct = true;
2069 }
2070 #[cfg(feature = "shadow-dom")]
2071 if element
2072 .assigned_slot
2073 .is_some_and(|id| removed.contains(&id))
2074 {
2075 element.assigned_slot = None;
2076 reconstruct = true;
2077 }
2078 }
2079 if table_removed {
2080 node.flags.remove(NodeFlags::IS_TABLE_ROOT);
2081 }
2082
2083 if reconstruct || repaint {
2084 node.layout_children.get_mut().take();
2085 node.paint_children.get_mut().take();
2086 if let Some(element) = node.data.downcast_element_mut() {
2087 element.inline_layout_data = None;
2088 }
2089 dirty.insert(id);
2090 }
2091 }
2092
2093 let mut stack: Vec<NodeId> = dirty.iter().copied().collect();
2097 let mut visited = FxHashSet::default();
2098 while let Some(id) = stack.pop() {
2099 if !visited.insert(id) || removed.contains(&id) {
2100 continue;
2101 }
2102 let Some(node) = self.nodes.get_mut(id) else {
2103 continue;
2104 };
2105 stack.extend(node.parent);
2106 stack.extend(node.layout_parent.get());
2107 #[cfg(feature = "shadow-dom")]
2108 {
2109 stack.extend(node.assigned_slot);
2110 if let Some(root) = node.shadow_root_data() {
2111 stack.push(root.host);
2112 }
2113 }
2114 node.set_dirty_descendants();
2115 if node.stylo_element_data_opt().is_some() {
2116 node.insert_damage(if dirty.contains(&id) {
2117 ALL_DAMAGE
2118 } else {
2119 crate::layout::damage::ONLY_RELAYOUT
2120 | style::selector_parser::RestyleDamage::REPAINT
2121 });
2122 node.cache_mut().clear();
2123 }
2124 if let Some(inline) = node
2125 .data
2126 .downcast_element_mut()
2127 .and_then(|element| element.inline_layout_data.as_mut())
2128 {
2129 inline.content_widths = None;
2130 }
2131 }
2132
2133 #[cfg(feature = "shadow-dom")]
2134 {
2135 let nodes = &self.nodes;
2136 self.shadow_host_nodes.retain(|id| nodes.contains_key(*id));
2137 self.dirty_shadow_hosts.retain(|id| nodes.contains_key(*id));
2138 self.pending_slot_changes
2139 .retain(|id| nodes.contains_key(*id));
2140 self.signaled_slots.retain(|id| nodes.contains_key(*id));
2141 self.dirty_shadow_hosts
2142 .extend(self.shadow_host_nodes.iter().copied());
2143 }
2144
2145 self.controls_to_form
2146 .retain(|control, form| !removed.contains(control) && !removed.contains(form));
2147 if dirty.iter().any(|id| {
2150 self.nodes
2151 .get(*id)
2152 .is_some_and(|node| node.flags.is_in_document())
2153 }) {
2154 if self.mutator_depth == 0 {
2155 self.shell_provider.request_redraw();
2156 } else {
2157 self.mutator_redraw_pending = true;
2158 }
2159 }
2160
2161 }
2164
2165 fn purge_dropped_node_references(&mut self) {
2166 if self.node_drop_depth != 0 || self.dropped_node_ids.is_empty() {
2167 return;
2168 }
2169 let removed = std::mem::take(&mut self.dropped_node_ids);
2170 let parents = std::mem::take(&mut self.dropped_node_parents);
2171 let owners = std::mem::take(&mut self.dropped_reference_owners);
2172 self.invalidate_node_references(&removed, parents, owners);
2173 }
2174
2175 pub(crate) fn remove_node_from_tree(&mut self, node_id: NodeId) -> Option<Node> {
2178 self.clear_interaction_state_for_removed_node(node_id);
2179 let indexed = self.get_node(node_id).and_then(|node| {
2180 Some((
2181 node.owner_document?,
2182 node.element_data()?.id.as_ref()?.to_string(),
2183 ))
2184 });
2185 if let Some((document_id, id)) = indexed {
2186 self.remove_from_document_id_map(document_id, &id, node_id);
2187 }
2188 self.forget_script_custom_element(node_id);
2189 if self.nodes.contains_key(node_id) {
2190 self.dropped_node_ids.insert(node_id);
2191 let mut owners = FxHashSet::default();
2192 Self::collect_live_reference_owners(
2193 &self.nodes,
2194 &self.hoisted_fixed_parents,
2195 &self.dropped_node_ids,
2196 node_id,
2197 &mut owners,
2198 );
2199 self.dropped_reference_owners.extend(owners);
2200 }
2201 let node = self.nodes.remove(node_id);
2202 if let Some(node) = &node {
2203 self.dropped_node_parents.extend(
2204 [node.parent, node.layout_parent.get()]
2205 .into_iter()
2206 .flatten(),
2207 );
2208 self.purge_dropped_node_references();
2209 }
2210 node
2211 }
2212
2213 fn nearest_surviving_element_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
2218 let mut current = self.get_node(node_id)?.parent;
2219 while let Some(id) = current {
2220 let node = self.get_node(id)?;
2221 if node.is_element() && node.flags.is_in_document() {
2222 return Some(id);
2223 }
2224 current = node.parent;
2225 }
2226 None
2227 }
2228
2229 pub(crate) fn clear_interaction_state_for_removed_node(&mut self, node_id: NodeId) {
2250 if !self.nodes.contains_key(node_id) {
2251 return;
2252 }
2253
2254 if self.hover_node_id == Some(node_id) {
2255 self.hover_node_id = self.nearest_surviving_element_ancestor(node_id);
2256 self.hover_node_is_text = false;
2257 }
2258 if self.hover_hit_node_id == Some(node_id) {
2259 self.hover_hit_node_id = None;
2260 }
2261 if self.semantic_hover_node_id == Some(node_id) {
2262 self.semantic_hover_node_id = None;
2263 }
2264 if self.active_node_id == Some(node_id) {
2265 self.active_node_id = self.nearest_surviving_element_ancestor(node_id);
2266 }
2267 if self.focus_node_id == Some(node_id) {
2268 let shell_provider = self.shell_provider.clone();
2269 self.nodes[node_id].blur(shell_provider);
2270 self.focus_node_id = None;
2271 }
2272 if self.mousedown_node_id == Some(node_id) {
2273 self.mousedown_node_id = None;
2274 }
2275 if self.text_selection.anchor.node_or_parent == Some(node_id)
2276 || self.text_selection.focus.node_or_parent == Some(node_id)
2277 {
2278 self.text_selection.clear();
2279 }
2280 if self
2281 .hovered_scrollbar
2282 .is_some_and(|scrollbar| scrollbar.node_id == node_id)
2283 {
2284 self.hovered_scrollbar = None;
2285 }
2286 let drag_references_node = match &self.drag_mode {
2287 DragMode::Panning(state) => state.target == node_id,
2288 DragMode::ScrollbarDrag(state) => state.scrollbar.node_id == node_id,
2289 DragMode::Selecting | DragMode::None => false,
2290 };
2291 if drag_references_node {
2292 self.drag_mode = DragMode::None;
2293 }
2294 if matches!(
2295 &self.scroll_animation,
2296 ScrollAnimationState::Fling(state) if state.target == node_id
2297 ) {
2298 self.scroll_animation = ScrollAnimationState::None;
2299 }
2300 self.scrollbar_activity.remove(&node_id);
2301
2302 self.controls_to_form.remove(&node_id);
2307 }
2308
2309 pub(crate) fn drop_node_ignoring_parent(&mut self, node_id: NodeId) -> Option<Node> {
2310 self.drop_node_ignoring_parent_with(node_id, &mut |_| {})
2311 }
2312
2313 pub(crate) fn drop_node_ignoring_parent_with(
2316 &mut self,
2317 node_id: NodeId,
2318 on_drop: &mut dyn FnMut(NodeId),
2319 ) -> Option<Node> {
2320 self.node_drop_depth += 1;
2321 let mut node = self.remove_node_from_tree(node_id);
2322 if let Some(node) = &mut node {
2323 on_drop(node_id);
2324 if let Some(before) = node.before() {
2325 self.drop_node_ignoring_parent_with(before, on_drop);
2326 }
2327 if let Some(after) = node.after() {
2328 self.drop_node_ignoring_parent_with(after, on_drop);
2329 }
2330
2331 for &child in &node.children {
2332 self.drop_node_ignoring_parent_with(child, on_drop);
2333 }
2334 if let Some(contents) = node
2335 .element_data()
2336 .and_then(|element| element.template_contents)
2337 {
2338 self.drop_node_ignoring_parent_with(contents, on_drop);
2339 }
2340
2341 for &anon_id in &node.anonymous_blocks {
2344 self.deallocate_anonymous_block(anon_id);
2345 }
2346
2347 #[cfg(feature = "shadow-dom")]
2350 if let Some(shadow_root_id) = node.shadow_root_id() {
2351 self.shadow_host_nodes.remove(&node_id);
2352 self.dirty_shadow_hosts.remove(&node_id);
2353 self.custom_element_nodes.remove(&node_id);
2354 self.drop_node_ignoring_parent_with(shadow_root_id, on_drop);
2355 }
2356 }
2357 self.node_drop_depth -= 1;
2358 self.purge_dropped_node_references();
2359 node
2360 }
2361
2362 pub(crate) fn deallocate_anonymous_block(&mut self, anon_id: NodeId) {
2365 if !self.nodes.contains_key(anon_id) {
2368 return;
2369 }
2370
2371 self.node_drop_depth += 1;
2373 let nested = std::mem::take(&mut self.nodes[anon_id].anonymous_blocks);
2374 for nested_id in nested {
2375 self.deallocate_anonymous_block(nested_id);
2376 }
2377
2378 self.remove_node_from_tree(anon_id);
2379 self.node_drop_depth -= 1;
2380 self.purge_dropped_node_references();
2381 }
2382
2383 pub fn create_text_node(&mut self, text: &str) -> NodeId {
2384 let content = text.to_string();
2385 let data = NodeData::Text(TextNodeData::new(content));
2386 self.create_node(data)
2387 }
2388
2389 pub fn deep_clone_node(&mut self, node_id: NodeId) -> NodeId {
2390 let node = &self.nodes[node_id];
2392 let mut data = node.data.clone();
2393
2394 match &mut data {
2395 NodeData::Element(elem) | NodeData::AnonymousBlock(elem) => {
2396 if let Some(arc) = elem.style_attribute.as_mut() {
2397 let read_guard = self.guard().read();
2398 let block = arc.read_with(&read_guard);
2399 *arc = ServoArc::new(self.guard().wrap(block.clone()));
2400 }
2401 }
2402 _ => {}
2403 }
2404
2405 let children = node.children.clone();
2406 #[cfg(feature = "shadow-dom")]
2407 let shadow_source = node.shadow_root_id();
2408 let template_contents = node
2409 .element_data()
2410 .and_then(|element| element.template_contents);
2411 let owner_document = node.owner_document;
2412 let markup = node.markup.clone();
2413 let inert_script = node
2414 .flags
2415 .contains(crate::node::NodeFlags::IS_PARSER_INERT_SCRIPT);
2416
2417 let new_node_id = self.create_node(data);
2419 self.nodes[new_node_id].owner_document = owner_document;
2420 self.nodes[new_node_id].markup = markup;
2421 self.nodes[new_node_id]
2422 .flags
2423 .set(crate::node::NodeFlags::IS_PARSER_INERT_SCRIPT, inert_script);
2424 if let Some(document_id) = owner_document {
2425 let id = self.nodes[new_node_id]
2426 .element_data()
2427 .and_then(|element| element.id.as_ref())
2428 .map(ToString::to_string);
2429 if let Some(id) = id {
2430 self.add_to_document_id_map(document_id, &id, new_node_id);
2431 }
2432 }
2433 if let Some(contents) = template_contents {
2434 let cloned_contents = self.deep_clone_node(contents);
2435 self.nodes[new_node_id]
2436 .element_data_mut()
2437 .expect("template clone must be an element")
2438 .template_contents = Some(cloned_contents);
2439 }
2440
2441 let new_children: ThinVec<NodeId> = children
2443 .into_iter()
2444 .map(|child_id| self.deep_clone_node(child_id))
2445 .collect();
2446 for &child_id in &new_children {
2447 self.nodes[child_id].parent = Some(new_node_id);
2448 self.attach_script_custom_element_subtree(child_id, new_node_id);
2449 }
2450 self.nodes[new_node_id].children = new_children;
2451
2452 #[cfg(feature = "shadow-dom")]
2455 if let Some(source_root) = shadow_source {
2456 let clonable = self
2457 .nodes
2458 .get(source_root)
2459 .and_then(|node| node.shadow_root_data())
2460 .is_some_and(|root| root.clonable);
2461 if clonable {
2462 let mode = self.nodes[source_root].shadow_root_data().unwrap().mode;
2463 let serializable = self.nodes[source_root]
2464 .shadow_root_data()
2465 .unwrap()
2466 .serializable;
2467 let delegates_focus = self.nodes[source_root]
2468 .shadow_root_data()
2469 .unwrap()
2470 .delegates_focus;
2471 let declarative = self.nodes[source_root]
2472 .shadow_root_data()
2473 .unwrap()
2474 .declarative;
2475 let shadow_children = self.nodes[source_root].children.clone();
2476 let new_root = self.attach_shadow(new_node_id, mode);
2477 if let Some(root) = self.nodes[new_root].shadow_root_data_mut() {
2478 root.clonable = true;
2479 root.serializable = serializable;
2480 root.delegates_focus = delegates_focus;
2481 root.declarative = declarative;
2482 }
2483 let cloned_shadow_children: ThinVec<NodeId> = shadow_children
2484 .into_iter()
2485 .map(|child_id| self.deep_clone_node(child_id))
2486 .collect();
2487 for &child_id in &cloned_shadow_children {
2488 self.nodes[child_id].parent = Some(new_root);
2489 self.attach_script_custom_element_subtree(child_id, new_root);
2490 }
2491 self.nodes[new_root].children = cloned_shadow_children;
2492 }
2493 }
2494
2495 new_node_id
2496 }
2497
2498 pub(crate) fn remove_and_drop_pe(&mut self, node_id: NodeId) -> Option<Node> {
2499 fn remove_pe_ignoring_parent(doc: &mut BaseDocument, node_id: NodeId) -> Option<Node> {
2500 let mut node = doc.remove_node_from_tree(node_id);
2501 if let Some(node) = &mut node {
2502 for &child in &node.children {
2503 remove_pe_ignoring_parent(doc, child);
2504 }
2505 for &anon_id in &node.anonymous_blocks {
2506 doc.deallocate_anonymous_block(anon_id);
2507 }
2508 }
2509 node
2510 }
2511
2512 self.node_drop_depth += 1;
2513 let node = remove_pe_ignoring_parent(self, node_id);
2514
2515 if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
2517 let parent = &mut self.nodes[parent_id];
2518 parent.children.retain(|id| *id != node_id);
2519 }
2520
2521 self.node_drop_depth -= 1;
2522 self.purge_dropped_node_references();
2523 node
2524 }
2525
2526 pub(crate) fn resolve_url(&self, raw: &str) -> url::Url {
2527 self.url.resolve_relative(raw).unwrap_or_else(|| {
2528 panic!(
2529 "to be able to resolve {raw} with the base_url: {:?}",
2530 *self.url
2531 )
2532 })
2533 }
2534
2535 pub fn navigate_to_url(&self, raw: &str) -> bool {
2543 let Some(url) = self.url.resolve_relative(raw) else {
2544 return false;
2545 };
2546 self.navigation_provider
2547 .navigate_to(blitz_traits::navigation::NavigationOptions::new(
2548 url,
2549 None,
2550 self.id(),
2551 ));
2552 true
2553 }
2554
2555 pub fn current_url(&self) -> String {
2557 self.url.to_string()
2558 }
2559
2560 pub fn print_tree(&self) {
2561 crate::util::walk_tree(0, self.root_node());
2562 }
2563
2564 pub fn print_subtree(&self, node_id: NodeId) {
2565 crate::util::walk_tree(0, &self.nodes[node_id]);
2566 }
2567
2568 pub fn reload_resource_by_href(&mut self, href_to_reload: &str) {
2569 for &node_id in self.nodes_to_stylesheet.keys() {
2570 let node = &self.nodes[node_id];
2571 let Some(element) = node.element_data() else {
2572 continue;
2573 };
2574
2575 if element.name.local == local_name!("link") {
2576 if let Some(href) = element.attr(local_name!("href")) {
2577 if href == href_to_reload {
2579 let resolved_href = self.resolve_url(href);
2580 self.net_provider.fetch(
2581 self.id(),
2582 self.build_request(resolved_href.clone())
2583 .destination(RequestDestination::Style),
2584 ResourceHandler::boxed(
2585 self.tx.clone(),
2586 self.id,
2587 Some(node_id),
2588 self.shell_provider.clone(),
2589 StylesheetHandler {
2590 source_url: resolved_href,
2591 guard: self.guard.clone(),
2592 net_provider: self.net_provider.clone(),
2593 abort_signal: self.abort_signal.clone(),
2594 },
2595 ),
2596 );
2597 }
2598 }
2599 }
2600 }
2601 }
2602
2603 pub fn process_style_element(&mut self, target_id: NodeId) {
2604 let css = self.nodes[target_id].text_content();
2605 let css = html_escape::decode_html_entities(&css);
2606 let sheet = self.make_stylesheet(&css, Origin::Author);
2607 self.add_stylesheet_for_node(sheet, target_id);
2608 }
2609
2610 pub fn remove_user_agent_stylesheet(&mut self, contents: &str) {
2611 if let Some(sheet) = self.ua_stylesheets.remove(contents) {
2612 self.stylist.remove_stylesheet(sheet, &self.guard.read());
2613 }
2614 }
2615
2616 pub fn url(&self) -> &url::Url {
2618 &self.url
2619 }
2620
2621 pub fn author_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
2624 self.nodes_to_stylesheet.values()
2625 }
2626
2627 pub fn useragent_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
2629 self.ua_stylesheets.values()
2630 }
2631
2632 pub fn add_user_agent_stylesheet(&mut self, css: &str) {
2633 let sheet = self.make_stylesheet(css, Origin::UserAgent);
2634 self.ua_stylesheets.insert(css.to_string(), sheet.clone());
2635 self.stylist.append_stylesheet(sheet, &self.guard.read());
2636 }
2637
2638 pub fn make_stylesheet(&self, css: impl AsRef<str>, origin: Origin) -> DocumentStyleSheet {
2639 let data = Stylesheet::from_str(
2640 css.as_ref(),
2641 self.url.url_extra_data(),
2642 origin,
2643 ServoArc::new(self.guard.wrap(MediaList::empty())),
2644 self.guard.clone(),
2645 Some(&StylesheetLoader {
2646 tx: self.tx.clone(),
2647 doc_id: self.id,
2648 net_provider: self.net_provider.clone(),
2649 shell_provider: self.shell_provider.clone(),
2650 abort_signal: self.abort_signal.clone(),
2651 }),
2652 None,
2653 QuirksMode::NoQuirks,
2654 AllowImportRules::Yes,
2655 );
2656
2657 DocumentStyleSheet(ServoArc::new(data))
2658 }
2659
2660 pub fn upsert_stylesheet_for_node(&mut self, node_id: NodeId) {
2661 let raw_styles = self.nodes[node_id].text_content();
2662 let sheet = self.make_stylesheet(raw_styles, Origin::Author);
2663 self.add_stylesheet_for_node(sheet, node_id);
2664 }
2665
2666 pub fn add_stylesheet_for_node(&mut self, stylesheet: DocumentStyleSheet, node_id: NodeId) {
2667 self.platform_initial_sheet_media(&stylesheet, node_id);
2668 #[cfg(feature = "shadow-dom")]
2669 if let Some(root_id) = self.containing_shadow_root(node_id) {
2670 if self.document_sheet_nodes.remove(&node_id)
2673 && let Some(old) = self.nodes_to_stylesheet.get(&node_id).cloned()
2674 {
2675 self.stylist.remove_stylesheet(old, &self.guard.read());
2676 }
2677 self.install_shadow_stylesheet(root_id, node_id, stylesheet);
2678 return;
2679 }
2680
2681 let old = self.nodes_to_stylesheet.insert(node_id, stylesheet.clone());
2682
2683 if let Some(old) = old
2684 && self.document_sheet_nodes.contains(&node_id)
2685 {
2686 self.stylist.remove_stylesheet(old, &self.guard.read())
2687 }
2688 self.document_sheet_nodes.insert(node_id);
2689
2690 crate::net::fetch_font_face(
2692 self.tx.clone(),
2693 self.id,
2694 Some(node_id),
2695 &stylesheet.0,
2696 &self.net_provider,
2697 &self.shell_provider,
2698 &self.guard.read(),
2699 self.abort_signal.as_ref(),
2700 &mut self.web_fonts,
2701 self.font_epoch,
2702 );
2703
2704 let element = &mut self.nodes[node_id].element_data_mut().unwrap();
2706 element.special_data = SpecialElementData::Stylesheet(stylesheet.clone());
2707
2708 let insertion_point = self
2712 .nodes_to_stylesheet
2713 .range((Bound::Excluded(node_id), Bound::Unbounded))
2714 .find(|(id, _)| self.document_sheet_nodes.contains(id))
2715 .map(|(_, sheet)| sheet);
2716
2717 if let Some(insertion_point) = insertion_point {
2718 self.stylist.insert_stylesheet_before(
2719 stylesheet,
2720 insertion_point.clone(),
2721 &self.guard.read(),
2722 )
2723 } else {
2724 self.stylist
2725 .append_stylesheet(stylesheet, &self.guard.read())
2726 }
2727 }
2728
2729 pub fn handle_messages(&mut self) {
2730 let rx = self.rx.take().unwrap();
2733
2734 while let Ok(msg) = rx.try_recv() {
2735 self.handle_message(msg);
2736 }
2737
2738 self.rx = Some(rx);
2740 }
2741
2742 pub fn handle_message(&mut self, msg: DocumentEvent) {
2743 match msg {
2744 DocumentEvent::ResourceLoad(resource) => self.load_resource(resource),
2745 DocumentEvent::NavigateIframe { node_id, url } => self.navigate_iframe(node_id, url),
2746 }
2747 }
2748
2749 pub fn has_pending_critical_resources(&self) -> bool {
2751 !self.pending_critical_resources.is_empty()
2752 }
2753
2754 pub fn pending_image_count(&self) -> usize {
2761 self.pending_images.len()
2762 }
2763
2764 pub fn load_resource(&mut self, res: ResourceLoadResponse) {
2765 self.pending_critical_resources.remove(&res.request_id);
2766
2767 let resource = match res.result {
2768 Ok(resource) => resource,
2769 Err(err) => {
2770 if let Some(url) = res.resolved_url.as_ref() {
2771 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
2772 #[cfg(feature = "tracing")]
2773 tracing::warn!(
2774 url = url.as_str(),
2775 waiting_nodes = waiting_nodes.len(),
2776 error = err.as_str(),
2777 "Resource load failed"
2778 );
2779 #[cfg(not(feature = "tracing"))]
2780 let _ = (waiting_nodes, err);
2781 } else {
2782 #[cfg(feature = "tracing")]
2783 tracing::warn!(error = err.as_str(), "Resource load failed (no url)");
2784 #[cfg(not(feature = "tracing"))]
2785 let _ = err;
2786 }
2787 return;
2788 }
2789 };
2790
2791 let (resource, font_ticket) = match resource {
2792 Resource::TrackedFont(bytes, overrides, ticket) => {
2793 if ticket.css && ticket.epoch != self.font_epoch {
2794 ticket.finish(false);
2795 return;
2796 }
2797 (Resource::Font(bytes, overrides), Some(ticket))
2798 }
2799 resource => (resource, None),
2800 };
2801 if matches!(&resource, Resource::Css(_) | Resource::Font(_, _))
2802 && res.node_id.is_some_and(|id| {
2803 self.get_node(id)
2804 .is_none_or(|node| !node.flags.is_in_document())
2805 })
2806 {
2807 if let Some(ticket) = font_ticket {
2808 ticket.finish(false);
2809 }
2810 return;
2811 }
2812
2813 match resource {
2814 Resource::Css(css) => {
2815 let node_id = res.node_id.unwrap();
2816 self.add_stylesheet_for_node(css, node_id);
2817 }
2818 Resource::ImportSheet(import_rule, sheet) => {
2819 {
2828 let mut guard = self.guard.write();
2829 import_rule.write_with(&mut guard).stylesheet =
2830 style::stylesheets::import_rule::ImportSheet::Sheet(sheet.clone());
2831 }
2832
2833 crate::net::fetch_font_face(
2837 self.tx.clone(),
2838 self.id,
2839 res.node_id,
2840 &sheet,
2841 &self.net_provider,
2842 &self.shell_provider,
2843 &self.guard.read(),
2844 self.abort_signal.as_ref(),
2845 &mut self.web_fonts,
2846 self.font_epoch,
2847 );
2848 }
2849 Resource::Image(_kind, width, height, image_data) => {
2850 let image = ImageData::Raster(RasterImageData::new(width, height, image_data));
2852
2853 let Some(url) = res.resolved_url.as_ref() else {
2854 return;
2855 };
2856
2857 self.apply_loaded_image(url, image);
2858 }
2859 #[cfg(feature = "svg")]
2860 Resource::Svg(_kind, svg) => {
2861 let image = ImageData::Svg(svg);
2863
2864 let Some(url) = res.resolved_url.as_ref() else {
2865 return;
2866 };
2867
2868 self.apply_loaded_image(url, image);
2869 }
2870 Resource::DocumentSrc(html) => {
2871 let Some(node_id) = res.node_id else {
2872 return;
2873 };
2874 self.apply_iframe_html(node_id, res.request_id, res.resolved_url, &html);
2875 }
2876 Resource::Font(bytes, overrides) => {
2877 let font = Blob::new(Arc::new(bytes));
2878
2879 let weight_override = overrides.weight.map(parley::fontique::FontWeight::new);
2885 let info_override = parley::fontique::FontInfoOverride {
2886 family_name: overrides.family_name.as_deref(),
2887 weight: weight_override,
2888 style: overrides.style,
2889 ..Default::default()
2890 };
2891
2892 let mut global_font_ctx = self.font_ctx.lock().unwrap();
2894 let registered = global_font_ctx
2895 .collection
2896 .register_fonts(font.clone(), Some(info_override));
2897 let registered = !registered.is_empty();
2898
2899 #[cfg(feature = "parallel-construct")]
2900 {
2901 rayon::broadcast(|_ctx| {
2902 let mut font_ctx = self
2903 .thread_font_contexts
2904 .get_or(|| RefCell::new(Box::new(global_font_ctx.clone())))
2905 .borrow_mut();
2906 font_ctx
2907 .collection
2908 .register_fonts(font.clone(), Some(info_override));
2909 });
2910 }
2911 drop(global_font_ctx);
2912
2913 self.invalidate_inline_contexts();
2915 if let Some(ticket) = font_ticket {
2916 ticket.finish(registered);
2917 }
2918 }
2919 Resource::TrackedFont(..) => unreachable!("tracked font was normalized"),
2920 Resource::None => {
2921 }
2923 }
2924 }
2925
2926 fn apply_loaded_image(&mut self, url: &str, image: ImageData) {
2929 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
2931
2932 #[cfg(feature = "tracing")]
2933 tracing::info!(
2934 "Image {url} loaded, applying to {} nodes",
2935 waiting_nodes.len()
2936 );
2937
2938 self.image_cache.insert(url.to_string(), image.clone());
2940
2941 for (node_id, image_type) in waiting_nodes {
2943 let Some(node) = self.get_node_mut(node_id) else {
2944 continue;
2945 };
2946
2947 match image_type {
2948 ImageType::Image => {
2949 node.element_data_mut().unwrap().special_data =
2950 SpecialElementData::Image(Box::new(image.clone()));
2951
2952 node.cache_mut().clear();
2954 node.insert_damage(ALL_DAMAGE);
2955 }
2956 ImageType::Background(idx) | ImageType::Mask(idx) => {
2957 let layer_image = node.element_data_mut().and_then(|el| {
2958 let images = match image_type {
2959 ImageType::Background(_) => &mut el.background_images,
2960 ImageType::Mask(_) => &mut el.mask_images,
2961 ImageType::Image => unreachable!(),
2962 };
2963 images.get_mut(idx)
2964 });
2965 if let Some(Some(layer_image)) = layer_image {
2966 layer_image.status = Status::Ok;
2967 layer_image.image = image.clone();
2968 }
2969 }
2970 }
2971 }
2972 }
2973
2974 pub fn snapshot_node(&mut self, node_id: NodeId) {
2975 let node = &mut self.nodes[node_id];
2976
2977 let has_been_styled = node.primary_styles().is_some();
2982 if !has_been_styled {
2983 return;
2984 }
2985
2986 let opaque_node_id = TNode::opaque(&&*node);
2987 node.set_has_snapshot(true);
2988 node.snapshot_handled()
2989 .store(false, std::sync::atomic::Ordering::SeqCst);
2990
2991 if let Some(_existing_snapshot) = self.snapshots.get_mut(&opaque_node_id) {
2993 } else {
2996 let attrs: Option<Vec<_>> = node.attrs().map(|attrs| {
2997 attrs
2998 .iter()
2999 .map(|attr| {
3000 let ident = AttrIdentifier {
3001 local_name: GenericAtomIdent(attr.name.local.clone()),
3002 name: GenericAtomIdent(attr.name.local.clone()),
3003 namespace: GenericAtomIdent(attr.name.ns.clone()),
3004 prefix: None,
3005 };
3006
3007 let value = if attr.name.local == local_name!("id") {
3008 AttrValue::Atom(Atom::from(&*attr.value))
3009 } else if attr.name.local == local_name!("class") {
3010 let classes = attr
3011 .value
3012 .split_ascii_whitespace()
3013 .map(Atom::from)
3014 .collect();
3015 AttrValue::TokenList(OnceLock::from(attr.value.to_string()), classes)
3021 } else {
3022 AttrValue::String(attr.value.to_string())
3023 };
3024
3025 (ident, value)
3026 })
3027 .collect()
3028 });
3029
3030 let changed_attrs = attrs
3031 .as_ref()
3032 .map(|attrs| attrs.iter().map(|attr| attr.0.name.clone()).collect())
3033 .unwrap_or_default();
3034
3035 self.snapshots.insert(
3036 opaque_node_id,
3037 ServoElementSnapshot {
3038 state: Some(*node.element_state()),
3039 attrs,
3040 changed_attrs,
3041 class_changed: true,
3042 id_changed: true,
3043 other_attributes_changed: true,
3044 },
3045 );
3046 }
3047 }
3048
3049 pub(crate) fn snapshot_attribute(&mut self, node_id: NodeId, name: &markup5ever::LocalName) {
3051 self.snapshot_node(node_id);
3052 let opaque_node_id = TNode::opaque(&&self.nodes[node_id]);
3053 if let Some(snapshot) = self.snapshots.get_mut(&opaque_node_id) {
3054 let changed_attr = GenericAtomIdent(name.clone());
3055 if !snapshot.changed_attrs.contains(&changed_attr) {
3056 snapshot.changed_attrs.extend(std::iter::once(changed_attr));
3057 }
3058 }
3059 }
3060
3061 pub fn snapshot_node_and(&mut self, node_id: NodeId, cb: impl FnOnce(&mut Node)) {
3068 if !self.nodes.contains_key(node_id) {
3069 return;
3070 }
3071 self.snapshot_node(node_id);
3072 cb(&mut self.nodes[node_id]);
3073 }
3074
3075 pub fn hit(&self, x: f32, y: f32) -> Option<HitResult> {
3077 self.hit_with_scrollbar(x, y).0
3078 }
3079
3080 pub fn nearest_non_anonymous_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
3093 let mut node = self.get_node(node_id)?;
3097 loop {
3098 let parent = match node.parent {
3099 Some(parent_id) => self.get_node(parent_id)?,
3100 None => return Some(node.id),
3101 };
3102 if !node.is_anonymous() && !parent.is_anonymous() {
3103 return Some(node.id);
3104 }
3105 node = parent;
3106 }
3107 }
3108
3109 pub fn focus_next_node(&mut self) -> Option<NodeId> {
3110 let focussed_node_id = self.get_focussed_node_id()?;
3111 let id = self.next_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
3112 self.set_focus_to(id);
3113 Some(id)
3114 }
3115
3116 pub fn focus_prev_node(&mut self) -> Option<NodeId> {
3118 let focussed_node_id = self.get_focussed_node_id()?;
3119 let id = self.prev_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
3120 self.set_focus_to(id);
3121 Some(id)
3122 }
3123
3124 pub fn clear_focus(&mut self) {
3126 if let Some(id) = self.focus_node_id {
3127 let shell_provider = self.shell_provider.clone();
3128 self.snapshot_node_and(id, |node| node.blur(shell_provider));
3129 self.focus_node_id = None;
3130 }
3131 }
3132
3133 pub fn set_mousedown_node_id(&mut self, node_id: Option<NodeId>) {
3134 self.mousedown_node_id = node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
3135 }
3136 pub fn set_focus_to(&mut self, focus_node_id: NodeId) -> bool {
3137 let Some(focus_node_id) = self.nearest_non_anonymous_ancestor(focus_node_id) else {
3138 return false;
3139 };
3140 if Some(focus_node_id) == self.focus_node_id {
3141 return false;
3142 }
3143
3144 #[cfg(feature = "tracing")]
3145 tracing::info!("Focussed node {focus_node_id}");
3146
3147 let shell_provider = self.shell_provider.clone();
3148
3149 if let Some(id) = self.focus_node_id {
3151 self.snapshot_node_and(id, |node| node.blur(shell_provider.clone()));
3152 }
3153
3154 self.snapshot_node_and(focus_node_id, |node| node.focus(shell_provider));
3156
3157 self.focus_node_id = Some(focus_node_id);
3158
3159 true
3160 }
3161
3162 pub fn active_node(&mut self) -> bool {
3163 let Some(hover_node_id) = self.get_hover_node_id() else {
3164 return false;
3165 };
3166
3167 if let Some(active_node_id) = self.active_node_id {
3168 if active_node_id == hover_node_id {
3169 return true;
3170 }
3171 self.unactive_node();
3172 }
3173
3174 debug_assert!(
3176 self.get_node(hover_node_id)
3177 .is_some_and(|node| !node.is_anonymous()),
3178 "interaction state must reference DOM nodes, not layout-generated nodes"
3179 );
3180 let active_node_id = Some(hover_node_id);
3181
3182 let node_path = self.maybe_node_layout_ancestors(active_node_id);
3183 for &id in node_path.iter() {
3184 self.snapshot_node_and(id, |node| node.active());
3185 }
3186
3187 self.active_node_id = active_node_id;
3188
3189 true
3190 }
3191
3192 pub fn unactive_node(&mut self) -> bool {
3193 let Some(active_node_id) = self.active_node_id.take() else {
3194 return false;
3195 };
3196
3197 let node_path = self.maybe_node_layout_ancestors(Some(active_node_id));
3198 for &id in node_path.iter() {
3199 self.snapshot_node_and(id, |node| node.unactive());
3200 }
3201
3202 true
3203 }
3204
3205 pub fn hovered_scrollbar(&self) -> Option<crate::node::ScrollbarRef> {
3207 self.hovered_scrollbar
3208 }
3209
3210 pub fn scrollbar_drag_target(&self) -> Option<crate::node::ScrollbarRef> {
3212 match &self.drag_mode {
3213 DragMode::ScrollbarDrag(state) => Some(state.scrollbar),
3214 _ => None,
3215 }
3216 }
3217
3218 pub fn scrollbar_opacity(&self, node_id: NodeId) -> f32 {
3223 let interacting = |scrollbar: &crate::node::ScrollbarRef| scrollbar.node_id == node_id;
3224 if self.hovered_scrollbar.as_ref().is_some_and(interacting)
3225 || self
3226 .scrollbar_drag_target()
3227 .as_ref()
3228 .is_some_and(interacting)
3229 {
3230 return 1.0;
3231 }
3232 self.scrollbar_activity.get(&node_id).map_or(1.0, |last| {
3233 crate::node::scrollbar::opacity_at(last.elapsed())
3234 })
3235 }
3236
3237 pub(crate) fn show_scrollbars(&mut self, node_id: NodeId) {
3240 if cfg!(feature = "scrollbars") {
3241 self.scrollbar_activity.insert(node_id, Instant::now());
3242 }
3243 }
3244
3245 fn scrollbars_animating(&self) -> bool {
3248 use crate::node::scrollbar::{FADE_DELAY, FADE_DURATION};
3249 self.scrollbar_activity
3250 .values()
3251 .any(|last| last.elapsed() < FADE_DELAY + FADE_DURATION)
3252 }
3253
3254 pub(crate) fn hit_with_scrollbar(
3258 &self,
3259 x: f32,
3260 y: f32,
3261 ) -> (Option<HitResult>, Option<crate::node::ScrollbarRef>) {
3262 if TDocument::as_node(&self.root_node())
3263 .first_element_child()
3264 .is_none()
3265 {
3266 #[cfg(feature = "tracing")]
3267 tracing::warn!("No DOM - not resolving hit test");
3268 return (None, None);
3269 }
3270 let mut scrollbar = None;
3271 let hit = self
3272 .root_element()
3273 .hit_inner(x, y, self.viewport().scale_f64(), &mut scrollbar);
3274 (hit, scrollbar)
3275 }
3276
3277 pub fn set_hover_to(&mut self, x: f32, y: f32) -> bool {
3278 self.semantic_hover_node_id = None;
3279 self.last_client_pointer_position = Some(taffy::Point {
3283 x: x - self.viewport_scroll.x as f32,
3284 y: y - self.viewport_scroll.y as f32,
3285 });
3286
3287 let (hit, hovered_scrollbar) = self.hit_with_scrollbar(x, y);
3288 let hovered_scrollbar =
3291 hovered_scrollbar.filter(|scrollbar| self.scrollbar_opacity(scrollbar.node_id) > 0.0);
3292 let scrollbar_changed = hovered_scrollbar != self.hovered_scrollbar;
3296 if scrollbar_changed {
3297 for scrollbar in [self.hovered_scrollbar, hovered_scrollbar]
3300 .into_iter()
3301 .flatten()
3302 {
3303 self.show_scrollbars(scrollbar.node_id);
3304 }
3305 }
3306 self.hovered_scrollbar = hovered_scrollbar;
3307
3308 let hit_node_id = hit.map(|hit| hit.node_id);
3313 let hover_node_id = hit_node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
3314 let new_is_text = hit.map(|hit| hit.is_text).unwrap_or(false);
3315
3316 self.apply_hover_target(hit_node_id, hover_node_id, new_is_text, scrollbar_changed)
3317 }
3318
3319 pub fn set_hover_to_node(&mut self, node_id: NodeId, x: f32, y: f32) -> bool {
3327 self.semantic_hover_node_id = Some(node_id);
3328 self.last_client_pointer_position = Some(taffy::Point {
3329 x: x - self.viewport_scroll.x as f32,
3330 y: y - self.viewport_scroll.y as f32,
3331 });
3332
3333 let hovered_scrollbar = self.hovered_scrollbar.take();
3334 let scrollbar_changed = hovered_scrollbar.is_some();
3335 if let Some(scrollbar) = hovered_scrollbar {
3336 self.show_scrollbars(scrollbar.node_id);
3337 }
3338 let hover_node_id = self.nearest_non_anonymous_ancestor(node_id);
3339 self.apply_hover_target(Some(node_id), hover_node_id, false, scrollbar_changed)
3340 }
3341
3342 fn apply_hover_target(
3343 &mut self,
3344 hit_node_id: Option<NodeId>,
3345 hover_node_id: Option<NodeId>,
3346 new_is_text: bool,
3347 scrollbar_changed: bool,
3348 ) -> bool {
3349 let hit_changed =
3350 hit_node_id != self.hover_hit_node_id || new_is_text != self.hover_node_is_text;
3351 self.hover_hit_node_id = hit_node_id;
3352 self.hover_node_is_text = new_is_text;
3353
3354 if hover_node_id == self.hover_node_id {
3356 if hit_changed {
3357 self.shell_provider.set_cursor(self.get_cursor());
3361 }
3362 return scrollbar_changed;
3363 }
3364
3365 let old_node_path = self.maybe_node_layout_ancestors(self.hover_node_id);
3366 let new_node_path = self.maybe_node_layout_ancestors(hover_node_id);
3367 let same_count = old_node_path
3368 .iter()
3369 .zip(&new_node_path)
3370 .take_while(|(o, n)| o == n)
3371 .count();
3372 for &id in old_node_path.iter().skip(same_count) {
3373 self.snapshot_node_and(id, |node| node.unhover());
3374 }
3375 for &id in new_node_path.iter().skip(same_count) {
3376 self.snapshot_node_and(id, |node| node.hover());
3377 }
3378
3379 self.hover_node_id = hover_node_id;
3380
3381 self.shell_provider.set_cursor(self.get_cursor());
3383
3384 self.shell_provider.request_redraw();
3386
3387 true
3388 }
3389
3390 pub fn clear_hover(&mut self) -> bool {
3391 self.last_client_pointer_position = None;
3394 self.semantic_hover_node_id = None;
3395 self.hover_hit_node_id = None;
3396
3397 let Some(hover_node_id) = self.hover_node_id else {
3398 return false;
3399 };
3400
3401 let old_node_path = self.maybe_node_layout_ancestors(Some(hover_node_id));
3402 for &id in old_node_path.iter() {
3403 self.snapshot_node_and(id, |node| node.unhover());
3404 }
3405
3406 self.hover_node_id = None;
3407 self.hover_node_is_text = false;
3408
3409 self.shell_provider.set_cursor(self.get_cursor());
3411
3412 self.shell_provider.request_redraw();
3414
3415 true
3416 }
3417
3418 pub fn refresh_hover(&mut self) -> bool {
3424 if let Some(node_id) = self.semantic_hover_node_id {
3425 if self.get_node(node_id).is_some() {
3426 let hover_node_id = self.nearest_non_anonymous_ancestor(node_id);
3427 return self.apply_hover_target(Some(node_id), hover_node_id, false, false);
3428 }
3429 self.semantic_hover_node_id = None;
3430 }
3431 let Some(pos) = self.last_client_pointer_position else {
3432 return false;
3433 };
3434 let x = pos.x + self.viewport_scroll.x as f32;
3435 let y = pos.y + self.viewport_scroll.y as f32;
3436 self.set_hover_to(x, y)
3437 }
3438
3439 pub fn get_hover_node_id(&self) -> Option<NodeId> {
3440 self.hover_node_id
3441 }
3442
3443 pub fn get_mousedown_node_id(&self) -> Option<NodeId> {
3444 self.mousedown_node_id
3445 }
3446
3447 pub fn set_viewport(&mut self, viewport: Viewport) {
3448 let scale_has_changed = viewport.scale_f64() != self.viewport.scale_f64();
3449 self.viewport = viewport;
3450 self.set_stylist_device(make_device(
3451 &self.viewport,
3452 self.media_type.clone(),
3453 self.font_ctx.clone(),
3454 ));
3455 self.scroll_viewport_by(0.0, 0.0); if scale_has_changed {
3458 self.invalidate_inline_contexts();
3459 self.shell_provider.request_redraw();
3460 }
3461 }
3462
3463 pub fn media_type(&self) -> &MediaType {
3465 &self.media_type
3466 }
3467
3468 pub fn set_media_type(&mut self, media_type: MediaType) {
3471 if self.media_type == media_type {
3472 return;
3473 }
3474 self.media_type = media_type;
3475 self.set_stylist_device(make_device(
3476 &self.viewport,
3477 self.media_type.clone(),
3478 self.font_ctx.clone(),
3479 ));
3480 }
3481
3482 pub fn viewport(&self) -> &Viewport {
3483 &self.viewport
3484 }
3485
3486 pub fn viewport_mut(&mut self) -> ViewportMut<'_> {
3487 ViewportMut::new(self)
3488 }
3489
3490 pub fn zoom_by(&mut self, increment: f32) {
3491 let mut viewport = self.viewport.clone();
3492 viewport.zoom_by(increment);
3493 self.set_viewport(viewport);
3494 }
3495
3496 pub fn zoom_to(&mut self, zoom: f32) {
3497 let mut viewport = self.viewport.clone();
3498 viewport.set_zoom(zoom);
3499 self.set_viewport(viewport);
3500 }
3501
3502 pub fn get_viewport(&self) -> Viewport {
3503 self.viewport.clone()
3504 }
3505
3506 pub fn incremental_layout(&self) -> bool {
3508 self.incremental_layout
3509 }
3510
3511 pub fn set_incremental_layout(&mut self, enabled: bool) {
3513 self.incremental_layout = enabled;
3514 }
3515
3516 pub fn devtools(&self) -> &DevtoolSettings {
3517 &self.devtool_settings
3518 }
3519
3520 pub fn devtools_mut(&mut self) -> &mut DevtoolSettings {
3521 &mut self.devtool_settings
3522 }
3523
3524 pub fn subdoc(&self, node_id: NodeId) -> Option<&dyn Document> {
3525 self.get_node(node_id)
3526 .and_then(|node| node.element_data())
3527 .and_then(|el| el.sub_doc_data())
3528 }
3529
3530 pub fn subdoc_mut(&mut self, node_id: NodeId) -> Option<&mut dyn Document> {
3531 self.get_node_mut(node_id)
3532 .and_then(|node| node.element_data_mut())
3533 .and_then(|el| el.sub_doc_data_mut())
3534 }
3535
3536 pub fn is_animating(&self) -> bool {
3537 #[cfg(feature = "custom-widget")]
3538 let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
3539 self.nodes[node_id]
3540 .element_data()
3541 .and_then(|el| el.custom_widget_data())
3542 .is_some_and(|data| data.widget.requires_redraw())
3543 });
3544 #[cfg(not(feature = "custom-widget"))]
3545 let custom_widget_is_animating = false;
3546
3547 let animating = self.has_canvas
3548 | self.has_active_animations
3549 | (self.subdoc_animation_pacing != AnimationPacing::Idle)
3550 | custom_widget_is_animating
3551 | (self.scroll_animation != ScrollAnimationState::None)
3552 | self.scrollbars_animating();
3553
3554 if animating && crate::debug::animation_reasons_enabled() {
3555 crate::debug::report_animation_reasons(
3556 self.id(),
3557 self.has_canvas,
3558 self.has_active_animations,
3559 self.subdoc_animation_pacing != AnimationPacing::Idle,
3560 custom_widget_is_animating,
3561 self.scroll_animation != ScrollAnimationState::None,
3562 self.scrollbars_animating(),
3563 self.animating_node_names().as_deref(),
3564 );
3565 }
3566
3567 animating
3568 }
3569
3570 pub fn animation_pacing(&self) -> AnimationPacing {
3575 let focused_text_input = self.focus_node_id.is_some_and(|node_id| {
3576 self.nodes
3577 .get(node_id)
3578 .and_then(|node| node.element_data())
3579 .is_some_and(|element| element.text_input_data().is_some())
3580 });
3581 #[cfg(feature = "custom-widget")]
3582 let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
3583 self.nodes[node_id]
3584 .element_data()
3585 .and_then(|el| el.custom_widget_data())
3586 .is_some_and(|data| data.widget.requires_redraw())
3587 });
3588 #[cfg(not(feature = "custom-widget"))]
3589 let custom_widget_is_animating = false;
3590
3591 if self.has_canvas
3592 || custom_widget_is_animating
3593 || self.scroll_animation != ScrollAnimationState::None
3594 || self.scrollbars_animating()
3595 {
3596 AnimationPacing::Interactive
3597 } else if self.has_active_animations {
3598 const SLOW_ANIMATION_SECONDS: f64 = 2.0;
3599 let sets = self.animations.sets.read();
3600 let has_fast_animation_or_transition = sets.values().any(|set| {
3601 set.transitions.iter().any(|transition| {
3602 matches!(
3603 transition.state,
3604 AnimationState::Pending | AnimationState::Running
3605 )
3606 }) || set.animations.iter().any(|animation| {
3607 matches!(
3608 animation.state,
3609 AnimationState::Pending | AnimationState::Running
3610 ) && animation.duration < SLOW_ANIMATION_SECONDS
3611 })
3612 });
3613 if has_fast_animation_or_transition {
3614 AnimationPacing::Interactive
3615 } else {
3616 AnimationPacing::SlowCss
3617 }
3618 } else if focused_text_input {
3619 AnimationPacing::Caret
3620 } else if self.subdoc_animation_pacing != AnimationPacing::Idle {
3621 self.subdoc_animation_pacing
3622 } else {
3623 AnimationPacing::Idle
3624 }
3625 }
3626
3627 fn animating_node_names(&self) -> Option<String> {
3634 if !self.has_active_animations {
3635 return None;
3636 }
3637 let sets = self.animations.sets.read();
3638 let mut described: Vec<String> = sets
3639 .iter()
3640 .filter(|(_, state)| state.needs_animation_ticks())
3641 .filter_map(|(key, state)| {
3642 let node_id = NodeId::from_u64(key.node.id() as u64);
3643 let node = self.nodes.get(node_id)?;
3644 let element = node.element_data()?;
3645 let name = element
3646 .attr(local_name!("id"))
3647 .map(|id| format!("#{id}"))
3648 .or_else(|| {
3649 element
3650 .attr(local_name!("class"))
3651 .and_then(|c| c.split_ascii_whitespace().next())
3652 .map(|c| format!(".{c}"))
3653 })
3654 .unwrap_or_else(|| element.name.local.to_string());
3655 Some(format!(
3656 "{name}(anim={},trans={},in_doc={})",
3657 state.animations.len(),
3658 state.transitions.len(),
3659 node.flags.is_in_document(),
3660 ))
3661 })
3662 .collect();
3663 described.sort();
3664 described.truncate(12);
3665 Some(described.join(" "))
3666 }
3667
3668 pub fn set_stylist_device(&mut self, device: Device) {
3670 self.viewport_generation = self.viewport_generation.wrapping_add(1);
3671 let previous_viewport = self.stylist.device().au_viewport_size();
3674 let next_viewport = device.au_viewport_size();
3675
3676 let root_styles = self
3682 .try_root_element()
3683 .and_then(|root| root.primary_styles());
3684 if let Some(root_style) = root_styles.as_deref() {
3685 device.set_root_style(root_style);
3686
3687 let font = root_style.get_font();
3688 let font_size = font.clone_font_size().computed_size();
3689 device.set_root_font_size(root_style.effective_zoom.unzoom(font_size.px()));
3690
3691 let line_height = device
3692 .calc_line_height(font, root_style.writing_mode, None)
3693 .0;
3694 device.set_root_line_height(root_style.effective_zoom.unzoom(line_height.px()));
3695 }
3696 drop(root_styles);
3697
3698 let origins = {
3699 let guard = &self.guard;
3700 let guards = StylesheetGuards {
3701 author: &guard.read(),
3702 ua_or_user: &guard.read(),
3703 };
3704 self.stylist.set_device(device, &guards)
3705 };
3706 self.stylist.force_stylesheet_origins_dirty(origins);
3707 #[cfg(feature = "shadow-dom")]
3708 for host_id in self.shadow_host_node_ids() {
3709 if let Some(root_id) = self.shadow_root_id(host_id) {
3710 self.invalidate_shadow_styles(root_id);
3711 }
3712 }
3713
3714 if previous_viewport != next_viewport {
3721 self.abspos_layout_dirty = true;
3722 let invalidated = self
3723 .try_root_element()
3724 .is_some_and(style::invalidation::viewport_units::invalidate);
3725 if invalidated {
3726 self.shell_provider.request_redraw();
3727 }
3728 }
3729 }
3730
3731 pub fn stylist_device(&mut self) -> &Device {
3732 self.stylist.device()
3733 }
3734
3735 pub fn get_cursor(&self) -> Option<CursorIcon> {
3743 let node_id = self
3748 .hover_hit_node_id
3749 .filter(|&id| self.nodes.contains_key(id))
3750 .or(self.get_hover_node_id());
3751 let Some(node_id) = node_id else {
3752 return Some(CursorIcon::Default);
3753 };
3754 let node = &self.nodes[node_id];
3755
3756 if let Some(subdoc) = node.subdoc().map(|doc| doc.inner()) {
3757 if subdoc.hover_hit_node_id.is_some() || subdoc.get_hover_node_id().is_some() {
3763 return subdoc.get_cursor();
3764 }
3765 return Some(CursorIcon::Default);
3766 }
3767
3768 let Some(style) = node.primary_styles() else {
3769 return Some(CursorIcon::Default);
3770 };
3771 let user_select = style.clone_user_select();
3772 let keyword = style.clone_cursor().keyword;
3773
3774 if keyword != CursorKind::Auto {
3776 return stylo_to_cursor_icon(keyword);
3777 }
3778
3779 if node
3781 .element_data()
3782 .is_some_and(|e| e.text_input_data().is_some())
3783 {
3784 return Some(CursorIcon::Text);
3785 }
3786
3787 let mut maybe_node = Some(node);
3789 while let Some(node) = maybe_node {
3790 if node.is_link() {
3791 return Some(CursorIcon::Pointer);
3792 }
3793
3794 maybe_node = node.layout_parent.get().map(|node_id| node.with(node_id));
3795 }
3796
3797 if self.hover_node_is_text {
3799 return Some(match user_select {
3800 UserSelect::Text | UserSelect::All | UserSelect::Auto => CursorIcon::Text,
3801 UserSelect::None => CursorIcon::Default,
3802 });
3803 }
3804
3805 Some(CursorIcon::Default)
3807 }
3808
3809 pub fn scroll_node_by<F: FnMut(DomEvent)>(
3810 &mut self,
3811 node_id: NodeId,
3812 x: f64,
3813 y: f64,
3814 dispatch_event: F,
3815 ) {
3816 self.scroll_node_by_has_changed(node_id, x, y, dispatch_event);
3817 }
3818
3819 pub fn scroll_node_by_has_changed<F: FnMut(DomEvent)>(
3829 &mut self,
3830 node_id: NodeId,
3831 x: f64,
3832 y: f64,
3833 dispatch_event: F,
3834 ) -> bool {
3835 let has_changed = self.scroll_node_by_inner(node_id, x, y, dispatch_event);
3836 if has_changed {
3837 self.resolve_positioned_scroll();
3838 }
3839 has_changed
3840 }
3841
3842 fn scroll_node_by_inner<F: FnMut(DomEvent)>(
3843 &mut self,
3844 node_id: NodeId,
3845 x: f64,
3846 y: f64,
3847 mut dispatch_event: F,
3848 ) -> bool {
3849 if self.try_root_element().is_some_and(|el| el.id == node_id) {
3854 let has_changed = self.scroll_viewport_by_has_changed(x, y);
3855 if has_changed {
3856 let layout = *self.root_element().final_layout();
3857 let scale = self.viewport.scale() as f64;
3858 let event = BlitzScrollEvent {
3859 scroll_top: self.viewport_scroll.y,
3860 scroll_left: self.viewport_scroll.x,
3861 scroll_width: layout.size.width.max(layout.content_size.width) as i32,
3862 scroll_height: layout.size.height.max(layout.content_size.height) as i32,
3863 client_width: (self.viewport.window_size.0 as f64 / scale) as i32,
3864 client_height: (self.viewport.window_size.1 as f64 / scale) as i32,
3865 };
3866 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
3867 }
3868 return has_changed;
3869 }
3870
3871 let Some(node) = self.nodes.get_mut(node_id) else {
3872 return false;
3873 };
3874
3875 if node
3879 .element_data()
3880 .is_some_and(|el| el.text_input_data().is_some())
3881 {
3882 let parent = node.parent;
3883 let content_box_width = node.final_layout().content_box_width();
3884 let content_box_height = node.final_layout().content_box_height();
3885 let input = node
3886 .element_data_mut()
3887 .and_then(|el| el.text_input_data_mut())
3888 .unwrap();
3889
3890 let (bubble_x, bubble_y) = if input.is_multiline {
3891 (
3892 x,
3893 input.scroll_by(y as f32, content_box_width, content_box_height) as f64,
3894 )
3895 } else {
3896 (
3897 input.scroll_by(x as f32, content_box_width, content_box_height) as f64,
3898 y,
3899 )
3900 };
3901
3902 let has_changed = bubble_x != x || bubble_y != y;
3903
3904 if bubble_x != 0.0 || bubble_y != 0.0 {
3905 let bubbled = if let Some(parent) = parent {
3906 self.scroll_node_by_inner(parent, bubble_x, bubble_y, dispatch_event)
3907 } else {
3908 self.scroll_viewport_by_has_changed(bubble_x, bubble_y)
3909 };
3910 return bubbled | has_changed;
3911 }
3912
3913 return has_changed;
3914 }
3915
3916 let (can_x_scroll, can_y_scroll) = node
3917 .primary_styles()
3918 .map(|styles| {
3919 (
3920 matches!(styles.clone_overflow_x(), Overflow::Scroll | Overflow::Auto),
3921 matches!(styles.clone_overflow_y(), Overflow::Scroll | Overflow::Auto),
3922 )
3923 })
3924 .unwrap_or((false, false));
3925
3926 let initial = *node.scroll_offset();
3927 let new_x = node.scroll_offset().x - x;
3928 let new_y = node.scroll_offset().y - y;
3929
3930 let mut bubble_x = 0.0;
3931 let mut bubble_y = 0.0;
3932
3933 let scroll_width = node.final_layout().scroll_width() as f64;
3934 let scroll_height = node.final_layout().scroll_height() as f64;
3935
3936 if let Some(mut sub_doc) = node.subdoc_mut().map(|doc| doc.inner_mut()) {
3938 let has_changed = if let Some(hover_node_id) = sub_doc.get_hover_node_id() {
3939 sub_doc.scroll_node_by_has_changed(hover_node_id, x, y, dispatch_event)
3940 } else {
3941 sub_doc.scroll_viewport_by_has_changed(x, y)
3942 };
3943
3944 return has_changed;
3946 }
3947
3948 if !can_x_scroll {
3950 bubble_x = x
3951 } else if new_x < 0.0 {
3952 bubble_x = -new_x;
3953 node.scroll_offset_mut().x = 0.0;
3954 } else if new_x > scroll_width {
3955 bubble_x = scroll_width - new_x;
3956 node.scroll_offset_mut().x = scroll_width;
3957 } else {
3958 node.scroll_offset_mut().x = new_x;
3959 }
3960
3961 if !can_y_scroll {
3962 bubble_y = y
3963 } else if new_y < 0.0 {
3964 bubble_y = -new_y;
3965 node.scroll_offset_mut().y = 0.0;
3966 } else if new_y > scroll_height {
3967 bubble_y = scroll_height - new_y;
3968 node.scroll_offset_mut().y = scroll_height;
3969 } else {
3970 node.scroll_offset_mut().y = new_y;
3971 }
3972
3973 let has_changed = *node.scroll_offset() != initial;
3974
3975 if has_changed {
3976 let layout = *node.final_layout();
3977 let event = BlitzScrollEvent {
3978 scroll_top: node.scroll_offset().y,
3979 scroll_left: node.scroll_offset().x,
3980 scroll_width: layout.scroll_width() as i32,
3981 scroll_height: layout.scroll_height() as i32,
3982 client_width: layout.size.width as i32,
3983 client_height: layout.size.height as i32,
3984 };
3985
3986 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
3987 }
3988
3989 let parent = node.parent;
3990 if has_changed {
3991 self.show_scrollbars(node_id);
3992 }
3993
3994 if bubble_x != 0.0 || bubble_y != 0.0 {
3995 if let Some(parent) = parent {
3996 return self.scroll_node_by_inner(parent, bubble_x, bubble_y, dispatch_event)
3997 | has_changed;
3998 } else {
3999 return self.scroll_viewport_by_has_changed(bubble_x, bubble_y) | has_changed;
4000 }
4001 }
4002
4003 has_changed
4004 }
4005
4006 pub fn scroll_viewport_by(&mut self, x: f64, y: f64) {
4007 self.scroll_viewport_by_has_changed(x, y);
4008 }
4009
4010 pub fn scroll_viewport_by_has_changed(&mut self, x: f64, y: f64) -> bool {
4012 let (content_width, content_height) = match self.try_root_element() {
4017 Some(root) => {
4018 let root_layout = root.final_layout();
4019 (
4020 root_layout.size.width.max(root_layout.content_size.width) as f64,
4021 root_layout.size.height.max(root_layout.content_size.height) as f64,
4022 )
4023 }
4024 None => (0.0, 0.0),
4025 };
4026 let new_scroll = (self.viewport_scroll.x - x, self.viewport_scroll.y - y);
4027 let window_width = self.viewport.window_size.0 as f64 / self.viewport.scale() as f64;
4028 let window_height = self.viewport.window_size.1 as f64 / self.viewport.scale() as f64;
4029
4030 let initial = self.viewport_scroll;
4031 self.viewport_scroll.x =
4032 f64::max(0.0, f64::min(new_scroll.0, content_width - window_width));
4033 self.viewport_scroll.y =
4034 f64::max(0.0, f64::min(new_scroll.1, content_height - window_height));
4035
4036 let has_changed = self.viewport_scroll != initial;
4037 if has_changed {
4038 self.resolve_positioned_scroll();
4043 }
4044 has_changed
4045 }
4046
4047 pub fn scroll_by(
4048 &mut self,
4049 anchor_node_id: Option<NodeId>,
4050 scroll_x: f64,
4051 scroll_y: f64,
4052 dispatch_event: &mut dyn FnMut(DomEvent),
4053 ) -> bool {
4054 if let Some(anchor_node_id) = anchor_node_id {
4055 self.scroll_node_by_has_changed(anchor_node_id, scroll_x, scroll_y, dispatch_event)
4056 } else {
4057 self.scroll_viewport_by_has_changed(scroll_x, scroll_y)
4058 }
4059 }
4060
4061 pub fn viewport_scroll(&self) -> crate::Point<f64> {
4062 self.viewport_scroll
4063 }
4064
4065 pub fn set_viewport_scroll(&mut self, scroll: crate::Point<f64>) {
4066 self.viewport_scroll = scroll;
4067 }
4068
4069 pub fn get_fragment_target(&self, fragment: &str) -> Option<NodeId> {
4074 if let Some(node_id) = self.get_element_by_id(fragment) {
4075 return Some(node_id);
4076 }
4077
4078 self.nodes.iter().find_map(|(id, node)| {
4080 let el = node.element_data()?;
4081 (el.name.local == local_name!("a") && el.attr(local_name!("name")) == Some(fragment))
4082 .then_some(id)
4083 })
4084 }
4085
4086 pub fn nearest_scroll_container(&self, node_id: NodeId) -> Option<NodeId> {
4097 let mut current = Some(node_id);
4098 for _ in 0..64 {
4099 let id = current?;
4100 let node = self.nodes.get(id)?;
4101 if node.style().overflow.x.is_scroll_container()
4102 || node.style().overflow.y.is_scroll_container()
4103 {
4104 return Some(id);
4105 }
4106 current = node.parent;
4107 }
4108 None
4109 }
4110
4111 pub fn scroll_nearest_container_by(&mut self, node_id: NodeId, x: f64, y: f64) -> bool {
4112 self.scroll_nearest_container_by_with_events(node_id, x, y, |_| {})
4113 }
4114
4115 pub fn scroll_nearest_container_by_with_events<F: FnMut(DomEvent)>(
4116 &mut self,
4117 node_id: NodeId,
4118 x: f64,
4119 y: f64,
4120 mut dispatch_event: F,
4121 ) -> bool {
4122 let mut current = Some(node_id);
4123 for _ in 0..64 {
4124 let Some(id) = current else { break };
4125 let Some(node) = self.nodes.get(id) else {
4126 break;
4127 };
4128 let scrolls = node.style().overflow.x.is_scroll_container()
4129 || node.style().overflow.y.is_scroll_container();
4130 if scrolls {
4131 self.scroll_node_by(id, x, y, &mut dispatch_event);
4132 return true;
4133 }
4134 current = node.parent;
4135 }
4136 self.scroll_viewport_by(x, y);
4137 false
4138 }
4139
4140 pub fn scroll_to_node(&mut self, node_id: NodeId) {
4141 self.scroll_to_node_with_events(node_id, |_| {});
4142 }
4143
4144 pub fn scroll_to_node_with_events<F: FnMut(DomEvent)>(
4145 &mut self,
4146 node_id: NodeId,
4147 dispatch_event: F,
4148 ) {
4149 self.scroll_to_node_aligned_with_events(node_id, false, dispatch_event);
4150 }
4151
4152 pub fn scroll_to_node_centered_with_events<F: FnMut(DomEvent)>(
4158 &mut self,
4159 node_id: NodeId,
4160 dispatch_event: F,
4161 ) {
4162 self.scroll_to_node_aligned_with_events(node_id, true, dispatch_event);
4163 }
4164
4165 fn scroll_to_node_aligned_with_events<F: FnMut(DomEvent)>(
4166 &mut self,
4167 node_id: NodeId,
4168 centered: bool,
4169 mut dispatch_event: F,
4170 ) {
4171 let mut scroll_target = Some(node_id);
4178 let node_id = loop {
4179 let Some(candidate) = scroll_target else {
4180 return;
4181 };
4182 let Some(node) = self.nodes.get(candidate) else {
4183 return;
4184 };
4185 if matches!(
4186 node.data,
4187 NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
4188 ) {
4189 break candidate;
4190 }
4191 scroll_target = node.layout_parent.get().or(node.parent);
4192 };
4193
4194 let mut chain = Vec::new();
4206 let mut current = self.nodes.get(node_id).and_then(|node| node.parent);
4207 while let Some(id) = current {
4208 let Some(node) = self.nodes.get(id) else {
4209 break;
4210 };
4211 let scrolls = node.style().overflow.x.is_scroll_container()
4212 || node.style().overflow.y.is_scroll_container();
4213 if scrolls {
4214 chain.push(id);
4215 }
4216 current = node.parent;
4217 }
4218
4219 for container in chain {
4223 let Some(node) = self.nodes.get(node_id) else {
4224 return;
4225 };
4226 let target = node.absolute_position(0.0, 0.0);
4227 let Some(scroller) = self.nodes.get(container) else {
4228 continue;
4229 };
4230 let box_ = scroller.absolute_position(0.0, 0.0);
4231 let layout = scroller.final_layout();
4232 let target_layout = node.final_layout();
4233 let (dx, dy) = if centered {
4237 (
4238 f64::from(
4239 box_.x + layout.size.width / 2.0
4240 - (target.x + target_layout.size.width / 2.0),
4241 ),
4242 f64::from(
4243 box_.y + layout.size.height / 2.0
4244 - (target.y + target_layout.size.height / 2.0),
4245 ),
4246 )
4247 } else {
4248 (f64::from(box_.x - target.x), f64::from(box_.y - target.y))
4249 };
4250 self.scroll_node_by(container, dx, dy, &mut dispatch_event);
4251 }
4252
4253 let Some(node) = self.nodes.get(node_id) else {
4256 return;
4257 };
4258 let target = node.absolute_position(0.0, 0.0);
4259 let current = self.viewport_scroll;
4260 let (desired_x, desired_y) = if centered {
4261 let target_layout = node.final_layout();
4262 let scale = self.viewport.scale();
4263 let viewport_width = self.viewport.window_size.0 as f32 / scale;
4264 let viewport_height = self.viewport.window_size.1 as f32 / scale;
4265 (
4266 f64::from(target.x + target_layout.size.width / 2.0 - viewport_width / 2.0)
4267 .max(0.0),
4268 f64::from(target.y + target_layout.size.height / 2.0 - viewport_height / 2.0)
4269 .max(0.0),
4270 )
4271 } else {
4272 (f64::from(target.x), f64::from(target.y))
4273 };
4274
4275 let dx = current.x - desired_x;
4278 let dy = current.y - desired_y;
4279 if let Some(root) = self.try_root_element().map(|element| element.id) {
4280 self.scroll_node_by(root, dx, dy, dispatch_event);
4281 } else {
4282 self.scroll_viewport_by(dx, dy);
4283 }
4284 }
4285
4286 pub fn scroll_to_fragment(&mut self, fragment: &str) -> bool {
4292 let decoded = percent_encoding::percent_decode_str(fragment)
4294 .decode_utf8_lossy()
4295 .into_owned();
4296
4297 if !decoded.is_empty() {
4298 if let Some(node_id) = self.get_fragment_target(&decoded) {
4299 self.scroll_to_node(node_id);
4300 return true;
4301 }
4302 }
4303
4304 if decoded.is_empty() || decoded.eq_ignore_ascii_case("top") {
4307 let current = self.viewport_scroll;
4308 self.scroll_viewport_by(current.x, current.y);
4309 return true;
4310 }
4311
4312 false
4313 }
4314
4315 pub fn get_svg_bbox(&self, node_id: NodeId) -> Option<BoundingRect> {
4318 #[cfg(feature = "svg")]
4319 {
4320 self.svg_user_bbox(node_id)
4321 }
4322 #[cfg(not(feature = "svg"))]
4323 {
4324 let _ = node_id;
4325 None
4326 }
4327 }
4328
4329 pub fn get_client_bounding_rect(&self, node_id: NodeId) -> Option<BoundingRect> {
4331 #[cfg(feature = "svg")]
4332 if let Some(root) = self.svg_geometry_root(node_id) {
4333 return self.svg_client_rect(root, node_id);
4334 }
4335
4336 if let Some(rects) = self.inline_fragment_rects(node_id) {
4339 let rects: Vec<_> = rects
4340 .into_iter()
4341 .map(|rect| self.transformed_client_rect(node_id, rect))
4342 .collect();
4343 let x0 = rects.iter().map(|r| r.x).fold(f64::INFINITY, f64::min);
4344 let y0 = rects.iter().map(|r| r.y).fold(f64::INFINITY, f64::min);
4345 let x1 = rects
4346 .iter()
4347 .map(|r| r.x + r.width)
4348 .fold(f64::NEG_INFINITY, f64::max);
4349 let y1 = rects
4350 .iter()
4351 .map(|r| r.y + r.height)
4352 .fold(f64::NEG_INFINITY, f64::max);
4353 return match rects.is_empty() {
4354 true => None,
4355 false => Some(BoundingRect {
4356 x: x0,
4357 y: y0,
4358 width: x1 - x0,
4359 height: y1 - y0,
4360 }),
4361 };
4362 }
4363
4364 let node = self.get_node(node_id)?;
4365 if !matches!(
4366 node.data,
4367 NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
4368 ) {
4369 return None;
4370 }
4371 let pos = node.absolute_position(0.0, 0.0);
4372
4373 Some(self.transformed_client_rect(
4374 node_id,
4375 BoundingRect {
4376 x: pos.x as f64 - self.viewport_scroll.x,
4377 y: pos.y as f64 - self.viewport_scroll.y,
4378 width: node.unrounded_layout().size.width as f64,
4379 height: node.unrounded_layout().size.height as f64,
4380 },
4381 ))
4382 }
4383
4384 pub(crate) fn transformed_client_rect(
4387 &self,
4388 node_id: NodeId,
4389 rect: BoundingRect,
4390 ) -> BoundingRect {
4391 let mut matrix = kurbo::Affine::IDENTITY;
4392 let mut current = self.get_node(node_id);
4393 let scale = self.viewport.scale_f64();
4394 while let Some(node) = current {
4395 if matches!(
4396 node.data,
4397 NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
4398 ) && let Some(transform) = *node.transform()
4399 {
4400 let [a, b, c, d, e, f] = transform.as_coeffs();
4401 let css_transform = kurbo::Affine::new([a, b, c, d, e / scale, f / scale]);
4402 let origin = node.absolute_position(0.0, 0.0);
4403 let translate = kurbo::Affine::translate((
4404 origin.x as f64 - self.viewport_scroll.x,
4405 origin.y as f64 - self.viewport_scroll.y,
4406 ));
4407 matrix = translate * css_transform * translate.inverse() * matrix;
4408 }
4409 current = node
4410 .layout_parent
4411 .get()
4412 .or(node.parent)
4413 .and_then(|id| self.get_node(id));
4414 }
4415 let transformed = matrix.transform_rect_bbox(kurbo::Rect::new(
4416 rect.x,
4417 rect.y,
4418 rect.x + rect.width,
4419 rect.y + rect.height,
4420 ));
4421 BoundingRect {
4422 x: transformed.x0,
4423 y: transformed.y0,
4424 width: transformed.width(),
4425 height: transformed.height(),
4426 }
4427 }
4428
4429 pub fn node_client_rects(&self, node_id: NodeId) -> Vec<BoundingRect> {
4434 #[cfg(feature = "svg")]
4435 if self.svg_geometry_root(node_id).is_some() {
4436 return self.get_client_bounding_rect(node_id).into_iter().collect();
4437 }
4438 match self.inline_fragment_rects(node_id) {
4439 Some(rects) => rects
4440 .into_iter()
4441 .map(|rect| self.transformed_client_rect(node_id, rect))
4442 .collect(),
4443 None => self.get_client_bounding_rect(node_id).into_iter().collect(),
4444 }
4445 }
4446
4447 pub(crate) fn trace_escaped_inline_fragments(&self) {
4461 static TRACE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4462 if !*TRACE.get_or_init(|| std::env::var_os("BLITZ_TRACE_INLINE").is_some()) {
4463 return;
4464 }
4465 let mut reported = 0;
4466 for (id, node) in self.nodes.iter() {
4467 if !node.is_element() {
4468 continue;
4469 }
4470 let Some(rects) = self.inline_fragment_rects(id) else {
4471 continue;
4472 };
4473 let Some(root) = node.inline_root_ancestor() else {
4474 continue;
4475 };
4476 let root_layout = root.final_layout();
4477 let root_pos = root.absolute_position(0.0, 0.0);
4478 let root_right =
4479 root_pos.x as f64 + root_layout.size.width as f64 - self.viewport_scroll.x;
4480 for rect in &rects {
4481 if rect.x + rect.width > root_right + 1.0 {
4482 reported += 1;
4483 if reported <= 12 {
4484 eprintln!(
4485 "escaped-fragment node={id:?} rect=[{:.1},{:.1} {:.1}x{:.1}] \
4486root={:?} root_right={root_right:.1} root_w={:.1} lines={} layout_scale={:.2} vp_scale={:.2} layout_w={:.1}",
4487 rect.x,
4488 rect.y,
4489 rect.width,
4490 rect.height,
4491 root.id,
4492 root_layout.size.width,
4493 root.element_data()
4494 .and_then(|e| e.inline_layout_data.as_ref())
4495 .map(|i| i.layout.len())
4496 .unwrap_or(0),
4497 root.element_data()
4498 .and_then(|e| e.inline_layout_data.as_ref())
4499 .map(|i| i.layout.scale())
4500 .unwrap_or(0.0),
4501 self.viewport.scale(),
4502 root.element_data()
4503 .and_then(|e| e.inline_layout_data.as_ref())
4504 .map(|i| i.layout.width())
4505 .unwrap_or(0.0),
4506 );
4507 }
4508 break;
4509 }
4510 }
4511 }
4512 if reported > 0 {
4513 eprintln!("escaped-fragment total={reported}");
4514 }
4515
4516 let mut narrow = 0;
4521 for (id, node) in self.nodes.iter() {
4522 let Some(inline) = node
4523 .data
4524 .downcast_element()
4525 .and_then(|element| element.inline_layout_data.as_ref())
4526 else {
4527 continue;
4528 };
4529 let box_width = node.final_layout().size.width as f64 * self.viewport.scale() as f64;
4530 let broken_at = inline.layout.width() as f64;
4531 let full = inline.layout.calculate_content_widths().max as f64;
4534 if box_width > 40.0 && broken_at < box_width * 0.6 && full > box_width * 0.9 {
4535 narrow += 1;
4536 if narrow <= 12 {
4537 eprintln!(
4538 "narrow-break node={id:?} broken_at={broken_at:.1} box={box_width:.1} \
4539 max_content={full:.1} lines={} text={:?}",
4540 inline.layout.len(),
4541 inline.text.chars().take(40).collect::<String>(),
4542 );
4543 }
4544 }
4545 }
4546 if narrow > 0 {
4547 eprintln!("narrow-break total={narrow}");
4548 }
4549 }
4550
4551 pub fn inline_fragment_rects(&self, node_id: NodeId) -> Option<Vec<BoundingRect>> {
4552 use parley::PositionedLayoutItem;
4553
4554 let node = self.get_node(node_id)?;
4555
4556 let is_text = node.is_text_node();
4559 if !is_text {
4560 if !node.is_element() || node.flags.is_inline_root() {
4561 return None;
4562 }
4563 let display = node.primary_styles()?.clone_display();
4564 if !(display.outside() == DisplayOutside::Inline
4565 && display.inside() == DisplayInside::Flow)
4566 {
4567 return None;
4568 }
4569 }
4570
4571 let inline_root = node.inline_root_ancestor()?;
4572 let inline_layout = inline_root.element_data()?.inline_layout_data.as_ref()?;
4573 let layout = &inline_layout.layout;
4574 let scale = layout.scale() as f64;
4575
4576 let is_in_target = |mut id: NodeId| -> bool {
4579 loop {
4580 if id == node_id {
4581 return true;
4582 }
4583 if id == inline_root.id {
4584 return false;
4585 }
4586 match self.get_node(id).and_then(|n| n.parent) {
4587 Some(parent) => id = parent,
4588 None => return false,
4589 }
4590 }
4591 };
4592
4593 let root_layout = inline_root.final_layout();
4595 let root_pos = inline_root.absolute_position(0.0, 0.0);
4596 let origin_x = root_pos.x as f64
4597 + (root_layout.padding.left + root_layout.border.left) as f64
4598 - self.viewport_scroll.x;
4599 let origin_y = root_pos.y as f64
4600 + (root_layout.padding.top + root_layout.border.top) as f64
4601 - self.viewport_scroll.y;
4602
4603 let mut rects: Vec<BoundingRect> = Vec::new();
4604 for line in layout.lines() {
4605 let line_metrics = line.metrics();
4606 let mut line_rect: Option<(f64, f64, f64, f64)> = None;
4608 let mut add = |x0: f64, y0: f64, x1: f64, y1: f64| {
4609 line_rect = Some(match line_rect {
4610 Some((lx0, ly0, lx1, ly1)) => {
4611 (lx0.min(x0), ly0.min(y0), lx1.max(x1), ly1.max(y1))
4612 }
4613 None => (x0, y0, x1, y1),
4614 });
4615 };
4616
4617 for item in line.items() {
4618 match item {
4619 PositionedLayoutItem::GlyphRun(glyph_run) => {
4620 let brush = glyph_run.style().brush;
4621 let matches = if is_text {
4622 brush.text_node == Some(node_id)
4623 } else {
4624 is_in_target(brush.id)
4625 };
4626 if !matches {
4627 continue;
4628 }
4629 let x0 = glyph_run.offset() as f64;
4630 let x1 = x0 + glyph_run.advance() as f64;
4631 let y0 = line_metrics.block_min_coord as f64;
4637 let y1 = line_metrics.block_max_coord as f64;
4638 add(x0, y0, x1, y1);
4639 }
4640 PositionedLayoutItem::InlineBox(inline_box) => {
4641 if is_text || !is_in_target(NodeId::from_u64(inline_box.id)) {
4642 continue;
4643 }
4644 let x0 = inline_box.x as f64;
4645 let y0 = inline_box.y as f64;
4646 add(
4647 x0,
4648 y0,
4649 x0 + inline_box.width as f64,
4650 y0 + inline_box.height as f64,
4651 );
4652 }
4653 }
4654 }
4655
4656 if let Some((x0, y0, x1, y1)) = line_rect {
4657 rects.push(BoundingRect {
4658 x: origin_x + x0 / scale,
4659 y: origin_y + y0 / scale,
4660 width: (x1 - x0) / scale,
4661 height: (y1 - y0) / scale,
4662 });
4663 }
4664 }
4665
4666 Some(rects)
4667 }
4668
4669 pub fn find_title_node(&self) -> Option<&Node> {
4670 TreeTraverser::new(self)
4671 .find(|node_id| {
4672 let node = &self.nodes[*node_id];
4673 let Some(element) = node.element_data() else {
4674 return false;
4675 };
4676 if element.name.ns != ns!(html) || element.name.local != local_name!("title") {
4677 return false;
4678 }
4679 node.parent
4680 .and_then(|parent_id| self.nodes.get(parent_id))
4681 .and_then(Node::element_data)
4682 .is_some_and(|parent| {
4683 parent.name.ns == ns!(html) && parent.name.local == local_name!("head")
4684 })
4685 })
4686 .map(|node_id| &self.nodes[node_id])
4687 }
4688
4689 pub fn with_text_input(
4690 &mut self,
4691 node_id: NodeId,
4692 cb: impl FnOnce(PlainEditorDriver<TextBrush>),
4693 ) {
4694 let Some(node) = self.nodes.get_mut(node_id) else {
4695 return;
4696 };
4697
4698 if let Some(text_input) = node
4699 .element_data_mut()
4700 .and_then(|el| el.text_input_data_mut())
4701 {
4702 let mut font_ctx = self.font_ctx.lock().unwrap();
4703 let layout_ctx = &mut self.layout_ctx;
4704 let driver = text_input.editor.driver(&mut font_ctx, layout_ctx);
4705 cb(driver)
4706 }
4707 }
4708
4709 pub(crate) fn clamp_text_input_scroll(&mut self, node_id: NodeId) {
4712 let Some(node) = self.nodes.get_mut(node_id) else {
4713 return;
4714 };
4715
4716 let content_box_width = node.final_layout().content_box_width();
4717 let content_box_height = node.final_layout().content_box_height();
4718
4719 if let Some(text_input) = node
4720 .element_data_mut()
4721 .and_then(|el| el.text_input_data_mut())
4722 {
4723 text_input.clamp_scroll_offset(content_box_width, content_box_height);
4724 }
4725 }
4726
4727 pub(crate) fn compute_has_canvas(&self) -> bool {
4728 TreeTraverser::new(self).any(|node_id| {
4729 let node = &self.nodes[node_id];
4730 let Some(element) = node.element_data() else {
4731 return false;
4732 };
4733 if element.name.local == local_name!("canvas") && element.has_attr(local_name!("src")) {
4734 return true;
4735 }
4736
4737 false
4738 })
4739 }
4740
4741 pub fn find_text_position(&self, x: f32, y: f32) -> Option<(NodeId, usize)> {
4747 let hit = self.hit(x, y)?;
4748 let hit_node = self.get_node(hit.node_id)?;
4749 let inline_root = hit_node.inline_root_ancestor()?;
4750 let byte_offset = inline_root.text_offset_at_point(hit.x, hit.y)?;
4751 Some((inline_root.id, byte_offset))
4752 }
4753
4754 pub fn find_text_range(
4760 &self,
4761 x: f32,
4762 y: f32,
4763 granularity: TextGranularity,
4764 ) -> Option<(NodeId, usize, usize)> {
4765 let hit = self.hit(x, y)?;
4766 let hit_node = self.get_node(hit.node_id)?;
4767 let inline_root = hit_node.inline_root_ancestor()?;
4768 let range = inline_root.text_range_at_point(hit.x, hit.y, granularity)?;
4769 Some((inline_root.id, range.start, range.end))
4770 }
4771
4772 pub fn set_text_selection(
4774 &mut self,
4775 anchor_node: NodeId,
4776 anchor_offset: usize,
4777 focus_node: NodeId,
4778 focus_offset: usize,
4779 ) {
4780 self.dom_selection = None;
4781 self.text_selection =
4782 TextSelection::new(anchor_node, anchor_offset, focus_node, focus_offset);
4783
4784 if let (Some(parent), Some(idx)) = self.anonymous_block_location(anchor_node) {
4786 self.text_selection
4787 .anchor
4788 .set_anonymous(parent, idx, anchor_offset);
4789 }
4790 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
4791 self.text_selection
4792 .focus
4793 .set_anonymous(parent, idx, focus_offset);
4794 }
4795 }
4796
4797 fn anonymous_block_location(&self, node_id: NodeId) -> (Option<NodeId>, Option<usize>) {
4800 let Some(node) = self.get_node(node_id) else {
4801 return (None, None);
4802 };
4803
4804 if !node.is_anonymous() {
4805 return (None, None);
4806 }
4807
4808 let Some(parent_id) = node.parent else {
4809 return (None, None);
4810 };
4811
4812 let Some(parent) = self.get_node(parent_id) else {
4813 return (Some(parent_id), None);
4814 };
4815
4816 let layout_children = parent.layout_children.borrow();
4817 let Some(children) = layout_children.as_ref() else {
4818 return (Some(parent_id), None);
4819 };
4820
4821 let mut anon_index = 0;
4823 for &child_id in children.iter() {
4824 if child_id == node_id {
4825 return (Some(parent_id), Some(anon_index));
4826 }
4827 if self.get_node(child_id).is_some_and(|n| n.is_anonymous()) {
4828 anon_index += 1;
4829 }
4830 }
4831
4832 (Some(parent_id), None)
4833 }
4834
4835 pub fn clear_text_selection(&mut self) {
4837 self.dom_selection = None;
4838 self.text_selection.clear();
4839 }
4840
4841 pub fn update_selection_focus(&mut self, focus_node: NodeId, focus_offset: usize) {
4843 self.dom_selection = None;
4844 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
4846 self.text_selection
4847 .focus
4848 .set_anonymous(parent, idx, focus_offset);
4849 } else {
4850 self.text_selection.set_focus(focus_node, focus_offset);
4851 }
4852 }
4853
4854 pub fn extend_text_selection_to_point(&mut self, x: f32, y: f32) -> bool {
4857 if !self.text_selection.anchor.is_some() {
4858 return false;
4859 }
4860
4861 if let Some((node, offset)) = self.find_text_position(x, y) {
4862 self.update_selection_focus(node, offset);
4863 self.shell_provider.request_redraw();
4864 true
4865 } else {
4866 false
4867 }
4868 }
4869
4870 fn find_anonymous_block_by_index(
4872 &self,
4873 parent_id: NodeId,
4874 target_index: usize,
4875 ) -> Option<NodeId> {
4876 let parent = self.get_node(parent_id)?;
4877 let layout_children = parent.layout_children.borrow();
4878 let children = layout_children.as_ref()?;
4879
4880 children
4881 .iter()
4882 .filter(|&&child_id| self.get_node(child_id).is_some_and(|n| n.is_anonymous()))
4883 .nth(target_index)
4884 .copied()
4885 }
4886
4887 pub fn has_text_selection(&self) -> bool {
4889 self.dom_selection.as_ref().map_or_else(
4890 || self.text_selection.is_active(),
4891 |range| !range.bounds().collapsed(),
4892 )
4893 }
4894
4895 pub fn get_selected_text(&self) -> Option<String> {
4897 if let Some(range) = &self.dom_selection {
4898 let text = self.range_string(range.bounds());
4899 return (!text.is_empty()).then_some(text);
4900 }
4901 let ranges = self.get_text_selection_ranges();
4902 if ranges.is_empty() {
4903 return None;
4904 }
4905
4906 let mut result = String::new();
4907 for (node_id, start, end) in &ranges {
4908 let node = self.get_node(*node_id)?;
4909 let element_data = node.element_data()?;
4910 let inline_layout = element_data.inline_layout_data.as_ref()?;
4911
4912 if *end > inline_layout.text.len() {
4913 continue;
4914 }
4915
4916 if !result.is_empty() {
4917 result.push(' ');
4918 }
4919 result.push_str(&inline_layout.text[*start..*end]);
4920 }
4921
4922 if result.is_empty() {
4923 None
4924 } else {
4925 Some(result)
4926 }
4927 }
4928
4929 pub fn get_selected_html(&self) -> Option<String> {
4942 let ranges = self.get_text_selection_ranges();
4943 if ranges.is_empty() {
4944 return None;
4945 }
4946
4947 let mut result = String::new();
4948 for (node_id, start, end) in &ranges {
4949 let Some(node) = self.get_node(*node_id) else {
4950 continue;
4951 };
4952 let Some(element_data) = node.element_data() else {
4953 continue;
4954 };
4955 let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
4956 continue;
4957 };
4958 if *end > inline_layout.text.len() {
4959 continue;
4960 }
4961
4962 if !result.is_empty() {
4963 result.push(' ');
4964 }
4965
4966 let tag = element_data.name.local.as_ref();
4967 let fragment = escape_html(&inline_layout.text[*start..*end]);
4968 if matches!(
4973 tag,
4974 "b" | "strong" | "i" | "em" | "code" | "kbd" | "mark" | "s" | "u" | "sub" | "sup"
4975 ) {
4976 result.push_str(&format!("<{tag}>{fragment}</{tag}>"));
4977 } else {
4978 result.push_str(&fragment);
4979 }
4980 }
4981
4982 if result.is_empty() {
4983 None
4984 } else {
4985 Some(result)
4986 }
4987 }
4988
4989 pub fn get_text_selection_ranges(&self) -> Vec<(NodeId, usize, usize)> {
4992 if let Some(range) = &self.dom_selection {
4993 return self.range_layout_ranges(range.bounds());
4994 }
4995 let lookup = |parent_id, idx| self.find_anonymous_block_by_index(parent_id, idx);
4996
4997 let anchor_node = match self.text_selection.anchor.resolve_node_id(lookup) {
4998 Some(id) => id,
4999 None => return Vec::new(),
5000 };
5001 let focus_node = match self.text_selection.focus.resolve_node_id(lookup) {
5002 Some(id) => id,
5003 None => return Vec::new(),
5004 };
5005
5006 let node_is_in_doc = |node_id: NodeId| {
5009 self.nodes
5010 .get(node_id)
5011 .is_some_and(|node| node.flags.is_in_document())
5012 };
5013 if !node_is_in_doc(anchor_node) || !node_is_in_doc(focus_node) {
5014 return Vec::new();
5015 }
5016
5017 if anchor_node == focus_node {
5019 let start = self
5020 .text_selection
5021 .anchor
5022 .offset
5023 .min(self.text_selection.focus.offset);
5024 let end = self
5025 .text_selection
5026 .anchor
5027 .offset
5028 .max(self.text_selection.focus.offset);
5029
5030 if start == end {
5031 return Vec::new();
5032 }
5033 return vec![(anchor_node, start, end)];
5034 }
5035
5036 let inline_roots = self.collect_inline_roots_in_range(anchor_node, focus_node);
5038 if inline_roots.is_empty() {
5039 return Vec::new();
5040 }
5041
5042 let first_in_roots = inline_roots[0];
5045
5046 let (first_node, first_offset, last_node, last_offset) =
5047 if first_in_roots == anchor_node || (first_in_roots != focus_node) {
5048 (
5050 anchor_node,
5051 self.text_selection.anchor.offset,
5052 focus_node,
5053 self.text_selection.focus.offset,
5054 )
5055 } else {
5056 (
5058 focus_node,
5059 self.text_selection.focus.offset,
5060 anchor_node,
5061 self.text_selection.anchor.offset,
5062 )
5063 };
5064
5065 let mut ranges = Vec::with_capacity(inline_roots.len());
5066
5067 for &node_id in &inline_roots {
5068 let Some(node) = self.get_node(node_id) else {
5069 continue;
5070 };
5071 let Some(element_data) = node.element_data() else {
5072 continue;
5073 };
5074 let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
5075 continue;
5076 };
5077
5078 let text_len = inline_layout.text.len();
5079
5080 if node_id == first_node && node_id == last_node {
5081 let start = first_offset.min(last_offset);
5082 let end = first_offset.max(last_offset);
5083 if start < end && end <= text_len {
5084 ranges.push((node_id, start, end));
5085 }
5086 } else if node_id == first_node {
5087 if first_offset < text_len {
5088 ranges.push((node_id, first_offset, text_len));
5089 }
5090 } else if node_id == last_node {
5091 if last_offset > 0 && last_offset <= text_len {
5092 ranges.push((node_id, 0, last_offset));
5093 }
5094 } else if text_len > 0 {
5095 ranges.push((node_id, 0, text_len));
5096 }
5097 }
5098
5099 ranges
5100 }
5101}
5102
5103#[derive(Debug, Clone, Copy, PartialEq)]
5104pub struct BoundingRect {
5105 pub x: f64,
5106 pub y: f64,
5107 pub width: f64,
5108 pub height: f64,
5109}
5110
5111impl BaseDocument {
5114 pub(crate) fn record_details_open(
5117 &mut self,
5118 node_id: NodeId,
5119 name: &markup5ever::QualName,
5120 present: bool,
5121 ) {
5122 if name.local != local_name!("open") || !name.ns.is_empty() {
5123 return;
5124 }
5125 let Some(element) = self.nodes[node_id].element_data() else {
5126 return;
5127 };
5128 if element.name.local != local_name!("details") || element.name.ns != ns!(html) {
5129 return;
5130 }
5131 let was_open = element.attr(local_name!("open")).is_some();
5132 if was_open != present && !self.details_toggles.iter().any(|(id, _)| *id == node_id) {
5133 self.details_toggles.push((node_id, was_open));
5134 }
5135 }
5136
5137 pub(crate) fn template_contents_owner(&mut self) -> NodeId {
5139 if let Some(owner) = self.template_contents_owner {
5140 return owner;
5141 }
5142 let mut data = crate::node::DocumentData::default();
5143 data.content_type = "text/html";
5144 let owner = self.create_node(NodeData::Document(Box::new(data)));
5145 self.template_contents_owner = Some(owner);
5146 owner
5147 }
5148
5149 pub fn viewport_generation(&self) -> u64 {
5151 self.viewport_generation
5152 }
5153
5154 pub fn take_details_toggles(&mut self) -> Vec<(NodeId, bool)> {
5156 std::mem::take(&mut self.details_toggles)
5157 }
5158
5159 pub(crate) fn record_inline_handler_attribute(
5162 &mut self,
5163 node_id: NodeId,
5164 name: &crate::QualName,
5165 value: Option<&str>,
5166 ) {
5167 if name.ns != markup5ever::ns!() || !name.local.as_ref().starts_with("on") {
5168 return;
5169 }
5170 let Some(element) = self
5171 .nodes
5172 .get_mut(node_id)
5173 .and_then(|node| node.element_data_mut())
5174 else {
5175 return;
5176 };
5177 let value = value.map(crate::node::AttrAtom::from);
5178 if let Some(value) = &value {
5179 let attributes = element
5180 .inline_event_attributes
5181 .get_or_insert_with(|| Box::new(Vec::new()));
5182 if let Some(attribute) = attributes
5183 .iter_mut()
5184 .find(|attribute| attribute.name.local == name.local)
5185 {
5186 attribute.name = name.clone();
5187 attribute.value = value.clone();
5188 } else {
5189 attributes.push(crate::node::Attribute {
5190 name: name.clone(),
5191 value: value.clone(),
5192 });
5193 }
5194 } else {
5195 let Some(attributes) = element.inline_event_attributes.as_mut() else {
5196 return;
5197 };
5198 let Some(index) = attributes
5199 .iter()
5200 .position(|attribute| attribute.name.local == name.local)
5201 else {
5202 return;
5203 };
5204 attributes.remove(index);
5205 if attributes.is_empty() {
5206 element.inline_event_attributes = None;
5207 }
5208 }
5209 self.inline_handler_changes
5210 .push((node_id, name.clone(), value));
5211 }
5212
5213 pub fn take_inline_handler_changes(
5215 &mut self,
5216 ) -> Vec<(NodeId, crate::QualName, Option<crate::node::AttrAtom>)> {
5217 std::mem::take(&mut self.inline_handler_changes)
5218 }
5219
5220 pub fn set_recording_mutations(&mut self, recording: bool) {
5223 match (recording, self.mutation_log.is_some()) {
5224 (true, false) => self.mutation_log = Some(Vec::new()),
5225 (false, true) => self.mutation_log = None,
5226 _ => {}
5227 }
5228 }
5229
5230 pub fn is_recording_mutations(&self) -> bool {
5232 self.mutation_log.is_some()
5233 }
5234
5235 pub fn take_mutations(&mut self) -> Vec<crate::DomMutation> {
5237 self.mutation_log
5238 .as_mut()
5239 .map(std::mem::take)
5240 .unwrap_or_default()
5241 }
5242
5243 pub(crate) fn record_mutation(&mut self, mutation: crate::DomMutation) {
5244 if let Some(log) = self.mutation_log.as_mut() {
5245 if log.len() < crate::mutation_record::MUTATION_LOG_CAP {
5246 log.push(mutation);
5247 }
5248 }
5249 }
5250
5251 pub(crate) fn sibling_context(
5254 &self,
5255 node_id: NodeId,
5256 ) -> Option<(NodeId, Option<NodeId>, Option<NodeId>)> {
5257 let parent_id = self.nodes.get(node_id)?.parent?;
5258 let siblings = &self.nodes.get(parent_id)?.children;
5259 let index = siblings.iter().position(|id| *id == node_id)?;
5260 let previous = index.checked_sub(1).map(|i| siblings[i]);
5261 let next = siblings.get(index + 1).copied();
5262 Some((parent_id, previous, next))
5263 }
5264}
5265
5266impl AsRef<BaseDocument> for BaseDocument {
5267 fn as_ref(&self) -> &BaseDocument {
5268 self
5269 }
5270}
5271
5272impl AsMut<BaseDocument> for BaseDocument {
5273 fn as_mut(&mut self) -> &mut BaseDocument {
5274 self
5275 }
5276}
5277
5278#[cfg(test)]
5279mod hover_state_tests {
5280 use super::*;
5281 use crate::{Attribute, qual_name};
5282 use blitz_traits::shell::ColorScheme;
5283
5284 fn make_doc() -> (BaseDocument, NodeId) {
5291 let mut doc = BaseDocument::new(DocumentConfig {
5292 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
5293 ..Default::default()
5294 });
5295 let root_id = doc.root_node().id;
5296 let style = |value: &str| Attribute {
5297 name: qual_name!("style"),
5298 value: value.into(),
5299 };
5300
5301 let mut mutator = doc.mutate();
5302 let html = mutator.create_element(qual_name!("html"), vec![]);
5303 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
5304 let container = mutator.create_element(qual_name!("div"), vec![style("width:300px")]);
5305 let text = mutator.create_text_node("some text");
5306 let block = mutator.create_element(qual_name!("div"), vec![style("height:50px")]);
5307 mutator.append_children(container, &[text, block]);
5308 mutator.append_children(body, &[container]);
5309 mutator.append_children(html, &[body]);
5310 mutator.append_children(root_id, &[html]);
5311 drop(mutator);
5312
5313 doc.resolve(0.0);
5314 (doc, container)
5315 }
5316
5317 fn text_has_size(doc: &BaseDocument, container: NodeId) -> bool {
5321 doc.nodes[container].final_layout().size.height > 50.0
5322 }
5323
5324 #[test]
5330 fn hovering_text_in_anonymous_block_reports_text_cursor() {
5331 let (mut doc, container) = make_doc();
5332 if !text_has_size(&doc, container) {
5333 eprintln!("skipping: no usable font (text measures 0x0)");
5334 return;
5335 }
5336
5337 doc.set_hover_to(5.0, 8.0);
5338 assert!(doc.hover_node_is_text, "expected a text hit");
5339 let hit_id = doc.hover_hit_node_id.expect("expected a hit node");
5340 assert!(
5341 doc.nodes[hit_id].is_anonymous(),
5342 "expected the hit node to be the anonymous inline root"
5343 );
5344 assert_eq!(
5345 doc.get_hover_node_id(),
5346 Some(container),
5347 "expected the stored hover target to be the containing element"
5348 );
5349 assert_eq!(doc.get_cursor(), Some(CursorIcon::Text));
5350 }
5351
5352 #[test]
5353 fn semantic_hover_keeps_the_resolved_node_instead_of_hit_testing_again() {
5354 let (mut doc, container) = make_doc();
5355
5356 doc.set_hover_to_node(container, 350.0, 250.0);
5360
5361 assert_eq!(doc.get_hover_node_id(), Some(container));
5362 assert_eq!(doc.hover_hit_node_id, Some(container));
5363
5364 doc.resolve(0.0);
5365 assert_eq!(
5366 doc.get_hover_node_id(),
5367 Some(container),
5368 "a resolve must not turn semantic identity back into a coordinate hit"
5369 );
5370 }
5371
5372 #[test]
5375 fn hovering_anonymous_block_whitespace_reports_default_cursor() {
5376 let (mut doc, container) = make_doc();
5377 if !text_has_size(&doc, container) {
5378 eprintln!("skipping: no usable font (text measures 0x0)");
5379 return;
5380 }
5381
5382 doc.set_hover_to(250.0, 8.0);
5383 assert!(!doc.hover_node_is_text);
5384 assert_eq!(doc.get_hover_node_id(), Some(container));
5385 assert_eq!(doc.get_cursor(), Some(CursorIcon::Default));
5386 }
5387}
5388
5389#[cfg(test)]
5390mod control_scroll_tests {
5391 use super::*;
5392 use crate::{Attribute, qual_name};
5393 use blitz_traits::shell::ColorScheme;
5394
5395 #[test]
5396 fn controlled_scroll_dispatches_the_dom_scroll_event() {
5397 let mut doc = BaseDocument::new(DocumentConfig {
5398 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
5399 ..Default::default()
5400 });
5401 let root_id = doc.root_node().id;
5402 let style = |value: &str| Attribute {
5403 name: qual_name!("style"),
5404 value: value.into(),
5405 };
5406
5407 let mut mutator = doc.mutate();
5408 let html = mutator.create_element(qual_name!("html"), vec![]);
5409 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
5410 let scroller = mutator.create_element(
5411 qual_name!("div"),
5412 vec![style("width:200px;height:100px;overflow-y:scroll")],
5413 );
5414 let spacer = mutator.create_element(qual_name!("div"), vec![style("height:400px")]);
5415 let target = mutator.create_element(qual_name!("button"), vec![style("height:40px")]);
5416 let target_text = mutator.create_text_node("Reveal me");
5417 mutator.append_children(target, &[target_text]);
5418 mutator.append_children(scroller, &[spacer, target]);
5419 mutator.append_children(body, &[scroller]);
5420 mutator.append_children(html, &[body]);
5421 mutator.append_children(root_id, &[html]);
5422 drop(mutator);
5423 doc.resolve(0.0);
5424
5425 doc.nodes[html].final_layout_mut().size.height = 300.0;
5429 doc.nodes[html].final_layout_mut().content_size.height = 600.0;
5430 doc.nodes[target].final_layout_mut().location.y = 400.0;
5431
5432 let mut events = Vec::new();
5433 doc.scroll_to_node_centered_with_events(target_text, |event| events.push(event));
5437
5438 assert!(
5439 (doc.viewport_scroll.y - 270.0).abs() < 0.1,
5440 "the 40px target should be centred in the 300px viewport"
5441 );
5442 assert!(
5443 events
5444 .iter()
5445 .any(|event| { event.target == html && event.name() == "scroll" })
5446 );
5447 }
5448}
5449
5450#[cfg(test)]
5451mod font_face_override_tests {
5452 use super::*;
5453 use crate::net::{FontFaceOverrides, Resource, ResourceLoadResponse};
5454
5455 #[test]
5471 fn font_face_overrides_alias_family_name() {
5472 const ALIAS: &str = "AliasedFamily";
5473
5474 let mut document = BaseDocument::new(DocumentConfig::default());
5475
5476 {
5478 let mut ctx = document.font_ctx.lock().unwrap();
5479 assert!(
5480 ctx.collection.family_id(ALIAS).is_none(),
5481 "alias must not exist before registration",
5482 );
5483 }
5484
5485 let response = ResourceLoadResponse {
5490 request_id: 0,
5491 node_id: None,
5492 resolved_url: Some(String::from("test://aliased-family")),
5493 result: Ok(Resource::Font(
5494 blitz_traits::net::Bytes::from_static(crate::BULLET_FONT),
5495 FontFaceOverrides {
5496 family_name: Some(String::from(ALIAS)),
5497 weight: Some(800.0),
5498 style: Some(parley::fontique::FontStyle::Italic),
5499 },
5500 )),
5501 };
5502 document.load_resource(response);
5503
5504 let mut ctx = document.font_ctx.lock().unwrap();
5507 let family_id = ctx
5508 .collection
5509 .family_id(ALIAS)
5510 .expect("CSS-declared family name should be registered as a family alias");
5511 let resolved_name = ctx
5512 .collection
5513 .family_name(family_id)
5514 .expect("family id should resolve back to a name");
5515 assert_eq!(
5516 resolved_name, ALIAS,
5517 "registered family should report the CSS-declared name, \
5518 not the font file's internal `name` table entry",
5519 );
5520 }
5521}
5522
5523#[cfg(test)]
5524mod clipboard_html_tests {
5525 use super::escape_html;
5526
5527 #[test]
5530 fn markup_characters_in_a_selection_are_escaped() {
5531 assert_eq!(
5532 escape_html("a < b && c > d"),
5533 "a < b && c > d"
5534 );
5535 }
5536
5537 #[test]
5540 fn an_ampersand_is_not_double_escaped() {
5541 assert_eq!(escape_html("<"), "&lt;");
5542 }
5543
5544 #[test]
5547 fn prose_is_untouched() {
5548 assert_eq!(escape_html("it's \"fine\""), "it's "fine"");
5549 }
5550}