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