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 pub(crate) style_revision: u64,
303 layout_revision: u64,
306 template_contents_owner: Option<NodeId>,
309 dropped_node_ids: FxHashSet<NodeId>,
310 dropped_node_parents: FxHashSet<NodeId>,
311 dropped_reference_owners: FxHashSet<NodeId>,
317
318 pub(crate) root_node_id: NodeId,
320
321 pub(crate) hoisted_fixed_parents: HashMap<NodeId, NodeId>,
331
332 pub(crate) hoisted_fixed_indices: HashMap<NodeId, usize>,
334
335 pub(crate) abspos_placements: Vec<crate::layout::abspos::AbsposPlacement>,
337
338 pub(crate) abspos_candidates: Vec<crate::layout::abspos::AbsposCandidate>,
340 pub(crate) abspos_candidate_ids: HashSet<NodeId>,
341 pub(crate) abspos_inputs: HashMap<NodeId, crate::layout::abspos::AbsposInputs>,
342 pub(crate) abspos_written: HashSet<NodeId>,
344 pub(crate) abspos_candidates_dirty: bool,
345 pub(crate) abspos_layout_dirty: bool,
346 pub(crate) abspos_normal_layout: bool,
348
349 pub(crate) fixed_nodes: Vec<NodeId>,
355
356 pub(crate) fixed_scroll_offset: crate::Point<f64>,
363
364 pub(crate) sticky_nodes: Vec<NodeId>,
372
373 pub(crate) sticky_offsets: HashMap<NodeId, taffy::Point<f32>>,
383
384 pub(crate) hoisted_clip_hosts: HashSet<NodeId>,
390
391 pub(crate) hoisted_position_hosts: HashSet<NodeId>,
393
394 pub(crate) stylist: Stylist,
397 pub(crate) animations: DocumentAnimationSet,
398 pub(crate) last_resolve_animation_time: f64,
405 pub(crate) guard: SharedRwLock,
407 pub(crate) snapshots: SnapshotMap,
409
410 pub(crate) font_ctx: Arc<Mutex<parley::FontContext>>,
413 #[cfg(feature = "parallel-construct")]
414 pub(crate) thread_font_contexts: ThreadLocal<RefCell<Box<FontContext>>>,
416 pub(crate) layout_ctx: parley::LayoutContext<TextBrush>,
418
419 pub(crate) hover_node_id: Option<NodeId>,
423 pub(crate) hover_hit_node_id: Option<NodeId>,
427 pub(crate) hover_node_is_text: bool,
429 pub(crate) last_client_pointer_position: Option<taffy::Point<f32>>,
431 pub(crate) semantic_hover_node_id: Option<NodeId>,
437 pub(crate) focus_node_id: Option<NodeId>,
439 pub(crate) active_node_id: Option<NodeId>,
441 pub(crate) mousedown_node_id: Option<NodeId>,
443 pub(crate) last_mousedown_time: Option<Instant>,
445 pub(crate) mousedown_position: taffy::Point<f32>,
447 pub(crate) click_count: u16,
449 pub(crate) drag_mode: DragMode,
451 pub(crate) hovered_scrollbar: Option<crate::node::ScrollbarRef>,
453 pub(crate) scrollbar_activity: HashMap<NodeId, Instant>,
456 pub(crate) scroll_animation: ScrollAnimationState,
458
459 pub(crate) text_selection: TextSelection,
461
462 pub(crate) has_active_animations: bool,
465 pub(crate) has_canvas: bool,
467 pub(crate) subdoc_animation_pacing: AnimationPacing,
469
470 pub(crate) nodes_to_id: HashMap<String, SmallVec<[NodeId; 1]>>,
474 pub(crate) nodes_to_stylesheet: BTreeMap<NodeId, DocumentStyleSheet>,
476 pub(crate) document_sheet_nodes: HashSet<NodeId>,
480 #[cfg(feature = "shadow-dom")]
484 pub(crate) shadow_sheet_roots: HashMap<NodeId, NodeId>,
485 pub(crate) ua_stylesheets: HashMap<String, DocumentStyleSheet>,
488 pub(crate) controls_to_form: HashMap<NodeId, NodeId>,
490 pub(crate) sub_document_nodes: HashSet<NodeId>,
492 pub(crate) iframe_loads: HashMap<NodeId, crate::iframe::IframeLoad>,
495 pub(crate) deferred_construction_nodes: Vec<ConstructionTask>,
497 pub(crate) paint_damage: crate::paint_damage::PaintDamageTracker,
503
504 #[cfg(feature = "custom-widget")]
506 pub(crate) custom_widget_nodes: HashSet<NodeId>,
507 #[cfg(feature = "custom-widget")]
509 pub(crate) pending_resource_deallocations: Vec<anyrender::ResourceId>,
510
511 #[cfg(feature = "shadow-dom")]
513 pub(crate) custom_element_registry: crate::node::CustomElementRegistry,
514 #[cfg(feature = "shadow-dom")]
516 pub(crate) shadow_host_nodes: HashSet<NodeId>,
517 #[cfg(feature = "shadow-dom")]
518 pub(crate) dirty_shadow_hosts: HashSet<NodeId>,
519 #[cfg(feature = "shadow-dom")]
520 pub(crate) pending_slot_changes: Vec<NodeId>,
521 #[cfg(feature = "shadow-dom")]
522 pub(crate) signaled_slots: HashSet<NodeId>,
523 #[cfg(feature = "shadow-dom")]
524 pub(crate) adopted_stylesheets: HashMap<NodeId, Vec<DocumentStyleSheet>>,
525 #[cfg(feature = "shadow-dom")]
527 pub(crate) custom_element_nodes: HashSet<NodeId>,
528 #[cfg(feature = "shadow-dom")]
532 pub(crate) declarative_template_shadows: HashMap<NodeId, NodeId>,
533
534 pub(crate) script_custom_elements: crate::node::script_custom_element::Registry,
536
537 pub(crate) image_cache: HashMap<String, ImageData>,
540
541 pub(crate) pending_images: HashMap<String, Vec<(NodeId, ImageType)>>,
545
546 pub(crate) pending_critical_resources: HashSet<usize>,
549
550 pub(crate) web_fonts: Vec<Arc<crate::net::web_fonts::WebFont>>,
551 pub(crate) font_epoch: usize,
552 document_referrer: String,
553 document_content_type: String,
554 document_window_focused: bool,
555
556 pub net_provider: Arc<dyn NetProvider>,
559 pub navigation_provider: Arc<dyn NavigationProvider>,
562 pub shell_provider: Arc<dyn ShellProvider>,
564 pub html_parser_provider: Arc<dyn HtmlParserProvider>,
566 pub(crate) abort_signal: Option<AbortSignal>,
570}
571
572pub(crate) fn make_device(
573 viewport: &Viewport,
574 media_type: MediaType,
575 font_ctx: Arc<Mutex<FontContext>>,
576) -> Device {
577 let width = viewport.window_size.0 as f32 / viewport.scale();
578 let height = viewport.window_size.1 as f32 / viewport.scale();
579 let viewport_size = euclid::Size2D::new(width, height);
580 let device_size = euclid::Size2D::new(width, height) * viewport.scale();
581 let device_pixel_ratio = euclid::Scale::new(viewport.scale());
582
583 Device::new(
584 media_type,
585 selectors::matching::QuirksMode::NoQuirks,
586 viewport_size,
587 device_size,
588 device_pixel_ratio,
589 Box::new(BlitzFontMetricsProvider { font_ctx }),
590 ComputedValues::initial_values_with_font_override(Font::initial_values()),
591 match viewport.color_scheme {
592 ColorScheme::Light => PrefersColorScheme::Light,
593 ColorScheme::Dark => PrefersColorScheme::Dark,
594 },
595 PointerCapabilities::default(),
596 PointerCapabilities::default(),
597 )
598}
599
600fn incremental_layout_default() -> bool {
615 !matches!(
616 std::env::var("BLITZ_INCREMENTAL").ok().as_deref(),
617 Some("0" | "false" | "off")
618 )
619}
620
621impl BaseDocument {
622 pub fn new(config: DocumentConfig) -> Self {
624 static ID_GENERATOR: AtomicUsize = AtomicUsize::new(1);
625
626 let id = ID_GENERATOR.fetch_add(1, Ordering::SeqCst);
627
628 let font_ctx = config
629 .font_ctx
630 .map(|mut font_ctx| {
631 font_ctx.source_cache.make_shared();
632 font_ctx
634 })
635 .unwrap_or_else(|| {
636 use parley::fontique::{Collection, CollectionOptions, SourceCache};
637 let mut font_ctx = FontContext {
638 source_cache: SourceCache::new_shared(),
639 collection: Collection::new(CollectionOptions {
640 shared: false,
641 system_fonts: cfg!(all(
642 feature = "system-fonts",
643 not(target_arch = "wasm32")
644 )),
645 }),
646 };
647 font_ctx
648 .collection
649 .register_fonts(Blob::new(Arc::new(crate::BULLET_FONT) as _), None);
650 font_ctx
651 });
652 let font_ctx = Arc::new(Mutex::new(font_ctx));
653 if let Some(fallback) = config.family_fallback {
654 crate::font_fallback::set(fallback);
655 }
656
657 style_config::set_pref!("layout.grid.enabled", true);
659 style_config::set_pref!("layout.unimplemented", true);
660 style_config::set_pref!("layout.columns.enabled", true);
661 style_config::set_pref!("layout.css.basic-shape-shape.enabled", true);
662 style_config::set_pref!("layout.threads", -1);
663
664 let viewport = config.viewport.unwrap_or_default();
665 let media_type = config.media_type.unwrap_or_else(MediaType::screen);
666 let device = make_device(&viewport, media_type.clone(), font_ctx.clone());
667 let stylist = Stylist::new(device, QuirksMode::NoQuirks);
668 let snapshots = SnapshotMap::new();
669 let nodes = Box::new(NodeTree::new());
670 let guard = SharedRwLock::new();
671 let nodes_to_id = HashMap::new();
672
673 let base_url = config
674 .base_url
675 .and_then(|url| DocumentUrl::from_str(&url).ok())
676 .unwrap_or_default();
677
678 let net_provider = config
679 .net_provider
680 .unwrap_or_else(|| Arc::new(DummyNetProvider));
681 let navigation_provider = config
682 .navigation_provider
683 .unwrap_or_else(|| Arc::new(DummyNavigationProvider));
684 let shell_provider = config
685 .shell_provider
686 .unwrap_or_else(|| Arc::new(DummyShellProvider));
687 let html_parser_provider = config
688 .html_parser_provider
689 .unwrap_or_else(|| Arc::new(DummyHtmlParserProvider));
690
691 let (tx, rx) = channel();
692
693 let mut doc = Self {
694 node_drop_depth: 0,
695 mutator_depth: 0,
696 mutator_redraw_pending: false,
697 details_toggles: Vec::new(),
698 viewport_generation: 0,
699 style_revision: 0,
700 layout_revision: 0,
701 template_contents_owner: None,
702 dropped_node_ids: FxHashSet::default(),
703 dropped_node_parents: FxHashSet::default(),
704 dropped_reference_owners: FxHashSet::default(),
705 hoisted_fixed_parents: HashMap::new(),
706 hoisted_fixed_indices: HashMap::new(),
707 abspos_placements: Vec::new(),
708 abspos_candidates: Vec::new(),
709 abspos_candidate_ids: HashSet::new(),
710 abspos_inputs: HashMap::new(),
711 abspos_written: HashSet::new(),
712 abspos_candidates_dirty: true,
713 abspos_layout_dirty: true,
714 abspos_normal_layout: false,
715 fixed_nodes: Vec::new(),
716 fixed_scroll_offset: crate::Point::ZERO,
717 sticky_nodes: Vec::new(),
718 sticky_offsets: HashMap::new(),
719 hoisted_clip_hosts: HashSet::new(),
720 hoisted_position_hosts: HashSet::new(),
721 id,
722 tx,
723 rx: Some(rx),
724
725 guard,
726 nodes,
727 root_node_id: NodeId::default(),
728 stylist,
729 animations: DocumentAnimationSet::default(),
730 last_resolve_animation_time: 0.0,
731 snapshots,
732 nodes_to_id,
733 viewport,
734 media_type,
735 style_threading: config.style_threading,
736 incremental_layout: config
737 .incremental
738 .unwrap_or_else(incremental_layout_default),
739 subdocument_depth: config.subdocument_depth,
740 devtool_settings: DevtoolSettings::default(),
741 viewport_scroll: crate::Point::ZERO,
742 mutation_log: None,
743 inline_handler_changes: Vec::new(),
744 live_ranges: Vec::new(),
745 dom_selection: None,
746 url: base_url,
747 ua_stylesheets: HashMap::new(),
748 nodes_to_stylesheet: BTreeMap::new(),
749 document_sheet_nodes: HashSet::new(),
750 #[cfg(feature = "shadow-dom")]
751 shadow_sheet_roots: HashMap::new(),
752 font_ctx,
753 #[cfg(feature = "parallel-construct")]
754 thread_font_contexts: ThreadLocal::new(),
755 layout_ctx: parley::LayoutContext::new(),
756
757 hover_node_id: None,
758 hover_hit_node_id: None,
759 hover_node_is_text: false,
760 last_client_pointer_position: None,
761 semantic_hover_node_id: None,
762 focus_node_id: None,
763 active_node_id: None,
764 mousedown_node_id: None,
765 has_active_animations: false,
766 subdoc_animation_pacing: AnimationPacing::Idle,
767 has_canvas: false,
768 sub_document_nodes: HashSet::new(),
769 iframe_loads: HashMap::new(),
770
771 #[cfg(feature = "custom-widget")]
772 custom_widget_nodes: HashSet::new(),
773 #[cfg(feature = "custom-widget")]
774 pending_resource_deallocations: Vec::new(),
775
776 #[cfg(feature = "shadow-dom")]
777 custom_element_registry: crate::node::CustomElementRegistry::new(),
778 #[cfg(feature = "shadow-dom")]
779 shadow_host_nodes: HashSet::new(),
780 #[cfg(feature = "shadow-dom")]
781 dirty_shadow_hosts: HashSet::new(),
782 #[cfg(feature = "shadow-dom")]
783 pending_slot_changes: Vec::new(),
784 #[cfg(feature = "shadow-dom")]
785 signaled_slots: HashSet::new(),
786 #[cfg(feature = "shadow-dom")]
787 adopted_stylesheets: HashMap::new(),
788 #[cfg(feature = "shadow-dom")]
789 custom_element_nodes: HashSet::new(),
790 #[cfg(feature = "shadow-dom")]
791 declarative_template_shadows: HashMap::new(),
792
793 deferred_construction_nodes: Vec::new(),
794 paint_damage: Default::default(),
795 script_custom_elements: Default::default(),
796 image_cache: HashMap::new(),
797 pending_images: HashMap::new(),
798 pending_critical_resources: HashSet::new(),
799 web_fonts: Vec::new(),
800 font_epoch: 0,
801 document_referrer: String::new(),
802 document_content_type: String::from("text/html"),
803 document_window_focused: false,
804 controls_to_form: HashMap::new(),
805 net_provider,
806 navigation_provider,
807 shell_provider,
808 html_parser_provider,
809 abort_signal: config.abort_signal,
810 last_mousedown_time: None,
811 mousedown_position: taffy::Point::ZERO,
812 click_count: 0,
813 drag_mode: DragMode::None,
814 hovered_scrollbar: None,
815 scrollbar_activity: HashMap::new(),
816 scroll_animation: ScrollAnimationState::None,
817 text_selection: TextSelection::default(),
818 };
819
820 doc.root_node_id = doc.create_node(NodeData::Document(Box::default()));
822 doc.root_node_mut().flags.insert(NodeFlags::IS_IN_DOCUMENT);
823
824 match config.ua_stylesheets {
825 Some(stylesheets) => {
826 for ss in &stylesheets {
827 doc.add_user_agent_stylesheet(ss);
828 }
829 }
830 None => doc.add_user_agent_stylesheet(DEFAULT_CSS),
831 }
832
833 let stylo_element_data = StyloElementData {
835 styles: ElementStyles {
836 primary: Some(
837 ComputedValues::initial_values_with_font_override(Font::initial_values())
838 .to_arc(),
839 ),
840 ..Default::default()
841 },
842 ..Default::default()
843 };
844 let stylo_data = doc.root_node_mut().stylo_element_data_mut();
845 *stylo_data.ensure_init_mut() = stylo_element_data;
846
847 doc
848 }
849
850 pub fn set_net_provider(&mut self, net_provider: Arc<dyn NetProvider>) {
852 self.net_provider = net_provider;
853 }
854
855 pub fn set_navigation_provider(&mut self, navigation_provider: Arc<dyn NavigationProvider>) {
857 self.navigation_provider = navigation_provider;
858 }
859
860 pub fn set_shell_provider(&mut self, shell_provider: Arc<dyn ShellProvider>) {
862 self.shell_provider = shell_provider;
863 }
864
865 pub fn set_html_parser_provider(&mut self, html_parser_provider: Arc<dyn HtmlParserProvider>) {
867 self.html_parser_provider = html_parser_provider;
868 }
869
870 pub fn set_base_url(&mut self, url: &str) {
872 self.url = DocumentUrl::from(Url::parse(url).unwrap());
873 }
874
875 pub fn guard(&self) -> &SharedRwLock {
876 &self.guard
877 }
878
879 pub fn tree(&self) -> &NodeTree {
880 &self.nodes
881 }
882
883 pub fn id(&self) -> usize {
884 self.id
885 }
886
887 pub(crate) fn build_request(&self, url: url::Url) -> Request {
890 crate::net::with_referrer(
891 crate::net::stamped_request(url, self.abort_signal.as_ref()),
892 self.url(),
893 )
894 }
895
896 pub fn favicon_url(&self) -> Option<String> {
897 self.tree().iter().find_map(|(_, node)| {
898 let data = &node.data;
899 if !data.is_element_with_tag_name(&local_name!("link")) {
900 return None;
901 }
902 let rel = data.attr(local_name!("rel"))?;
903 if !rel
904 .split_ascii_whitespace()
905 .any(|v| v.eq_ignore_ascii_case("icon"))
906 {
907 return None;
908 }
909 data.attr(local_name!("href")).map(|s| s.to_string())
910 })
911 }
912
913 pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
914 self.nodes.get(node_id)
915 }
916
917 pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
918 self.nodes.get_mut(node_id)
919 }
920
921 pub fn get_focussed_node_id(&self) -> Option<NodeId> {
922 self.focus_node_id
923 .or(self.try_root_element().map(|el| el.id))
924 }
925
926 pub fn mutate<'doc>(&'doc mut self) -> DocumentMutator<'doc> {
927 DocumentMutator::new(self)
928 }
929
930 pub fn handle_dom_event<F: FnMut(DomEvent)>(
931 &mut self,
932 event: &mut DomEvent,
933 dispatch_event: F,
934 ) {
935 handle_dom_event(self, event, dispatch_event)
936 }
937
938 pub fn as_any_mut(&mut self) -> &mut dyn Any {
939 self
940 }
941
942 pub fn label_bound_input_element(&self, label_node_id: NodeId) -> Option<&Node> {
949 let label_element = self.nodes[label_node_id].element_data()?;
950 if let Some(target_element_dom_id) = label_element.attr(local_name!("for")) {
951 TreeTraverser::new(self)
952 .filter_map(|id| {
953 let node = self.get_node(id)?;
954 let element_data = node.element_data()?;
955 if element_data.name.local != local_name!("input") {
956 return None;
957 }
958 let id = element_data.id.as_ref()?;
959 if *id == *target_element_dom_id {
960 Some(node)
961 } else {
962 None
963 }
964 })
965 .next()
966 } else {
967 TreeTraverser::new_with_root(self, label_node_id)
968 .filter_map(|child_id| {
969 let node = self.get_node(child_id)?;
970 let element_data = node.element_data()?;
971 if element_data.name.local == local_name!("input") {
972 Some(node)
973 } else {
974 None
975 }
976 })
977 .next()
978 }
979 }
980
981 pub fn run_pre_click_activation(&mut self, target: NodeId) -> Option<PreClickActivation> {
987 let node_id = crate::events::pointer::checkable_activation_target(self, target)?;
988 let el = self.get_node(node_id)?.data.downcast_element()?;
989 let is_radio = el.attr(local_name!("type")) == Some("radio");
990
991 if !is_radio {
992 let previous = el.checkbox_input_checked()?;
993 let el = self.get_node_mut(node_id)?.data.downcast_element_mut()?;
994 Self::toggle_checkbox(el);
995 return Some(PreClickActivation {
996 previous: vec![(node_id, previous)],
997 });
998 }
999
1000 let radio_set = el.attr(local_name!("name")).map(str::to_string);
1001 let Some(radio_set) = radio_set else {
1002 let previous = el.checkbox_input_checked()?;
1003 let el = self.get_node_mut(node_id)?.data.downcast_element_mut()?;
1004 *el.checkbox_input_checked_mut()? = true;
1005 return Some(PreClickActivation {
1006 previous: vec![(node_id, previous)],
1007 });
1008 };
1009
1010 let mut previous: Vec<(NodeId, bool)> = Vec::new();
1022 for (id, node) in self.nodes.iter_mut() {
1023 let Some(el) = node.data.downcast_element_mut() else {
1024 continue;
1025 };
1026 if el.attr(local_name!("name")) != Some(&*radio_set) {
1027 continue;
1028 }
1029 let Some(is_checked) = el.checkbox_input_checked_mut() else {
1030 continue;
1031 };
1032 previous.push((id, *is_checked));
1033 *is_checked = id == node_id;
1034 }
1035 Some(PreClickActivation { previous })
1036 }
1037
1038 pub fn undo_pre_click_activation(&mut self, activation: PreClickActivation) {
1041 for (node_id, was_checked) in activation.previous {
1042 let Some(node) = self.get_node_mut(node_id) else {
1043 continue;
1044 };
1045 let Some(el) = node.data.downcast_element_mut() else {
1046 continue;
1047 };
1048 if let Some(is_checked) = el.checkbox_input_checked_mut() {
1049 *is_checked = was_checked;
1050 }
1051 }
1052 }
1053
1054 pub fn toggle_checkbox(el: &mut ElementData) -> bool {
1055 let Some(is_checked) = el.checkbox_input_checked_mut() else {
1056 return false;
1057 };
1058 *is_checked = !*is_checked;
1059
1060 *is_checked
1061 }
1062
1063 pub fn toggle_radio(&mut self, radio_set_name: String, target_radio_id: NodeId) {
1064 for (i, node) in self.nodes.iter_mut() {
1065 if let Some(node_data) = node.data.downcast_element_mut() {
1066 if node_data.attr(local_name!("name")) == Some(&radio_set_name) {
1067 let was_clicked = i == target_radio_id;
1068 let Some(is_checked) = node_data.checkbox_input_checked_mut() else {
1069 continue;
1070 };
1071 *is_checked = was_clicked;
1072 }
1073 }
1074 }
1075 }
1076
1077 pub fn toggle_details_open(&mut self, details_id: NodeId) {
1081 use crate::qual_name;
1082
1083 let node = &self.nodes[details_id];
1084 if !node.data.is_element_with_tag_name(&local_name!("details")) {
1085 return;
1086 }
1087 let is_open = node.data.has_attr(local_name!("open"));
1088
1089 let mut mutator = self.mutate();
1093 if is_open {
1094 mutator.clear_attribute(details_id, qual_name!("open"));
1095 } else {
1096 mutator.set_attribute(details_id, qual_name!("open"), "");
1097 }
1098 drop(mutator);
1099
1100 self.shell_provider.request_redraw();
1101 }
1102
1103 pub fn set_style_property(&mut self, node_id: NodeId, name: &str, value: &str) {
1104 let node = &mut self.nodes[node_id];
1105 let did_change = node.element_data_mut().unwrap().set_style_property(
1106 name,
1107 value,
1108 &self.guard,
1109 self.url.url_extra_data(),
1110 );
1111 if did_change {
1112 node.mark_style_attr_updated();
1113 }
1114 }
1115
1116 pub fn remove_style_property(&mut self, node_id: NodeId, name: &str) {
1117 let node = &mut self.nodes[node_id];
1118 let did_change = node.element_data_mut().unwrap().remove_style_property(
1119 name,
1120 &self.guard,
1121 self.url.url_extra_data(),
1122 );
1123 if did_change {
1124 node.mark_style_attr_updated();
1125 }
1126 }
1127
1128 pub fn sub_document_node_ids(&self) -> Vec<NodeId> {
1129 self.sub_document_nodes.iter().copied().collect()
1130 }
1131
1132 pub fn set_navigation_metadata(&mut self, request: &Request, content_type: Option<&str>) {
1134 self.document_referrer = request
1135 .headers
1136 .get(blitz_traits::net::http::header::REFERER)
1137 .and_then(|value| value.to_str().ok())
1138 .and_then(|value| Url::parse(value).ok())
1139 .map(|mut url| {
1140 let _ = url.set_username("");
1141 let _ = url.set_password(None);
1142 url.set_fragment(None);
1143 url.to_string()
1144 })
1145 .unwrap_or_default();
1146 self.document_content_type = content_type
1147 .and_then(|value| value.split(';').next())
1148 .map(str::trim)
1149 .filter(|value| !value.is_empty())
1150 .unwrap_or("text/html")
1151 .to_ascii_lowercase();
1152 }
1153
1154 pub fn referrer(&self) -> &str {
1155 &self.document_referrer
1156 }
1157
1158 pub fn content_type(&self) -> &str {
1159 &self.document_content_type
1160 }
1161
1162 pub fn window_focused(&self) -> bool {
1163 self.document_window_focused
1164 }
1165
1166 pub fn set_window_focused(&mut self, focused: bool) {
1168 self.document_window_focused = focused;
1169 for id in self.sub_document_node_ids() {
1170 if let Some(child) = self
1171 .get_node_mut(id)
1172 .and_then(|node| node.element_data_mut())
1173 .and_then(|element| element.sub_doc_data_mut())
1174 {
1175 child.inner_mut().set_window_focused(focused);
1176 }
1177 }
1178 }
1179
1180 pub fn set_document_quirks_mode(&mut self, mode: u8) {
1182 if let NodeData::Document(data) = &mut self.root_node_mut().data {
1183 data.quirks_mode = mode;
1184 }
1185 }
1186
1187 pub fn compat_mode(&self) -> &'static str {
1188 if matches!(&self.root_node().data, NodeData::Document(data) if data.quirks_mode == 2) {
1189 "BackCompat"
1190 } else {
1191 "CSS1Compat"
1192 }
1193 }
1194
1195 pub fn begin_document_stream(&mut self) {
1196 self.reset_web_fonts();
1197 self.pending_critical_resources.clear();
1198 self.pending_images.clear();
1199 self.set_document_quirks_mode(0);
1200 }
1201
1202 pub fn set_sub_document(&mut self, node_id: NodeId, mut sub_document: Box<dyn Document>) {
1203 sub_document
1204 .inner_mut()
1205 .set_window_focused(self.document_window_focused);
1206 self.nodes[node_id]
1207 .element_data_mut()
1208 .unwrap()
1209 .set_sub_document(sub_document);
1210 self.sub_document_nodes.insert(node_id);
1211 }
1212
1213 pub fn remove_sub_document(&mut self, node_id: NodeId) {
1214 self.nodes[node_id]
1215 .element_data_mut()
1216 .unwrap()
1217 .remove_sub_document();
1218 self.sub_document_nodes.remove(&node_id);
1219 if let Some(load) = self.iframe_loads.remove(&node_id) {
1220 load.abort_controller.abort();
1221 }
1222 }
1223
1224 pub fn poll_subdocuments(&mut self, waker: Option<&Waker>) -> bool {
1230 self.poll_subdocuments_inner(waker, None)
1231 }
1232
1233 pub fn poll_subdocuments_for_settle(
1235 &mut self,
1236 waker: Option<&Waker>,
1237 run_timers: bool,
1238 ) -> bool {
1239 self.poll_subdocuments_inner(waker, Some(run_timers))
1240 }
1241
1242 fn poll_subdocuments_inner(
1243 &mut self,
1244 waker: Option<&Waker>,
1245 settle_timers: Option<bool>,
1246 ) -> bool {
1247 let mut has_changes = false;
1248 let node_ids: Vec<NodeId> = self.sub_document_nodes.iter().copied().collect();
1249 for node_id in node_ids {
1250 let Some(sub_doc) = self
1251 .nodes
1252 .get_mut(node_id)
1253 .and_then(|node| node.subdoc_mut())
1254 else {
1255 continue;
1256 };
1257 let task_context = waker.map(TaskContext::from_waker);
1258 has_changes |= match settle_timers {
1259 Some(run_timers) => sub_doc.poll_for_settle(task_context, run_timers),
1260 None => sub_doc.poll(task_context),
1261 };
1262 }
1263 has_changes
1264 }
1265
1266 #[cfg(feature = "custom-widget")]
1267 pub fn custom_widget_node_ids(&self) -> Vec<NodeId> {
1268 self.custom_widget_nodes.iter().copied().collect()
1269 }
1270
1271 #[cfg(feature = "custom-widget")]
1272 pub fn take_pending_resource_deallocations(&mut self) -> Vec<anyrender::ResourceId> {
1273 std::mem::take(&mut self.pending_resource_deallocations)
1274 }
1275
1276 #[cfg(feature = "custom-widget")]
1277 pub fn set_custom_widget(&mut self, node_id: NodeId, widget: Box<dyn crate::Widget>) {
1278 self.nodes[node_id]
1279 .element_data_mut()
1280 .unwrap()
1281 .set_custom_widget(widget);
1282 self.custom_widget_nodes.insert(node_id);
1283 }
1284
1285 #[cfg(feature = "custom-widget")]
1286 pub fn remove_custom_widget(&mut self, node_id: NodeId) {
1287 let resources_to_deallocate = self.nodes[node_id]
1288 .element_data_mut()
1289 .unwrap()
1290 .remove_custom_widget();
1291 self.pending_resource_deallocations
1292 .extend_from_slice(&resources_to_deallocate);
1293 self.custom_widget_nodes.remove(&node_id);
1294 }
1295
1296 #[cfg(feature = "shadow-dom")]
1300 pub fn custom_elements_mut(&mut self) -> &mut crate::node::CustomElementRegistry {
1301 &mut self.custom_element_registry
1302 }
1303
1304 #[cfg(feature = "shadow-dom")]
1307 pub fn define_custom_element(
1308 &mut self,
1309 name: markup5ever::LocalName,
1310 definition: crate::node::CustomElementDefinition,
1311 ) {
1312 self.custom_element_registry.define(name, definition);
1313 }
1314
1315 #[cfg(feature = "shadow-dom")]
1317 pub fn shadow_host_node_ids(&self) -> Vec<NodeId> {
1318 self.shadow_host_nodes.iter().copied().collect()
1319 }
1320
1321 #[cfg(feature = "shadow-dom")]
1323 pub fn shadow_root_id(&self, host_id: NodeId) -> Option<NodeId> {
1324 self.get_node(host_id)
1325 .and_then(|node| node.shadow_root_id())
1326 }
1327
1328 #[cfg(feature = "shadow-dom")]
1332 pub fn attach_shadow(&mut self, host_id: NodeId, mode: crate::node::ShadowRootMode) -> NodeId {
1333 if let Some(existing) = self.nodes[host_id].shadow_root_id() {
1334 return existing;
1335 }
1336
1337 let shadow_root_id = self.create_node(NodeData::ShadowRoot(
1338 crate::node::ShadowRootData::new(host_id, mode),
1339 ));
1340
1341 self.nodes[shadow_root_id].parent = Some(host_id);
1345 self.nodes[shadow_root_id].owner_document = self.nodes[host_id].owner_document;
1346 if self.nodes[host_id].flags.is_in_document() {
1347 self.nodes[shadow_root_id]
1348 .flags
1349 .insert(NodeFlags::IS_IN_DOCUMENT);
1350 }
1351
1352 self.nodes[host_id]
1353 .element_data_mut()
1354 .expect("Shadow host must be an element")
1355 .shadow_root = Some(shadow_root_id);
1356 self.shadow_host_nodes.insert(host_id);
1357 self.dirty_shadow_hosts.insert(host_id);
1358 self.nodes[host_id].set_restyle_hint(
1359 style::invalidation::element::restyle_hints::RestyleHint::restyle_subtree(),
1360 );
1361
1362 self.nodes[host_id].insert_damage(ALL_DAMAGE);
1364 self.nodes[host_id].mark_ancestors_dirty();
1365
1366 shadow_root_id
1367 }
1368
1369 #[cfg(feature = "shadow-dom")]
1374 pub fn element_can_be_shadow_host(element: &ElementData) -> bool {
1375 let tag = element.name.local.as_ref();
1376 element.name.ns == ns!(html)
1377 && (tag.contains('-')
1378 || matches!(
1379 tag,
1380 "article"
1381 | "aside"
1382 | "blockquote"
1383 | "body"
1384 | "div"
1385 | "footer"
1386 | "h1"
1387 | "h2"
1388 | "h3"
1389 | "h4"
1390 | "h5"
1391 | "h6"
1392 | "header"
1393 | "main"
1394 | "nav"
1395 | "p"
1396 | "section"
1397 | "span"
1398 ))
1399 }
1400
1401 #[cfg(feature = "shadow-dom")]
1407 pub fn try_attach_declarative_shadow(
1408 &mut self,
1409 host_id: NodeId,
1410 template_id: NodeId,
1411 mode: crate::node::ShadowRootMode,
1412 clonable: bool,
1413 serializable: bool,
1414 delegates_focus: bool,
1415 ) -> bool {
1416 let can_attach = self.nodes.get(host_id).is_some_and(|node| {
1417 node.element_data().is_some_and(|element| {
1418 Self::element_can_be_shadow_host(element) && element.shadow_root.is_none()
1419 })
1420 });
1421 if !can_attach {
1422 return false;
1423 }
1424 let root_id = self.attach_shadow(host_id, mode);
1425 if let Some(root) = self.nodes[root_id].shadow_root_data_mut() {
1426 root.declarative = true;
1427 root.clonable = clonable;
1428 root.serializable = serializable;
1429 root.delegates_focus = delegates_focus;
1430 }
1431 self.declarative_template_shadows
1432 .insert(template_id, root_id);
1433 true
1434 }
1435
1436 #[cfg(feature = "shadow-dom")]
1438 pub fn detach_shadow(&mut self, host_id: NodeId) {
1439 let shadow_root_id = self.nodes[host_id]
1440 .element_data_mut()
1441 .and_then(|el| el.shadow_root.take());
1442 if let Some(shadow_root_id) = shadow_root_id {
1443 self.drop_node_ignoring_parent(shadow_root_id);
1444 self.shadow_host_nodes.remove(&host_id);
1445 self.dirty_shadow_hosts.remove(&host_id);
1446 self.nodes[host_id].flattened_children = None;
1447 self.nodes[host_id].set_restyle_hint(
1448 style::invalidation::element::restyle_hints::RestyleHint::restyle_subtree(),
1449 );
1450 self.nodes[host_id].insert_damage(ALL_DAMAGE);
1451 self.nodes[host_id].mark_ancestors_dirty();
1452 }
1453 }
1454
1455 #[cfg(feature = "shadow-dom")]
1457 pub fn set_custom_element(
1458 &mut self,
1459 node_id: NodeId,
1460 controller: Box<dyn crate::node::CustomElement>,
1461 ) {
1462 use crate::node::{CustomElementData, SpecialElementData};
1463 self.nodes[node_id]
1464 .element_data_mut()
1465 .expect("Custom element host must be an element")
1466 .special_data = SpecialElementData::CustomElement(CustomElementData::new(controller));
1467 self.custom_element_nodes.insert(node_id);
1468 }
1469
1470 #[cfg(feature = "shadow-dom")]
1473 pub fn take_custom_element(
1474 &mut self,
1475 node_id: NodeId,
1476 ) -> Option<Box<dyn crate::node::CustomElement>> {
1477 use crate::node::SpecialElementData;
1478 self.custom_element_nodes.remove(&node_id);
1479 let element = self.nodes[node_id].element_data_mut()?;
1480 if matches!(element.special_data, SpecialElementData::CustomElement(_)) {
1481 if let SpecialElementData::CustomElement(mut data) = element.special_data.take() {
1482 return data.controller.take();
1483 }
1484 }
1485 None
1486 }
1487
1488 pub fn root_node(&self) -> &Node {
1489 &self.nodes[self.root_node_id]
1490 }
1491
1492 pub fn root_node_mut(&mut self) -> &mut Node {
1493 &mut self.nodes[self.root_node_id]
1494 }
1495
1496 pub fn set_paint_damage_tracking(&mut self, enabled: bool) {
1513 self.paint_damage.set_enabled(enabled);
1514 }
1515
1516 pub fn paint_damage_tracking(&self) -> bool {
1518 self.paint_damage.is_enabled()
1519 }
1520
1521 pub fn paint_damage(&self) -> &crate::paint_damage::PaintDamage {
1529 self.paint_damage.damage()
1530 }
1531
1532 pub fn try_root_element(&self) -> Option<&Node> {
1533 TDocument::as_node(&self.root_node()).first_element_child()
1534 }
1535
1536 pub fn root_element(&self) -> &Node {
1537 TDocument::as_node(&self.root_node())
1538 .first_element_child()
1539 .unwrap()
1540 .as_element()
1541 .unwrap()
1542 }
1543
1544 pub fn create_node(&mut self, node_data: NodeData) -> NodeId {
1545 let tree_ptr = self.nodes.as_mut() as *mut NodeTree;
1546 let guard = self.guard.clone();
1547 let is_document = matches!(node_data, NodeData::Document(_));
1548 let id = self
1549 .nodes
1550 .insert_with_key(|id| Node::new(tree_ptr, id, guard, node_data));
1551 if !is_document {
1552 self.nodes[id].owner_document = Some(self.root_node_id);
1553 }
1554 self.register_script_custom_element(id);
1555 if let Some(attributes) = self.nodes[id]
1556 .element_data()
1557 .and_then(|element| element.inline_event_attributes.as_deref())
1558 {
1559 self.inline_handler_changes.extend(
1560 attributes
1561 .iter()
1562 .map(|attr| (id, attr.name.clone(), Some(attr.value.clone()))),
1563 );
1564 }
1565 id
1566 }
1567
1568 pub fn add_to_document_id_map(&mut self, document_id: NodeId, id: &str, node_id: NodeId) {
1569 if document_id == self.root_node_id || id.is_empty() {
1570 return;
1571 }
1572 let Some(node) = self.get_node_mut(document_id) else {
1573 return;
1574 };
1575 let NodeData::Document(data) = &mut node.data else {
1576 return;
1577 };
1578 let candidates = data.ids.entry(id.to_owned()).or_default();
1579 if !candidates.contains(&node_id) {
1580 candidates.push(node_id);
1581 }
1582 }
1583
1584 pub fn remove_from_document_id_map(&mut self, document_id: NodeId, id: &str, node_id: NodeId) {
1585 if document_id == self.root_node_id {
1586 return;
1587 }
1588 let Some(node) = self.get_node_mut(document_id) else {
1589 return;
1590 };
1591 let NodeData::Document(data) = &mut node.data else {
1592 return;
1593 };
1594 if let Some(candidates) = data.ids.get_mut(id) {
1595 candidates.retain(|candidate| *candidate != node_id);
1596 if candidates.is_empty() {
1597 data.ids.remove(id);
1598 }
1599 }
1600 }
1601
1602 pub fn get_element_by_id_in(&self, document_id: NodeId, id: &str) -> Option<NodeId> {
1605 if document_id == self.root_node_id {
1606 return self.get_element_by_id(id);
1607 }
1608 if id.is_empty() {
1609 return None;
1610 }
1611 let NodeData::Document(data) = &self.get_node(document_id)?.data else {
1612 return None;
1613 };
1614 data.ids
1615 .get(id)?
1616 .iter()
1617 .filter_map(|candidate| {
1618 let mut current = *candidate;
1619 let mut path = Vec::new();
1620 while current != document_id {
1621 let node = self.get_node(current)?;
1622 let parent_id = node.parent?;
1623 let parent = self.get_node(parent_id)?;
1624 path.push(parent.index_of_child(current)?);
1625 current = parent_id;
1626 }
1627 path.reverse();
1628 Some((path, *candidate))
1629 })
1630 .min_by(|left, right| left.0.cmp(&right.0))
1631 .map(|(_, node_id)| node_id)
1632 }
1633
1634 fn collect_live_reference_owners(
1646 nodes: &NodeTree,
1647 hoisted_fixed_parents: &HashMap<NodeId, NodeId>,
1648 removed: &FxHashSet<NodeId>,
1649 node_id: NodeId,
1650 candidates: &mut FxHashSet<NodeId>,
1651 ) {
1652 fn push_survivor(
1653 removed: &FxHashSet<NodeId>,
1654 candidates: &mut FxHashSet<NodeId>,
1655 id: NodeId,
1656 ) {
1657 if !removed.contains(&id) {
1658 candidates.insert(id);
1659 }
1660 }
1661
1662 let (removed_is_text, dom_parent, layout_parent, seeds) = {
1663 let Some(node) = nodes.get(node_id) else {
1664 return;
1665 };
1666 let mut seeds: SmallVec<[NodeId; 8]> = SmallVec::new();
1667 {
1668 let borrowed = node.layout_children.borrow();
1669 if let Some(children) = borrowed.as_ref() {
1670 seeds.extend(children.iter().copied());
1671 }
1672 }
1673 seeds.extend(node.children.iter().copied());
1674 seeds.extend(node.anonymous_blocks.iter().copied());
1675 seeds.extend(node.before());
1676 seeds.extend(node.after());
1677 #[cfg(feature = "shadow-dom")]
1678 {
1679 seeds.extend(node.assigned_slot);
1680 seeds.extend(
1681 node.element_data()
1682 .and_then(|element| element.assigned_slot),
1683 );
1684 if let Some(assigned) = node.assigned_nodes.as_ref() {
1685 seeds.extend(assigned.iter().copied());
1686 }
1687 if let Some(root) = node.shadow_root_data() {
1688 seeds.push(root.host);
1689 }
1690 }
1691 (
1692 node.is_text_node(),
1693 node.parent,
1694 node.layout_parent.get(),
1695 seeds,
1696 )
1697 };
1698
1699 for parent_id in [dom_parent, layout_parent].into_iter().flatten() {
1700 let Some(parent) = nodes.get(parent_id) else {
1701 continue;
1702 };
1703 let anons: SmallVec<[NodeId; 4]> = parent.anonymous_blocks.iter().copied().collect();
1704 for anon in anons {
1705 let Some(anon_node) = nodes.get(anon) else {
1706 continue;
1707 };
1708 let listed = anon_node
1709 .children
1710 .iter()
1711 .any(|child| removed.contains(child))
1712 || anon_node
1713 .layout_children
1714 .borrow()
1715 .as_ref()
1716 .is_some_and(|children| {
1717 children.iter().any(|child| removed.contains(child))
1718 });
1719 if listed {
1720 push_survivor(removed, candidates, anon);
1721 }
1722 }
1723 }
1724 for id in seeds {
1725 push_survivor(removed, candidates, id);
1726 }
1727
1728 let mut stack: SmallVec<[NodeId; 16]> = SmallVec::new();
1729 if let Some(id) = dom_parent {
1730 if !removed.contains(&id) {
1731 stack.push(id);
1732 }
1733 }
1734 if let Some(id) = layout_parent {
1735 if !removed.contains(&id) {
1736 stack.push(id);
1737 }
1738 }
1739 if let Some(origin) = hoisted_fixed_parents.get(&node_id).copied() {
1740 push_survivor(removed, candidates, origin);
1743 if !removed.contains(&origin) {
1744 stack.push(origin);
1745 }
1746 }
1747
1748 let mut seen: SmallVec<[NodeId; 16]> = SmallVec::new();
1749 seen.push(node_id);
1750 while let Some(id) = stack.pop() {
1751 if seen.contains(&id) {
1752 continue;
1753 }
1754 seen.push(id);
1755 let Some(ancestor) = nodes.get(id) else {
1756 continue;
1757 };
1758 let stacking = ancestor.stacking_context.as_ref().is_some_and(|context| {
1759 context.children.iter().any(|child| {
1760 removed.contains(&child.node_id)
1761 || child
1762 .clip_ancestors
1763 .iter()
1764 .any(|clip| removed.contains(clip))
1765 })
1766 });
1767 let inline_or_list = ancestor.element_data().is_some_and(|element| {
1768 element.inline_layout_data.is_some() || element.list_item_data.is_some()
1769 });
1770 let table_root = !removed_is_text && ancestor.flags.is_table_root();
1772 #[cfg(feature = "shadow-dom")]
1773 let shadow_hit = {
1774 let mut hit = ancestor
1775 .flattened_children
1776 .as_ref()
1777 .is_some_and(|children| children.iter().any(|child| removed.contains(child)))
1778 || ancestor.assigned_nodes.as_ref().is_some_and(|assigned| {
1779 assigned.iter().any(|child| removed.contains(child))
1780 });
1781 if let Some(root) = ancestor.shadow_root_data() {
1782 let host = root.host;
1783 hit = hit
1784 || root.slots.iter().any(|slot| removed.contains(slot))
1785 || root
1786 .slottables
1787 .iter()
1788 .any(|slottable| removed.contains(slottable))
1789 || root
1790 .stylesheet_nodes
1791 .iter()
1792 .any(|sheet| removed.contains(sheet));
1793 stack.push(host);
1794 }
1795 hit
1796 };
1797 #[cfg(not(feature = "shadow-dom"))]
1798 let shadow_hit = false;
1799 if stacking || inline_or_list || table_root || shadow_hit {
1800 push_survivor(removed, candidates, id);
1801 }
1802 if let Some(parent) = ancestor.parent {
1803 stack.push(parent);
1804 }
1805 if let Some(parent) = ancestor.layout_parent.get() {
1806 stack.push(parent);
1807 }
1808 }
1809 }
1810
1811 fn collect_stacking_context_owners(
1815 nodes: &NodeTree,
1816 position_hosts: &HashSet<NodeId>,
1817 clip_hosts: &HashSet<NodeId>,
1818 removed: &FxHashSet<NodeId>,
1819 candidates: &mut FxHashSet<NodeId>,
1820 ) {
1821 for host in position_hosts.iter().chain(clip_hosts.iter()).copied() {
1822 if removed.contains(&host) {
1823 continue;
1824 }
1825 let Some(node) = nodes.get(host) else {
1826 continue;
1827 };
1828 let Some(context) = node.stacking_context.as_ref() else {
1829 continue;
1830 };
1831 let references = context.children.iter().any(|child| {
1832 removed.contains(&child.node_id)
1833 || child
1834 .clip_ancestors
1835 .iter()
1836 .any(|clip| removed.contains(clip))
1837 });
1838 if references {
1839 candidates.insert(host);
1840 }
1841 }
1842 }
1843
1844 pub(crate) fn invalidate_node_references(
1855 &mut self,
1856 removed: &FxHashSet<NodeId>,
1857 mut parents: FxHashSet<NodeId>,
1858 extra_owners: FxHashSet<NodeId>,
1859 ) {
1860 if removed.is_empty() {
1861 return;
1862 }
1863
1864 fn layout_references_removed(
1865 layout: &parley::Layout<TextBrush>,
1866 removed: &FxHashSet<NodeId>,
1867 ) -> bool {
1868 layout
1869 .inline_boxes()
1870 .iter()
1871 .any(|ibox| removed.contains(&NodeId::from_u64(ibox.id)))
1872 || layout.styles().iter().any(|style| {
1873 removed.contains(&style.brush.id)
1874 || style
1875 .brush
1876 .text_node
1877 .is_some_and(|id| removed.contains(&id))
1878 })
1879 }
1880
1881 for (&id, &origin) in &self.hoisted_fixed_parents {
1885 if removed.contains(&id) || removed.contains(&origin) {
1886 if let Some(node) = self.nodes.get(id) {
1887 parents.extend(node.layout_parent.get());
1888 }
1889 }
1890 }
1891
1892 let mut candidates: FxHashSet<NodeId> = FxHashSet::with_capacity_and_hasher(
1893 parents.len() + extra_owners.len() + removed.len(),
1894 Default::default(),
1895 );
1896 for &id in &parents {
1897 if !removed.contains(&id) {
1898 candidates.insert(id);
1899 }
1900 }
1901 for id in extra_owners {
1902 if !removed.contains(&id) {
1903 candidates.insert(id);
1904 }
1905 }
1906 for &id in removed {
1907 let detach = self
1908 .nodes
1909 .get(id)
1910 .and_then(|node| node.layout_parent.get())
1911 .is_some_and(|parent| !removed.contains(&parent));
1912 if detach {
1913 candidates.insert(id);
1914 }
1915 if self.nodes.contains_key(id) {
1916 Self::collect_live_reference_owners(
1917 &self.nodes,
1918 &self.hoisted_fixed_parents,
1919 removed,
1920 id,
1921 &mut candidates,
1922 );
1923 }
1924 }
1925 Self::collect_stacking_context_owners(
1926 &self.nodes,
1927 &self.hoisted_position_hosts,
1928 &self.hoisted_clip_hosts,
1929 removed,
1930 &mut candidates,
1931 );
1932
1933 self.fixed_nodes.retain(|id| !removed.contains(id));
1934 self.sticky_nodes.retain(|id| !removed.contains(id));
1935 self.sticky_offsets.retain(|id, _| !removed.contains(id));
1936 self.hoisted_fixed_parents
1937 .retain(|id, origin| !removed.contains(id) && !removed.contains(origin));
1938 let fixed_parents = &self.hoisted_fixed_parents;
1939 self.hoisted_fixed_indices
1940 .retain(|id, _| fixed_parents.contains_key(id));
1941 self.hoisted_clip_hosts.retain(|id| !removed.contains(id));
1942 self.hoisted_position_hosts
1943 .retain(|id| !removed.contains(id));
1944
1945 self.abspos_placements.clear();
1949 self.abspos_candidates.clear();
1950 self.abspos_candidate_ids.clear();
1951 self.abspos_inputs.clear();
1952 self.abspos_written.clear();
1953 self.abspos_candidates_dirty = true;
1954 self.abspos_layout_dirty = true;
1955
1956 let mut dirty = FxHashSet::default();
1957 for id in candidates {
1958 let Some(node) = self.nodes.get_mut(id) else {
1959 continue;
1960 };
1961 if removed.contains(&id) {
1964 if node
1965 .layout_parent
1966 .get()
1967 .is_some_and(|parent| !removed.contains(&parent))
1968 {
1969 node.layout_parent.set(None);
1970 }
1971 continue;
1972 }
1973
1974 let mut reconstruct = parents.contains(&id);
1975 reconstruct |= node
1976 .layout_children
1977 .get_mut()
1978 .as_ref()
1979 .is_some_and(|children| children.iter().any(|id| removed.contains(id)));
1980 let mut repaint = node
1981 .paint_children
1982 .get_mut()
1983 .as_ref()
1984 .is_some_and(|children| children.iter().any(|id| removed.contains(id)));
1985
1986 let old_anonymous_count = node.anonymous_blocks.len();
1987 node.anonymous_blocks.retain(|id| !removed.contains(id));
1988 reconstruct |= old_anonymous_count != node.anonymous_blocks.len();
1989 if node.is_anonymous() {
1990 let old_child_count = node.children.len();
1991 node.children.retain(|id| !removed.contains(id));
1992 reconstruct |= old_child_count != node.children.len();
1993 }
1994 if node
1995 .layout_parent
1996 .get()
1997 .is_some_and(|parent| removed.contains(&parent))
1998 {
1999 node.layout_parent.set(None);
2000 reconstruct = true;
2001 }
2002
2003 if let Some(context) = node.stacking_context.as_mut() {
2004 let old_count = context.children.len();
2005 context.children.retain(|child| {
2006 !removed.contains(&child.node_id)
2007 && !child.clip_ancestors.iter().any(|id| removed.contains(id))
2008 });
2009 if old_count != context.children.len() {
2010 context.sort();
2013 context.content_area = taffy::Rect::ZERO;
2014 repaint = true;
2015 }
2016 }
2017
2018 #[cfg(feature = "shadow-dom")]
2019 {
2020 for children in [
2021 node.flattened_children.as_mut(),
2022 node.assigned_nodes.as_mut(),
2023 ]
2024 .into_iter()
2025 .flatten()
2026 {
2027 let old_count = children.len();
2028 children.retain(|id| !removed.contains(id));
2029 reconstruct |= old_count != children.len();
2030 }
2031 if node.assigned_slot.is_some_and(|id| removed.contains(&id)) {
2032 node.assigned_slot = None;
2033 reconstruct = true;
2034 }
2035 if let Some(root) = node.shadow_root_data_mut() {
2036 let old_count = root.slots.len() + root.slottables.len();
2037 root.slots.retain(|id| !removed.contains(id));
2038 root.slottables.retain(|id| !removed.contains(id));
2039 root.stylesheet_nodes.retain(|id| !removed.contains(id));
2040 reconstruct |= old_count != root.slots.len() + root.slottables.len();
2041 }
2042 }
2043
2044 let mut table_removed = false;
2045 if let Some(element) = node.data.downcast_element_mut() {
2046 for pseudo in [&mut element.before, &mut element.after] {
2047 if pseudo.is_some_and(|id| removed.contains(&id)) {
2048 *pseudo = None;
2049 reconstruct = true;
2050 }
2051 }
2052 if element
2053 .inline_layout_data
2054 .as_ref()
2055 .is_some_and(|inline| layout_references_removed(&inline.layout, removed))
2056 {
2057 element.inline_layout_data = None;
2058 reconstruct = true;
2059 }
2060 if element
2061 .list_item_data
2062 .as_ref()
2063 .is_some_and(|item| match &item.position {
2064 crate::node::ListItemLayoutPosition::Outside(layout) => {
2065 layout_references_removed(layout, removed)
2066 }
2067 crate::node::ListItemLayoutPosition::Inside => false,
2068 })
2069 {
2070 element.list_item_data = None;
2071 reconstruct = true;
2072 }
2073 if let SpecialElementData::TableRoot(context) = &element.special_data {
2074 table_removed = context.references_removed_nodes(removed);
2075 }
2076 if table_removed {
2077 element.special_data = SpecialElementData::None;
2078 reconstruct = true;
2079 }
2080 #[cfg(feature = "shadow-dom")]
2081 if element
2082 .assigned_slot
2083 .is_some_and(|id| removed.contains(&id))
2084 {
2085 element.assigned_slot = None;
2086 reconstruct = true;
2087 }
2088 }
2089 if table_removed {
2090 node.flags.remove(NodeFlags::IS_TABLE_ROOT);
2091 }
2092
2093 if reconstruct || repaint {
2094 node.layout_children.get_mut().take();
2095 node.paint_children.get_mut().take();
2096 if let Some(element) = node.data.downcast_element_mut() {
2097 element.inline_layout_data = None;
2098 }
2099 dirty.insert(id);
2100 }
2101 }
2102
2103 let mut stack: Vec<NodeId> = dirty.iter().copied().collect();
2107 let mut visited = FxHashSet::default();
2108 while let Some(id) = stack.pop() {
2109 if !visited.insert(id) || removed.contains(&id) {
2110 continue;
2111 }
2112 let Some(node) = self.nodes.get_mut(id) else {
2113 continue;
2114 };
2115 stack.extend(node.parent);
2116 stack.extend(node.layout_parent.get());
2117 #[cfg(feature = "shadow-dom")]
2118 {
2119 stack.extend(node.assigned_slot);
2120 if let Some(root) = node.shadow_root_data() {
2121 stack.push(root.host);
2122 }
2123 }
2124 node.set_dirty_descendants();
2125 if node.stylo_element_data_opt().is_some() {
2126 node.insert_damage(if dirty.contains(&id) {
2127 ALL_DAMAGE
2128 } else {
2129 crate::layout::damage::ONLY_RELAYOUT
2130 | style::selector_parser::RestyleDamage::REPAINT
2131 });
2132 node.cache_mut().clear();
2133 }
2134 if let Some(inline) = node
2135 .data
2136 .downcast_element_mut()
2137 .and_then(|element| element.inline_layout_data.as_mut())
2138 {
2139 inline.content_widths = None;
2140 }
2141 }
2142
2143 #[cfg(feature = "shadow-dom")]
2144 {
2145 let nodes = &self.nodes;
2146 self.shadow_host_nodes.retain(|id| nodes.contains_key(*id));
2147 self.dirty_shadow_hosts.retain(|id| nodes.contains_key(*id));
2148 self.pending_slot_changes
2149 .retain(|id| nodes.contains_key(*id));
2150 self.signaled_slots.retain(|id| nodes.contains_key(*id));
2151 self.dirty_shadow_hosts
2152 .extend(self.shadow_host_nodes.iter().copied());
2153 }
2154
2155 self.controls_to_form
2156 .retain(|control, form| !removed.contains(control) && !removed.contains(form));
2157 if dirty.iter().any(|id| {
2160 self.nodes
2161 .get(*id)
2162 .is_some_and(|node| node.flags.is_in_document())
2163 }) {
2164 if self.mutator_depth == 0 {
2165 self.shell_provider.request_redraw();
2166 } else {
2167 self.mutator_redraw_pending = true;
2168 }
2169 }
2170
2171 }
2174
2175 fn purge_dropped_node_references(&mut self) {
2176 if self.node_drop_depth != 0 || self.dropped_node_ids.is_empty() {
2177 return;
2178 }
2179 let removed = std::mem::take(&mut self.dropped_node_ids);
2180 let parents = std::mem::take(&mut self.dropped_node_parents);
2181 let owners = std::mem::take(&mut self.dropped_reference_owners);
2182 self.invalidate_node_references(&removed, parents, owners);
2183 }
2184
2185 pub(crate) fn remove_node_from_tree(&mut self, node_id: NodeId) -> Option<Node> {
2188 self.clear_interaction_state_for_removed_node(node_id);
2189 let indexed = self.get_node(node_id).and_then(|node| {
2190 Some((
2191 node.owner_document?,
2192 node.element_data()?.id.as_ref()?.to_string(),
2193 ))
2194 });
2195 if let Some((document_id, id)) = indexed {
2196 self.remove_from_document_id_map(document_id, &id, node_id);
2197 }
2198 self.forget_script_custom_element(node_id);
2199 if self.nodes.contains_key(node_id) {
2200 self.dropped_node_ids.insert(node_id);
2201 let mut owners = FxHashSet::default();
2202 Self::collect_live_reference_owners(
2203 &self.nodes,
2204 &self.hoisted_fixed_parents,
2205 &self.dropped_node_ids,
2206 node_id,
2207 &mut owners,
2208 );
2209 self.dropped_reference_owners.extend(owners);
2210 }
2211 let node = self.nodes.remove(node_id);
2212 if let Some(node) = &node {
2213 self.dropped_node_parents.extend(
2214 [node.parent, node.layout_parent.get()]
2215 .into_iter()
2216 .flatten(),
2217 );
2218 self.purge_dropped_node_references();
2219 }
2220 node
2221 }
2222
2223 fn nearest_surviving_element_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
2228 let mut current = self.get_node(node_id)?.parent;
2229 while let Some(id) = current {
2230 let node = self.get_node(id)?;
2231 if node.is_element() && node.flags.is_in_document() {
2232 return Some(id);
2233 }
2234 current = node.parent;
2235 }
2236 None
2237 }
2238
2239 pub(crate) fn clear_interaction_state_for_removed_node(&mut self, node_id: NodeId) {
2260 if !self.nodes.contains_key(node_id) {
2261 return;
2262 }
2263
2264 if self.hover_node_id == Some(node_id) {
2265 self.hover_node_id = self.nearest_surviving_element_ancestor(node_id);
2266 self.hover_node_is_text = false;
2267 }
2268 if self.hover_hit_node_id == Some(node_id) {
2269 self.hover_hit_node_id = None;
2270 }
2271 if self.semantic_hover_node_id == Some(node_id) {
2272 self.semantic_hover_node_id = None;
2273 }
2274 if self.active_node_id == Some(node_id) {
2275 self.active_node_id = self.nearest_surviving_element_ancestor(node_id);
2276 }
2277 if self.focus_node_id == Some(node_id) {
2278 let shell_provider = self.shell_provider.clone();
2279 self.nodes[node_id].blur(shell_provider);
2280 self.focus_node_id = None;
2281 }
2282 if self.mousedown_node_id == Some(node_id) {
2283 self.mousedown_node_id = None;
2284 }
2285 if self.text_selection.anchor.node_or_parent == Some(node_id)
2286 || self.text_selection.focus.node_or_parent == Some(node_id)
2287 {
2288 self.text_selection.clear();
2289 }
2290 if self
2291 .hovered_scrollbar
2292 .is_some_and(|scrollbar| scrollbar.node_id == node_id)
2293 {
2294 self.hovered_scrollbar = None;
2295 }
2296 let drag_references_node = match &self.drag_mode {
2297 DragMode::Panning(state) => state.target == node_id,
2298 DragMode::ScrollbarDrag(state) => state.scrollbar.node_id == node_id,
2299 DragMode::Selecting | DragMode::None => false,
2300 };
2301 if drag_references_node {
2302 self.drag_mode = DragMode::None;
2303 }
2304 if matches!(
2305 &self.scroll_animation,
2306 ScrollAnimationState::Fling(state) if state.target == node_id
2307 ) {
2308 self.scroll_animation = ScrollAnimationState::None;
2309 }
2310 self.scrollbar_activity.remove(&node_id);
2311
2312 self.controls_to_form.remove(&node_id);
2317 }
2318
2319 pub(crate) fn drop_node_ignoring_parent(&mut self, node_id: NodeId) -> Option<Node> {
2320 self.drop_node_ignoring_parent_with(node_id, &mut |_| {})
2321 }
2322
2323 pub(crate) fn drop_node_ignoring_parent_with(
2326 &mut self,
2327 node_id: NodeId,
2328 on_drop: &mut dyn FnMut(NodeId),
2329 ) -> Option<Node> {
2330 self.node_drop_depth += 1;
2331 let mut node = self.remove_node_from_tree(node_id);
2332 if let Some(node) = &mut node {
2333 on_drop(node_id);
2334 if let Some(before) = node.before() {
2335 self.drop_node_ignoring_parent_with(before, on_drop);
2336 }
2337 if let Some(after) = node.after() {
2338 self.drop_node_ignoring_parent_with(after, on_drop);
2339 }
2340
2341 for &child in &node.children {
2342 self.drop_node_ignoring_parent_with(child, on_drop);
2343 }
2344 if let Some(contents) = node
2345 .element_data()
2346 .and_then(|element| element.template_contents)
2347 {
2348 self.drop_node_ignoring_parent_with(contents, on_drop);
2349 }
2350
2351 for &anon_id in &node.anonymous_blocks {
2354 self.deallocate_anonymous_block(anon_id);
2355 }
2356
2357 #[cfg(feature = "shadow-dom")]
2360 if let Some(shadow_root_id) = node.shadow_root_id() {
2361 self.shadow_host_nodes.remove(&node_id);
2362 self.dirty_shadow_hosts.remove(&node_id);
2363 self.custom_element_nodes.remove(&node_id);
2364 self.drop_node_ignoring_parent_with(shadow_root_id, on_drop);
2365 }
2366 }
2367 self.node_drop_depth -= 1;
2368 self.purge_dropped_node_references();
2369 node
2370 }
2371
2372 pub(crate) fn deallocate_anonymous_block(&mut self, anon_id: NodeId) {
2375 if !self.nodes.contains_key(anon_id) {
2378 return;
2379 }
2380
2381 self.node_drop_depth += 1;
2383 let nested = std::mem::take(&mut self.nodes[anon_id].anonymous_blocks);
2384 for nested_id in nested {
2385 self.deallocate_anonymous_block(nested_id);
2386 }
2387
2388 self.remove_node_from_tree(anon_id);
2389 self.node_drop_depth -= 1;
2390 self.purge_dropped_node_references();
2391 }
2392
2393 pub fn create_text_node(&mut self, text: &str) -> NodeId {
2394 let content = text.to_string();
2395 let data = NodeData::Text(TextNodeData::new(content));
2396 self.create_node(data)
2397 }
2398
2399 pub fn deep_clone_node(&mut self, node_id: NodeId) -> NodeId {
2400 let node = &self.nodes[node_id];
2402 let mut data = node.data.clone();
2403
2404 match &mut data {
2405 NodeData::Element(elem) | NodeData::AnonymousBlock(elem) => {
2406 if let Some(arc) = elem.style_attribute.as_mut() {
2407 let read_guard = self.guard().read();
2408 let block = arc.read_with(&read_guard);
2409 *arc = ServoArc::new(self.guard().wrap(block.clone()));
2410 }
2411 }
2412 _ => {}
2413 }
2414
2415 let children = node.children.clone();
2416 #[cfg(feature = "shadow-dom")]
2417 let shadow_source = node.shadow_root_id();
2418 let template_contents = node
2419 .element_data()
2420 .and_then(|element| element.template_contents);
2421 let owner_document = node.owner_document;
2422 let markup = node.markup.clone();
2423 let inert_script = node
2424 .flags
2425 .contains(crate::node::NodeFlags::IS_PARSER_INERT_SCRIPT);
2426
2427 let new_node_id = self.create_node(data);
2429 self.nodes[new_node_id].owner_document = owner_document;
2430 self.nodes[new_node_id].markup = markup;
2431 self.nodes[new_node_id]
2432 .flags
2433 .set(crate::node::NodeFlags::IS_PARSER_INERT_SCRIPT, inert_script);
2434 if let Some(document_id) = owner_document {
2435 let id = self.nodes[new_node_id]
2436 .element_data()
2437 .and_then(|element| element.id.as_ref())
2438 .map(ToString::to_string);
2439 if let Some(id) = id {
2440 self.add_to_document_id_map(document_id, &id, new_node_id);
2441 }
2442 }
2443 if let Some(contents) = template_contents {
2444 let cloned_contents = self.deep_clone_node(contents);
2445 self.nodes[new_node_id]
2446 .element_data_mut()
2447 .expect("template clone must be an element")
2448 .template_contents = Some(cloned_contents);
2449 }
2450
2451 let new_children: ThinVec<NodeId> = children
2453 .into_iter()
2454 .map(|child_id| self.deep_clone_node(child_id))
2455 .collect();
2456 for &child_id in &new_children {
2457 self.nodes[child_id].parent = Some(new_node_id);
2458 self.attach_script_custom_element_subtree(child_id, new_node_id);
2459 }
2460 self.nodes[new_node_id].children = new_children;
2461
2462 #[cfg(feature = "shadow-dom")]
2465 if let Some(source_root) = shadow_source {
2466 let clonable = self
2467 .nodes
2468 .get(source_root)
2469 .and_then(|node| node.shadow_root_data())
2470 .is_some_and(|root| root.clonable);
2471 if clonable {
2472 let mode = self.nodes[source_root].shadow_root_data().unwrap().mode;
2473 let serializable = self.nodes[source_root]
2474 .shadow_root_data()
2475 .unwrap()
2476 .serializable;
2477 let delegates_focus = self.nodes[source_root]
2478 .shadow_root_data()
2479 .unwrap()
2480 .delegates_focus;
2481 let declarative = self.nodes[source_root]
2482 .shadow_root_data()
2483 .unwrap()
2484 .declarative;
2485 let shadow_children = self.nodes[source_root].children.clone();
2486 let new_root = self.attach_shadow(new_node_id, mode);
2487 if let Some(root) = self.nodes[new_root].shadow_root_data_mut() {
2488 root.clonable = true;
2489 root.serializable = serializable;
2490 root.delegates_focus = delegates_focus;
2491 root.declarative = declarative;
2492 }
2493 let cloned_shadow_children: ThinVec<NodeId> = shadow_children
2494 .into_iter()
2495 .map(|child_id| self.deep_clone_node(child_id))
2496 .collect();
2497 for &child_id in &cloned_shadow_children {
2498 self.nodes[child_id].parent = Some(new_root);
2499 self.attach_script_custom_element_subtree(child_id, new_root);
2500 }
2501 self.nodes[new_root].children = cloned_shadow_children;
2502 }
2503 }
2504
2505 new_node_id
2506 }
2507
2508 pub(crate) fn remove_and_drop_pe(&mut self, node_id: NodeId) -> Option<Node> {
2509 fn remove_pe_ignoring_parent(doc: &mut BaseDocument, node_id: NodeId) -> Option<Node> {
2510 let mut node = doc.remove_node_from_tree(node_id);
2511 if let Some(node) = &mut node {
2512 for &child in &node.children {
2513 remove_pe_ignoring_parent(doc, child);
2514 }
2515 for &anon_id in &node.anonymous_blocks {
2516 doc.deallocate_anonymous_block(anon_id);
2517 }
2518 }
2519 node
2520 }
2521
2522 self.node_drop_depth += 1;
2523 let node = remove_pe_ignoring_parent(self, node_id);
2524
2525 if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
2527 let parent = &mut self.nodes[parent_id];
2528 parent.children.retain(|id| *id != node_id);
2529 }
2530
2531 self.node_drop_depth -= 1;
2532 self.purge_dropped_node_references();
2533 node
2534 }
2535
2536 pub(crate) fn resolve_url(&self, raw: &str) -> url::Url {
2537 self.url.resolve_relative(raw).unwrap_or_else(|| {
2538 panic!(
2539 "to be able to resolve {raw} with the base_url: {:?}",
2540 *self.url
2541 )
2542 })
2543 }
2544
2545 pub fn navigate_to_url(&self, raw: &str) -> bool {
2553 let Some(url) = self.url.resolve_relative(raw) else {
2554 return false;
2555 };
2556 self.navigation_provider
2557 .navigate_to(blitz_traits::navigation::NavigationOptions::new(
2558 url,
2559 None,
2560 self.id(),
2561 ));
2562 true
2563 }
2564
2565 pub fn current_url(&self) -> String {
2567 self.url.to_string()
2568 }
2569
2570 pub fn print_tree(&self) {
2571 crate::util::walk_tree(0, self.root_node());
2572 }
2573
2574 pub fn print_subtree(&self, node_id: NodeId) {
2575 crate::util::walk_tree(0, &self.nodes[node_id]);
2576 }
2577
2578 pub fn reload_resource_by_href(&mut self, href_to_reload: &str) {
2579 for &node_id in self.nodes_to_stylesheet.keys() {
2580 let node = &self.nodes[node_id];
2581 let Some(element) = node.element_data() else {
2582 continue;
2583 };
2584
2585 if element.name.local == local_name!("link") {
2586 if let Some(href) = element.attr(local_name!("href")) {
2587 if href == href_to_reload {
2589 let resolved_href = self.resolve_url(href);
2590 self.net_provider.fetch(
2591 self.id(),
2592 self.build_request(resolved_href.clone())
2593 .destination(RequestDestination::Style),
2594 ResourceHandler::boxed(
2595 self.tx.clone(),
2596 self.id,
2597 Some(node_id),
2598 self.shell_provider.clone(),
2599 StylesheetHandler {
2600 source_url: resolved_href,
2601 guard: self.guard.clone(),
2602 net_provider: self.net_provider.clone(),
2603 abort_signal: self.abort_signal.clone(),
2604 },
2605 ),
2606 );
2607 }
2608 }
2609 }
2610 }
2611 }
2612
2613 pub fn process_style_element(&mut self, target_id: NodeId) {
2614 let css = self.nodes[target_id].text_content();
2615 let css = html_escape::decode_html_entities(&css);
2616 let sheet = self.make_stylesheet(&css, Origin::Author);
2617 self.add_stylesheet_for_node(sheet, target_id);
2618 }
2619
2620 pub fn remove_user_agent_stylesheet(&mut self, contents: &str) {
2621 if let Some(sheet) = self.ua_stylesheets.remove(contents) {
2622 self.stylist.remove_stylesheet(sheet, &self.guard.read());
2623 }
2624 }
2625
2626 pub fn url(&self) -> &url::Url {
2628 &self.url
2629 }
2630
2631 pub fn author_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
2634 self.nodes_to_stylesheet.values()
2635 }
2636
2637 pub fn useragent_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
2639 self.ua_stylesheets.values()
2640 }
2641
2642 pub fn add_user_agent_stylesheet(&mut self, css: &str) {
2643 let sheet = self.make_stylesheet(css, Origin::UserAgent);
2644 self.ua_stylesheets.insert(css.to_string(), sheet.clone());
2645 self.stylist.append_stylesheet(sheet, &self.guard.read());
2646 }
2647
2648 pub fn make_stylesheet(&self, css: impl AsRef<str>, origin: Origin) -> DocumentStyleSheet {
2649 let data = Stylesheet::from_str(
2650 css.as_ref(),
2651 self.url.url_extra_data(),
2652 origin,
2653 ServoArc::new(self.guard.wrap(MediaList::empty())),
2654 self.guard.clone(),
2655 Some(&StylesheetLoader {
2656 tx: self.tx.clone(),
2657 doc_id: self.id,
2658 net_provider: self.net_provider.clone(),
2659 shell_provider: self.shell_provider.clone(),
2660 abort_signal: self.abort_signal.clone(),
2661 }),
2662 None,
2663 QuirksMode::NoQuirks,
2664 AllowImportRules::Yes,
2665 );
2666
2667 DocumentStyleSheet(ServoArc::new(data))
2668 }
2669
2670 pub fn upsert_stylesheet_for_node(&mut self, node_id: NodeId) {
2671 let raw_styles = self.nodes[node_id].text_content();
2672 let sheet = self.make_stylesheet(raw_styles, Origin::Author);
2673 self.add_stylesheet_for_node(sheet, node_id);
2674 }
2675
2676 pub fn add_stylesheet_for_node(&mut self, stylesheet: DocumentStyleSheet, node_id: NodeId) {
2677 self.platform_initial_sheet_media(&stylesheet, node_id);
2678 #[cfg(feature = "shadow-dom")]
2679 if let Some(root_id) = self.containing_shadow_root(node_id) {
2680 if self.document_sheet_nodes.remove(&node_id)
2683 && let Some(old) = self.nodes_to_stylesheet.get(&node_id).cloned()
2684 {
2685 self.stylist.remove_stylesheet(old, &self.guard.read());
2686 }
2687 self.install_shadow_stylesheet(root_id, node_id, stylesheet);
2688 return;
2689 }
2690
2691 let old = self.nodes_to_stylesheet.insert(node_id, stylesheet.clone());
2692
2693 if let Some(old) = old
2694 && self.document_sheet_nodes.contains(&node_id)
2695 {
2696 self.stylist.remove_stylesheet(old, &self.guard.read())
2697 }
2698 self.document_sheet_nodes.insert(node_id);
2699
2700 crate::net::fetch_font_face(
2702 self.tx.clone(),
2703 self.id,
2704 Some(node_id),
2705 &stylesheet.0,
2706 &self.net_provider,
2707 &self.shell_provider,
2708 &self.guard.read(),
2709 self.abort_signal.as_ref(),
2710 &mut self.web_fonts,
2711 self.font_epoch,
2712 );
2713
2714 let element = &mut self.nodes[node_id].element_data_mut().unwrap();
2716 element.special_data = SpecialElementData::Stylesheet(stylesheet.clone());
2717
2718 let insertion_point = self
2722 .nodes_to_stylesheet
2723 .range((Bound::Excluded(node_id), Bound::Unbounded))
2724 .find(|(id, _)| self.document_sheet_nodes.contains(id))
2725 .map(|(_, sheet)| sheet);
2726
2727 if let Some(insertion_point) = insertion_point {
2728 self.stylist.insert_stylesheet_before(
2729 stylesheet,
2730 insertion_point.clone(),
2731 &self.guard.read(),
2732 )
2733 } else {
2734 self.stylist
2735 .append_stylesheet(stylesheet, &self.guard.read())
2736 }
2737 }
2738
2739 pub fn handle_messages(&mut self) {
2740 let rx = self.rx.take().unwrap();
2743
2744 while let Ok(msg) = rx.try_recv() {
2745 self.handle_message(msg);
2746 }
2747
2748 self.rx = Some(rx);
2750 }
2751
2752 pub fn handle_message(&mut self, msg: DocumentEvent) {
2753 match msg {
2754 DocumentEvent::ResourceLoad(resource) => self.load_resource(resource),
2755 DocumentEvent::NavigateIframe { node_id, url } => self.navigate_iframe(node_id, url),
2756 }
2757 }
2758
2759 pub fn has_pending_critical_resources(&self) -> bool {
2761 !self.pending_critical_resources.is_empty()
2762 }
2763
2764 pub fn pending_image_count(&self) -> usize {
2771 self.pending_images.len()
2772 }
2773
2774 pub fn load_resource(&mut self, res: ResourceLoadResponse) {
2775 self.pending_critical_resources.remove(&res.request_id);
2776
2777 let resource = match res.result {
2778 Ok(resource) => resource,
2779 Err(err) => {
2780 if let Some(url) = res.resolved_url.as_ref() {
2781 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
2782 #[cfg(feature = "tracing")]
2783 tracing::warn!(
2784 url = url.as_str(),
2785 waiting_nodes = waiting_nodes.len(),
2786 error = err.as_str(),
2787 "Resource load failed"
2788 );
2789 #[cfg(not(feature = "tracing"))]
2790 let _ = (waiting_nodes, err);
2791 } else {
2792 #[cfg(feature = "tracing")]
2793 tracing::warn!(error = err.as_str(), "Resource load failed (no url)");
2794 #[cfg(not(feature = "tracing"))]
2795 let _ = err;
2796 }
2797 return;
2798 }
2799 };
2800
2801 let (resource, font_ticket) = match resource {
2802 Resource::TrackedFont(bytes, overrides, ticket) => {
2803 if ticket.css && ticket.epoch != self.font_epoch {
2804 ticket.finish(false);
2805 return;
2806 }
2807 (Resource::Font(bytes, overrides), Some(ticket))
2808 }
2809 resource => (resource, None),
2810 };
2811 if matches!(&resource, Resource::Css(_) | Resource::Font(_, _))
2812 && res.node_id.is_some_and(|id| {
2813 self.get_node(id)
2814 .is_none_or(|node| !node.flags.is_in_document())
2815 })
2816 {
2817 if let Some(ticket) = font_ticket {
2818 ticket.finish(false);
2819 }
2820 return;
2821 }
2822
2823 match resource {
2824 Resource::Css(css) => {
2825 let node_id = res.node_id.unwrap();
2826 self.add_stylesheet_for_node(css, node_id);
2827 }
2828 Resource::ImportSheet(import_rule, sheet) => {
2829 {
2838 let mut guard = self.guard.write();
2839 import_rule.write_with(&mut guard).stylesheet =
2840 style::stylesheets::import_rule::ImportSheet::Sheet(sheet.clone());
2841 }
2842
2843 crate::net::fetch_font_face(
2847 self.tx.clone(),
2848 self.id,
2849 res.node_id,
2850 &sheet,
2851 &self.net_provider,
2852 &self.shell_provider,
2853 &self.guard.read(),
2854 self.abort_signal.as_ref(),
2855 &mut self.web_fonts,
2856 self.font_epoch,
2857 );
2858 }
2859 Resource::Image(_kind, width, height, image_data) => {
2860 let image = ImageData::Raster(RasterImageData::new(width, height, image_data));
2862
2863 let Some(url) = res.resolved_url.as_ref() else {
2864 return;
2865 };
2866
2867 self.apply_loaded_image(url, image);
2868 }
2869 #[cfg(feature = "svg")]
2870 Resource::Svg(_kind, svg) => {
2871 let image = ImageData::Svg(svg);
2873
2874 let Some(url) = res.resolved_url.as_ref() else {
2875 return;
2876 };
2877
2878 self.apply_loaded_image(url, image);
2879 }
2880 Resource::DocumentSrc(html) => {
2881 let Some(node_id) = res.node_id else {
2882 return;
2883 };
2884 self.apply_iframe_html(node_id, res.request_id, res.resolved_url, &html);
2885 }
2886 Resource::Font(bytes, overrides) => {
2887 let font = Blob::new(Arc::new(bytes));
2888
2889 let weight_override = overrides.weight.map(parley::fontique::FontWeight::new);
2895 let info_override = parley::fontique::FontInfoOverride {
2896 family_name: overrides.family_name.as_deref(),
2897 weight: weight_override,
2898 style: overrides.style,
2899 ..Default::default()
2900 };
2901
2902 let mut global_font_ctx = self.font_ctx.lock().unwrap();
2904 let registered = global_font_ctx
2905 .collection
2906 .register_fonts(font.clone(), Some(info_override));
2907 let registered = !registered.is_empty();
2908
2909 #[cfg(feature = "parallel-construct")]
2910 {
2911 rayon::broadcast(|_ctx| {
2912 let mut font_ctx = self
2913 .thread_font_contexts
2914 .get_or(|| RefCell::new(Box::new(global_font_ctx.clone())))
2915 .borrow_mut();
2916 font_ctx
2917 .collection
2918 .register_fonts(font.clone(), Some(info_override));
2919 });
2920 }
2921 drop(global_font_ctx);
2922
2923 self.invalidate_inline_contexts();
2925 if let Some(ticket) = font_ticket {
2926 ticket.finish(registered);
2927 }
2928 }
2929 Resource::TrackedFont(..) => unreachable!("tracked font was normalized"),
2930 Resource::None => {
2931 }
2933 }
2934 }
2935
2936 fn apply_loaded_image(&mut self, url: &str, image: ImageData) {
2939 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
2941
2942 #[cfg(feature = "tracing")]
2943 tracing::info!(
2944 "Image {url} loaded, applying to {} nodes",
2945 waiting_nodes.len()
2946 );
2947
2948 self.image_cache.insert(url.to_string(), image.clone());
2950
2951 for (node_id, image_type) in waiting_nodes {
2953 let Some(node) = self.get_node_mut(node_id) else {
2954 continue;
2955 };
2956
2957 match image_type {
2958 ImageType::Image => {
2959 node.element_data_mut().unwrap().special_data =
2960 SpecialElementData::Image(Box::new(image.clone()));
2961
2962 node.cache_mut().clear();
2964 node.insert_damage(ALL_DAMAGE);
2965 }
2966 ImageType::Background(idx) | ImageType::Mask(idx) => {
2967 let layer_image = node.element_data_mut().and_then(|el| {
2968 let images = match image_type {
2969 ImageType::Background(_) => &mut el.background_images,
2970 ImageType::Mask(_) => &mut el.mask_images,
2971 ImageType::Image => unreachable!(),
2972 };
2973 images.get_mut(idx)
2974 });
2975 if let Some(Some(layer_image)) = layer_image {
2976 layer_image.status = Status::Ok;
2977 layer_image.image = image.clone();
2978 }
2979 }
2980 }
2981 }
2982 }
2983
2984 pub fn snapshot_node(&mut self, node_id: NodeId) {
2985 let node = &mut self.nodes[node_id];
2986
2987 let has_been_styled = node.primary_styles().is_some();
2992 if !has_been_styled {
2993 return;
2994 }
2995
2996 let opaque_node_id = TNode::opaque(&&*node);
2997 node.set_has_snapshot(true);
2998 node.snapshot_handled()
2999 .store(false, std::sync::atomic::Ordering::SeqCst);
3000
3001 if let Some(_existing_snapshot) = self.snapshots.get_mut(&opaque_node_id) {
3003 } else {
3006 let attrs: Option<Vec<_>> = node.attrs().map(|attrs| {
3007 attrs
3008 .iter()
3009 .map(|attr| {
3010 let ident = AttrIdentifier {
3011 local_name: GenericAtomIdent(attr.name.local.clone()),
3012 name: GenericAtomIdent(attr.name.local.clone()),
3013 namespace: GenericAtomIdent(attr.name.ns.clone()),
3014 prefix: None,
3015 };
3016
3017 let value = if attr.name.local == local_name!("id") {
3018 AttrValue::Atom(Atom::from(&*attr.value))
3019 } else if attr.name.local == local_name!("class") {
3020 let classes = attr
3021 .value
3022 .split_ascii_whitespace()
3023 .map(Atom::from)
3024 .collect();
3025 AttrValue::TokenList(OnceLock::from(attr.value.to_string()), classes)
3031 } else {
3032 AttrValue::String(attr.value.to_string())
3033 };
3034
3035 (ident, value)
3036 })
3037 .collect()
3038 });
3039
3040 let changed_attrs = attrs
3041 .as_ref()
3042 .map(|attrs| attrs.iter().map(|attr| attr.0.name.clone()).collect())
3043 .unwrap_or_default();
3044
3045 self.snapshots.insert(
3046 opaque_node_id,
3047 ServoElementSnapshot {
3048 state: Some(*node.element_state()),
3049 attrs,
3050 changed_attrs,
3051 class_changed: true,
3052 id_changed: true,
3053 other_attributes_changed: true,
3054 },
3055 );
3056 }
3057 }
3058
3059 pub(crate) fn snapshot_attribute(&mut self, node_id: NodeId, name: &markup5ever::LocalName) {
3061 self.snapshot_node(node_id);
3062 let opaque_node_id = TNode::opaque(&&self.nodes[node_id]);
3063 if let Some(snapshot) = self.snapshots.get_mut(&opaque_node_id) {
3064 let changed_attr = GenericAtomIdent(name.clone());
3065 if !snapshot.changed_attrs.contains(&changed_attr) {
3066 snapshot.changed_attrs.extend(std::iter::once(changed_attr));
3067 }
3068 }
3069 }
3070
3071 pub fn snapshot_node_and(&mut self, node_id: NodeId, cb: impl FnOnce(&mut Node)) {
3078 if !self.nodes.contains_key(node_id) {
3079 return;
3080 }
3081 self.snapshot_node(node_id);
3082 cb(&mut self.nodes[node_id]);
3083 }
3084
3085 pub fn hit(&self, x: f32, y: f32) -> Option<HitResult> {
3087 self.hit_with_scrollbar(x, y).0
3088 }
3089
3090 pub fn nearest_non_anonymous_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
3103 let mut node = self.get_node(node_id)?;
3107 loop {
3108 let parent = match node.parent {
3109 Some(parent_id) => self.get_node(parent_id)?,
3110 None => return Some(node.id),
3111 };
3112 if !node.is_anonymous() && !parent.is_anonymous() {
3113 return Some(node.id);
3114 }
3115 node = parent;
3116 }
3117 }
3118
3119 pub fn focus_next_node(&mut self) -> Option<NodeId> {
3120 let focussed_node_id = self.get_focussed_node_id()?;
3121 let id = self.next_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
3122 self.set_focus_to(id);
3123 Some(id)
3124 }
3125
3126 pub fn focus_prev_node(&mut self) -> Option<NodeId> {
3128 let focussed_node_id = self.get_focussed_node_id()?;
3129 let id = self.prev_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
3130 self.set_focus_to(id);
3131 Some(id)
3132 }
3133
3134 pub fn clear_focus(&mut self) {
3136 if let Some(id) = self.focus_node_id {
3137 let shell_provider = self.shell_provider.clone();
3138 self.snapshot_node_and(id, |node| node.blur(shell_provider));
3139 self.focus_node_id = None;
3140 }
3141 }
3142
3143 pub fn set_mousedown_node_id(&mut self, node_id: Option<NodeId>) {
3144 self.mousedown_node_id = node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
3145 }
3146 pub fn set_focus_to(&mut self, focus_node_id: NodeId) -> bool {
3147 let Some(focus_node_id) = self.nearest_non_anonymous_ancestor(focus_node_id) else {
3148 return false;
3149 };
3150 if Some(focus_node_id) == self.focus_node_id {
3151 return false;
3152 }
3153
3154 #[cfg(feature = "tracing")]
3155 tracing::info!("Focussed node {focus_node_id}");
3156
3157 let shell_provider = self.shell_provider.clone();
3158
3159 if let Some(id) = self.focus_node_id {
3161 self.snapshot_node_and(id, |node| node.blur(shell_provider.clone()));
3162 }
3163
3164 self.snapshot_node_and(focus_node_id, |node| node.focus(shell_provider));
3166
3167 self.focus_node_id = Some(focus_node_id);
3168
3169 true
3170 }
3171
3172 pub fn active_node(&mut self) -> bool {
3173 let Some(hover_node_id) = self.get_hover_node_id() else {
3174 return false;
3175 };
3176
3177 if let Some(active_node_id) = self.active_node_id {
3178 if active_node_id == hover_node_id {
3179 return true;
3180 }
3181 self.unactive_node();
3182 }
3183
3184 debug_assert!(
3186 self.get_node(hover_node_id)
3187 .is_some_and(|node| !node.is_anonymous()),
3188 "interaction state must reference DOM nodes, not layout-generated nodes"
3189 );
3190 let active_node_id = Some(hover_node_id);
3191
3192 let node_path = self.maybe_node_layout_ancestors(active_node_id);
3193 for &id in node_path.iter() {
3194 self.snapshot_node_and(id, |node| node.active());
3195 }
3196
3197 self.active_node_id = active_node_id;
3198
3199 true
3200 }
3201
3202 pub fn unactive_node(&mut self) -> bool {
3203 let Some(active_node_id) = self.active_node_id.take() else {
3204 return false;
3205 };
3206
3207 let node_path = self.maybe_node_layout_ancestors(Some(active_node_id));
3208 for &id in node_path.iter() {
3209 self.snapshot_node_and(id, |node| node.unactive());
3210 }
3211
3212 true
3213 }
3214
3215 pub fn hovered_scrollbar(&self) -> Option<crate::node::ScrollbarRef> {
3217 self.hovered_scrollbar
3218 }
3219
3220 pub fn scrollbar_drag_target(&self) -> Option<crate::node::ScrollbarRef> {
3222 match &self.drag_mode {
3223 DragMode::ScrollbarDrag(state) => Some(state.scrollbar),
3224 _ => None,
3225 }
3226 }
3227
3228 pub fn scrollbar_opacity(&self, node_id: NodeId) -> f32 {
3233 let interacting = |scrollbar: &crate::node::ScrollbarRef| scrollbar.node_id == node_id;
3234 if self.hovered_scrollbar.as_ref().is_some_and(interacting)
3235 || self
3236 .scrollbar_drag_target()
3237 .as_ref()
3238 .is_some_and(interacting)
3239 {
3240 return 1.0;
3241 }
3242 self.scrollbar_activity.get(&node_id).map_or(1.0, |last| {
3243 crate::node::scrollbar::opacity_at(last.elapsed())
3244 })
3245 }
3246
3247 pub(crate) fn show_scrollbars(&mut self, node_id: NodeId) {
3250 if cfg!(feature = "scrollbars") {
3251 self.scrollbar_activity.insert(node_id, Instant::now());
3252 }
3253 }
3254
3255 fn scrollbars_animating(&self) -> bool {
3258 use crate::node::scrollbar::{FADE_DELAY, FADE_DURATION};
3259 self.scrollbar_activity
3260 .values()
3261 .any(|last| last.elapsed() < FADE_DELAY + FADE_DURATION)
3262 }
3263
3264 pub(crate) fn hit_with_scrollbar(
3268 &self,
3269 x: f32,
3270 y: f32,
3271 ) -> (Option<HitResult>, Option<crate::node::ScrollbarRef>) {
3272 if TDocument::as_node(&self.root_node())
3273 .first_element_child()
3274 .is_none()
3275 {
3276 #[cfg(feature = "tracing")]
3277 tracing::warn!("No DOM - not resolving hit test");
3278 return (None, None);
3279 }
3280 let mut scrollbar = None;
3281 let hit = self
3282 .root_element()
3283 .hit_inner(x, y, self.viewport().scale_f64(), &mut scrollbar);
3284 (hit, scrollbar)
3285 }
3286
3287 pub fn set_hover_to(&mut self, x: f32, y: f32) -> bool {
3288 self.semantic_hover_node_id = None;
3289 self.last_client_pointer_position = Some(taffy::Point {
3293 x: x - self.viewport_scroll.x as f32,
3294 y: y - self.viewport_scroll.y as f32,
3295 });
3296
3297 let (hit, hovered_scrollbar) = self.hit_with_scrollbar(x, y);
3298 let hovered_scrollbar =
3301 hovered_scrollbar.filter(|scrollbar| self.scrollbar_opacity(scrollbar.node_id) > 0.0);
3302 let scrollbar_changed = hovered_scrollbar != self.hovered_scrollbar;
3306 if scrollbar_changed {
3307 for scrollbar in [self.hovered_scrollbar, hovered_scrollbar]
3310 .into_iter()
3311 .flatten()
3312 {
3313 self.show_scrollbars(scrollbar.node_id);
3314 }
3315 }
3316 self.hovered_scrollbar = hovered_scrollbar;
3317
3318 let hit_node_id = hit.map(|hit| hit.node_id);
3323 let hover_node_id = hit_node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
3324 let new_is_text = hit.map(|hit| hit.is_text).unwrap_or(false);
3325
3326 self.apply_hover_target(hit_node_id, hover_node_id, new_is_text, scrollbar_changed)
3327 }
3328
3329 pub fn set_hover_to_node(&mut self, node_id: NodeId, x: f32, y: f32) -> bool {
3337 self.semantic_hover_node_id = Some(node_id);
3338 self.last_client_pointer_position = Some(taffy::Point {
3339 x: x - self.viewport_scroll.x as f32,
3340 y: y - self.viewport_scroll.y as f32,
3341 });
3342
3343 let hovered_scrollbar = self.hovered_scrollbar.take();
3344 let scrollbar_changed = hovered_scrollbar.is_some();
3345 if let Some(scrollbar) = hovered_scrollbar {
3346 self.show_scrollbars(scrollbar.node_id);
3347 }
3348 let hover_node_id = self.nearest_non_anonymous_ancestor(node_id);
3349 self.apply_hover_target(Some(node_id), hover_node_id, false, scrollbar_changed)
3350 }
3351
3352 fn apply_hover_target(
3353 &mut self,
3354 hit_node_id: Option<NodeId>,
3355 hover_node_id: Option<NodeId>,
3356 new_is_text: bool,
3357 scrollbar_changed: bool,
3358 ) -> bool {
3359 let hit_changed =
3360 hit_node_id != self.hover_hit_node_id || new_is_text != self.hover_node_is_text;
3361 self.hover_hit_node_id = hit_node_id;
3362 self.hover_node_is_text = new_is_text;
3363
3364 if hover_node_id == self.hover_node_id {
3366 if hit_changed {
3367 self.shell_provider.set_cursor(self.get_cursor());
3371 }
3372 return scrollbar_changed;
3373 }
3374
3375 let old_node_path = self.maybe_node_layout_ancestors(self.hover_node_id);
3376 let new_node_path = self.maybe_node_layout_ancestors(hover_node_id);
3377 let same_count = old_node_path
3378 .iter()
3379 .zip(&new_node_path)
3380 .take_while(|(o, n)| o == n)
3381 .count();
3382 for &id in old_node_path.iter().skip(same_count) {
3383 self.snapshot_node_and(id, |node| node.unhover());
3384 }
3385 for &id in new_node_path.iter().skip(same_count) {
3386 self.snapshot_node_and(id, |node| node.hover());
3387 }
3388
3389 self.hover_node_id = hover_node_id;
3390
3391 self.shell_provider.set_cursor(self.get_cursor());
3393
3394 self.shell_provider.request_redraw();
3396
3397 true
3398 }
3399
3400 pub fn clear_hover(&mut self) -> bool {
3401 self.last_client_pointer_position = None;
3404 self.semantic_hover_node_id = None;
3405 self.hover_hit_node_id = None;
3406
3407 let Some(hover_node_id) = self.hover_node_id else {
3408 return false;
3409 };
3410
3411 let old_node_path = self.maybe_node_layout_ancestors(Some(hover_node_id));
3412 for &id in old_node_path.iter() {
3413 self.snapshot_node_and(id, |node| node.unhover());
3414 }
3415
3416 self.hover_node_id = None;
3417 self.hover_node_is_text = false;
3418
3419 self.shell_provider.set_cursor(self.get_cursor());
3421
3422 self.shell_provider.request_redraw();
3424
3425 true
3426 }
3427
3428 pub fn refresh_hover(&mut self) -> bool {
3434 if let Some(node_id) = self.semantic_hover_node_id {
3435 if self.get_node(node_id).is_some() {
3436 let hover_node_id = self.nearest_non_anonymous_ancestor(node_id);
3437 return self.apply_hover_target(Some(node_id), hover_node_id, false, false);
3438 }
3439 self.semantic_hover_node_id = None;
3440 }
3441 let Some(pos) = self.last_client_pointer_position else {
3442 return false;
3443 };
3444 let x = pos.x + self.viewport_scroll.x as f32;
3445 let y = pos.y + self.viewport_scroll.y as f32;
3446 self.set_hover_to(x, y)
3447 }
3448
3449 pub fn get_hover_node_id(&self) -> Option<NodeId> {
3450 self.hover_node_id
3451 }
3452
3453 pub fn get_mousedown_node_id(&self) -> Option<NodeId> {
3454 self.mousedown_node_id
3455 }
3456
3457 pub fn set_viewport(&mut self, viewport: Viewport) {
3458 let scale_has_changed = viewport.scale_f64() != self.viewport.scale_f64();
3459 self.viewport = viewport;
3460 self.set_stylist_device(make_device(
3461 &self.viewport,
3462 self.media_type.clone(),
3463 self.font_ctx.clone(),
3464 ));
3465 self.scroll_viewport_by(0.0, 0.0); if scale_has_changed {
3468 self.invalidate_inline_contexts();
3469 self.shell_provider.request_redraw();
3470 }
3471 }
3472
3473 pub fn media_type(&self) -> &MediaType {
3475 &self.media_type
3476 }
3477
3478 pub fn set_media_type(&mut self, media_type: MediaType) {
3481 if self.media_type == media_type {
3482 return;
3483 }
3484 self.media_type = media_type;
3485 self.set_stylist_device(make_device(
3486 &self.viewport,
3487 self.media_type.clone(),
3488 self.font_ctx.clone(),
3489 ));
3490 }
3491
3492 pub fn viewport(&self) -> &Viewport {
3493 &self.viewport
3494 }
3495
3496 pub fn viewport_mut(&mut self) -> ViewportMut<'_> {
3497 ViewportMut::new(self)
3498 }
3499
3500 pub fn zoom_by(&mut self, increment: f32) {
3501 let mut viewport = self.viewport.clone();
3502 viewport.zoom_by(increment);
3503 self.set_viewport(viewport);
3504 }
3505
3506 pub fn zoom_to(&mut self, zoom: f32) {
3507 let mut viewport = self.viewport.clone();
3508 viewport.set_zoom(zoom);
3509 self.set_viewport(viewport);
3510 }
3511
3512 pub fn get_viewport(&self) -> Viewport {
3513 self.viewport.clone()
3514 }
3515
3516 pub fn incremental_layout(&self) -> bool {
3518 self.incremental_layout
3519 }
3520
3521 pub fn set_incremental_layout(&mut self, enabled: bool) {
3523 self.incremental_layout = enabled;
3524 }
3525
3526 pub fn devtools(&self) -> &DevtoolSettings {
3527 &self.devtool_settings
3528 }
3529
3530 pub fn devtools_mut(&mut self) -> &mut DevtoolSettings {
3531 &mut self.devtool_settings
3532 }
3533
3534 pub fn subdoc(&self, node_id: NodeId) -> Option<&dyn Document> {
3535 self.get_node(node_id)
3536 .and_then(|node| node.element_data())
3537 .and_then(|el| el.sub_doc_data())
3538 }
3539
3540 pub fn subdoc_mut(&mut self, node_id: NodeId) -> Option<&mut dyn Document> {
3541 self.get_node_mut(node_id)
3542 .and_then(|node| node.element_data_mut())
3543 .and_then(|el| el.sub_doc_data_mut())
3544 }
3545
3546 pub fn is_animating(&self) -> bool {
3547 #[cfg(feature = "custom-widget")]
3548 let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
3549 self.nodes[node_id]
3550 .element_data()
3551 .and_then(|el| el.custom_widget_data())
3552 .is_some_and(|data| data.widget.requires_redraw())
3553 });
3554 #[cfg(not(feature = "custom-widget"))]
3555 let custom_widget_is_animating = false;
3556
3557 let animating = self.has_canvas
3558 | self.has_active_animations
3559 | (self.subdoc_animation_pacing != AnimationPacing::Idle)
3560 | custom_widget_is_animating
3561 | (self.scroll_animation != ScrollAnimationState::None)
3562 | self.scrollbars_animating();
3563
3564 if animating && crate::debug::animation_reasons_enabled() {
3565 crate::debug::report_animation_reasons(
3566 self.id(),
3567 self.has_canvas,
3568 self.has_active_animations,
3569 self.subdoc_animation_pacing != AnimationPacing::Idle,
3570 custom_widget_is_animating,
3571 self.scroll_animation != ScrollAnimationState::None,
3572 self.scrollbars_animating(),
3573 self.animating_node_names().as_deref(),
3574 );
3575 }
3576
3577 animating
3578 }
3579
3580 pub fn animation_pacing(&self) -> AnimationPacing {
3585 let focused_text_input = self.focus_node_id.is_some_and(|node_id| {
3586 self.nodes
3587 .get(node_id)
3588 .and_then(|node| node.element_data())
3589 .is_some_and(|element| element.text_input_data().is_some())
3590 });
3591 #[cfg(feature = "custom-widget")]
3592 let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
3593 self.nodes[node_id]
3594 .element_data()
3595 .and_then(|el| el.custom_widget_data())
3596 .is_some_and(|data| data.widget.requires_redraw())
3597 });
3598 #[cfg(not(feature = "custom-widget"))]
3599 let custom_widget_is_animating = false;
3600
3601 if self.has_canvas
3602 || custom_widget_is_animating
3603 || self.scroll_animation != ScrollAnimationState::None
3604 || self.scrollbars_animating()
3605 {
3606 AnimationPacing::Interactive
3607 } else if self.has_active_animations {
3608 const SLOW_ANIMATION_SECONDS: f64 = 2.0;
3609 let sets = self.animations.sets.read();
3610 let has_fast_animation_or_transition = sets.values().any(|set| {
3611 set.transitions.iter().any(|transition| {
3612 matches!(
3613 transition.state,
3614 AnimationState::Pending | AnimationState::Running
3615 )
3616 }) || set.animations.iter().any(|animation| {
3617 matches!(
3618 animation.state,
3619 AnimationState::Pending | AnimationState::Running
3620 ) && animation.duration < SLOW_ANIMATION_SECONDS
3621 })
3622 });
3623 if has_fast_animation_or_transition {
3624 AnimationPacing::Interactive
3625 } else {
3626 AnimationPacing::SlowCss
3627 }
3628 } else if focused_text_input {
3629 AnimationPacing::Caret
3630 } else if self.subdoc_animation_pacing != AnimationPacing::Idle {
3631 self.subdoc_animation_pacing
3632 } else {
3633 AnimationPacing::Idle
3634 }
3635 }
3636
3637 fn animating_node_names(&self) -> Option<String> {
3644 if !self.has_active_animations {
3645 return None;
3646 }
3647 let sets = self.animations.sets.read();
3648 let mut described: Vec<String> = sets
3649 .iter()
3650 .filter(|(_, state)| state.needs_animation_ticks())
3651 .filter_map(|(key, state)| {
3652 let node_id = NodeId::from_u64(key.node.id() as u64);
3653 let node = self.nodes.get(node_id)?;
3654 let element = node.element_data()?;
3655 let name = element
3656 .attr(local_name!("id"))
3657 .map(|id| format!("#{id}"))
3658 .or_else(|| {
3659 element
3660 .attr(local_name!("class"))
3661 .and_then(|c| c.split_ascii_whitespace().next())
3662 .map(|c| format!(".{c}"))
3663 })
3664 .unwrap_or_else(|| element.name.local.to_string());
3665 Some(format!(
3666 "{name}(anim={},trans={},in_doc={})",
3667 state.animations.len(),
3668 state.transitions.len(),
3669 node.flags.is_in_document(),
3670 ))
3671 })
3672 .collect();
3673 described.sort();
3674 described.truncate(12);
3675 Some(described.join(" "))
3676 }
3677
3678 pub fn set_stylist_device(&mut self, device: Device) {
3680 self.viewport_generation = self.viewport_generation.wrapping_add(1);
3681 let previous_viewport = self.stylist.device().au_viewport_size();
3684 let next_viewport = device.au_viewport_size();
3685
3686 let root_styles = self
3692 .try_root_element()
3693 .and_then(|root| root.primary_styles());
3694 if let Some(root_style) = root_styles.as_deref() {
3695 device.set_root_style(root_style);
3696
3697 let font = root_style.get_font();
3698 let font_size = font.clone_font_size().computed_size();
3699 device.set_root_font_size(root_style.effective_zoom.unzoom(font_size.px()));
3700
3701 let line_height = device
3702 .calc_line_height(font, root_style.writing_mode, None)
3703 .0;
3704 device.set_root_line_height(root_style.effective_zoom.unzoom(line_height.px()));
3705 }
3706 drop(root_styles);
3707
3708 let origins = {
3709 let guard = &self.guard;
3710 let guards = StylesheetGuards {
3711 author: &guard.read(),
3712 ua_or_user: &guard.read(),
3713 };
3714 self.stylist.set_device(device, &guards)
3715 };
3716 self.stylist.force_stylesheet_origins_dirty(origins);
3717 #[cfg(feature = "shadow-dom")]
3718 for host_id in self.shadow_host_node_ids() {
3719 if let Some(root_id) = self.shadow_root_id(host_id) {
3720 self.invalidate_shadow_styles(root_id);
3721 }
3722 }
3723
3724 if let Some(root_id) = self.try_root_element().map(|root| root.id) {
3727 self.nodes[root_id].set_restyle_hint(RestyleHint::recascade_subtree());
3728 self.shell_provider.request_redraw();
3729 }
3730 if previous_viewport != next_viewport {
3731 self.abspos_layout_dirty = true;
3732 }
3733 }
3734
3735 pub fn stylist_device(&mut self) -> &Device {
3736 self.stylist.device()
3737 }
3738
3739 pub fn get_cursor(&self) -> Option<CursorIcon> {
3747 let node_id = self
3752 .hover_hit_node_id
3753 .filter(|&id| self.nodes.contains_key(id))
3754 .or(self.get_hover_node_id());
3755 let Some(node_id) = node_id else {
3756 return Some(CursorIcon::Default);
3757 };
3758 let node = &self.nodes[node_id];
3759
3760 if let Some(subdoc) = node.subdoc().map(|doc| doc.inner()) {
3761 if subdoc.hover_hit_node_id.is_some() || subdoc.get_hover_node_id().is_some() {
3767 return subdoc.get_cursor();
3768 }
3769 return Some(CursorIcon::Default);
3770 }
3771
3772 let Some(style) = node.primary_styles() else {
3773 return Some(CursorIcon::Default);
3774 };
3775 let user_select = style.clone_user_select();
3776 let keyword = style.clone_cursor().keyword;
3777
3778 if keyword != CursorKind::Auto {
3780 return stylo_to_cursor_icon(keyword);
3781 }
3782
3783 if node
3785 .element_data()
3786 .is_some_and(|e| e.text_input_data().is_some())
3787 {
3788 return Some(CursorIcon::Text);
3789 }
3790
3791 let mut maybe_node = Some(node);
3793 while let Some(node) = maybe_node {
3794 if node.is_link() {
3795 return Some(CursorIcon::Pointer);
3796 }
3797
3798 maybe_node = node.layout_parent.get().map(|node_id| node.with(node_id));
3799 }
3800
3801 if self.hover_node_is_text {
3803 return Some(match user_select {
3804 UserSelect::Text | UserSelect::All | UserSelect::Auto => CursorIcon::Text,
3805 UserSelect::None => CursorIcon::Default,
3806 });
3807 }
3808
3809 Some(CursorIcon::Default)
3811 }
3812
3813 pub fn scroll_node_by<F: FnMut(DomEvent)>(
3814 &mut self,
3815 node_id: NodeId,
3816 x: f64,
3817 y: f64,
3818 dispatch_event: F,
3819 ) {
3820 self.scroll_node_by_has_changed(node_id, x, y, dispatch_event);
3821 }
3822
3823 pub fn scroll_node_by_has_changed<F: FnMut(DomEvent)>(
3833 &mut self,
3834 node_id: NodeId,
3835 x: f64,
3836 y: f64,
3837 dispatch_event: F,
3838 ) -> bool {
3839 let has_changed = self.scroll_node_by_inner(node_id, x, y, dispatch_event);
3840 if has_changed {
3841 self.resolve_positioned_scroll();
3842 }
3843 has_changed
3844 }
3845
3846 fn scroll_node_by_inner<F: FnMut(DomEvent)>(
3847 &mut self,
3848 node_id: NodeId,
3849 x: f64,
3850 y: f64,
3851 mut dispatch_event: F,
3852 ) -> bool {
3853 if self.try_root_element().is_some_and(|el| el.id == node_id) {
3858 let has_changed = self.scroll_viewport_by_has_changed(x, y);
3859 if has_changed {
3860 let layout = *self.root_element().final_layout();
3861 let scale = self.viewport.scale() as f64;
3862 let event = BlitzScrollEvent {
3863 scroll_top: self.viewport_scroll.y,
3864 scroll_left: self.viewport_scroll.x,
3865 scroll_width: layout.size.width.max(layout.content_size.width) as i32,
3866 scroll_height: layout.size.height.max(layout.content_size.height) as i32,
3867 client_width: (self.viewport.window_size.0 as f64 / scale) as i32,
3868 client_height: (self.viewport.window_size.1 as f64 / scale) as i32,
3869 };
3870 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
3871 }
3872 return has_changed;
3873 }
3874
3875 let Some(node) = self.nodes.get_mut(node_id) else {
3876 return false;
3877 };
3878
3879 if node
3883 .element_data()
3884 .is_some_and(|el| el.text_input_data().is_some())
3885 {
3886 let parent = node.parent;
3887 let content_box_width = node.final_layout().content_box_width();
3888 let content_box_height = node.final_layout().content_box_height();
3889 let input = node
3890 .element_data_mut()
3891 .and_then(|el| el.text_input_data_mut())
3892 .unwrap();
3893
3894 let (bubble_x, bubble_y) = if input.is_multiline {
3895 (
3896 x,
3897 input.scroll_by(y as f32, content_box_width, content_box_height) as f64,
3898 )
3899 } else {
3900 (
3901 input.scroll_by(x as f32, content_box_width, content_box_height) as f64,
3902 y,
3903 )
3904 };
3905
3906 let has_changed = bubble_x != x || bubble_y != y;
3907
3908 if bubble_x != 0.0 || bubble_y != 0.0 {
3909 let bubbled = if let Some(parent) = parent {
3910 self.scroll_node_by_inner(parent, bubble_x, bubble_y, dispatch_event)
3911 } else {
3912 self.scroll_viewport_by_has_changed(bubble_x, bubble_y)
3913 };
3914 return bubbled | has_changed;
3915 }
3916
3917 return has_changed;
3918 }
3919
3920 let (can_x_scroll, can_y_scroll) = node
3921 .primary_styles()
3922 .map(|styles| {
3923 (
3924 matches!(styles.clone_overflow_x(), Overflow::Scroll | Overflow::Auto),
3925 matches!(styles.clone_overflow_y(), Overflow::Scroll | Overflow::Auto),
3926 )
3927 })
3928 .unwrap_or((false, false));
3929
3930 let initial = *node.scroll_offset();
3931 let new_x = node.scroll_offset().x - x;
3932 let new_y = node.scroll_offset().y - y;
3933
3934 let mut bubble_x = 0.0;
3935 let mut bubble_y = 0.0;
3936
3937 let scroll_width = node.final_layout().scroll_width() as f64;
3938 let scroll_height = node.final_layout().scroll_height() as f64;
3939
3940 if let Some(mut sub_doc) = node.subdoc_mut().map(|doc| doc.inner_mut()) {
3942 let has_changed = if let Some(hover_node_id) = sub_doc.get_hover_node_id() {
3943 sub_doc.scroll_node_by_has_changed(hover_node_id, x, y, dispatch_event)
3944 } else {
3945 sub_doc.scroll_viewport_by_has_changed(x, y)
3946 };
3947
3948 return has_changed;
3950 }
3951
3952 if !can_x_scroll {
3954 bubble_x = x
3955 } else if new_x < 0.0 {
3956 bubble_x = -new_x;
3957 node.scroll_offset_mut().x = 0.0;
3958 } else if new_x > scroll_width {
3959 bubble_x = scroll_width - new_x;
3960 node.scroll_offset_mut().x = scroll_width;
3961 } else {
3962 node.scroll_offset_mut().x = new_x;
3963 }
3964
3965 if !can_y_scroll {
3966 bubble_y = y
3967 } else if new_y < 0.0 {
3968 bubble_y = -new_y;
3969 node.scroll_offset_mut().y = 0.0;
3970 } else if new_y > scroll_height {
3971 bubble_y = scroll_height - new_y;
3972 node.scroll_offset_mut().y = scroll_height;
3973 } else {
3974 node.scroll_offset_mut().y = new_y;
3975 }
3976
3977 let has_changed = *node.scroll_offset() != initial;
3978
3979 if has_changed {
3980 let layout = *node.final_layout();
3981 let event = BlitzScrollEvent {
3982 scroll_top: node.scroll_offset().y,
3983 scroll_left: node.scroll_offset().x,
3984 scroll_width: layout.scroll_width() as i32,
3985 scroll_height: layout.scroll_height() as i32,
3986 client_width: layout.size.width as i32,
3987 client_height: layout.size.height as i32,
3988 };
3989
3990 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
3991 }
3992
3993 let parent = node.parent;
3994 if has_changed {
3995 self.show_scrollbars(node_id);
3996 }
3997
3998 if bubble_x != 0.0 || bubble_y != 0.0 {
3999 if let Some(parent) = parent {
4000 return self.scroll_node_by_inner(parent, bubble_x, bubble_y, dispatch_event)
4001 | has_changed;
4002 } else {
4003 return self.scroll_viewport_by_has_changed(bubble_x, bubble_y) | has_changed;
4004 }
4005 }
4006
4007 has_changed
4008 }
4009
4010 pub fn scroll_viewport_by(&mut self, x: f64, y: f64) {
4011 self.scroll_viewport_by_has_changed(x, y);
4012 }
4013
4014 pub fn scroll_viewport_by_has_changed(&mut self, x: f64, y: f64) -> bool {
4016 let (content_width, content_height) = match self.try_root_element() {
4021 Some(root) => {
4022 let root_layout = root.final_layout();
4023 (
4024 root_layout.size.width.max(root_layout.content_size.width) as f64,
4025 root_layout.size.height.max(root_layout.content_size.height) as f64,
4026 )
4027 }
4028 None => (0.0, 0.0),
4029 };
4030 let new_scroll = (self.viewport_scroll.x - x, self.viewport_scroll.y - y);
4031 let window_width = self.viewport.window_size.0 as f64 / self.viewport.scale() as f64;
4032 let window_height = self.viewport.window_size.1 as f64 / self.viewport.scale() as f64;
4033
4034 let initial = self.viewport_scroll;
4035 self.viewport_scroll.x =
4036 f64::max(0.0, f64::min(new_scroll.0, content_width - window_width));
4037 self.viewport_scroll.y =
4038 f64::max(0.0, f64::min(new_scroll.1, content_height - window_height));
4039
4040 let has_changed = self.viewport_scroll != initial;
4041 if has_changed {
4042 self.resolve_positioned_scroll();
4047 }
4048 has_changed
4049 }
4050
4051 pub fn scroll_by(
4052 &mut self,
4053 anchor_node_id: Option<NodeId>,
4054 scroll_x: f64,
4055 scroll_y: f64,
4056 dispatch_event: &mut dyn FnMut(DomEvent),
4057 ) -> bool {
4058 if let Some(anchor_node_id) = anchor_node_id {
4059 self.scroll_node_by_has_changed(anchor_node_id, scroll_x, scroll_y, dispatch_event)
4060 } else {
4061 self.scroll_viewport_by_has_changed(scroll_x, scroll_y)
4062 }
4063 }
4064
4065 pub fn viewport_scroll(&self) -> crate::Point<f64> {
4066 self.viewport_scroll
4067 }
4068
4069 pub fn set_viewport_scroll(&mut self, scroll: crate::Point<f64>) {
4070 self.viewport_scroll = scroll;
4071 }
4072
4073 pub fn get_fragment_target(&self, fragment: &str) -> Option<NodeId> {
4078 if let Some(node_id) = self.get_element_by_id(fragment) {
4079 return Some(node_id);
4080 }
4081
4082 self.nodes.iter().find_map(|(id, node)| {
4084 let el = node.element_data()?;
4085 (el.name.local == local_name!("a") && el.attr(local_name!("name")) == Some(fragment))
4086 .then_some(id)
4087 })
4088 }
4089
4090 pub fn nearest_scroll_container(&self, node_id: NodeId) -> Option<NodeId> {
4101 let mut current = Some(node_id);
4102 for _ in 0..64 {
4103 let id = current?;
4104 let node = self.nodes.get(id)?;
4105 if node.style().overflow.x.is_scroll_container()
4106 || node.style().overflow.y.is_scroll_container()
4107 {
4108 return Some(id);
4109 }
4110 current = node.parent;
4111 }
4112 None
4113 }
4114
4115 pub fn scroll_nearest_container_by(&mut self, node_id: NodeId, x: f64, y: f64) -> bool {
4116 self.scroll_nearest_container_by_with_events(node_id, x, y, |_| {})
4117 }
4118
4119 pub fn scroll_nearest_container_by_with_events<F: FnMut(DomEvent)>(
4120 &mut self,
4121 node_id: NodeId,
4122 x: f64,
4123 y: f64,
4124 mut dispatch_event: F,
4125 ) -> bool {
4126 let mut current = Some(node_id);
4127 for _ in 0..64 {
4128 let Some(id) = current else { break };
4129 let Some(node) = self.nodes.get(id) else {
4130 break;
4131 };
4132 let scrolls = node.style().overflow.x.is_scroll_container()
4133 || node.style().overflow.y.is_scroll_container();
4134 if scrolls {
4135 self.scroll_node_by(id, x, y, &mut dispatch_event);
4136 return true;
4137 }
4138 current = node.parent;
4139 }
4140 self.scroll_viewport_by(x, y);
4141 false
4142 }
4143
4144 pub fn scroll_to_node(&mut self, node_id: NodeId) {
4145 self.scroll_to_node_with_events(node_id, |_| {});
4146 }
4147
4148 pub fn scroll_to_node_with_events<F: FnMut(DomEvent)>(
4149 &mut self,
4150 node_id: NodeId,
4151 dispatch_event: F,
4152 ) {
4153 self.scroll_to_node_aligned_with_events(node_id, false, dispatch_event);
4154 }
4155
4156 pub fn scroll_to_node_centered_with_events<F: FnMut(DomEvent)>(
4162 &mut self,
4163 node_id: NodeId,
4164 dispatch_event: F,
4165 ) {
4166 self.scroll_to_node_aligned_with_events(node_id, true, dispatch_event);
4167 }
4168
4169 fn scroll_to_node_aligned_with_events<F: FnMut(DomEvent)>(
4170 &mut self,
4171 node_id: NodeId,
4172 centered: bool,
4173 mut dispatch_event: F,
4174 ) {
4175 let mut scroll_target = Some(node_id);
4182 let node_id = loop {
4183 let Some(candidate) = scroll_target else {
4184 return;
4185 };
4186 let Some(node) = self.nodes.get(candidate) else {
4187 return;
4188 };
4189 if matches!(
4190 node.data,
4191 NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
4192 ) {
4193 break candidate;
4194 }
4195 scroll_target = node.layout_parent.get().or(node.parent);
4196 };
4197
4198 let mut chain = Vec::new();
4210 let mut current = self.nodes.get(node_id).and_then(|node| node.parent);
4211 while let Some(id) = current {
4212 let Some(node) = self.nodes.get(id) else {
4213 break;
4214 };
4215 let scrolls = node.style().overflow.x.is_scroll_container()
4216 || node.style().overflow.y.is_scroll_container();
4217 if scrolls {
4218 chain.push(id);
4219 }
4220 current = node.parent;
4221 }
4222
4223 for container in chain {
4227 let Some(node) = self.nodes.get(node_id) else {
4228 return;
4229 };
4230 let target = node.absolute_position(0.0, 0.0);
4231 let Some(scroller) = self.nodes.get(container) else {
4232 continue;
4233 };
4234 let box_ = scroller.absolute_position(0.0, 0.0);
4235 let layout = scroller.final_layout();
4236 let target_layout = node.final_layout();
4237 let (dx, dy) = if centered {
4241 (
4242 f64::from(
4243 box_.x + layout.size.width / 2.0
4244 - (target.x + target_layout.size.width / 2.0),
4245 ),
4246 f64::from(
4247 box_.y + layout.size.height / 2.0
4248 - (target.y + target_layout.size.height / 2.0),
4249 ),
4250 )
4251 } else {
4252 (f64::from(box_.x - target.x), f64::from(box_.y - target.y))
4253 };
4254 self.scroll_node_by(container, dx, dy, &mut dispatch_event);
4255 }
4256
4257 let Some(node) = self.nodes.get(node_id) else {
4260 return;
4261 };
4262 let target = node.absolute_position(0.0, 0.0);
4263 let current = self.viewport_scroll;
4264 let (desired_x, desired_y) = if centered {
4265 let target_layout = node.final_layout();
4266 let scale = self.viewport.scale();
4267 let viewport_width = self.viewport.window_size.0 as f32 / scale;
4268 let viewport_height = self.viewport.window_size.1 as f32 / scale;
4269 (
4270 f64::from(target.x + target_layout.size.width / 2.0 - viewport_width / 2.0)
4271 .max(0.0),
4272 f64::from(target.y + target_layout.size.height / 2.0 - viewport_height / 2.0)
4273 .max(0.0),
4274 )
4275 } else {
4276 (f64::from(target.x), f64::from(target.y))
4277 };
4278
4279 let dx = current.x - desired_x;
4282 let dy = current.y - desired_y;
4283 if let Some(root) = self.try_root_element().map(|element| element.id) {
4284 self.scroll_node_by(root, dx, dy, dispatch_event);
4285 } else {
4286 self.scroll_viewport_by(dx, dy);
4287 }
4288 }
4289
4290 pub fn scroll_to_fragment(&mut self, fragment: &str) -> bool {
4296 let decoded = percent_encoding::percent_decode_str(fragment)
4298 .decode_utf8_lossy()
4299 .into_owned();
4300
4301 if !decoded.is_empty() {
4302 if let Some(node_id) = self.get_fragment_target(&decoded) {
4303 self.scroll_to_node(node_id);
4304 return true;
4305 }
4306 }
4307
4308 if decoded.is_empty() || decoded.eq_ignore_ascii_case("top") {
4311 let current = self.viewport_scroll;
4312 self.scroll_viewport_by(current.x, current.y);
4313 return true;
4314 }
4315
4316 false
4317 }
4318
4319 pub fn get_svg_bbox(&self, node_id: NodeId) -> Option<BoundingRect> {
4322 #[cfg(feature = "svg")]
4323 {
4324 self.svg_user_bbox(node_id)
4325 }
4326 #[cfg(not(feature = "svg"))]
4327 {
4328 let _ = node_id;
4329 None
4330 }
4331 }
4332
4333 pub fn get_client_bounding_rect(&self, node_id: NodeId) -> Option<BoundingRect> {
4335 #[cfg(feature = "svg")]
4336 if let Some(root) = self.svg_geometry_root(node_id) {
4337 return self.svg_client_rect(root, node_id);
4338 }
4339
4340 if let Some(rects) = self.inline_fragment_rects(node_id) {
4343 let rects: Vec<_> = rects
4344 .into_iter()
4345 .map(|rect| self.transformed_client_rect(node_id, rect))
4346 .collect();
4347 let x0 = rects.iter().map(|r| r.x).fold(f64::INFINITY, f64::min);
4348 let y0 = rects.iter().map(|r| r.y).fold(f64::INFINITY, f64::min);
4349 let x1 = rects
4350 .iter()
4351 .map(|r| r.x + r.width)
4352 .fold(f64::NEG_INFINITY, f64::max);
4353 let y1 = rects
4354 .iter()
4355 .map(|r| r.y + r.height)
4356 .fold(f64::NEG_INFINITY, f64::max);
4357 return match rects.is_empty() {
4358 true => None,
4359 false => Some(BoundingRect {
4360 x: x0,
4361 y: y0,
4362 width: x1 - x0,
4363 height: y1 - y0,
4364 }),
4365 };
4366 }
4367
4368 let node = self.get_node(node_id)?;
4369 if !matches!(
4370 node.data,
4371 NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
4372 ) {
4373 return None;
4374 }
4375 let pos = node.absolute_position(0.0, 0.0);
4376
4377 Some(self.transformed_client_rect(
4378 node_id,
4379 BoundingRect {
4380 x: pos.x as f64 - self.viewport_scroll.x,
4381 y: pos.y as f64 - self.viewport_scroll.y,
4382 width: node.unrounded_layout().size.width as f64,
4383 height: node.unrounded_layout().size.height as f64,
4384 },
4385 ))
4386 }
4387
4388 pub(crate) fn transformed_client_rect(
4391 &self,
4392 node_id: NodeId,
4393 rect: BoundingRect,
4394 ) -> BoundingRect {
4395 let mut matrix = kurbo::Affine::IDENTITY;
4396 let mut current = self.get_node(node_id);
4397 let scale = self.viewport.scale_f64();
4398 while let Some(node) = current {
4399 if matches!(
4400 node.data,
4401 NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
4402 ) && let Some(transform) = *node.transform()
4403 {
4404 let [a, b, c, d, e, f] = transform.as_coeffs();
4405 let css_transform = kurbo::Affine::new([a, b, c, d, e / scale, f / scale]);
4406 let origin = node.absolute_position(0.0, 0.0);
4407 let translate = kurbo::Affine::translate((
4408 origin.x as f64 - self.viewport_scroll.x,
4409 origin.y as f64 - self.viewport_scroll.y,
4410 ));
4411 matrix = translate * css_transform * translate.inverse() * matrix;
4412 }
4413 current = node
4414 .layout_parent
4415 .get()
4416 .or(node.parent)
4417 .and_then(|id| self.get_node(id));
4418 }
4419 let transformed = matrix.transform_rect_bbox(kurbo::Rect::new(
4420 rect.x,
4421 rect.y,
4422 rect.x + rect.width,
4423 rect.y + rect.height,
4424 ));
4425 BoundingRect {
4426 x: transformed.x0,
4427 y: transformed.y0,
4428 width: transformed.width(),
4429 height: transformed.height(),
4430 }
4431 }
4432
4433 pub fn node_client_rects(&self, node_id: NodeId) -> Vec<BoundingRect> {
4438 #[cfg(feature = "svg")]
4439 if self.svg_geometry_root(node_id).is_some() {
4440 return self.get_client_bounding_rect(node_id).into_iter().collect();
4441 }
4442 match self.inline_fragment_rects(node_id) {
4443 Some(rects) => rects
4444 .into_iter()
4445 .map(|rect| self.transformed_client_rect(node_id, rect))
4446 .collect(),
4447 None => self.get_client_bounding_rect(node_id).into_iter().collect(),
4448 }
4449 }
4450
4451 pub(crate) fn trace_escaped_inline_fragments(&self) {
4465 static TRACE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4466 if !*TRACE.get_or_init(|| std::env::var_os("BLITZ_TRACE_INLINE").is_some()) {
4467 return;
4468 }
4469 let mut reported = 0;
4470 for (id, node) in self.nodes.iter() {
4471 if !node.is_element() {
4472 continue;
4473 }
4474 let Some(rects) = self.inline_fragment_rects(id) else {
4475 continue;
4476 };
4477 let Some(root) = node.inline_root_ancestor() else {
4478 continue;
4479 };
4480 let root_layout = root.final_layout();
4481 let root_pos = root.absolute_position(0.0, 0.0);
4482 let root_right =
4483 root_pos.x as f64 + root_layout.size.width as f64 - self.viewport_scroll.x;
4484 for rect in &rects {
4485 if rect.x + rect.width > root_right + 1.0 {
4486 reported += 1;
4487 if reported <= 12 {
4488 eprintln!(
4489 "escaped-fragment node={id:?} rect=[{:.1},{:.1} {:.1}x{:.1}] \
4490root={:?} root_right={root_right:.1} root_w={:.1} lines={} layout_scale={:.2} vp_scale={:.2} layout_w={:.1}",
4491 rect.x,
4492 rect.y,
4493 rect.width,
4494 rect.height,
4495 root.id,
4496 root_layout.size.width,
4497 root.element_data()
4498 .and_then(|e| e.inline_layout_data.as_ref())
4499 .map(|i| i.layout.len())
4500 .unwrap_or(0),
4501 root.element_data()
4502 .and_then(|e| e.inline_layout_data.as_ref())
4503 .map(|i| i.layout.scale())
4504 .unwrap_or(0.0),
4505 self.viewport.scale(),
4506 root.element_data()
4507 .and_then(|e| e.inline_layout_data.as_ref())
4508 .map(|i| i.layout.width())
4509 .unwrap_or(0.0),
4510 );
4511 }
4512 break;
4513 }
4514 }
4515 }
4516 if reported > 0 {
4517 eprintln!("escaped-fragment total={reported}");
4518 }
4519
4520 let mut narrow = 0;
4525 for (id, node) in self.nodes.iter() {
4526 let Some(inline) = node
4527 .data
4528 .downcast_element()
4529 .and_then(|element| element.inline_layout_data.as_ref())
4530 else {
4531 continue;
4532 };
4533 let box_width = node.final_layout().size.width as f64 * self.viewport.scale() as f64;
4534 let broken_at = inline.layout.width() as f64;
4535 let full = inline.layout.calculate_content_widths().max as f64;
4538 if box_width > 40.0 && broken_at < box_width * 0.6 && full > box_width * 0.9 {
4539 narrow += 1;
4540 if narrow <= 12 {
4541 eprintln!(
4542 "narrow-break node={id:?} broken_at={broken_at:.1} box={box_width:.1} \
4543 max_content={full:.1} lines={} text={:?}",
4544 inline.layout.len(),
4545 inline.text.chars().take(40).collect::<String>(),
4546 );
4547 }
4548 }
4549 }
4550 if narrow > 0 {
4551 eprintln!("narrow-break total={narrow}");
4552 }
4553 }
4554
4555 pub fn inline_fragment_rects(&self, node_id: NodeId) -> Option<Vec<BoundingRect>> {
4556 use parley::PositionedLayoutItem;
4557
4558 let node = self.get_node(node_id)?;
4559
4560 let is_text = node.is_text_node();
4563 if !is_text {
4564 if !node.is_element() || node.flags.is_inline_root() {
4565 return None;
4566 }
4567 let display = node.primary_styles()?.clone_display();
4568 if !(display.outside() == DisplayOutside::Inline
4569 && display.inside() == DisplayInside::Flow)
4570 {
4571 return None;
4572 }
4573 }
4574
4575 let inline_root = node.inline_root_ancestor()?;
4576 let inline_layout = inline_root.element_data()?.inline_layout_data.as_ref()?;
4577 let layout = &inline_layout.layout;
4578 let scale = layout.scale() as f64;
4579
4580 let is_in_target = |mut id: NodeId| -> bool {
4583 loop {
4584 if id == node_id {
4585 return true;
4586 }
4587 if id == inline_root.id {
4588 return false;
4589 }
4590 match self.get_node(id).and_then(|n| n.parent) {
4591 Some(parent) => id = parent,
4592 None => return false,
4593 }
4594 }
4595 };
4596
4597 let root_layout = inline_root.final_layout();
4599 let root_pos = inline_root.absolute_position(0.0, 0.0);
4600 let origin_x = root_pos.x as f64
4601 + (root_layout.padding.left + root_layout.border.left) as f64
4602 - self.viewport_scroll.x;
4603 let origin_y = root_pos.y as f64
4604 + (root_layout.padding.top + root_layout.border.top) as f64
4605 - self.viewport_scroll.y;
4606
4607 let mut rects: Vec<BoundingRect> = Vec::new();
4608 for line in layout.lines() {
4609 let line_metrics = line.metrics();
4610 let mut line_rect: Option<(f64, f64, f64, f64)> = None;
4612 let mut add = |x0: f64, y0: f64, x1: f64, y1: f64| {
4613 line_rect = Some(match line_rect {
4614 Some((lx0, ly0, lx1, ly1)) => {
4615 (lx0.min(x0), ly0.min(y0), lx1.max(x1), ly1.max(y1))
4616 }
4617 None => (x0, y0, x1, y1),
4618 });
4619 };
4620
4621 for item in line.items() {
4622 match item {
4623 PositionedLayoutItem::GlyphRun(glyph_run) => {
4624 let brush = glyph_run.style().brush;
4625 let matches = if is_text {
4626 brush.text_node == Some(node_id)
4627 } else {
4628 is_in_target(brush.id)
4629 };
4630 if !matches {
4631 continue;
4632 }
4633 let x0 = glyph_run.offset() as f64;
4634 let x1 = x0 + glyph_run.advance() as f64;
4635 let y0 = line_metrics.block_min_coord as f64;
4641 let y1 = line_metrics.block_max_coord as f64;
4642 add(x0, y0, x1, y1);
4643 }
4644 PositionedLayoutItem::InlineBox(inline_box) => {
4645 if is_text || !is_in_target(NodeId::from_u64(inline_box.id)) {
4646 continue;
4647 }
4648 let x0 = inline_box.x as f64;
4649 let y0 = inline_box.y as f64;
4650 add(
4651 x0,
4652 y0,
4653 x0 + inline_box.width as f64,
4654 y0 + inline_box.height as f64,
4655 );
4656 }
4657 }
4658 }
4659
4660 if let Some((x0, y0, x1, y1)) = line_rect {
4661 rects.push(BoundingRect {
4662 x: origin_x + x0 / scale,
4663 y: origin_y + y0 / scale,
4664 width: (x1 - x0) / scale,
4665 height: (y1 - y0) / scale,
4666 });
4667 }
4668 }
4669
4670 Some(rects)
4671 }
4672
4673 pub fn find_title_node(&self) -> Option<&Node> {
4674 TreeTraverser::new(self)
4675 .find(|node_id| {
4676 let node = &self.nodes[*node_id];
4677 let Some(element) = node.element_data() else {
4678 return false;
4679 };
4680 if element.name.ns != ns!(html) || element.name.local != local_name!("title") {
4681 return false;
4682 }
4683 node.parent
4684 .and_then(|parent_id| self.nodes.get(parent_id))
4685 .and_then(Node::element_data)
4686 .is_some_and(|parent| {
4687 parent.name.ns == ns!(html) && parent.name.local == local_name!("head")
4688 })
4689 })
4690 .map(|node_id| &self.nodes[node_id])
4691 }
4692
4693 pub fn with_text_input(
4694 &mut self,
4695 node_id: NodeId,
4696 cb: impl FnOnce(PlainEditorDriver<TextBrush>),
4697 ) {
4698 let Some(node) = self.nodes.get_mut(node_id) else {
4699 return;
4700 };
4701
4702 if let Some(text_input) = node
4703 .element_data_mut()
4704 .and_then(|el| el.text_input_data_mut())
4705 {
4706 let mut font_ctx = self.font_ctx.lock().unwrap();
4707 let layout_ctx = &mut self.layout_ctx;
4708 let driver = text_input.editor.driver(&mut font_ctx, layout_ctx);
4709 cb(driver)
4710 }
4711 }
4712
4713 pub(crate) fn clamp_text_input_scroll(&mut self, node_id: NodeId) {
4716 let Some(node) = self.nodes.get_mut(node_id) else {
4717 return;
4718 };
4719
4720 let content_box_width = node.final_layout().content_box_width();
4721 let content_box_height = node.final_layout().content_box_height();
4722
4723 if let Some(text_input) = node
4724 .element_data_mut()
4725 .and_then(|el| el.text_input_data_mut())
4726 {
4727 text_input.clamp_scroll_offset(content_box_width, content_box_height);
4728 }
4729 }
4730
4731 pub(crate) fn compute_has_canvas(&self) -> bool {
4732 TreeTraverser::new(self).any(|node_id| {
4733 let node = &self.nodes[node_id];
4734 let Some(element) = node.element_data() else {
4735 return false;
4736 };
4737 if element.name.local == local_name!("canvas") && element.has_attr(local_name!("src")) {
4738 return true;
4739 }
4740
4741 false
4742 })
4743 }
4744
4745 pub fn find_text_position(&self, x: f32, y: f32) -> Option<(NodeId, usize)> {
4751 let hit = self.hit(x, y)?;
4752 let hit_node = self.get_node(hit.node_id)?;
4753 let inline_root = hit_node.inline_root_ancestor()?;
4754 let byte_offset = inline_root.text_offset_at_point(hit.x, hit.y)?;
4755 Some((inline_root.id, byte_offset))
4756 }
4757
4758 pub fn find_text_range(
4764 &self,
4765 x: f32,
4766 y: f32,
4767 granularity: TextGranularity,
4768 ) -> Option<(NodeId, usize, usize)> {
4769 let hit = self.hit(x, y)?;
4770 let hit_node = self.get_node(hit.node_id)?;
4771 let inline_root = hit_node.inline_root_ancestor()?;
4772 let range = inline_root.text_range_at_point(hit.x, hit.y, granularity)?;
4773 Some((inline_root.id, range.start, range.end))
4774 }
4775
4776 pub fn set_text_selection(
4778 &mut self,
4779 anchor_node: NodeId,
4780 anchor_offset: usize,
4781 focus_node: NodeId,
4782 focus_offset: usize,
4783 ) {
4784 self.dom_selection = None;
4785 self.text_selection =
4786 TextSelection::new(anchor_node, anchor_offset, focus_node, focus_offset);
4787
4788 if let (Some(parent), Some(idx)) = self.anonymous_block_location(anchor_node) {
4790 self.text_selection
4791 .anchor
4792 .set_anonymous(parent, idx, anchor_offset);
4793 }
4794 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
4795 self.text_selection
4796 .focus
4797 .set_anonymous(parent, idx, focus_offset);
4798 }
4799 }
4800
4801 fn anonymous_block_location(&self, node_id: NodeId) -> (Option<NodeId>, Option<usize>) {
4804 let Some(node) = self.get_node(node_id) else {
4805 return (None, None);
4806 };
4807
4808 if !node.is_anonymous() {
4809 return (None, None);
4810 }
4811
4812 let Some(parent_id) = node.parent else {
4813 return (None, None);
4814 };
4815
4816 let Some(parent) = self.get_node(parent_id) else {
4817 return (Some(parent_id), None);
4818 };
4819
4820 let layout_children = parent.layout_children.borrow();
4821 let Some(children) = layout_children.as_ref() else {
4822 return (Some(parent_id), None);
4823 };
4824
4825 let mut anon_index = 0;
4827 for &child_id in children.iter() {
4828 if child_id == node_id {
4829 return (Some(parent_id), Some(anon_index));
4830 }
4831 if self.get_node(child_id).is_some_and(|n| n.is_anonymous()) {
4832 anon_index += 1;
4833 }
4834 }
4835
4836 (Some(parent_id), None)
4837 }
4838
4839 pub fn clear_text_selection(&mut self) {
4841 self.dom_selection = None;
4842 self.text_selection.clear();
4843 }
4844
4845 pub fn update_selection_focus(&mut self, focus_node: NodeId, focus_offset: usize) {
4847 self.dom_selection = None;
4848 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
4850 self.text_selection
4851 .focus
4852 .set_anonymous(parent, idx, focus_offset);
4853 } else {
4854 self.text_selection.set_focus(focus_node, focus_offset);
4855 }
4856 }
4857
4858 pub fn extend_text_selection_to_point(&mut self, x: f32, y: f32) -> bool {
4861 if !self.text_selection.anchor.is_some() {
4862 return false;
4863 }
4864
4865 if let Some((node, offset)) = self.find_text_position(x, y) {
4866 self.update_selection_focus(node, offset);
4867 self.shell_provider.request_redraw();
4868 true
4869 } else {
4870 false
4871 }
4872 }
4873
4874 fn find_anonymous_block_by_index(
4876 &self,
4877 parent_id: NodeId,
4878 target_index: usize,
4879 ) -> Option<NodeId> {
4880 let parent = self.get_node(parent_id)?;
4881 let layout_children = parent.layout_children.borrow();
4882 let children = layout_children.as_ref()?;
4883
4884 children
4885 .iter()
4886 .filter(|&&child_id| self.get_node(child_id).is_some_and(|n| n.is_anonymous()))
4887 .nth(target_index)
4888 .copied()
4889 }
4890
4891 pub fn has_text_selection(&self) -> bool {
4893 self.dom_selection.as_ref().map_or_else(
4894 || self.text_selection.is_active(),
4895 |range| !range.bounds().collapsed(),
4896 )
4897 }
4898
4899 pub fn get_selected_text(&self) -> Option<String> {
4901 if let Some(range) = &self.dom_selection {
4902 let text = self.range_string(range.bounds());
4903 return (!text.is_empty()).then_some(text);
4904 }
4905 let ranges = self.get_text_selection_ranges();
4906 if ranges.is_empty() {
4907 return None;
4908 }
4909
4910 let mut result = String::new();
4911 for (node_id, start, end) in &ranges {
4912 let node = self.get_node(*node_id)?;
4913 let element_data = node.element_data()?;
4914 let inline_layout = element_data.inline_layout_data.as_ref()?;
4915
4916 if *end > inline_layout.text.len() {
4917 continue;
4918 }
4919
4920 if !result.is_empty() {
4921 result.push(' ');
4922 }
4923 result.push_str(&inline_layout.text[*start..*end]);
4924 }
4925
4926 if result.is_empty() {
4927 None
4928 } else {
4929 Some(result)
4930 }
4931 }
4932
4933 pub fn get_selected_html(&self) -> Option<String> {
4946 let ranges = self.get_text_selection_ranges();
4947 if ranges.is_empty() {
4948 return None;
4949 }
4950
4951 let mut result = String::new();
4952 for (node_id, start, end) in &ranges {
4953 let Some(node) = self.get_node(*node_id) else {
4954 continue;
4955 };
4956 let Some(element_data) = node.element_data() else {
4957 continue;
4958 };
4959 let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
4960 continue;
4961 };
4962 if *end > inline_layout.text.len() {
4963 continue;
4964 }
4965
4966 if !result.is_empty() {
4967 result.push(' ');
4968 }
4969
4970 let tag = element_data.name.local.as_ref();
4971 let fragment = escape_html(&inline_layout.text[*start..*end]);
4972 if matches!(
4977 tag,
4978 "b" | "strong" | "i" | "em" | "code" | "kbd" | "mark" | "s" | "u" | "sub" | "sup"
4979 ) {
4980 result.push_str(&format!("<{tag}>{fragment}</{tag}>"));
4981 } else {
4982 result.push_str(&fragment);
4983 }
4984 }
4985
4986 if result.is_empty() {
4987 None
4988 } else {
4989 Some(result)
4990 }
4991 }
4992
4993 pub fn get_text_selection_ranges(&self) -> Vec<(NodeId, usize, usize)> {
4996 if let Some(range) = &self.dom_selection {
4997 return self.range_layout_ranges(range.bounds());
4998 }
4999 let lookup = |parent_id, idx| self.find_anonymous_block_by_index(parent_id, idx);
5000
5001 let anchor_node = match self.text_selection.anchor.resolve_node_id(lookup) {
5002 Some(id) => id,
5003 None => return Vec::new(),
5004 };
5005 let focus_node = match self.text_selection.focus.resolve_node_id(lookup) {
5006 Some(id) => id,
5007 None => return Vec::new(),
5008 };
5009
5010 let node_is_in_doc = |node_id: NodeId| {
5013 self.nodes
5014 .get(node_id)
5015 .is_some_and(|node| node.flags.is_in_document())
5016 };
5017 if !node_is_in_doc(anchor_node) || !node_is_in_doc(focus_node) {
5018 return Vec::new();
5019 }
5020
5021 if anchor_node == focus_node {
5023 let start = self
5024 .text_selection
5025 .anchor
5026 .offset
5027 .min(self.text_selection.focus.offset);
5028 let end = self
5029 .text_selection
5030 .anchor
5031 .offset
5032 .max(self.text_selection.focus.offset);
5033
5034 if start == end {
5035 return Vec::new();
5036 }
5037 return vec![(anchor_node, start, end)];
5038 }
5039
5040 let inline_roots = self.collect_inline_roots_in_range(anchor_node, focus_node);
5042 if inline_roots.is_empty() {
5043 return Vec::new();
5044 }
5045
5046 let first_in_roots = inline_roots[0];
5049
5050 let (first_node, first_offset, last_node, last_offset) =
5051 if first_in_roots == anchor_node || (first_in_roots != focus_node) {
5052 (
5054 anchor_node,
5055 self.text_selection.anchor.offset,
5056 focus_node,
5057 self.text_selection.focus.offset,
5058 )
5059 } else {
5060 (
5062 focus_node,
5063 self.text_selection.focus.offset,
5064 anchor_node,
5065 self.text_selection.anchor.offset,
5066 )
5067 };
5068
5069 let mut ranges = Vec::with_capacity(inline_roots.len());
5070
5071 for &node_id in &inline_roots {
5072 let Some(node) = self.get_node(node_id) else {
5073 continue;
5074 };
5075 let Some(element_data) = node.element_data() else {
5076 continue;
5077 };
5078 let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
5079 continue;
5080 };
5081
5082 let text_len = inline_layout.text.len();
5083
5084 if node_id == first_node && node_id == last_node {
5085 let start = first_offset.min(last_offset);
5086 let end = first_offset.max(last_offset);
5087 if start < end && end <= text_len {
5088 ranges.push((node_id, start, end));
5089 }
5090 } else if node_id == first_node {
5091 if first_offset < text_len {
5092 ranges.push((node_id, first_offset, text_len));
5093 }
5094 } else if node_id == last_node {
5095 if last_offset > 0 && last_offset <= text_len {
5096 ranges.push((node_id, 0, last_offset));
5097 }
5098 } else if text_len > 0 {
5099 ranges.push((node_id, 0, text_len));
5100 }
5101 }
5102
5103 ranges
5104 }
5105}
5106
5107#[derive(Debug, Clone, Copy, PartialEq)]
5108pub struct BoundingRect {
5109 pub x: f64,
5110 pub y: f64,
5111 pub width: f64,
5112 pub height: f64,
5113}
5114
5115impl BaseDocument {
5118 pub(crate) fn record_details_open(
5121 &mut self,
5122 node_id: NodeId,
5123 name: &markup5ever::QualName,
5124 present: bool,
5125 ) {
5126 if name.local != local_name!("open") || !name.ns.is_empty() {
5127 return;
5128 }
5129 let Some(element) = self.nodes[node_id].element_data() else {
5130 return;
5131 };
5132 if element.name.local != local_name!("details") || element.name.ns != ns!(html) {
5133 return;
5134 }
5135 let was_open = element.attr(local_name!("open")).is_some();
5136 if was_open != present && !self.details_toggles.iter().any(|(id, _)| *id == node_id) {
5137 self.details_toggles.push((node_id, was_open));
5138 }
5139 }
5140
5141 pub(crate) fn template_contents_owner(&mut self) -> NodeId {
5143 if let Some(owner) = self.template_contents_owner {
5144 return owner;
5145 }
5146 let mut data = crate::node::DocumentData::default();
5147 data.content_type = "text/html";
5148 let owner = self.create_node(NodeData::Document(Box::new(data)));
5149 self.template_contents_owner = Some(owner);
5150 owner
5151 }
5152
5153 pub fn style_revision(&self) -> u64 {
5155 self.style_revision
5156 }
5157
5158 pub fn layout_revision(&self) -> u64 {
5160 self.layout_revision
5161 }
5162
5163 pub(crate) fn note_layout_resolved(&mut self) {
5164 self.layout_revision = self.layout_revision.wrapping_add(1);
5165 }
5166
5167 pub fn viewport_generation(&self) -> u64 {
5169 self.viewport_generation
5170 }
5171
5172 pub fn take_details_toggles(&mut self) -> Vec<(NodeId, bool)> {
5174 std::mem::take(&mut self.details_toggles)
5175 }
5176
5177 pub(crate) fn record_inline_handler_attribute(
5180 &mut self,
5181 node_id: NodeId,
5182 name: &crate::QualName,
5183 value: Option<&str>,
5184 ) {
5185 if name.ns != markup5ever::ns!() || !name.local.as_ref().starts_with("on") {
5186 return;
5187 }
5188 let Some(element) = self
5189 .nodes
5190 .get_mut(node_id)
5191 .and_then(|node| node.element_data_mut())
5192 else {
5193 return;
5194 };
5195 let value = value.map(crate::node::AttrAtom::from);
5196 if let Some(value) = &value {
5197 let attributes = element
5198 .inline_event_attributes
5199 .get_or_insert_with(|| Box::new(Vec::new()));
5200 if let Some(attribute) = attributes
5201 .iter_mut()
5202 .find(|attribute| attribute.name.local == name.local)
5203 {
5204 attribute.name = name.clone();
5205 attribute.value = value.clone();
5206 } else {
5207 attributes.push(crate::node::Attribute {
5208 name: name.clone(),
5209 value: value.clone(),
5210 });
5211 }
5212 } else {
5213 let Some(attributes) = element.inline_event_attributes.as_mut() else {
5214 return;
5215 };
5216 let Some(index) = attributes
5217 .iter()
5218 .position(|attribute| attribute.name.local == name.local)
5219 else {
5220 return;
5221 };
5222 attributes.remove(index);
5223 if attributes.is_empty() {
5224 element.inline_event_attributes = None;
5225 }
5226 }
5227 self.inline_handler_changes
5228 .push((node_id, name.clone(), value));
5229 }
5230
5231 pub fn take_inline_handler_changes(
5233 &mut self,
5234 ) -> Vec<(NodeId, crate::QualName, Option<crate::node::AttrAtom>)> {
5235 std::mem::take(&mut self.inline_handler_changes)
5236 }
5237
5238 pub fn set_recording_mutations(&mut self, recording: bool) {
5241 match (recording, self.mutation_log.is_some()) {
5242 (true, false) => self.mutation_log = Some(Vec::new()),
5243 (false, true) => self.mutation_log = None,
5244 _ => {}
5245 }
5246 }
5247
5248 pub fn is_recording_mutations(&self) -> bool {
5250 self.mutation_log.is_some()
5251 }
5252
5253 pub fn take_mutations(&mut self) -> Vec<crate::DomMutation> {
5255 self.mutation_log
5256 .as_mut()
5257 .map(std::mem::take)
5258 .unwrap_or_default()
5259 }
5260
5261 pub(crate) fn record_mutation(&mut self, mutation: crate::DomMutation) {
5262 if let Some(log) = self.mutation_log.as_mut() {
5263 if log.len() < crate::mutation_record::MUTATION_LOG_CAP {
5264 log.push(mutation);
5265 }
5266 }
5267 }
5268
5269 pub(crate) fn sibling_context(
5272 &self,
5273 node_id: NodeId,
5274 ) -> Option<(NodeId, Option<NodeId>, Option<NodeId>)> {
5275 let parent_id = self.nodes.get(node_id)?.parent?;
5276 let siblings = &self.nodes.get(parent_id)?.children;
5277 let index = siblings.iter().position(|id| *id == node_id)?;
5278 let previous = index.checked_sub(1).map(|i| siblings[i]);
5279 let next = siblings.get(index + 1).copied();
5280 Some((parent_id, previous, next))
5281 }
5282}
5283
5284impl AsRef<BaseDocument> for BaseDocument {
5285 fn as_ref(&self) -> &BaseDocument {
5286 self
5287 }
5288}
5289
5290impl AsMut<BaseDocument> for BaseDocument {
5291 fn as_mut(&mut self) -> &mut BaseDocument {
5292 self
5293 }
5294}
5295
5296#[cfg(test)]
5297mod hover_state_tests {
5298 use super::*;
5299 use crate::{Attribute, qual_name};
5300 use blitz_traits::shell::ColorScheme;
5301
5302 fn make_doc() -> (BaseDocument, NodeId) {
5309 let mut doc = BaseDocument::new(DocumentConfig {
5310 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
5311 ..Default::default()
5312 });
5313 let root_id = doc.root_node().id;
5314 let style = |value: &str| Attribute {
5315 name: qual_name!("style"),
5316 value: value.into(),
5317 };
5318
5319 let mut mutator = doc.mutate();
5320 let html = mutator.create_element(qual_name!("html"), vec![]);
5321 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
5322 let container = mutator.create_element(qual_name!("div"), vec![style("width:300px")]);
5323 let text = mutator.create_text_node("some text");
5324 let block = mutator.create_element(qual_name!("div"), vec![style("height:50px")]);
5325 mutator.append_children(container, &[text, block]);
5326 mutator.append_children(body, &[container]);
5327 mutator.append_children(html, &[body]);
5328 mutator.append_children(root_id, &[html]);
5329 drop(mutator);
5330
5331 doc.resolve(0.0);
5332 (doc, container)
5333 }
5334
5335 fn text_has_size(doc: &BaseDocument, container: NodeId) -> bool {
5339 doc.nodes[container].final_layout().size.height > 50.0
5340 }
5341
5342 #[test]
5348 fn hovering_text_in_anonymous_block_reports_text_cursor() {
5349 let (mut doc, container) = make_doc();
5350 if !text_has_size(&doc, container) {
5351 eprintln!("skipping: no usable font (text measures 0x0)");
5352 return;
5353 }
5354
5355 doc.set_hover_to(5.0, 8.0);
5356 assert!(doc.hover_node_is_text, "expected a text hit");
5357 let hit_id = doc.hover_hit_node_id.expect("expected a hit node");
5358 assert!(
5359 doc.nodes[hit_id].is_anonymous(),
5360 "expected the hit node to be the anonymous inline root"
5361 );
5362 assert_eq!(
5363 doc.get_hover_node_id(),
5364 Some(container),
5365 "expected the stored hover target to be the containing element"
5366 );
5367 assert_eq!(doc.get_cursor(), Some(CursorIcon::Text));
5368 }
5369
5370 #[test]
5371 fn semantic_hover_keeps_the_resolved_node_instead_of_hit_testing_again() {
5372 let (mut doc, container) = make_doc();
5373
5374 doc.set_hover_to_node(container, 350.0, 250.0);
5378
5379 assert_eq!(doc.get_hover_node_id(), Some(container));
5380 assert_eq!(doc.hover_hit_node_id, Some(container));
5381
5382 doc.resolve(0.0);
5383 assert_eq!(
5384 doc.get_hover_node_id(),
5385 Some(container),
5386 "a resolve must not turn semantic identity back into a coordinate hit"
5387 );
5388 }
5389
5390 #[test]
5393 fn hovering_anonymous_block_whitespace_reports_default_cursor() {
5394 let (mut doc, container) = make_doc();
5395 if !text_has_size(&doc, container) {
5396 eprintln!("skipping: no usable font (text measures 0x0)");
5397 return;
5398 }
5399
5400 doc.set_hover_to(250.0, 8.0);
5401 assert!(!doc.hover_node_is_text);
5402 assert_eq!(doc.get_hover_node_id(), Some(container));
5403 assert_eq!(doc.get_cursor(), Some(CursorIcon::Default));
5404 }
5405}
5406
5407#[cfg(test)]
5408mod control_scroll_tests {
5409 use super::*;
5410 use crate::{Attribute, qual_name};
5411 use blitz_traits::shell::ColorScheme;
5412
5413 #[test]
5414 fn controlled_scroll_dispatches_the_dom_scroll_event() {
5415 let mut doc = BaseDocument::new(DocumentConfig {
5416 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
5417 ..Default::default()
5418 });
5419 let root_id = doc.root_node().id;
5420 let style = |value: &str| Attribute {
5421 name: qual_name!("style"),
5422 value: value.into(),
5423 };
5424
5425 let mut mutator = doc.mutate();
5426 let html = mutator.create_element(qual_name!("html"), vec![]);
5427 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
5428 let scroller = mutator.create_element(
5429 qual_name!("div"),
5430 vec![style("width:200px;height:100px;overflow-y:scroll")],
5431 );
5432 let spacer = mutator.create_element(qual_name!("div"), vec![style("height:400px")]);
5433 let target = mutator.create_element(qual_name!("button"), vec![style("height:40px")]);
5434 let target_text = mutator.create_text_node("Reveal me");
5435 mutator.append_children(target, &[target_text]);
5436 mutator.append_children(scroller, &[spacer, target]);
5437 mutator.append_children(body, &[scroller]);
5438 mutator.append_children(html, &[body]);
5439 mutator.append_children(root_id, &[html]);
5440 drop(mutator);
5441 doc.resolve(0.0);
5442
5443 doc.nodes[html].final_layout_mut().size.height = 300.0;
5447 doc.nodes[html].final_layout_mut().content_size.height = 600.0;
5448 doc.nodes[target].final_layout_mut().location.y = 400.0;
5449
5450 let mut events = Vec::new();
5451 doc.scroll_to_node_centered_with_events(target_text, |event| events.push(event));
5455
5456 assert!(
5457 (doc.viewport_scroll.y - 270.0).abs() < 0.1,
5458 "the 40px target should be centred in the 300px viewport"
5459 );
5460 assert!(
5461 events
5462 .iter()
5463 .any(|event| { event.target == html && event.name() == "scroll" })
5464 );
5465 }
5466}
5467
5468#[cfg(test)]
5469mod font_face_override_tests {
5470 use super::*;
5471 use crate::net::{FontFaceOverrides, Resource, ResourceLoadResponse};
5472
5473 #[test]
5489 fn font_face_overrides_alias_family_name() {
5490 const ALIAS: &str = "AliasedFamily";
5491
5492 let mut document = BaseDocument::new(DocumentConfig::default());
5493
5494 {
5496 let mut ctx = document.font_ctx.lock().unwrap();
5497 assert!(
5498 ctx.collection.family_id(ALIAS).is_none(),
5499 "alias must not exist before registration",
5500 );
5501 }
5502
5503 let response = ResourceLoadResponse {
5508 request_id: 0,
5509 node_id: None,
5510 resolved_url: Some(String::from("test://aliased-family")),
5511 result: Ok(Resource::Font(
5512 blitz_traits::net::Bytes::from_static(crate::BULLET_FONT),
5513 FontFaceOverrides {
5514 family_name: Some(String::from(ALIAS)),
5515 weight: Some(800.0),
5516 style: Some(parley::fontique::FontStyle::Italic),
5517 },
5518 )),
5519 };
5520 document.load_resource(response);
5521
5522 let mut ctx = document.font_ctx.lock().unwrap();
5525 let family_id = ctx
5526 .collection
5527 .family_id(ALIAS)
5528 .expect("CSS-declared family name should be registered as a family alias");
5529 let resolved_name = ctx
5530 .collection
5531 .family_name(family_id)
5532 .expect("family id should resolve back to a name");
5533 assert_eq!(
5534 resolved_name, ALIAS,
5535 "registered family should report the CSS-declared name, \
5536 not the font file's internal `name` table entry",
5537 );
5538 }
5539}
5540
5541#[cfg(test)]
5542mod clipboard_html_tests {
5543 use super::escape_html;
5544
5545 #[test]
5548 fn markup_characters_in_a_selection_are_escaped() {
5549 assert_eq!(
5550 escape_html("a < b && c > d"),
5551 "a < b && c > d"
5552 );
5553 }
5554
5555 #[test]
5558 fn an_ampersand_is_not_double_escaped() {
5559 assert_eq!(escape_html("<"), "&lt;");
5560 }
5561
5562 #[test]
5565 fn prose_is_untouched() {
5566 assert_eq!(escape_html("it's \"fine\""), "it's "fine"");
5567 }
5568}