1use crate::NodeTree;
2use crate::events::{DragMode, ScrollAnimationState, handle_dom_event};
3use crate::font_metrics::BlitzFontMetricsProvider;
4use crate::layout::construct::ConstructionTask;
5use crate::layout::damage::ALL_DAMAGE;
6use crate::mutator::ViewportMut;
7use crate::net::{
8 Resource, ResourceHandler, ResourceLoadResponse, StylesheetHandler, StylesheetLoader,
9};
10use crate::node::{
11 ImageData, NodeFlags, RasterImageData, SpecialElementData, Status, TextBrush, TextGranularity,
12};
13use crate::selection::TextSelection;
14use crate::stylo_to_cursor_icon::stylo_to_cursor_icon;
15use crate::traversal::TreeTraverser;
16use crate::url::DocumentUrl;
17use crate::util::ImageType;
18use crate::{
19 DEFAULT_CSS, DocumentConfig, DocumentMutator, DummyHtmlParserProvider, ElementData,
20 EventDriver, HtmlParserProvider, Node, NodeData, NoopEventHandler, StyleThreading,
21 TextNodeData,
22};
23use blitz_traits::devtools::DevtoolSettings;
24use blitz_traits::events::{BlitzScrollEvent, DomEvent, DomEventData, HitResult, UiEvent};
25use blitz_traits::navigation::{DummyNavigationProvider, NavigationProvider};
26use blitz_traits::net::{AbortSignal, DummyNetProvider, NetProvider, Request, RequestDestination};
27use blitz_traits::node_id::NodeId;
28use blitz_traits::shell::{ColorScheme, DummyShellProvider, ShellProvider, Viewport};
29use cursor_icon::CursorIcon;
30use linebender_resource_handle::Blob;
31use markup5ever::{local_name, ns};
32use parley::{FontContext, PlainEditorDriver};
33use rustc_hash::FxHashSet;
34use selectors::{Element, matching::QuirksMode};
35use smallvec::SmallVec;
36use std::any::Any;
37use std::cell::RefCell;
38use std::collections::{BTreeMap, Bound, HashMap, HashSet};
39use std::ops::{Deref, DerefMut};
40use std::rc::Rc;
41use std::str::FromStr;
42use std::sync::atomic::{AtomicUsize, Ordering};
43use std::sync::mpsc::{Receiver, Sender, channel};
44use std::sync::{Arc, Mutex, MutexGuard, OnceLock, RwLockReadGuard, RwLockWriteGuard};
45use std::task::{Context as TaskContext, Waker};
46use style::Atom;
47use style::animation::{AnimationState, DocumentAnimationSet};
48use style::attr::{AttrIdentifier, AttrValue};
49use style::data::{ElementData as StyloElementData, ElementStyles};
50use style::media_queries::MediaType;
51use style::properties::ComputedValues;
52use style::properties::style_structs::Font;
53use style::queries::values::PrefersColorScheme;
54use style::selector_parser::ServoElementSnapshot;
55use style::servo::media_features::PointerCapabilities;
56use style::servo_arc::Arc as ServoArc;
57use style::values::GenericAtomIdent;
58use style::values::computed::ui::CursorKind;
59use style::values::computed::{Overflow, UserSelect};
60use style::values::specified::box_::{DisplayInside, DisplayOutside};
61use style::{
62 device::Device,
63 dom::{TDocument, TNode},
64 media_queries::MediaList,
65 selector_parser::SnapshotMap,
66 shared_lock::{SharedRwLock, StylesheetGuards},
67 stylesheets::{AllowImportRules, DocumentStyleSheet, Origin, Stylesheet},
68 stylist::Stylist,
69};
70use thin_vec::ThinVec;
71use url::Url;
72use web_time::Instant;
73
74#[cfg(feature = "parallel-construct")]
75use thread_local::ThreadLocal;
76
77pub enum DocGuard<'a> {
78 Ref(&'a BaseDocument),
79 RefCell(std::cell::Ref<'a, BaseDocument>),
80 RwLock(RwLockReadGuard<'a, BaseDocument>),
81 Mutex(MutexGuard<'a, BaseDocument>),
82}
83
84impl Deref for DocGuard<'_> {
85 type Target = BaseDocument;
86 #[inline(always)]
87 fn deref(&self) -> &Self::Target {
88 match self {
89 Self::Ref(base_document) => base_document,
90 Self::RefCell(refcell_guard) => refcell_guard,
91 Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
92 Self::Mutex(mutex_guard) => mutex_guard,
93 }
94 }
95}
96
97pub enum DocGuardMut<'a> {
98 Ref(&'a mut BaseDocument),
99 RefCell(std::cell::RefMut<'a, BaseDocument>),
100 RwLock(RwLockWriteGuard<'a, BaseDocument>),
101 Mutex(MutexGuard<'a, BaseDocument>),
102}
103
104impl Deref for DocGuardMut<'_> {
105 type Target = BaseDocument;
106 #[inline(always)]
107 fn deref(&self) -> &Self::Target {
108 match self {
109 Self::Ref(base_document) => base_document,
110 Self::RefCell(refcell_guard) => refcell_guard,
111 Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
112 Self::Mutex(mutex_guard) => mutex_guard,
113 }
114 }
115}
116
117impl DerefMut for DocGuardMut<'_> {
118 #[inline(always)]
119 fn deref_mut(&mut self) -> &mut Self::Target {
120 match self {
121 Self::Ref(base_document) => base_document,
122 Self::RefCell(refcell_guard) => &mut *refcell_guard,
123 Self::RwLock(rw_lock_read_guard) => &mut *rw_lock_read_guard,
124 Self::Mutex(mutex_guard) => &mut *mutex_guard,
125 }
126 }
127}
128
129pub trait Document: Any + 'static {
132 fn inner(&self) -> DocGuard<'_>;
133 fn inner_mut(&mut self) -> DocGuardMut<'_>;
134
135 fn handle_ui_event(&mut self, event: UiEvent) {
137 let mut doc = self.inner_mut();
138 let mut driver = EventDriver::new(&mut *doc, NoopEventHandler);
139 driver.handle_ui_event(event);
140 }
141
142 fn poll(&mut self, task_context: Option<TaskContext>) -> bool {
144 let _ = task_context;
146 false
147 }
148
149 fn poll_for_settle(&mut self, task_context: Option<TaskContext>, run_timers: bool) -> bool {
158 let _ = run_timers;
159 self.poll(task_context)
160 }
161
162 fn id(&self) -> usize {
164 self.inner().id
165 }
166}
167
168#[derive(Debug, Clone, PartialEq)]
171pub struct PreClickActivation {
172 previous: Vec<(NodeId, bool)>,
174}
175
176fn escape_html(text: &str) -> String {
182 let mut out = String::with_capacity(text.len());
183 for ch in text.chars() {
184 match ch {
185 '&' => out.push_str("&"),
186 '<' => out.push_str("<"),
187 '>' => out.push_str(">"),
188 '"' => out.push_str("""),
189 _ => out.push(ch),
190 }
191 }
192 out
193}
194
195pub struct PlainDocument(pub BaseDocument);
196impl Document for PlainDocument {
197 fn inner(&self) -> DocGuard<'_> {
198 DocGuard::Ref(&self.0)
199 }
200 fn inner_mut(&mut self) -> DocGuardMut<'_> {
201 DocGuardMut::Ref(&mut self.0)
202 }
203}
204
205impl Document for BaseDocument {
206 fn inner(&self) -> DocGuard<'_> {
207 DocGuard::Ref(self)
208 }
209 fn inner_mut(&mut self) -> DocGuardMut<'_> {
210 DocGuardMut::Ref(self)
211 }
212}
213
214impl Document for Rc<RefCell<BaseDocument>> {
215 fn inner(&self) -> DocGuard<'_> {
216 DocGuard::RefCell(self.borrow())
217 }
218
219 fn inner_mut(&mut self) -> DocGuardMut<'_> {
220 DocGuardMut::RefCell(self.borrow_mut())
221 }
222}
223
224pub enum DocumentEvent {
225 ResourceLoad(ResourceLoadResponse),
226 NavigateIframe {
229 node_id: NodeId,
230 url: Url,
231 },
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
236pub enum AnimationPacing {
237 Idle,
238 Caret,
239 SlowCss,
240 Interactive,
241}
242
243pub struct BaseDocument {
244 id: usize,
246
247 pub(crate) url: DocumentUrl,
250 pub(crate) devtool_settings: DevtoolSettings,
252 pub(crate) viewport: Viewport,
254 pub(crate) viewport_scroll: crate::Point<f64>,
256 pub(crate) mutation_log: Option<Vec<crate::DomMutation>>,
259 inline_handler_changes: Vec<(NodeId, crate::QualName, Option<crate::node::AttrAtom>)>,
261 pub(crate) live_ranges: Vec<crate::range::WeakRange>,
263 pub dom_selection: Option<crate::LiveRange>,
265 pub(crate) media_type: MediaType,
267 pub(crate) style_threading: StyleThreading,
269 pub(crate) incremental_layout: bool,
271 pub(crate) subdocument_depth: usize,
274
275 pub(crate) tx: Sender<DocumentEvent>,
277 pub(crate) rx: Option<Receiver<DocumentEvent>>,
279
280 pub(crate) nodes: Box<NodeTree>,
285
286 node_drop_depth: usize,
288 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 .destination(RequestDestination::Style),
2569 ResourceHandler::boxed(
2570 self.tx.clone(),
2571 self.id,
2572 Some(node_id),
2573 self.shell_provider.clone(),
2574 StylesheetHandler {
2575 source_url: resolved_href,
2576 guard: self.guard.clone(),
2577 net_provider: self.net_provider.clone(),
2578 abort_signal: self.abort_signal.clone(),
2579 },
2580 ),
2581 );
2582 }
2583 }
2584 }
2585 }
2586 }
2587
2588 pub fn process_style_element(&mut self, target_id: NodeId) {
2589 let css = self.nodes[target_id].text_content();
2590 let css = html_escape::decode_html_entities(&css);
2591 let sheet = self.make_stylesheet(&css, Origin::Author);
2592 self.add_stylesheet_for_node(sheet, target_id);
2593 }
2594
2595 pub fn remove_user_agent_stylesheet(&mut self, contents: &str) {
2596 if let Some(sheet) = self.ua_stylesheets.remove(contents) {
2597 self.stylist.remove_stylesheet(sheet, &self.guard.read());
2598 }
2599 }
2600
2601 pub fn url(&self) -> &url::Url {
2603 &self.url
2604 }
2605
2606 pub fn author_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
2609 self.nodes_to_stylesheet.values()
2610 }
2611
2612 pub fn useragent_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
2614 self.ua_stylesheets.values()
2615 }
2616
2617 pub fn add_user_agent_stylesheet(&mut self, css: &str) {
2618 let sheet = self.make_stylesheet(css, Origin::UserAgent);
2619 self.ua_stylesheets.insert(css.to_string(), sheet.clone());
2620 self.stylist.append_stylesheet(sheet, &self.guard.read());
2621 }
2622
2623 pub fn make_stylesheet(&self, css: impl AsRef<str>, origin: Origin) -> DocumentStyleSheet {
2624 let data = Stylesheet::from_str(
2625 css.as_ref(),
2626 self.url.url_extra_data(),
2627 origin,
2628 ServoArc::new(self.guard.wrap(MediaList::empty())),
2629 self.guard.clone(),
2630 Some(&StylesheetLoader {
2631 tx: self.tx.clone(),
2632 doc_id: self.id,
2633 net_provider: self.net_provider.clone(),
2634 shell_provider: self.shell_provider.clone(),
2635 abort_signal: self.abort_signal.clone(),
2636 }),
2637 None,
2638 QuirksMode::NoQuirks,
2639 AllowImportRules::Yes,
2640 );
2641
2642 DocumentStyleSheet(ServoArc::new(data))
2643 }
2644
2645 pub fn upsert_stylesheet_for_node(&mut self, node_id: NodeId) {
2646 let raw_styles = self.nodes[node_id].text_content();
2647 let sheet = self.make_stylesheet(raw_styles, Origin::Author);
2648 self.add_stylesheet_for_node(sheet, node_id);
2649 }
2650
2651 pub fn add_stylesheet_for_node(&mut self, stylesheet: DocumentStyleSheet, node_id: NodeId) {
2652 self.platform_initial_sheet_media(&stylesheet, node_id);
2653 #[cfg(feature = "shadow-dom")]
2654 if let Some(root_id) = self.containing_shadow_root(node_id) {
2655 if self.document_sheet_nodes.remove(&node_id)
2658 && let Some(old) = self.nodes_to_stylesheet.get(&node_id).cloned()
2659 {
2660 self.stylist.remove_stylesheet(old, &self.guard.read());
2661 }
2662 self.install_shadow_stylesheet(root_id, node_id, stylesheet);
2663 return;
2664 }
2665
2666 let old = self.nodes_to_stylesheet.insert(node_id, stylesheet.clone());
2667
2668 if let Some(old) = old
2669 && self.document_sheet_nodes.contains(&node_id)
2670 {
2671 self.stylist.remove_stylesheet(old, &self.guard.read())
2672 }
2673 self.document_sheet_nodes.insert(node_id);
2674
2675 crate::net::fetch_font_face(
2677 self.tx.clone(),
2678 self.id,
2679 Some(node_id),
2680 &stylesheet.0,
2681 &self.net_provider,
2682 &self.shell_provider,
2683 &self.guard.read(),
2684 self.abort_signal.as_ref(),
2685 &mut self.web_fonts,
2686 self.font_epoch,
2687 );
2688
2689 let element = &mut self.nodes[node_id].element_data_mut().unwrap();
2691 element.special_data = SpecialElementData::Stylesheet(stylesheet.clone());
2692
2693 let insertion_point = self
2697 .nodes_to_stylesheet
2698 .range((Bound::Excluded(node_id), Bound::Unbounded))
2699 .find(|(id, _)| self.document_sheet_nodes.contains(id))
2700 .map(|(_, sheet)| sheet);
2701
2702 if let Some(insertion_point) = insertion_point {
2703 self.stylist.insert_stylesheet_before(
2704 stylesheet,
2705 insertion_point.clone(),
2706 &self.guard.read(),
2707 )
2708 } else {
2709 self.stylist
2710 .append_stylesheet(stylesheet, &self.guard.read())
2711 }
2712 }
2713
2714 pub fn handle_messages(&mut self) {
2715 let rx = self.rx.take().unwrap();
2718
2719 while let Ok(msg) = rx.try_recv() {
2720 self.handle_message(msg);
2721 }
2722
2723 self.rx = Some(rx);
2725 }
2726
2727 pub fn handle_message(&mut self, msg: DocumentEvent) {
2728 match msg {
2729 DocumentEvent::ResourceLoad(resource) => self.load_resource(resource),
2730 DocumentEvent::NavigateIframe { node_id, url } => self.navigate_iframe(node_id, url),
2731 }
2732 }
2733
2734 pub fn has_pending_critical_resources(&self) -> bool {
2736 !self.pending_critical_resources.is_empty()
2737 }
2738
2739 pub fn pending_image_count(&self) -> usize {
2746 self.pending_images.len()
2747 }
2748
2749 pub fn load_resource(&mut self, res: ResourceLoadResponse) {
2750 self.pending_critical_resources.remove(&res.request_id);
2751
2752 let resource = match res.result {
2753 Ok(resource) => resource,
2754 Err(err) => {
2755 if let Some(url) = res.resolved_url.as_ref() {
2756 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
2757 #[cfg(feature = "tracing")]
2758 tracing::warn!(
2759 url = url.as_str(),
2760 waiting_nodes = waiting_nodes.len(),
2761 error = err.as_str(),
2762 "Resource load failed"
2763 );
2764 #[cfg(not(feature = "tracing"))]
2765 let _ = (waiting_nodes, err);
2766 } else {
2767 #[cfg(feature = "tracing")]
2768 tracing::warn!(error = err.as_str(), "Resource load failed (no url)");
2769 #[cfg(not(feature = "tracing"))]
2770 let _ = err;
2771 }
2772 return;
2773 }
2774 };
2775
2776 let (resource, font_ticket) = match resource {
2777 Resource::TrackedFont(bytes, overrides, ticket) => {
2778 if ticket.css && ticket.epoch != self.font_epoch {
2779 ticket.finish(false);
2780 return;
2781 }
2782 (Resource::Font(bytes, overrides), Some(ticket))
2783 }
2784 resource => (resource, None),
2785 };
2786 if matches!(&resource, Resource::Css(_) | Resource::Font(_, _))
2787 && res.node_id.is_some_and(|id| {
2788 self.get_node(id)
2789 .is_none_or(|node| !node.flags.is_in_document())
2790 })
2791 {
2792 if let Some(ticket) = font_ticket {
2793 ticket.finish(false);
2794 }
2795 return;
2796 }
2797
2798 match resource {
2799 Resource::Css(css) => {
2800 let node_id = res.node_id.unwrap();
2801 self.add_stylesheet_for_node(css, node_id);
2802 }
2803 Resource::ImportSheet(import_rule, sheet) => {
2804 {
2813 let mut guard = self.guard.write();
2814 import_rule.write_with(&mut guard).stylesheet =
2815 style::stylesheets::import_rule::ImportSheet::Sheet(sheet.clone());
2816 }
2817
2818 crate::net::fetch_font_face(
2822 self.tx.clone(),
2823 self.id,
2824 res.node_id,
2825 &sheet,
2826 &self.net_provider,
2827 &self.shell_provider,
2828 &self.guard.read(),
2829 self.abort_signal.as_ref(),
2830 &mut self.web_fonts,
2831 self.font_epoch,
2832 );
2833 }
2834 Resource::Image(_kind, width, height, image_data) => {
2835 let image = ImageData::Raster(RasterImageData::new(width, height, image_data));
2837
2838 let Some(url) = res.resolved_url.as_ref() else {
2839 return;
2840 };
2841
2842 self.apply_loaded_image(url, image);
2843 }
2844 #[cfg(feature = "svg")]
2845 Resource::Svg(_kind, svg) => {
2846 let image = ImageData::Svg(svg);
2848
2849 let Some(url) = res.resolved_url.as_ref() else {
2850 return;
2851 };
2852
2853 self.apply_loaded_image(url, image);
2854 }
2855 Resource::DocumentSrc(html) => {
2856 let Some(node_id) = res.node_id else {
2857 return;
2858 };
2859 self.apply_iframe_html(node_id, res.request_id, res.resolved_url, &html);
2860 }
2861 Resource::Font(bytes, overrides) => {
2862 let font = Blob::new(Arc::new(bytes));
2863
2864 let weight_override = overrides.weight.map(parley::fontique::FontWeight::new);
2870 let info_override = parley::fontique::FontInfoOverride {
2871 family_name: overrides.family_name.as_deref(),
2872 weight: weight_override,
2873 style: overrides.style,
2874 ..Default::default()
2875 };
2876
2877 let mut global_font_ctx = self.font_ctx.lock().unwrap();
2879 let registered = global_font_ctx
2880 .collection
2881 .register_fonts(font.clone(), Some(info_override));
2882 let registered = !registered.is_empty();
2883
2884 #[cfg(feature = "parallel-construct")]
2885 {
2886 rayon::broadcast(|_ctx| {
2887 let mut font_ctx = self
2888 .thread_font_contexts
2889 .get_or(|| RefCell::new(Box::new(global_font_ctx.clone())))
2890 .borrow_mut();
2891 font_ctx
2892 .collection
2893 .register_fonts(font.clone(), Some(info_override));
2894 });
2895 }
2896 drop(global_font_ctx);
2897
2898 self.invalidate_inline_contexts();
2900 if let Some(ticket) = font_ticket {
2901 ticket.finish(registered);
2902 }
2903 }
2904 Resource::TrackedFont(..) => unreachable!("tracked font was normalized"),
2905 Resource::None => {
2906 }
2908 }
2909 }
2910
2911 fn apply_loaded_image(&mut self, url: &str, image: ImageData) {
2914 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
2916
2917 #[cfg(feature = "tracing")]
2918 tracing::info!(
2919 "Image {url} loaded, applying to {} nodes",
2920 waiting_nodes.len()
2921 );
2922
2923 self.image_cache.insert(url.to_string(), image.clone());
2925
2926 for (node_id, image_type) in waiting_nodes {
2928 let Some(node) = self.get_node_mut(node_id) else {
2929 continue;
2930 };
2931
2932 match image_type {
2933 ImageType::Image => {
2934 node.element_data_mut().unwrap().special_data =
2935 SpecialElementData::Image(Box::new(image.clone()));
2936
2937 node.cache_mut().clear();
2939 node.insert_damage(ALL_DAMAGE);
2940 }
2941 ImageType::Background(idx) | ImageType::Mask(idx) => {
2942 let layer_image = node.element_data_mut().and_then(|el| {
2943 let images = match image_type {
2944 ImageType::Background(_) => &mut el.background_images,
2945 ImageType::Mask(_) => &mut el.mask_images,
2946 ImageType::Image => unreachable!(),
2947 };
2948 images.get_mut(idx)
2949 });
2950 if let Some(Some(layer_image)) = layer_image {
2951 layer_image.status = Status::Ok;
2952 layer_image.image = image.clone();
2953 }
2954 }
2955 }
2956 }
2957 }
2958
2959 pub fn snapshot_node(&mut self, node_id: NodeId) {
2960 let node = &mut self.nodes[node_id];
2961
2962 let has_been_styled = node.primary_styles().is_some();
2967 if !has_been_styled {
2968 return;
2969 }
2970
2971 let opaque_node_id = TNode::opaque(&&*node);
2972 node.set_has_snapshot(true);
2973 node.snapshot_handled()
2974 .store(false, std::sync::atomic::Ordering::SeqCst);
2975
2976 if let Some(_existing_snapshot) = self.snapshots.get_mut(&opaque_node_id) {
2978 } else {
2981 let attrs: Option<Vec<_>> = node.attrs().map(|attrs| {
2982 attrs
2983 .iter()
2984 .map(|attr| {
2985 let ident = AttrIdentifier {
2986 local_name: GenericAtomIdent(attr.name.local.clone()),
2987 name: GenericAtomIdent(attr.name.local.clone()),
2988 namespace: GenericAtomIdent(attr.name.ns.clone()),
2989 prefix: None,
2990 };
2991
2992 let value = if attr.name.local == local_name!("id") {
2993 AttrValue::Atom(Atom::from(&*attr.value))
2994 } else if attr.name.local == local_name!("class") {
2995 let classes = attr
2996 .value
2997 .split_ascii_whitespace()
2998 .map(Atom::from)
2999 .collect();
3000 AttrValue::TokenList(OnceLock::from(attr.value.to_string()), classes)
3006 } else {
3007 AttrValue::String(attr.value.to_string())
3008 };
3009
3010 (ident, value)
3011 })
3012 .collect()
3013 });
3014
3015 let changed_attrs = attrs
3016 .as_ref()
3017 .map(|attrs| attrs.iter().map(|attr| attr.0.name.clone()).collect())
3018 .unwrap_or_default();
3019
3020 self.snapshots.insert(
3021 opaque_node_id,
3022 ServoElementSnapshot {
3023 state: Some(*node.element_state()),
3024 attrs,
3025 changed_attrs,
3026 class_changed: true,
3027 id_changed: true,
3028 other_attributes_changed: true,
3029 },
3030 );
3031 }
3032 }
3033
3034 pub(crate) fn snapshot_attribute(&mut self, node_id: NodeId, name: &markup5ever::LocalName) {
3036 self.snapshot_node(node_id);
3037 let opaque_node_id = TNode::opaque(&&self.nodes[node_id]);
3038 if let Some(snapshot) = self.snapshots.get_mut(&opaque_node_id) {
3039 let changed_attr = GenericAtomIdent(name.clone());
3040 if !snapshot.changed_attrs.contains(&changed_attr) {
3041 snapshot.changed_attrs.extend(std::iter::once(changed_attr));
3042 }
3043 }
3044 }
3045
3046 pub fn snapshot_node_and(&mut self, node_id: NodeId, cb: impl FnOnce(&mut Node)) {
3053 if !self.nodes.contains_key(node_id) {
3054 return;
3055 }
3056 self.snapshot_node(node_id);
3057 cb(&mut self.nodes[node_id]);
3058 }
3059
3060 pub fn hit(&self, x: f32, y: f32) -> Option<HitResult> {
3062 self.hit_with_scrollbar(x, y).0
3063 }
3064
3065 pub fn nearest_non_anonymous_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
3078 let mut node = self.get_node(node_id)?;
3082 loop {
3083 let parent = match node.parent {
3084 Some(parent_id) => self.get_node(parent_id)?,
3085 None => return Some(node.id),
3086 };
3087 if !node.is_anonymous() && !parent.is_anonymous() {
3088 return Some(node.id);
3089 }
3090 node = parent;
3091 }
3092 }
3093
3094 pub fn focus_next_node(&mut self) -> Option<NodeId> {
3095 let focussed_node_id = self.get_focussed_node_id()?;
3096 let id = self.next_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
3097 self.set_focus_to(id);
3098 Some(id)
3099 }
3100
3101 pub fn focus_prev_node(&mut self) -> Option<NodeId> {
3103 let focussed_node_id = self.get_focussed_node_id()?;
3104 let id = self.prev_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
3105 self.set_focus_to(id);
3106 Some(id)
3107 }
3108
3109 pub fn clear_focus(&mut self) {
3111 if let Some(id) = self.focus_node_id {
3112 let shell_provider = self.shell_provider.clone();
3113 self.snapshot_node_and(id, |node| node.blur(shell_provider));
3114 self.focus_node_id = None;
3115 }
3116 }
3117
3118 pub fn set_mousedown_node_id(&mut self, node_id: Option<NodeId>) {
3119 self.mousedown_node_id = node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
3120 }
3121 pub fn set_focus_to(&mut self, focus_node_id: NodeId) -> bool {
3122 let Some(focus_node_id) = self.nearest_non_anonymous_ancestor(focus_node_id) else {
3123 return false;
3124 };
3125 if Some(focus_node_id) == self.focus_node_id {
3126 return false;
3127 }
3128
3129 #[cfg(feature = "tracing")]
3130 tracing::info!("Focussed node {focus_node_id}");
3131
3132 let shell_provider = self.shell_provider.clone();
3133
3134 if let Some(id) = self.focus_node_id {
3136 self.snapshot_node_and(id, |node| node.blur(shell_provider.clone()));
3137 }
3138
3139 self.snapshot_node_and(focus_node_id, |node| node.focus(shell_provider));
3141
3142 self.focus_node_id = Some(focus_node_id);
3143
3144 true
3145 }
3146
3147 pub fn active_node(&mut self) -> bool {
3148 let Some(hover_node_id) = self.get_hover_node_id() else {
3149 return false;
3150 };
3151
3152 if let Some(active_node_id) = self.active_node_id {
3153 if active_node_id == hover_node_id {
3154 return true;
3155 }
3156 self.unactive_node();
3157 }
3158
3159 debug_assert!(
3161 self.get_node(hover_node_id)
3162 .is_some_and(|node| !node.is_anonymous()),
3163 "interaction state must reference DOM nodes, not layout-generated nodes"
3164 );
3165 let active_node_id = Some(hover_node_id);
3166
3167 let node_path = self.maybe_node_layout_ancestors(active_node_id);
3168 for &id in node_path.iter() {
3169 self.snapshot_node_and(id, |node| node.active());
3170 }
3171
3172 self.active_node_id = active_node_id;
3173
3174 true
3175 }
3176
3177 pub fn unactive_node(&mut self) -> bool {
3178 let Some(active_node_id) = self.active_node_id.take() else {
3179 return false;
3180 };
3181
3182 let node_path = self.maybe_node_layout_ancestors(Some(active_node_id));
3183 for &id in node_path.iter() {
3184 self.snapshot_node_and(id, |node| node.unactive());
3185 }
3186
3187 true
3188 }
3189
3190 pub fn hovered_scrollbar(&self) -> Option<crate::node::ScrollbarRef> {
3192 self.hovered_scrollbar
3193 }
3194
3195 pub fn scrollbar_drag_target(&self) -> Option<crate::node::ScrollbarRef> {
3197 match &self.drag_mode {
3198 DragMode::ScrollbarDrag(state) => Some(state.scrollbar),
3199 _ => None,
3200 }
3201 }
3202
3203 pub fn scrollbar_opacity(&self, node_id: NodeId) -> f32 {
3208 let interacting = |scrollbar: &crate::node::ScrollbarRef| scrollbar.node_id == node_id;
3209 if self.hovered_scrollbar.as_ref().is_some_and(interacting)
3210 || self
3211 .scrollbar_drag_target()
3212 .as_ref()
3213 .is_some_and(interacting)
3214 {
3215 return 1.0;
3216 }
3217 self.scrollbar_activity.get(&node_id).map_or(1.0, |last| {
3218 crate::node::scrollbar::opacity_at(last.elapsed())
3219 })
3220 }
3221
3222 pub(crate) fn show_scrollbars(&mut self, node_id: NodeId) {
3225 if cfg!(feature = "scrollbars") {
3226 self.scrollbar_activity.insert(node_id, Instant::now());
3227 }
3228 }
3229
3230 fn scrollbars_animating(&self) -> bool {
3233 use crate::node::scrollbar::{FADE_DELAY, FADE_DURATION};
3234 self.scrollbar_activity
3235 .values()
3236 .any(|last| last.elapsed() < FADE_DELAY + FADE_DURATION)
3237 }
3238
3239 pub(crate) fn hit_with_scrollbar(
3243 &self,
3244 x: f32,
3245 y: f32,
3246 ) -> (Option<HitResult>, Option<crate::node::ScrollbarRef>) {
3247 if TDocument::as_node(&self.root_node())
3248 .first_element_child()
3249 .is_none()
3250 {
3251 #[cfg(feature = "tracing")]
3252 tracing::warn!("No DOM - not resolving hit test");
3253 return (None, None);
3254 }
3255 let mut scrollbar = None;
3256 let hit = self
3257 .root_element()
3258 .hit_inner(x, y, self.viewport().scale_f64(), &mut scrollbar);
3259 (hit, scrollbar)
3260 }
3261
3262 pub fn set_hover_to(&mut self, x: f32, y: f32) -> bool {
3263 self.semantic_hover_node_id = None;
3264 self.last_client_pointer_position = Some(taffy::Point {
3268 x: x - self.viewport_scroll.x as f32,
3269 y: y - self.viewport_scroll.y as f32,
3270 });
3271
3272 let (hit, hovered_scrollbar) = self.hit_with_scrollbar(x, y);
3273 let hovered_scrollbar =
3276 hovered_scrollbar.filter(|scrollbar| self.scrollbar_opacity(scrollbar.node_id) > 0.0);
3277 let scrollbar_changed = hovered_scrollbar != self.hovered_scrollbar;
3281 if scrollbar_changed {
3282 for scrollbar in [self.hovered_scrollbar, hovered_scrollbar]
3285 .into_iter()
3286 .flatten()
3287 {
3288 self.show_scrollbars(scrollbar.node_id);
3289 }
3290 }
3291 self.hovered_scrollbar = hovered_scrollbar;
3292
3293 let hit_node_id = hit.map(|hit| hit.node_id);
3298 let hover_node_id = hit_node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
3299 let new_is_text = hit.map(|hit| hit.is_text).unwrap_or(false);
3300
3301 self.apply_hover_target(hit_node_id, hover_node_id, new_is_text, scrollbar_changed)
3302 }
3303
3304 pub fn set_hover_to_node(&mut self, node_id: NodeId, x: f32, y: f32) -> bool {
3312 self.semantic_hover_node_id = Some(node_id);
3313 self.last_client_pointer_position = Some(taffy::Point {
3314 x: x - self.viewport_scroll.x as f32,
3315 y: y - self.viewport_scroll.y as f32,
3316 });
3317
3318 let hovered_scrollbar = self.hovered_scrollbar.take();
3319 let scrollbar_changed = hovered_scrollbar.is_some();
3320 if let Some(scrollbar) = hovered_scrollbar {
3321 self.show_scrollbars(scrollbar.node_id);
3322 }
3323 let hover_node_id = self.nearest_non_anonymous_ancestor(node_id);
3324 self.apply_hover_target(Some(node_id), hover_node_id, false, scrollbar_changed)
3325 }
3326
3327 fn apply_hover_target(
3328 &mut self,
3329 hit_node_id: Option<NodeId>,
3330 hover_node_id: Option<NodeId>,
3331 new_is_text: bool,
3332 scrollbar_changed: bool,
3333 ) -> bool {
3334 let hit_changed =
3335 hit_node_id != self.hover_hit_node_id || new_is_text != self.hover_node_is_text;
3336 self.hover_hit_node_id = hit_node_id;
3337 self.hover_node_is_text = new_is_text;
3338
3339 if hover_node_id == self.hover_node_id {
3341 if hit_changed {
3342 self.shell_provider.set_cursor(self.get_cursor());
3346 }
3347 return scrollbar_changed;
3348 }
3349
3350 let old_node_path = self.maybe_node_layout_ancestors(self.hover_node_id);
3351 let new_node_path = self.maybe_node_layout_ancestors(hover_node_id);
3352 let same_count = old_node_path
3353 .iter()
3354 .zip(&new_node_path)
3355 .take_while(|(o, n)| o == n)
3356 .count();
3357 for &id in old_node_path.iter().skip(same_count) {
3358 self.snapshot_node_and(id, |node| node.unhover());
3359 }
3360 for &id in new_node_path.iter().skip(same_count) {
3361 self.snapshot_node_and(id, |node| node.hover());
3362 }
3363
3364 self.hover_node_id = hover_node_id;
3365
3366 self.shell_provider.set_cursor(self.get_cursor());
3368
3369 self.shell_provider.request_redraw();
3371
3372 true
3373 }
3374
3375 pub fn clear_hover(&mut self) -> bool {
3376 self.last_client_pointer_position = None;
3379 self.semantic_hover_node_id = None;
3380 self.hover_hit_node_id = None;
3381
3382 let Some(hover_node_id) = self.hover_node_id else {
3383 return false;
3384 };
3385
3386 let old_node_path = self.maybe_node_layout_ancestors(Some(hover_node_id));
3387 for &id in old_node_path.iter() {
3388 self.snapshot_node_and(id, |node| node.unhover());
3389 }
3390
3391 self.hover_node_id = None;
3392 self.hover_node_is_text = false;
3393
3394 self.shell_provider.set_cursor(self.get_cursor());
3396
3397 self.shell_provider.request_redraw();
3399
3400 true
3401 }
3402
3403 pub fn refresh_hover(&mut self) -> bool {
3409 if let Some(node_id) = self.semantic_hover_node_id {
3410 if self.get_node(node_id).is_some() {
3411 let hover_node_id = self.nearest_non_anonymous_ancestor(node_id);
3412 return self.apply_hover_target(Some(node_id), hover_node_id, false, false);
3413 }
3414 self.semantic_hover_node_id = None;
3415 }
3416 let Some(pos) = self.last_client_pointer_position else {
3417 return false;
3418 };
3419 let x = pos.x + self.viewport_scroll.x as f32;
3420 let y = pos.y + self.viewport_scroll.y as f32;
3421 self.set_hover_to(x, y)
3422 }
3423
3424 pub fn get_hover_node_id(&self) -> Option<NodeId> {
3425 self.hover_node_id
3426 }
3427
3428 pub fn get_mousedown_node_id(&self) -> Option<NodeId> {
3429 self.mousedown_node_id
3430 }
3431
3432 pub fn set_viewport(&mut self, viewport: Viewport) {
3433 let scale_has_changed = viewport.scale_f64() != self.viewport.scale_f64();
3434 self.viewport = viewport;
3435 self.set_stylist_device(make_device(
3436 &self.viewport,
3437 self.media_type.clone(),
3438 self.font_ctx.clone(),
3439 ));
3440 self.scroll_viewport_by(0.0, 0.0); if scale_has_changed {
3443 self.invalidate_inline_contexts();
3444 self.shell_provider.request_redraw();
3445 }
3446 }
3447
3448 pub fn media_type(&self) -> &MediaType {
3450 &self.media_type
3451 }
3452
3453 pub fn set_media_type(&mut self, media_type: MediaType) {
3456 if self.media_type == media_type {
3457 return;
3458 }
3459 self.media_type = media_type;
3460 self.set_stylist_device(make_device(
3461 &self.viewport,
3462 self.media_type.clone(),
3463 self.font_ctx.clone(),
3464 ));
3465 }
3466
3467 pub fn viewport(&self) -> &Viewport {
3468 &self.viewport
3469 }
3470
3471 pub fn viewport_mut(&mut self) -> ViewportMut<'_> {
3472 ViewportMut::new(self)
3473 }
3474
3475 pub fn zoom_by(&mut self, increment: f32) {
3476 *self.viewport.zoom_mut() += increment;
3477 self.set_viewport(self.viewport.clone());
3478 }
3479
3480 pub fn zoom_to(&mut self, zoom: f32) {
3481 *self.viewport.zoom_mut() = zoom;
3482 self.set_viewport(self.viewport.clone());
3483 }
3484
3485 pub fn get_viewport(&self) -> Viewport {
3486 self.viewport.clone()
3487 }
3488
3489 pub fn incremental_layout(&self) -> bool {
3491 self.incremental_layout
3492 }
3493
3494 pub fn set_incremental_layout(&mut self, enabled: bool) {
3496 self.incremental_layout = enabled;
3497 }
3498
3499 pub fn devtools(&self) -> &DevtoolSettings {
3500 &self.devtool_settings
3501 }
3502
3503 pub fn devtools_mut(&mut self) -> &mut DevtoolSettings {
3504 &mut self.devtool_settings
3505 }
3506
3507 pub fn subdoc(&self, node_id: NodeId) -> Option<&dyn Document> {
3508 self.get_node(node_id)
3509 .and_then(|node| node.element_data())
3510 .and_then(|el| el.sub_doc_data())
3511 }
3512
3513 pub fn subdoc_mut(&mut self, node_id: NodeId) -> Option<&mut dyn Document> {
3514 self.get_node_mut(node_id)
3515 .and_then(|node| node.element_data_mut())
3516 .and_then(|el| el.sub_doc_data_mut())
3517 }
3518
3519 pub fn is_animating(&self) -> bool {
3520 #[cfg(feature = "custom-widget")]
3521 let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
3522 self.nodes[node_id]
3523 .element_data()
3524 .and_then(|el| el.custom_widget_data())
3525 .is_some_and(|data| data.widget.requires_redraw())
3526 });
3527 #[cfg(not(feature = "custom-widget"))]
3528 let custom_widget_is_animating = false;
3529
3530 let animating = self.has_canvas
3531 | self.has_active_animations
3532 | (self.subdoc_animation_pacing != AnimationPacing::Idle)
3533 | custom_widget_is_animating
3534 | (self.scroll_animation != ScrollAnimationState::None)
3535 | self.scrollbars_animating();
3536
3537 if animating && crate::debug::animation_reasons_enabled() {
3538 crate::debug::report_animation_reasons(
3539 self.id(),
3540 self.has_canvas,
3541 self.has_active_animations,
3542 self.subdoc_animation_pacing != AnimationPacing::Idle,
3543 custom_widget_is_animating,
3544 self.scroll_animation != ScrollAnimationState::None,
3545 self.scrollbars_animating(),
3546 self.animating_node_names().as_deref(),
3547 );
3548 }
3549
3550 animating
3551 }
3552
3553 pub fn animation_pacing(&self) -> AnimationPacing {
3558 let focused_text_input = self.focus_node_id.is_some_and(|node_id| {
3559 self.nodes
3560 .get(node_id)
3561 .and_then(|node| node.element_data())
3562 .is_some_and(|element| element.text_input_data().is_some())
3563 });
3564 #[cfg(feature = "custom-widget")]
3565 let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
3566 self.nodes[node_id]
3567 .element_data()
3568 .and_then(|el| el.custom_widget_data())
3569 .is_some_and(|data| data.widget.requires_redraw())
3570 });
3571 #[cfg(not(feature = "custom-widget"))]
3572 let custom_widget_is_animating = false;
3573
3574 if self.has_canvas
3575 || custom_widget_is_animating
3576 || self.scroll_animation != ScrollAnimationState::None
3577 || self.scrollbars_animating()
3578 {
3579 AnimationPacing::Interactive
3580 } else if self.has_active_animations {
3581 const SLOW_ANIMATION_SECONDS: f64 = 2.0;
3582 let sets = self.animations.sets.read();
3583 let has_fast_animation_or_transition = sets.values().any(|set| {
3584 set.transitions.iter().any(|transition| {
3585 matches!(
3586 transition.state,
3587 AnimationState::Pending | AnimationState::Running
3588 )
3589 }) || set.animations.iter().any(|animation| {
3590 matches!(
3591 animation.state,
3592 AnimationState::Pending | AnimationState::Running
3593 ) && animation.duration < SLOW_ANIMATION_SECONDS
3594 })
3595 });
3596 if has_fast_animation_or_transition {
3597 AnimationPacing::Interactive
3598 } else {
3599 AnimationPacing::SlowCss
3600 }
3601 } else if focused_text_input {
3602 AnimationPacing::Caret
3603 } else if self.subdoc_animation_pacing != AnimationPacing::Idle {
3604 self.subdoc_animation_pacing
3605 } else {
3606 AnimationPacing::Idle
3607 }
3608 }
3609
3610 fn animating_node_names(&self) -> Option<String> {
3617 if !self.has_active_animations {
3618 return None;
3619 }
3620 let sets = self.animations.sets.read();
3621 let mut described: Vec<String> = sets
3622 .iter()
3623 .filter(|(_, state)| state.needs_animation_ticks())
3624 .filter_map(|(key, state)| {
3625 let node_id = NodeId::from_u64(key.node.id() as u64);
3626 let node = self.nodes.get(node_id)?;
3627 let element = node.element_data()?;
3628 let name = element
3629 .attr(local_name!("id"))
3630 .map(|id| format!("#{id}"))
3631 .or_else(|| {
3632 element
3633 .attr(local_name!("class"))
3634 .and_then(|c| c.split_ascii_whitespace().next())
3635 .map(|c| format!(".{c}"))
3636 })
3637 .unwrap_or_else(|| element.name.local.to_string());
3638 Some(format!(
3639 "{name}(anim={},trans={},in_doc={})",
3640 state.animations.len(),
3641 state.transitions.len(),
3642 node.flags.is_in_document(),
3643 ))
3644 })
3645 .collect();
3646 described.sort();
3647 described.truncate(12);
3648 Some(described.join(" "))
3649 }
3650
3651 pub fn set_stylist_device(&mut self, device: Device) {
3653 self.viewport_generation = self.viewport_generation.wrapping_add(1);
3654 let previous_viewport = self.stylist.device().au_viewport_size();
3657 let next_viewport = device.au_viewport_size();
3658
3659 let root_styles = self
3665 .try_root_element()
3666 .and_then(|root| root.primary_styles());
3667 if let Some(root_style) = root_styles.as_deref() {
3668 device.set_root_style(root_style);
3669
3670 let font = root_style.get_font();
3671 let font_size = font.clone_font_size().computed_size();
3672 device.set_root_font_size(root_style.effective_zoom.unzoom(font_size.px()));
3673
3674 let line_height = device
3675 .calc_line_height(font, root_style.writing_mode, None)
3676 .0;
3677 device.set_root_line_height(root_style.effective_zoom.unzoom(line_height.px()));
3678 }
3679 drop(root_styles);
3680
3681 let origins = {
3682 let guard = &self.guard;
3683 let guards = StylesheetGuards {
3684 author: &guard.read(),
3685 ua_or_user: &guard.read(),
3686 };
3687 self.stylist.set_device(device, &guards)
3688 };
3689 self.stylist.force_stylesheet_origins_dirty(origins);
3690 #[cfg(feature = "shadow-dom")]
3691 for host_id in self.shadow_host_node_ids() {
3692 if let Some(root_id) = self.shadow_root_id(host_id) {
3693 self.invalidate_shadow_styles(root_id);
3694 }
3695 }
3696
3697 if previous_viewport != next_viewport {
3704 self.abspos_layout_dirty = true;
3705 let invalidated = self
3706 .try_root_element()
3707 .is_some_and(|root| style::invalidation::viewport_units::invalidate(root));
3708 if invalidated {
3709 self.shell_provider.request_redraw();
3710 }
3711 }
3712 }
3713
3714 pub fn stylist_device(&mut self) -> &Device {
3715 self.stylist.device()
3716 }
3717
3718 pub fn get_cursor(&self) -> Option<CursorIcon> {
3726 let node_id = self
3731 .hover_hit_node_id
3732 .filter(|&id| self.nodes.contains_key(id))
3733 .or(self.get_hover_node_id());
3734 let Some(node_id) = node_id else {
3735 return Some(CursorIcon::Default);
3736 };
3737 let node = &self.nodes[node_id];
3738
3739 if let Some(subdoc) = node.subdoc().map(|doc| doc.inner()) {
3740 if subdoc.hover_hit_node_id.is_some() || subdoc.get_hover_node_id().is_some() {
3746 return subdoc.get_cursor();
3747 }
3748 return Some(CursorIcon::Default);
3749 }
3750
3751 let Some(style) = node.primary_styles() else {
3752 return Some(CursorIcon::Default);
3753 };
3754 let user_select = style.clone_user_select();
3755 let keyword = style.clone_cursor().keyword;
3756
3757 if keyword != CursorKind::Auto {
3759 return stylo_to_cursor_icon(keyword);
3760 }
3761
3762 if node
3764 .element_data()
3765 .is_some_and(|e| e.text_input_data().is_some())
3766 {
3767 return Some(CursorIcon::Text);
3768 }
3769
3770 let mut maybe_node = Some(node);
3772 while let Some(node) = maybe_node {
3773 if node.is_link() {
3774 return Some(CursorIcon::Pointer);
3775 }
3776
3777 maybe_node = node.layout_parent.get().map(|node_id| node.with(node_id));
3778 }
3779
3780 if self.hover_node_is_text {
3782 return Some(match user_select {
3783 UserSelect::Text | UserSelect::All | UserSelect::Auto => CursorIcon::Text,
3784 UserSelect::None => CursorIcon::Default,
3785 });
3786 }
3787
3788 Some(CursorIcon::Default)
3790 }
3791
3792 pub fn scroll_node_by<F: FnMut(DomEvent)>(
3793 &mut self,
3794 node_id: NodeId,
3795 x: f64,
3796 y: f64,
3797 dispatch_event: F,
3798 ) {
3799 self.scroll_node_by_has_changed(node_id, x, y, dispatch_event);
3800 }
3801
3802 pub fn scroll_node_by_has_changed<F: FnMut(DomEvent)>(
3812 &mut self,
3813 node_id: NodeId,
3814 x: f64,
3815 y: f64,
3816 dispatch_event: F,
3817 ) -> bool {
3818 let has_changed = self.scroll_node_by_inner(node_id, x, y, dispatch_event);
3819 if has_changed {
3820 self.resolve_positioned_scroll();
3821 }
3822 has_changed
3823 }
3824
3825 fn scroll_node_by_inner<F: FnMut(DomEvent)>(
3826 &mut self,
3827 node_id: NodeId,
3828 x: f64,
3829 y: f64,
3830 mut dispatch_event: F,
3831 ) -> bool {
3832 if self.try_root_element().is_some_and(|el| el.id == node_id) {
3837 let has_changed = self.scroll_viewport_by_has_changed(x, y);
3838 if has_changed {
3839 let layout = *self.root_element().final_layout();
3840 let scale = self.viewport.scale() as f64;
3841 let event = BlitzScrollEvent {
3842 scroll_top: self.viewport_scroll.y,
3843 scroll_left: self.viewport_scroll.x,
3844 scroll_width: layout.size.width.max(layout.content_size.width) as i32,
3845 scroll_height: layout.size.height.max(layout.content_size.height) as i32,
3846 client_width: (self.viewport.window_size.0 as f64 / scale) as i32,
3847 client_height: (self.viewport.window_size.1 as f64 / scale) as i32,
3848 };
3849 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
3850 }
3851 return has_changed;
3852 }
3853
3854 let Some(node) = self.nodes.get_mut(node_id) else {
3855 return false;
3856 };
3857
3858 if node
3862 .element_data()
3863 .is_some_and(|el| el.text_input_data().is_some())
3864 {
3865 let parent = node.parent;
3866 let content_box_width = node.final_layout().content_box_width();
3867 let content_box_height = node.final_layout().content_box_height();
3868 let input = node
3869 .element_data_mut()
3870 .and_then(|el| el.text_input_data_mut())
3871 .unwrap();
3872
3873 let (bubble_x, bubble_y) = if input.is_multiline {
3874 (
3875 x,
3876 input.scroll_by(y as f32, content_box_width, content_box_height) as f64,
3877 )
3878 } else {
3879 (
3880 input.scroll_by(x as f32, content_box_width, content_box_height) as f64,
3881 y,
3882 )
3883 };
3884
3885 let has_changed = bubble_x != x || bubble_y != y;
3886
3887 if bubble_x != 0.0 || bubble_y != 0.0 {
3888 let bubbled = if let Some(parent) = parent {
3889 self.scroll_node_by_inner(parent, bubble_x, bubble_y, dispatch_event)
3890 } else {
3891 self.scroll_viewport_by_has_changed(bubble_x, bubble_y)
3892 };
3893 return bubbled | has_changed;
3894 }
3895
3896 return has_changed;
3897 }
3898
3899 let (can_x_scroll, can_y_scroll) = node
3900 .primary_styles()
3901 .map(|styles| {
3902 (
3903 matches!(styles.clone_overflow_x(), Overflow::Scroll | Overflow::Auto),
3904 matches!(styles.clone_overflow_y(), Overflow::Scroll | Overflow::Auto),
3905 )
3906 })
3907 .unwrap_or((false, false));
3908
3909 let initial = *node.scroll_offset();
3910 let new_x = node.scroll_offset().x - x;
3911 let new_y = node.scroll_offset().y - y;
3912
3913 let mut bubble_x = 0.0;
3914 let mut bubble_y = 0.0;
3915
3916 let scroll_width = node.final_layout().scroll_width() as f64;
3917 let scroll_height = node.final_layout().scroll_height() as f64;
3918
3919 if let Some(mut sub_doc) = node.subdoc_mut().map(|doc| doc.inner_mut()) {
3921 let has_changed = if let Some(hover_node_id) = sub_doc.get_hover_node_id() {
3922 sub_doc.scroll_node_by_has_changed(hover_node_id, x, y, dispatch_event)
3923 } else {
3924 sub_doc.scroll_viewport_by_has_changed(x, y)
3925 };
3926
3927 return has_changed;
3929 }
3930
3931 if !can_x_scroll {
3933 bubble_x = x
3934 } else if new_x < 0.0 {
3935 bubble_x = -new_x;
3936 node.scroll_offset_mut().x = 0.0;
3937 } else if new_x > scroll_width {
3938 bubble_x = scroll_width - new_x;
3939 node.scroll_offset_mut().x = scroll_width;
3940 } else {
3941 node.scroll_offset_mut().x = new_x;
3942 }
3943
3944 if !can_y_scroll {
3945 bubble_y = y
3946 } else if new_y < 0.0 {
3947 bubble_y = -new_y;
3948 node.scroll_offset_mut().y = 0.0;
3949 } else if new_y > scroll_height {
3950 bubble_y = scroll_height - new_y;
3951 node.scroll_offset_mut().y = scroll_height;
3952 } else {
3953 node.scroll_offset_mut().y = new_y;
3954 }
3955
3956 let has_changed = *node.scroll_offset() != initial;
3957
3958 if has_changed {
3959 let layout = *node.final_layout();
3960 let event = BlitzScrollEvent {
3961 scroll_top: node.scroll_offset().y,
3962 scroll_left: node.scroll_offset().x,
3963 scroll_width: layout.scroll_width() as i32,
3964 scroll_height: layout.scroll_height() as i32,
3965 client_width: layout.size.width as i32,
3966 client_height: layout.size.height as i32,
3967 };
3968
3969 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
3970 }
3971
3972 let parent = node.parent;
3973 if has_changed {
3974 self.show_scrollbars(node_id);
3975 }
3976
3977 if bubble_x != 0.0 || bubble_y != 0.0 {
3978 if let Some(parent) = parent {
3979 return self.scroll_node_by_inner(parent, bubble_x, bubble_y, dispatch_event)
3980 | has_changed;
3981 } else {
3982 return self.scroll_viewport_by_has_changed(bubble_x, bubble_y) | has_changed;
3983 }
3984 }
3985
3986 has_changed
3987 }
3988
3989 pub fn scroll_viewport_by(&mut self, x: f64, y: f64) {
3990 self.scroll_viewport_by_has_changed(x, y);
3991 }
3992
3993 pub fn scroll_viewport_by_has_changed(&mut self, x: f64, y: f64) -> bool {
3995 let (content_width, content_height) = match self.try_root_element() {
4000 Some(root) => {
4001 let root_layout = root.final_layout();
4002 (
4003 root_layout.size.width.max(root_layout.content_size.width) as f64,
4004 root_layout.size.height.max(root_layout.content_size.height) as f64,
4005 )
4006 }
4007 None => (0.0, 0.0),
4008 };
4009 let new_scroll = (self.viewport_scroll.x - x, self.viewport_scroll.y - y);
4010 let window_width = self.viewport.window_size.0 as f64 / self.viewport.scale() as f64;
4011 let window_height = self.viewport.window_size.1 as f64 / self.viewport.scale() as f64;
4012
4013 let initial = self.viewport_scroll;
4014 self.viewport_scroll.x =
4015 f64::max(0.0, f64::min(new_scroll.0, content_width - window_width));
4016 self.viewport_scroll.y =
4017 f64::max(0.0, f64::min(new_scroll.1, content_height - window_height));
4018
4019 let has_changed = self.viewport_scroll != initial;
4020 if has_changed {
4021 self.resolve_positioned_scroll();
4026 }
4027 has_changed
4028 }
4029
4030 pub fn scroll_by(
4031 &mut self,
4032 anchor_node_id: Option<NodeId>,
4033 scroll_x: f64,
4034 scroll_y: f64,
4035 dispatch_event: &mut dyn FnMut(DomEvent),
4036 ) -> bool {
4037 if let Some(anchor_node_id) = anchor_node_id {
4038 self.scroll_node_by_has_changed(anchor_node_id, scroll_x, scroll_y, dispatch_event)
4039 } else {
4040 self.scroll_viewport_by_has_changed(scroll_x, scroll_y)
4041 }
4042 }
4043
4044 pub fn viewport_scroll(&self) -> crate::Point<f64> {
4045 self.viewport_scroll
4046 }
4047
4048 pub fn set_viewport_scroll(&mut self, scroll: crate::Point<f64>) {
4049 self.viewport_scroll = scroll;
4050 }
4051
4052 pub fn get_fragment_target(&self, fragment: &str) -> Option<NodeId> {
4057 if let Some(node_id) = self.get_element_by_id(fragment) {
4058 return Some(node_id);
4059 }
4060
4061 self.nodes.iter().find_map(|(id, node)| {
4063 let el = node.element_data()?;
4064 (el.name.local == local_name!("a") && el.attr(local_name!("name")) == Some(fragment))
4065 .then_some(id)
4066 })
4067 }
4068
4069 pub fn nearest_scroll_container(&self, node_id: NodeId) -> Option<NodeId> {
4080 let mut current = Some(node_id);
4081 for _ in 0..64 {
4082 let id = current?;
4083 let node = self.nodes.get(id)?;
4084 if node.style().overflow.x.is_scroll_container()
4085 || node.style().overflow.y.is_scroll_container()
4086 {
4087 return Some(id);
4088 }
4089 current = node.parent;
4090 }
4091 None
4092 }
4093
4094 pub fn scroll_nearest_container_by(&mut self, node_id: NodeId, x: f64, y: f64) -> bool {
4095 self.scroll_nearest_container_by_with_events(node_id, x, y, |_| {})
4096 }
4097
4098 pub fn scroll_nearest_container_by_with_events<F: FnMut(DomEvent)>(
4099 &mut self,
4100 node_id: NodeId,
4101 x: f64,
4102 y: f64,
4103 mut dispatch_event: F,
4104 ) -> bool {
4105 let mut current = Some(node_id);
4106 for _ in 0..64 {
4107 let Some(id) = current else { break };
4108 let Some(node) = self.nodes.get(id) else {
4109 break;
4110 };
4111 let scrolls = node.style().overflow.x.is_scroll_container()
4112 || node.style().overflow.y.is_scroll_container();
4113 if scrolls {
4114 self.scroll_node_by(id, x, y, &mut dispatch_event);
4115 return true;
4116 }
4117 current = node.parent;
4118 }
4119 self.scroll_viewport_by(x, y);
4120 false
4121 }
4122
4123 pub fn scroll_to_node(&mut self, node_id: NodeId) {
4124 self.scroll_to_node_with_events(node_id, |_| {});
4125 }
4126
4127 pub fn scroll_to_node_with_events<F: FnMut(DomEvent)>(
4128 &mut self,
4129 node_id: NodeId,
4130 dispatch_event: F,
4131 ) {
4132 self.scroll_to_node_aligned_with_events(node_id, false, dispatch_event);
4133 }
4134
4135 pub fn scroll_to_node_centered_with_events<F: FnMut(DomEvent)>(
4141 &mut self,
4142 node_id: NodeId,
4143 dispatch_event: F,
4144 ) {
4145 self.scroll_to_node_aligned_with_events(node_id, true, dispatch_event);
4146 }
4147
4148 fn scroll_to_node_aligned_with_events<F: FnMut(DomEvent)>(
4149 &mut self,
4150 node_id: NodeId,
4151 centered: bool,
4152 mut dispatch_event: F,
4153 ) {
4154 let mut scroll_target = Some(node_id);
4161 let node_id = loop {
4162 let Some(candidate) = scroll_target else {
4163 return;
4164 };
4165 let Some(node) = self.nodes.get(candidate) else {
4166 return;
4167 };
4168 if matches!(
4169 node.data,
4170 NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
4171 ) {
4172 break candidate;
4173 }
4174 scroll_target = node.layout_parent.get().or(node.parent);
4175 };
4176
4177 let mut chain = Vec::new();
4189 let mut current = self.nodes.get(node_id).and_then(|node| node.parent);
4190 while let Some(id) = current {
4191 let Some(node) = self.nodes.get(id) else {
4192 break;
4193 };
4194 let scrolls = node.style().overflow.x.is_scroll_container()
4195 || node.style().overflow.y.is_scroll_container();
4196 if scrolls {
4197 chain.push(id);
4198 }
4199 current = node.parent;
4200 }
4201
4202 for container in chain {
4206 let Some(node) = self.nodes.get(node_id) else {
4207 return;
4208 };
4209 let target = node.absolute_position(0.0, 0.0);
4210 let Some(scroller) = self.nodes.get(container) else {
4211 continue;
4212 };
4213 let box_ = scroller.absolute_position(0.0, 0.0);
4214 let layout = scroller.final_layout();
4215 let target_layout = node.final_layout();
4216 let (dx, dy) = if centered {
4220 (
4221 f64::from(
4222 box_.x + layout.size.width / 2.0
4223 - (target.x + target_layout.size.width / 2.0),
4224 ),
4225 f64::from(
4226 box_.y + layout.size.height / 2.0
4227 - (target.y + target_layout.size.height / 2.0),
4228 ),
4229 )
4230 } else {
4231 (f64::from(box_.x - target.x), f64::from(box_.y - target.y))
4232 };
4233 self.scroll_node_by(container, dx, dy, &mut dispatch_event);
4234 }
4235
4236 let Some(node) = self.nodes.get(node_id) else {
4239 return;
4240 };
4241 let target = node.absolute_position(0.0, 0.0);
4242 let current = self.viewport_scroll;
4243 let (desired_x, desired_y) = if centered {
4244 let target_layout = node.final_layout();
4245 let scale = self.viewport.scale();
4246 let viewport_width = self.viewport.window_size.0 as f32 / scale;
4247 let viewport_height = self.viewport.window_size.1 as f32 / scale;
4248 (
4249 f64::from(target.x + target_layout.size.width / 2.0 - viewport_width / 2.0)
4250 .max(0.0),
4251 f64::from(target.y + target_layout.size.height / 2.0 - viewport_height / 2.0)
4252 .max(0.0),
4253 )
4254 } else {
4255 (f64::from(target.x), f64::from(target.y))
4256 };
4257
4258 let dx = current.x - desired_x;
4261 let dy = current.y - desired_y;
4262 if let Some(root) = self.try_root_element().map(|element| element.id) {
4263 self.scroll_node_by(root, dx, dy, dispatch_event);
4264 } else {
4265 self.scroll_viewport_by(dx, dy);
4266 }
4267 }
4268
4269 pub fn scroll_to_fragment(&mut self, fragment: &str) -> bool {
4275 let decoded = percent_encoding::percent_decode_str(fragment)
4277 .decode_utf8_lossy()
4278 .into_owned();
4279
4280 if !decoded.is_empty() {
4281 if let Some(node_id) = self.get_fragment_target(&decoded) {
4282 self.scroll_to_node(node_id);
4283 return true;
4284 }
4285 }
4286
4287 if decoded.is_empty() || decoded.eq_ignore_ascii_case("top") {
4290 let current = self.viewport_scroll;
4291 self.scroll_viewport_by(current.x, current.y);
4292 return true;
4293 }
4294
4295 false
4296 }
4297
4298 pub fn get_svg_bbox(&self, node_id: NodeId) -> Option<BoundingRect> {
4301 #[cfg(feature = "svg")]
4302 {
4303 self.svg_user_bbox(node_id)
4304 }
4305 #[cfg(not(feature = "svg"))]
4306 {
4307 let _ = node_id;
4308 None
4309 }
4310 }
4311
4312 pub fn get_client_bounding_rect(&self, node_id: NodeId) -> Option<BoundingRect> {
4314 #[cfg(feature = "svg")]
4315 if let Some(root) = self.svg_geometry_root(node_id) {
4316 return self.svg_client_rect(root, node_id);
4317 }
4318
4319 if let Some(rects) = self.inline_fragment_rects(node_id) {
4322 let rects: Vec<_> = rects
4323 .into_iter()
4324 .map(|rect| self.transformed_client_rect(node_id, rect))
4325 .collect();
4326 let x0 = rects.iter().map(|r| r.x).fold(f64::INFINITY, f64::min);
4327 let y0 = rects.iter().map(|r| r.y).fold(f64::INFINITY, f64::min);
4328 let x1 = rects
4329 .iter()
4330 .map(|r| r.x + r.width)
4331 .fold(f64::NEG_INFINITY, f64::max);
4332 let y1 = rects
4333 .iter()
4334 .map(|r| r.y + r.height)
4335 .fold(f64::NEG_INFINITY, f64::max);
4336 return match rects.is_empty() {
4337 true => None,
4338 false => Some(BoundingRect {
4339 x: x0,
4340 y: y0,
4341 width: x1 - x0,
4342 height: y1 - y0,
4343 }),
4344 };
4345 }
4346
4347 let node = self.get_node(node_id)?;
4348 if !matches!(
4349 node.data,
4350 NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
4351 ) {
4352 return None;
4353 }
4354 let pos = node.absolute_position(0.0, 0.0);
4355
4356 Some(self.transformed_client_rect(
4357 node_id,
4358 BoundingRect {
4359 x: pos.x as f64 - self.viewport_scroll.x,
4360 y: pos.y as f64 - self.viewport_scroll.y,
4361 width: node.unrounded_layout().size.width as f64,
4362 height: node.unrounded_layout().size.height as f64,
4363 },
4364 ))
4365 }
4366
4367 pub(crate) fn transformed_client_rect(
4370 &self,
4371 node_id: NodeId,
4372 rect: BoundingRect,
4373 ) -> BoundingRect {
4374 let mut matrix = kurbo::Affine::IDENTITY;
4375 let mut current = self.get_node(node_id);
4376 let scale = self.viewport.scale_f64();
4377 while let Some(node) = current {
4378 if matches!(
4379 node.data,
4380 NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
4381 ) && let Some(transform) = *node.transform()
4382 {
4383 let [a, b, c, d, e, f] = transform.as_coeffs();
4384 let css_transform = kurbo::Affine::new([a, b, c, d, e / scale, f / scale]);
4385 let origin = node.absolute_position(0.0, 0.0);
4386 let translate = kurbo::Affine::translate((
4387 origin.x as f64 - self.viewport_scroll.x,
4388 origin.y as f64 - self.viewport_scroll.y,
4389 ));
4390 matrix = translate * css_transform * translate.inverse() * matrix;
4391 }
4392 current = node
4393 .layout_parent
4394 .get()
4395 .or(node.parent)
4396 .and_then(|id| self.get_node(id));
4397 }
4398 let transformed = matrix.transform_rect_bbox(kurbo::Rect::new(
4399 rect.x,
4400 rect.y,
4401 rect.x + rect.width,
4402 rect.y + rect.height,
4403 ));
4404 BoundingRect {
4405 x: transformed.x0,
4406 y: transformed.y0,
4407 width: transformed.width(),
4408 height: transformed.height(),
4409 }
4410 }
4411
4412 pub fn node_client_rects(&self, node_id: NodeId) -> Vec<BoundingRect> {
4417 #[cfg(feature = "svg")]
4418 if self.svg_geometry_root(node_id).is_some() {
4419 return self.get_client_bounding_rect(node_id).into_iter().collect();
4420 }
4421 match self.inline_fragment_rects(node_id) {
4422 Some(rects) => rects
4423 .into_iter()
4424 .map(|rect| self.transformed_client_rect(node_id, rect))
4425 .collect(),
4426 None => self.get_client_bounding_rect(node_id).into_iter().collect(),
4427 }
4428 }
4429
4430 pub(crate) fn trace_escaped_inline_fragments(&self) {
4444 static TRACE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4445 if !*TRACE.get_or_init(|| std::env::var_os("BLITZ_TRACE_INLINE").is_some()) {
4446 return;
4447 }
4448 let mut reported = 0;
4449 for (id, node) in self.nodes.iter() {
4450 if !node.is_element() {
4451 continue;
4452 }
4453 let Some(rects) = self.inline_fragment_rects(id) else {
4454 continue;
4455 };
4456 let Some(root) = node.inline_root_ancestor() else {
4457 continue;
4458 };
4459 let root_layout = root.final_layout();
4460 let root_pos = root.absolute_position(0.0, 0.0);
4461 let root_right =
4462 root_pos.x as f64 + root_layout.size.width as f64 - self.viewport_scroll.x;
4463 for rect in &rects {
4464 if rect.x + rect.width > root_right + 1.0 {
4465 reported += 1;
4466 if reported <= 12 {
4467 eprintln!(
4468 "escaped-fragment node={id:?} rect=[{:.1},{:.1} {:.1}x{:.1}] \
4469root={:?} root_right={root_right:.1} root_w={:.1} lines={} layout_scale={:.2} vp_scale={:.2} layout_w={:.1}",
4470 rect.x,
4471 rect.y,
4472 rect.width,
4473 rect.height,
4474 root.id,
4475 root_layout.size.width,
4476 root.element_data()
4477 .and_then(|e| e.inline_layout_data.as_ref())
4478 .map(|i| i.layout.len())
4479 .unwrap_or(0),
4480 root.element_data()
4481 .and_then(|e| e.inline_layout_data.as_ref())
4482 .map(|i| i.layout.scale())
4483 .unwrap_or(0.0),
4484 self.viewport.scale(),
4485 root.element_data()
4486 .and_then(|e| e.inline_layout_data.as_ref())
4487 .map(|i| i.layout.width())
4488 .unwrap_or(0.0),
4489 );
4490 }
4491 break;
4492 }
4493 }
4494 }
4495 if reported > 0 {
4496 eprintln!("escaped-fragment total={reported}");
4497 }
4498
4499 let mut narrow = 0;
4504 for (id, node) in self.nodes.iter() {
4505 let Some(inline) = node
4506 .data
4507 .downcast_element()
4508 .and_then(|element| element.inline_layout_data.as_ref())
4509 else {
4510 continue;
4511 };
4512 let box_width = node.final_layout().size.width as f64 * self.viewport.scale() as f64;
4513 let broken_at = inline.layout.width() as f64;
4514 let full = inline.layout.calculate_content_widths().max as f64;
4517 if box_width > 40.0 && broken_at < box_width * 0.6 && full > box_width * 0.9 {
4518 narrow += 1;
4519 if narrow <= 12 {
4520 eprintln!(
4521 "narrow-break node={id:?} broken_at={broken_at:.1} box={box_width:.1} \
4522 max_content={full:.1} lines={} text={:?}",
4523 inline.layout.len(),
4524 inline.text.chars().take(40).collect::<String>(),
4525 );
4526 }
4527 }
4528 }
4529 if narrow > 0 {
4530 eprintln!("narrow-break total={narrow}");
4531 }
4532 }
4533
4534 pub fn inline_fragment_rects(&self, node_id: NodeId) -> Option<Vec<BoundingRect>> {
4535 use parley::PositionedLayoutItem;
4536
4537 let node = self.get_node(node_id)?;
4538
4539 let is_text = node.is_text_node();
4542 if !is_text {
4543 if !node.is_element() || node.flags.is_inline_root() {
4544 return None;
4545 }
4546 let display = node.primary_styles()?.clone_display();
4547 if !(display.outside() == DisplayOutside::Inline
4548 && display.inside() == DisplayInside::Flow)
4549 {
4550 return None;
4551 }
4552 }
4553
4554 let inline_root = node.inline_root_ancestor()?;
4555 let inline_layout = inline_root.element_data()?.inline_layout_data.as_ref()?;
4556 let layout = &inline_layout.layout;
4557 let scale = layout.scale() as f64;
4558
4559 let is_in_target = |mut id: NodeId| -> bool {
4562 loop {
4563 if id == node_id {
4564 return true;
4565 }
4566 if id == inline_root.id {
4567 return false;
4568 }
4569 match self.get_node(id).and_then(|n| n.parent) {
4570 Some(parent) => id = parent,
4571 None => return false,
4572 }
4573 }
4574 };
4575
4576 let root_layout = inline_root.final_layout();
4578 let root_pos = inline_root.absolute_position(0.0, 0.0);
4579 let origin_x = root_pos.x as f64
4580 + (root_layout.padding.left + root_layout.border.left) as f64
4581 - self.viewport_scroll.x;
4582 let origin_y = root_pos.y as f64
4583 + (root_layout.padding.top + root_layout.border.top) as f64
4584 - self.viewport_scroll.y;
4585
4586 let mut rects: Vec<BoundingRect> = Vec::new();
4587 for line in layout.lines() {
4588 let line_metrics = line.metrics();
4589 let mut line_rect: Option<(f64, f64, f64, f64)> = None;
4591 let mut add = |x0: f64, y0: f64, x1: f64, y1: f64| {
4592 line_rect = Some(match line_rect {
4593 Some((lx0, ly0, lx1, ly1)) => {
4594 (lx0.min(x0), ly0.min(y0), lx1.max(x1), ly1.max(y1))
4595 }
4596 None => (x0, y0, x1, y1),
4597 });
4598 };
4599
4600 for item in line.items() {
4601 match item {
4602 PositionedLayoutItem::GlyphRun(glyph_run) => {
4603 let brush = glyph_run.style().brush;
4604 let matches = if is_text {
4605 brush.text_node == Some(node_id)
4606 } else {
4607 is_in_target(brush.id)
4608 };
4609 if !matches {
4610 continue;
4611 }
4612 let x0 = glyph_run.offset() as f64;
4613 let x1 = x0 + glyph_run.advance() as f64;
4614 let y0 = line_metrics.block_min_coord as f64;
4620 let y1 = line_metrics.block_max_coord as f64;
4621 add(x0, y0, x1, y1);
4622 }
4623 PositionedLayoutItem::InlineBox(inline_box) => {
4624 if is_text || !is_in_target(NodeId::from_u64(inline_box.id)) {
4625 continue;
4626 }
4627 let x0 = inline_box.x as f64;
4628 let y0 = inline_box.y as f64;
4629 add(
4630 x0,
4631 y0,
4632 x0 + inline_box.width as f64,
4633 y0 + inline_box.height as f64,
4634 );
4635 }
4636 }
4637 }
4638
4639 if let Some((x0, y0, x1, y1)) = line_rect {
4640 rects.push(BoundingRect {
4641 x: origin_x + x0 / scale,
4642 y: origin_y + y0 / scale,
4643 width: (x1 - x0) / scale,
4644 height: (y1 - y0) / scale,
4645 });
4646 }
4647 }
4648
4649 Some(rects)
4650 }
4651
4652 pub fn find_title_node(&self) -> Option<&Node> {
4653 TreeTraverser::new(self)
4654 .find(|node_id| {
4655 let node = &self.nodes[*node_id];
4656 let Some(element) = node.element_data() else {
4657 return false;
4658 };
4659 if element.name.ns != ns!(html) || element.name.local != local_name!("title") {
4660 return false;
4661 }
4662 node.parent
4663 .and_then(|parent_id| self.nodes.get(parent_id))
4664 .and_then(Node::element_data)
4665 .is_some_and(|parent| {
4666 parent.name.ns == ns!(html) && parent.name.local == local_name!("head")
4667 })
4668 })
4669 .map(|node_id| &self.nodes[node_id])
4670 }
4671
4672 pub fn with_text_input(
4673 &mut self,
4674 node_id: NodeId,
4675 cb: impl FnOnce(PlainEditorDriver<TextBrush>),
4676 ) {
4677 let Some(node) = self.nodes.get_mut(node_id) else {
4678 return;
4679 };
4680
4681 if let Some(text_input) = node
4682 .element_data_mut()
4683 .and_then(|el| el.text_input_data_mut())
4684 {
4685 let mut font_ctx = self.font_ctx.lock().unwrap();
4686 let layout_ctx = &mut self.layout_ctx;
4687 let driver = text_input.editor.driver(&mut font_ctx, layout_ctx);
4688 cb(driver)
4689 }
4690 }
4691
4692 pub(crate) fn clamp_text_input_scroll(&mut self, node_id: NodeId) {
4695 let Some(node) = self.nodes.get_mut(node_id) else {
4696 return;
4697 };
4698
4699 let content_box_width = node.final_layout().content_box_width();
4700 let content_box_height = node.final_layout().content_box_height();
4701
4702 if let Some(text_input) = node
4703 .element_data_mut()
4704 .and_then(|el| el.text_input_data_mut())
4705 {
4706 text_input.clamp_scroll_offset(content_box_width, content_box_height);
4707 }
4708 }
4709
4710 pub(crate) fn compute_has_canvas(&self) -> bool {
4711 TreeTraverser::new(self).any(|node_id| {
4712 let node = &self.nodes[node_id];
4713 let Some(element) = node.element_data() else {
4714 return false;
4715 };
4716 if element.name.local == local_name!("canvas") && element.has_attr(local_name!("src")) {
4717 return true;
4718 }
4719
4720 false
4721 })
4722 }
4723
4724 pub fn find_text_position(&self, x: f32, y: f32) -> Option<(NodeId, usize)> {
4730 let hit = self.hit(x, y)?;
4731 let hit_node = self.get_node(hit.node_id)?;
4732 let inline_root = hit_node.inline_root_ancestor()?;
4733 let byte_offset = inline_root.text_offset_at_point(hit.x, hit.y)?;
4734 Some((inline_root.id, byte_offset))
4735 }
4736
4737 pub fn find_text_range(
4743 &self,
4744 x: f32,
4745 y: f32,
4746 granularity: TextGranularity,
4747 ) -> Option<(NodeId, usize, usize)> {
4748 let hit = self.hit(x, y)?;
4749 let hit_node = self.get_node(hit.node_id)?;
4750 let inline_root = hit_node.inline_root_ancestor()?;
4751 let range = inline_root.text_range_at_point(hit.x, hit.y, granularity)?;
4752 Some((inline_root.id, range.start, range.end))
4753 }
4754
4755 pub fn set_text_selection(
4757 &mut self,
4758 anchor_node: NodeId,
4759 anchor_offset: usize,
4760 focus_node: NodeId,
4761 focus_offset: usize,
4762 ) {
4763 self.dom_selection = None;
4764 self.text_selection =
4765 TextSelection::new(anchor_node, anchor_offset, focus_node, focus_offset);
4766
4767 if let (Some(parent), Some(idx)) = self.anonymous_block_location(anchor_node) {
4769 self.text_selection
4770 .anchor
4771 .set_anonymous(parent, idx, anchor_offset);
4772 }
4773 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
4774 self.text_selection
4775 .focus
4776 .set_anonymous(parent, idx, focus_offset);
4777 }
4778 }
4779
4780 fn anonymous_block_location(&self, node_id: NodeId) -> (Option<NodeId>, Option<usize>) {
4783 let Some(node) = self.get_node(node_id) else {
4784 return (None, None);
4785 };
4786
4787 if !node.is_anonymous() {
4788 return (None, None);
4789 }
4790
4791 let Some(parent_id) = node.parent else {
4792 return (None, None);
4793 };
4794
4795 let Some(parent) = self.get_node(parent_id) else {
4796 return (Some(parent_id), None);
4797 };
4798
4799 let layout_children = parent.layout_children.borrow();
4800 let Some(children) = layout_children.as_ref() else {
4801 return (Some(parent_id), None);
4802 };
4803
4804 let mut anon_index = 0;
4806 for &child_id in children.iter() {
4807 if child_id == node_id {
4808 return (Some(parent_id), Some(anon_index));
4809 }
4810 if self.get_node(child_id).is_some_and(|n| n.is_anonymous()) {
4811 anon_index += 1;
4812 }
4813 }
4814
4815 (Some(parent_id), None)
4816 }
4817
4818 pub fn clear_text_selection(&mut self) {
4820 self.dom_selection = None;
4821 self.text_selection.clear();
4822 }
4823
4824 pub fn update_selection_focus(&mut self, focus_node: NodeId, focus_offset: usize) {
4826 self.dom_selection = None;
4827 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
4829 self.text_selection
4830 .focus
4831 .set_anonymous(parent, idx, focus_offset);
4832 } else {
4833 self.text_selection.set_focus(focus_node, focus_offset);
4834 }
4835 }
4836
4837 pub fn extend_text_selection_to_point(&mut self, x: f32, y: f32) -> bool {
4840 if !self.text_selection.anchor.is_some() {
4841 return false;
4842 }
4843
4844 if let Some((node, offset)) = self.find_text_position(x, y) {
4845 self.update_selection_focus(node, offset);
4846 self.shell_provider.request_redraw();
4847 true
4848 } else {
4849 false
4850 }
4851 }
4852
4853 fn find_anonymous_block_by_index(
4855 &self,
4856 parent_id: NodeId,
4857 target_index: usize,
4858 ) -> Option<NodeId> {
4859 let parent = self.get_node(parent_id)?;
4860 let layout_children = parent.layout_children.borrow();
4861 let children = layout_children.as_ref()?;
4862
4863 children
4864 .iter()
4865 .filter(|&&child_id| self.get_node(child_id).is_some_and(|n| n.is_anonymous()))
4866 .nth(target_index)
4867 .copied()
4868 }
4869
4870 pub fn has_text_selection(&self) -> bool {
4872 self.dom_selection.as_ref().map_or_else(
4873 || self.text_selection.is_active(),
4874 |range| !range.bounds().collapsed(),
4875 )
4876 }
4877
4878 pub fn get_selected_text(&self) -> Option<String> {
4880 if let Some(range) = &self.dom_selection {
4881 let text = self.range_string(range.bounds());
4882 return (!text.is_empty()).then_some(text);
4883 }
4884 let ranges = self.get_text_selection_ranges();
4885 if ranges.is_empty() {
4886 return None;
4887 }
4888
4889 let mut result = String::new();
4890 for (node_id, start, end) in &ranges {
4891 let node = self.get_node(*node_id)?;
4892 let element_data = node.element_data()?;
4893 let inline_layout = element_data.inline_layout_data.as_ref()?;
4894
4895 if *end > inline_layout.text.len() {
4896 continue;
4897 }
4898
4899 if !result.is_empty() {
4900 result.push(' ');
4901 }
4902 result.push_str(&inline_layout.text[*start..*end]);
4903 }
4904
4905 if result.is_empty() {
4906 None
4907 } else {
4908 Some(result)
4909 }
4910 }
4911
4912 pub fn get_selected_html(&self) -> Option<String> {
4925 let ranges = self.get_text_selection_ranges();
4926 if ranges.is_empty() {
4927 return None;
4928 }
4929
4930 let mut result = String::new();
4931 for (node_id, start, end) in &ranges {
4932 let Some(node) = self.get_node(*node_id) else {
4933 continue;
4934 };
4935 let Some(element_data) = node.element_data() else {
4936 continue;
4937 };
4938 let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
4939 continue;
4940 };
4941 if *end > inline_layout.text.len() {
4942 continue;
4943 }
4944
4945 if !result.is_empty() {
4946 result.push(' ');
4947 }
4948
4949 let tag = element_data.name.local.as_ref();
4950 let fragment = escape_html(&inline_layout.text[*start..*end]);
4951 if matches!(
4956 tag,
4957 "b" | "strong" | "i" | "em" | "code" | "kbd" | "mark" | "s" | "u" | "sub" | "sup"
4958 ) {
4959 result.push_str(&format!("<{tag}>{fragment}</{tag}>"));
4960 } else {
4961 result.push_str(&fragment);
4962 }
4963 }
4964
4965 if result.is_empty() {
4966 None
4967 } else {
4968 Some(result)
4969 }
4970 }
4971
4972 pub fn get_text_selection_ranges(&self) -> Vec<(NodeId, usize, usize)> {
4975 if let Some(range) = &self.dom_selection {
4976 return self.range_layout_ranges(range.bounds());
4977 }
4978 let lookup = |parent_id, idx| self.find_anonymous_block_by_index(parent_id, idx);
4979
4980 let anchor_node = match self.text_selection.anchor.resolve_node_id(lookup) {
4981 Some(id) => id,
4982 None => return Vec::new(),
4983 };
4984 let focus_node = match self.text_selection.focus.resolve_node_id(lookup) {
4985 Some(id) => id,
4986 None => return Vec::new(),
4987 };
4988
4989 let node_is_in_doc = |node_id: NodeId| {
4992 self.nodes
4993 .get(node_id)
4994 .is_some_and(|node| node.flags.is_in_document())
4995 };
4996 if !node_is_in_doc(anchor_node) || !node_is_in_doc(focus_node) {
4997 return Vec::new();
4998 }
4999
5000 if anchor_node == focus_node {
5002 let start = self
5003 .text_selection
5004 .anchor
5005 .offset
5006 .min(self.text_selection.focus.offset);
5007 let end = self
5008 .text_selection
5009 .anchor
5010 .offset
5011 .max(self.text_selection.focus.offset);
5012
5013 if start == end {
5014 return Vec::new();
5015 }
5016 return vec![(anchor_node, start, end)];
5017 }
5018
5019 let inline_roots = self.collect_inline_roots_in_range(anchor_node, focus_node);
5021 if inline_roots.is_empty() {
5022 return Vec::new();
5023 }
5024
5025 let first_in_roots = inline_roots[0];
5028
5029 let (first_node, first_offset, last_node, last_offset) =
5030 if first_in_roots == anchor_node || (first_in_roots != focus_node) {
5031 (
5033 anchor_node,
5034 self.text_selection.anchor.offset,
5035 focus_node,
5036 self.text_selection.focus.offset,
5037 )
5038 } else {
5039 (
5041 focus_node,
5042 self.text_selection.focus.offset,
5043 anchor_node,
5044 self.text_selection.anchor.offset,
5045 )
5046 };
5047
5048 let mut ranges = Vec::with_capacity(inline_roots.len());
5049
5050 for &node_id in &inline_roots {
5051 let Some(node) = self.get_node(node_id) else {
5052 continue;
5053 };
5054 let Some(element_data) = node.element_data() else {
5055 continue;
5056 };
5057 let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
5058 continue;
5059 };
5060
5061 let text_len = inline_layout.text.len();
5062
5063 if node_id == first_node && node_id == last_node {
5064 let start = first_offset.min(last_offset);
5065 let end = first_offset.max(last_offset);
5066 if start < end && end <= text_len {
5067 ranges.push((node_id, start, end));
5068 }
5069 } else if node_id == first_node {
5070 if first_offset < text_len {
5071 ranges.push((node_id, first_offset, text_len));
5072 }
5073 } else if node_id == last_node {
5074 if last_offset > 0 && last_offset <= text_len {
5075 ranges.push((node_id, 0, last_offset));
5076 }
5077 } else if text_len > 0 {
5078 ranges.push((node_id, 0, text_len));
5079 }
5080 }
5081
5082 ranges
5083 }
5084}
5085
5086#[derive(Debug, Clone, Copy, PartialEq)]
5087pub struct BoundingRect {
5088 pub x: f64,
5089 pub y: f64,
5090 pub width: f64,
5091 pub height: f64,
5092}
5093
5094impl BaseDocument {
5097 pub(crate) fn record_details_open(
5100 &mut self,
5101 node_id: NodeId,
5102 name: &markup5ever::QualName,
5103 present: bool,
5104 ) {
5105 if name.local != local_name!("open") || !name.ns.is_empty() {
5106 return;
5107 }
5108 let Some(element) = self.nodes[node_id].element_data() else {
5109 return;
5110 };
5111 if element.name.local != local_name!("details") || element.name.ns != ns!(html) {
5112 return;
5113 }
5114 let was_open = element.attr(local_name!("open")).is_some();
5115 if was_open != present && !self.details_toggles.iter().any(|(id, _)| *id == node_id) {
5116 self.details_toggles.push((node_id, was_open));
5117 }
5118 }
5119
5120 pub(crate) fn template_contents_owner(&mut self) -> NodeId {
5122 if let Some(owner) = self.template_contents_owner {
5123 return owner;
5124 }
5125 let mut data = crate::node::DocumentData::default();
5126 data.content_type = "text/html";
5127 let owner = self.create_node(NodeData::Document(Box::new(data)));
5128 self.template_contents_owner = Some(owner);
5129 owner
5130 }
5131
5132 pub fn viewport_generation(&self) -> u64 {
5134 self.viewport_generation
5135 }
5136
5137 pub fn take_details_toggles(&mut self) -> Vec<(NodeId, bool)> {
5139 std::mem::take(&mut self.details_toggles)
5140 }
5141
5142 pub(crate) fn record_inline_handler_attribute(
5145 &mut self,
5146 node_id: NodeId,
5147 name: &crate::QualName,
5148 value: Option<&str>,
5149 ) {
5150 if name.ns != markup5ever::ns!() || !name.local.as_ref().starts_with("on") {
5151 return;
5152 }
5153 let Some(element) = self
5154 .nodes
5155 .get_mut(node_id)
5156 .and_then(|node| node.element_data_mut())
5157 else {
5158 return;
5159 };
5160 let value = value.map(crate::node::AttrAtom::from);
5161 if let Some(value) = &value {
5162 let attributes = element
5163 .inline_event_attributes
5164 .get_or_insert_with(|| Box::new(Vec::new()));
5165 if let Some(attribute) = attributes
5166 .iter_mut()
5167 .find(|attribute| attribute.name.local == name.local)
5168 {
5169 attribute.name = name.clone();
5170 attribute.value = value.clone();
5171 } else {
5172 attributes.push(crate::node::Attribute {
5173 name: name.clone(),
5174 value: value.clone(),
5175 });
5176 }
5177 } else {
5178 let Some(attributes) = element.inline_event_attributes.as_mut() else {
5179 return;
5180 };
5181 let Some(index) = attributes
5182 .iter()
5183 .position(|attribute| attribute.name.local == name.local)
5184 else {
5185 return;
5186 };
5187 attributes.remove(index);
5188 if attributes.is_empty() {
5189 element.inline_event_attributes = None;
5190 }
5191 }
5192 self.inline_handler_changes
5193 .push((node_id, name.clone(), value));
5194 }
5195
5196 pub fn take_inline_handler_changes(
5198 &mut self,
5199 ) -> Vec<(NodeId, crate::QualName, Option<crate::node::AttrAtom>)> {
5200 std::mem::take(&mut self.inline_handler_changes)
5201 }
5202
5203 pub fn set_recording_mutations(&mut self, recording: bool) {
5206 match (recording, self.mutation_log.is_some()) {
5207 (true, false) => self.mutation_log = Some(Vec::new()),
5208 (false, true) => self.mutation_log = None,
5209 _ => {}
5210 }
5211 }
5212
5213 pub fn is_recording_mutations(&self) -> bool {
5215 self.mutation_log.is_some()
5216 }
5217
5218 pub fn take_mutations(&mut self) -> Vec<crate::DomMutation> {
5220 self.mutation_log
5221 .as_mut()
5222 .map(std::mem::take)
5223 .unwrap_or_default()
5224 }
5225
5226 pub(crate) fn record_mutation(&mut self, mutation: crate::DomMutation) {
5227 if let Some(log) = self.mutation_log.as_mut() {
5228 if log.len() < crate::mutation_record::MUTATION_LOG_CAP {
5229 log.push(mutation);
5230 }
5231 }
5232 }
5233
5234 pub(crate) fn sibling_context(
5237 &self,
5238 node_id: NodeId,
5239 ) -> Option<(NodeId, Option<NodeId>, Option<NodeId>)> {
5240 let parent_id = self.nodes.get(node_id)?.parent?;
5241 let siblings = &self.nodes.get(parent_id)?.children;
5242 let index = siblings.iter().position(|id| *id == node_id)?;
5243 let previous = index.checked_sub(1).map(|i| siblings[i]);
5244 let next = siblings.get(index + 1).copied();
5245 Some((parent_id, previous, next))
5246 }
5247}
5248
5249impl AsRef<BaseDocument> for BaseDocument {
5250 fn as_ref(&self) -> &BaseDocument {
5251 self
5252 }
5253}
5254
5255impl AsMut<BaseDocument> for BaseDocument {
5256 fn as_mut(&mut self) -> &mut BaseDocument {
5257 self
5258 }
5259}
5260
5261#[cfg(test)]
5262mod hover_state_tests {
5263 use super::*;
5264 use crate::{Attribute, qual_name};
5265 use blitz_traits::shell::ColorScheme;
5266
5267 fn make_doc() -> (BaseDocument, NodeId) {
5274 let mut doc = BaseDocument::new(DocumentConfig {
5275 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
5276 ..Default::default()
5277 });
5278 let root_id = doc.root_node().id;
5279 let style = |value: &str| Attribute {
5280 name: qual_name!("style"),
5281 value: value.into(),
5282 };
5283
5284 let mut mutator = doc.mutate();
5285 let html = mutator.create_element(qual_name!("html"), vec![]);
5286 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
5287 let container = mutator.create_element(qual_name!("div"), vec![style("width:300px")]);
5288 let text = mutator.create_text_node("some text");
5289 let block = mutator.create_element(qual_name!("div"), vec![style("height:50px")]);
5290 mutator.append_children(container, &[text, block]);
5291 mutator.append_children(body, &[container]);
5292 mutator.append_children(html, &[body]);
5293 mutator.append_children(root_id, &[html]);
5294 drop(mutator);
5295
5296 doc.resolve(0.0);
5297 (doc, container)
5298 }
5299
5300 fn text_has_size(doc: &BaseDocument, container: NodeId) -> bool {
5304 doc.nodes[container].final_layout().size.height > 50.0
5305 }
5306
5307 #[test]
5313 fn hovering_text_in_anonymous_block_reports_text_cursor() {
5314 let (mut doc, container) = make_doc();
5315 if !text_has_size(&doc, container) {
5316 eprintln!("skipping: no usable font (text measures 0x0)");
5317 return;
5318 }
5319
5320 doc.set_hover_to(5.0, 8.0);
5321 assert!(doc.hover_node_is_text, "expected a text hit");
5322 let hit_id = doc.hover_hit_node_id.expect("expected a hit node");
5323 assert!(
5324 doc.nodes[hit_id].is_anonymous(),
5325 "expected the hit node to be the anonymous inline root"
5326 );
5327 assert_eq!(
5328 doc.get_hover_node_id(),
5329 Some(container),
5330 "expected the stored hover target to be the containing element"
5331 );
5332 assert_eq!(doc.get_cursor(), Some(CursorIcon::Text));
5333 }
5334
5335 #[test]
5336 fn semantic_hover_keeps_the_resolved_node_instead_of_hit_testing_again() {
5337 let (mut doc, container) = make_doc();
5338
5339 doc.set_hover_to_node(container, 350.0, 250.0);
5343
5344 assert_eq!(doc.get_hover_node_id(), Some(container));
5345 assert_eq!(doc.hover_hit_node_id, Some(container));
5346
5347 doc.resolve(0.0);
5348 assert_eq!(
5349 doc.get_hover_node_id(),
5350 Some(container),
5351 "a resolve must not turn semantic identity back into a coordinate hit"
5352 );
5353 }
5354
5355 #[test]
5358 fn hovering_anonymous_block_whitespace_reports_default_cursor() {
5359 let (mut doc, container) = make_doc();
5360 if !text_has_size(&doc, container) {
5361 eprintln!("skipping: no usable font (text measures 0x0)");
5362 return;
5363 }
5364
5365 doc.set_hover_to(250.0, 8.0);
5366 assert!(!doc.hover_node_is_text);
5367 assert_eq!(doc.get_hover_node_id(), Some(container));
5368 assert_eq!(doc.get_cursor(), Some(CursorIcon::Default));
5369 }
5370}
5371
5372#[cfg(test)]
5373mod control_scroll_tests {
5374 use super::*;
5375 use crate::{Attribute, qual_name};
5376 use blitz_traits::shell::ColorScheme;
5377
5378 #[test]
5379 fn controlled_scroll_dispatches_the_dom_scroll_event() {
5380 let mut doc = BaseDocument::new(DocumentConfig {
5381 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
5382 ..Default::default()
5383 });
5384 let root_id = doc.root_node().id;
5385 let style = |value: &str| Attribute {
5386 name: qual_name!("style"),
5387 value: value.into(),
5388 };
5389
5390 let mut mutator = doc.mutate();
5391 let html = mutator.create_element(qual_name!("html"), vec![]);
5392 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
5393 let scroller = mutator.create_element(
5394 qual_name!("div"),
5395 vec![style("width:200px;height:100px;overflow-y:scroll")],
5396 );
5397 let spacer = mutator.create_element(qual_name!("div"), vec![style("height:400px")]);
5398 let target = mutator.create_element(qual_name!("button"), vec![style("height:40px")]);
5399 let target_text = mutator.create_text_node("Reveal me");
5400 mutator.append_children(target, &[target_text]);
5401 mutator.append_children(scroller, &[spacer, target]);
5402 mutator.append_children(body, &[scroller]);
5403 mutator.append_children(html, &[body]);
5404 mutator.append_children(root_id, &[html]);
5405 drop(mutator);
5406 doc.resolve(0.0);
5407
5408 doc.nodes[html].final_layout_mut().size.height = 300.0;
5412 doc.nodes[html].final_layout_mut().content_size.height = 600.0;
5413 doc.nodes[target].final_layout_mut().location.y = 400.0;
5414
5415 let mut events = Vec::new();
5416 doc.scroll_to_node_centered_with_events(target_text, |event| events.push(event));
5420
5421 assert!(
5422 (doc.viewport_scroll.y - 270.0).abs() < 0.1,
5423 "the 40px target should be centred in the 300px viewport"
5424 );
5425 assert!(
5426 events
5427 .iter()
5428 .any(|event| { event.target == html && event.name() == "scroll" })
5429 );
5430 }
5431}
5432
5433#[cfg(test)]
5434mod font_face_override_tests {
5435 use super::*;
5436 use crate::net::{FontFaceOverrides, Resource, ResourceLoadResponse};
5437
5438 #[test]
5454 fn font_face_overrides_alias_family_name() {
5455 const ALIAS: &str = "AliasedFamily";
5456
5457 let mut document = BaseDocument::new(DocumentConfig::default());
5458
5459 {
5461 let mut ctx = document.font_ctx.lock().unwrap();
5462 assert!(
5463 ctx.collection.family_id(ALIAS).is_none(),
5464 "alias must not exist before registration",
5465 );
5466 }
5467
5468 let response = ResourceLoadResponse {
5473 request_id: 0,
5474 node_id: None,
5475 resolved_url: Some(String::from("test://aliased-family")),
5476 result: Ok(Resource::Font(
5477 blitz_traits::net::Bytes::from_static(crate::BULLET_FONT),
5478 FontFaceOverrides {
5479 family_name: Some(String::from(ALIAS)),
5480 weight: Some(800.0),
5481 style: Some(parley::fontique::FontStyle::Italic),
5482 },
5483 )),
5484 };
5485 document.load_resource(response);
5486
5487 let mut ctx = document.font_ctx.lock().unwrap();
5490 let family_id = ctx
5491 .collection
5492 .family_id(ALIAS)
5493 .expect("CSS-declared family name should be registered as a family alias");
5494 let resolved_name = ctx
5495 .collection
5496 .family_name(family_id)
5497 .expect("family id should resolve back to a name");
5498 assert_eq!(
5499 resolved_name, ALIAS,
5500 "registered family should report the CSS-declared name, \
5501 not the font file's internal `name` table entry",
5502 );
5503 }
5504}
5505
5506#[cfg(test)]
5507mod clipboard_html_tests {
5508 use super::escape_html;
5509
5510 #[test]
5513 fn markup_characters_in_a_selection_are_escaped() {
5514 assert_eq!(
5515 escape_html("a < b && c > d"),
5516 "a < b && c > d"
5517 );
5518 }
5519
5520 #[test]
5523 fn an_ampersand_is_not_double_escaped() {
5524 assert_eq!(escape_html("<"), "&lt;");
5525 }
5526
5527 #[test]
5530 fn prose_is_untouched() {
5531 assert_eq!(escape_html("it's \"fine\""), "it's "fine"");
5532 }
5533}