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