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::handoff::{EventQueue, EventTarget};
26use blitz_traits::navigation::{DummyNavigationProvider, NavigationProvider};
27use blitz_traits::net::{AbortSignal, DummyNetProvider, NetProvider, Request, RequestDestination};
28use blitz_traits::node_id::NodeId;
29use blitz_traits::shell::{ColorScheme, DummyShellProvider, ShellProvider, Viewport};
30use cursor_icon::CursorIcon;
31use linebender_resource_handle::Blob;
32use markup5ever::{local_name, ns};
33use parley::{FontContext, PlainEditorDriver};
34use rustc_hash::FxHashSet;
35use selectors::{Element, matching::QuirksMode};
36use smallvec::SmallVec;
37use std::any::Any;
38use std::cell::RefCell;
39use std::collections::{BTreeMap, Bound, HashMap, HashSet};
40use std::ops::{Deref, DerefMut};
41use std::rc::Rc;
42use std::str::FromStr;
43use std::sync::atomic::{AtomicUsize, Ordering};
44use std::sync::{Arc, Mutex, MutexGuard, OnceLock, RwLockReadGuard, RwLockWriteGuard};
45use std::task::{Context as TaskContext, Waker};
46use style::Atom;
47use style::animation::{AnimationState, DocumentAnimationSet};
48use style::attr::{AttrIdentifier, AttrValue};
49use style::data::{ElementData as StyloElementData, ElementStyles};
50use style::invalidation::element::restyle_hints::RestyleHint;
51use style::media_queries::MediaType;
52use style::properties::ComputedValues;
53use style::properties::style_structs::Font;
54use style::queries::values::PrefersColorScheme;
55use style::selector_parser::ServoElementSnapshot;
56use style::servo::media_features::PointerCapabilities;
57use style::servo_arc::Arc as ServoArc;
58use style::values::GenericAtomIdent;
59use style::values::computed::ui::CursorKind;
60use style::values::computed::{Overflow, UserSelect};
61use style::values::specified::box_::{DisplayInside, DisplayOutside};
62use style::{
63 device::Device,
64 dom::{TDocument, TNode},
65 media_queries::MediaList,
66 selector_parser::SnapshotMap,
67 shared_lock::{SharedRwLock, StylesheetGuards},
68 stylesheets::{AllowImportRules, DocumentStyleSheet, Origin, Stylesheet},
69 stylist::Stylist,
70};
71use thin_vec::ThinVec;
72use url::Url;
73use web_time::Instant;
74
75#[cfg(feature = "parallel-construct")]
76use thread_local::ThreadLocal;
77
78pub enum DocGuard<'a> {
79 Ref(&'a BaseDocument),
80 RefCell(std::cell::Ref<'a, BaseDocument>),
81 RwLock(RwLockReadGuard<'a, BaseDocument>),
82 Mutex(MutexGuard<'a, BaseDocument>),
83}
84
85impl Deref for DocGuard<'_> {
86 type Target = BaseDocument;
87 #[inline(always)]
88 fn deref(&self) -> &Self::Target {
89 match self {
90 Self::Ref(base_document) => base_document,
91 Self::RefCell(refcell_guard) => refcell_guard,
92 Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
93 Self::Mutex(mutex_guard) => mutex_guard,
94 }
95 }
96}
97
98pub enum DocGuardMut<'a> {
99 Ref(&'a mut BaseDocument),
100 RefCell(std::cell::RefMut<'a, BaseDocument>),
101 RwLock(RwLockWriteGuard<'a, BaseDocument>),
102 Mutex(MutexGuard<'a, BaseDocument>),
103}
104
105impl Deref for DocGuardMut<'_> {
106 type Target = BaseDocument;
107 #[inline(always)]
108 fn deref(&self) -> &Self::Target {
109 match self {
110 Self::Ref(base_document) => base_document,
111 Self::RefCell(refcell_guard) => refcell_guard,
112 Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
113 Self::Mutex(mutex_guard) => mutex_guard,
114 }
115 }
116}
117
118impl DerefMut for DocGuardMut<'_> {
119 #[inline(always)]
120 fn deref_mut(&mut self) -> &mut Self::Target {
121 match self {
122 Self::Ref(base_document) => base_document,
123 Self::RefCell(refcell_guard) => &mut *refcell_guard,
124 Self::RwLock(rw_lock_read_guard) => &mut *rw_lock_read_guard,
125 Self::Mutex(mutex_guard) => &mut *mutex_guard,
126 }
127 }
128}
129
130pub trait Document: Any + 'static {
133 fn inner(&self) -> DocGuard<'_>;
134 fn inner_mut(&mut self) -> DocGuardMut<'_>;
135
136 fn handle_ui_event(&mut self, event: UiEvent) {
138 if self.inner().page_frozen || self.inner().page_retiring || self.inner().page_input_blocked
139 {
140 return;
141 }
142 let mut doc = self.inner_mut();
143 let mut driver = EventDriver::new(&mut *doc, NoopEventHandler);
144 driver.handle_ui_event(event);
145 }
146
147 fn poll(&mut self, task_context: Option<TaskContext>) -> bool {
149 let _ = task_context;
151 false
152 }
153
154 fn poll_for_settle(&mut self, task_context: Option<TaskContext>, run_timers: bool) -> bool {
163 let _ = run_timers;
164 self.poll(task_context)
165 }
166
167 fn prepare_teardown(&mut self) {
169 self.inner_mut().prepare_teardown();
170 }
171
172 fn dispose(mut self: Box<Self>) -> Arc<crate::release::Teardown> {
173 self.prepare_teardown();
174 let receipt = Arc::clone(&self.inner().teardown);
175 drop(self);
176 receipt
177 }
178
179 fn set_page_visible(&mut self, visible: bool) {
180 self.inner_mut().page_visible = visible;
181 }
182 fn freeze(&mut self) {
183 self.inner_mut().page_frozen = true;
184 }
185 fn finish_restore(&mut self) {
186 self.inner_mut().page_input_blocked = false;
187 }
188 fn resume(&mut self) {
189 self.inner_mut().page_frozen = false;
190 }
191
192 fn id(&self) -> usize {
194 self.inner().id
195 }
196}
197
198#[derive(Debug, Clone, PartialEq)]
201pub struct PreClickActivation {
202 previous: Vec<(NodeId, bool)>,
204}
205
206fn escape_html(text: &str) -> String {
212 let mut out = String::with_capacity(text.len());
213 for ch in text.chars() {
214 match ch {
215 '&' => out.push_str("&"),
216 '<' => out.push_str("<"),
217 '>' => out.push_str(">"),
218 '"' => out.push_str("""),
219 _ => out.push(ch),
220 }
221 }
222 out
223}
224
225pub struct PlainDocument(pub BaseDocument);
226impl Document for PlainDocument {
227 fn inner(&self) -> DocGuard<'_> {
228 DocGuard::Ref(&self.0)
229 }
230 fn inner_mut(&mut self) -> DocGuardMut<'_> {
231 DocGuardMut::Ref(&mut self.0)
232 }
233}
234
235impl Document for BaseDocument {
236 fn inner(&self) -> DocGuard<'_> {
237 DocGuard::Ref(self)
238 }
239 fn inner_mut(&mut self) -> DocGuardMut<'_> {
240 DocGuardMut::Ref(self)
241 }
242}
243
244impl Document for Rc<RefCell<BaseDocument>> {
245 fn inner(&self) -> DocGuard<'_> {
246 DocGuard::RefCell(self.borrow())
247 }
248
249 fn inner_mut(&mut self) -> DocGuardMut<'_> {
250 DocGuardMut::RefCell(self.borrow_mut())
251 }
252}
253
254pub enum DocumentEvent {
255 ResourceLoad(ResourceLoadResponse),
256 NavigateIframe {
259 node_id: NodeId,
260 url: Url,
261 },
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
266pub enum AnimationPacing {
267 Idle,
268 Caret,
269 SlowCss,
270 Interactive,
271}
272
273pub struct BaseDocument {
274 id: usize,
276
277 pub(crate) url: DocumentUrl,
280 pub(crate) devtool_settings: DevtoolSettings,
282 pub(crate) viewport: Viewport,
284 pub(crate) viewport_scroll: crate::Point<f64>,
286 pub(crate) mutation_log: Option<Vec<crate::DomMutation>>,
289 inline_handler_changes: Vec<(NodeId, crate::QualName, Option<crate::node::AttrAtom>)>,
291 pub(crate) live_ranges: Vec<crate::range::WeakRange>,
293 pub dom_selection: Option<crate::LiveRange>,
295 pub(crate) media_type: MediaType,
297 pub(crate) style_threading: StyleThreading,
299 pub(crate) incremental_layout: bool,
301 pub(crate) subdocument_depth: usize,
304
305 pub(crate) tx: EventTarget<DocumentEvent>,
307 pub(crate) rx: EventQueue<DocumentEvent>,
308
309 pub(crate) nodes: Box<NodeTree>,
314
315 node_drop_depth: usize,
317 pub(crate) mutator_depth: usize,
319 pub(crate) mutator_redraw_pending: bool,
320 details_toggles: Vec<(NodeId, bool)>,
324 viewport_generation: u64,
327 pub(crate) style_revision: u64,
331 layout_revision: u64,
334 template_contents_owner: Option<NodeId>,
337 dropped_node_ids: FxHashSet<NodeId>,
338 dropped_node_parents: FxHashSet<NodeId>,
339 dropped_reference_owners: FxHashSet<NodeId>,
345
346 pub(crate) root_node_id: NodeId,
348
349 pub(crate) hoisted_fixed_parents: HashMap<NodeId, NodeId>,
359
360 pub(crate) hoisted_fixed_indices: HashMap<NodeId, usize>,
362
363 pub(crate) abspos_placements: Vec<crate::layout::abspos::AbsposPlacement>,
365
366 pub(crate) abspos_candidates: Vec<crate::layout::abspos::AbsposCandidate>,
368 pub(crate) abspos_candidate_ids: HashSet<NodeId>,
369 pub(crate) abspos_inputs: HashMap<NodeId, crate::layout::abspos::AbsposInputs>,
370 pub(crate) abspos_written: HashSet<NodeId>,
372 pub(crate) abspos_candidates_dirty: bool,
373 pub(crate) abspos_layout_dirty: bool,
374 pub(crate) abspos_normal_layout: bool,
376
377 pub(crate) fixed_nodes: Vec<NodeId>,
383
384 pub(crate) fixed_scroll_offset: crate::Point<f64>,
391
392 pub(crate) sticky_nodes: Vec<NodeId>,
400
401 pub(crate) sticky_offsets: HashMap<NodeId, taffy::Point<f32>>,
411
412 pub(crate) hoisted_clip_hosts: HashSet<NodeId>,
418
419 pub(crate) hoisted_position_hosts: HashSet<NodeId>,
421
422 pub(crate) stylist: Stylist,
425 pub(crate) animations: DocumentAnimationSet,
426 pub(crate) last_resolve_animation_time: f64,
433 pub(crate) guard: SharedRwLock,
435 pub(crate) snapshots: SnapshotMap,
437
438 pub(crate) font_ctx: Arc<Mutex<parley::FontContext>>,
441 #[cfg(feature = "parallel-construct")]
442 pub(crate) thread_font_contexts: ThreadLocal<RefCell<Box<FontContext>>>,
444 pub(crate) layout_ctx: parley::LayoutContext<TextBrush>,
446
447 pub(crate) hover_node_id: Option<NodeId>,
451 pub(crate) hover_hit_node_id: Option<NodeId>,
455 pub(crate) hover_node_is_text: bool,
457 pub(crate) last_client_pointer_position: Option<taffy::Point<f32>>,
459 pub(crate) semantic_hover_node_id: Option<NodeId>,
465 pub(crate) focus_node_id: Option<NodeId>,
467 pub(crate) active_node_id: Option<NodeId>,
469 pub(crate) mousedown_node_id: Option<NodeId>,
471 pub(crate) last_mousedown_time: Option<Instant>,
473 pub(crate) mousedown_position: taffy::Point<f32>,
475 pub(crate) click_count: u16,
477 pub(crate) drag_mode: DragMode,
479 pub(crate) hovered_scrollbar: Option<crate::node::ScrollbarRef>,
481 pub(crate) scrollbar_activity: HashMap<NodeId, Instant>,
484 pub(crate) scroll_animation: ScrollAnimationState,
486
487 pub(crate) text_selection: TextSelection,
489
490 pub(crate) has_active_animations: bool,
493 pub(crate) has_canvas: bool,
495 pub(crate) subdoc_animation_pacing: AnimationPacing,
497
498 pub(crate) nodes_to_id: HashMap<String, SmallVec<[NodeId; 1]>>,
502 pub(crate) nodes_to_stylesheet: BTreeMap<NodeId, DocumentStyleSheet>,
504 pub(crate) document_sheet_nodes: HashSet<NodeId>,
508 #[cfg(feature = "shadow-dom")]
512 pub(crate) shadow_sheet_roots: HashMap<NodeId, NodeId>,
513 pub(crate) ua_stylesheets: HashMap<String, DocumentStyleSheet>,
516 pub(crate) controls_to_form: HashMap<NodeId, NodeId>,
518 pub(crate) sub_document_nodes: HashSet<NodeId>,
520 pub stash_policy: crate::stash_policy::StashPolicy,
521 pub teardown: Arc<crate::release::Teardown>,
522 _dom_lease: crate::release::ReleaseLease,
523 pub page_visible: bool,
524 pub page_frozen: bool,
525 pub page_input_blocked: bool,
526 pub page_retiring: bool,
527 pub was_discarded: bool,
528 pub paint_revision: std::cell::Cell<u64>,
529 pub stash_paint: bool,
530 pub restore_scroll: Option<crate::Point<f64>>,
531 #[cfg(feature = "custom-widget")]
532 retired_resources: Vec<crate::release::ResourceRetirement>,
533 pub(crate) iframe_loads: HashMap<NodeId, crate::iframe::IframeLoad>,
536 pub(crate) deferred_construction_nodes: Vec<ConstructionTask>,
538 pub(crate) paint_damage: crate::paint_damage::PaintDamageTracker,
544
545 #[cfg(feature = "custom-widget")]
547 pub(crate) custom_widget_nodes: HashSet<NodeId>,
548 #[cfg(feature = "custom-widget")]
550 pub(crate) pending_resource_deallocations: Vec<anyrender::ResourceId>,
551
552 #[cfg(feature = "shadow-dom")]
554 pub(crate) custom_element_registry: crate::node::CustomElementRegistry,
555 #[cfg(feature = "shadow-dom")]
557 pub(crate) shadow_host_nodes: HashSet<NodeId>,
558 #[cfg(feature = "shadow-dom")]
559 pub(crate) dirty_shadow_hosts: HashSet<NodeId>,
560 #[cfg(feature = "shadow-dom")]
561 pub(crate) pending_slot_changes: Vec<NodeId>,
562 #[cfg(feature = "shadow-dom")]
563 pub(crate) signaled_slots: HashSet<NodeId>,
564 #[cfg(feature = "shadow-dom")]
565 pub(crate) adopted_stylesheets: HashMap<NodeId, Vec<DocumentStyleSheet>>,
566 #[cfg(feature = "shadow-dom")]
568 pub(crate) custom_element_nodes: HashSet<NodeId>,
569 #[cfg(feature = "shadow-dom")]
573 pub(crate) declarative_template_shadows: HashMap<NodeId, NodeId>,
574
575 pub(crate) script_custom_elements: crate::node::script_custom_element::Registry,
577
578 pub(crate) image_cache: HashMap<String, ImageData>,
581
582 pub(crate) pending_images: HashMap<String, Vec<(NodeId, ImageType)>>,
586
587 pub(crate) pending_critical_resources: HashSet<usize>,
590
591 pub(crate) web_fonts: Vec<Arc<crate::net::web_fonts::WebFont>>,
592 pub(crate) font_epoch: usize,
593 document_referrer: String,
594 document_content_type: String,
595 document_window_focused: bool,
596
597 pub net_provider: Arc<dyn NetProvider>,
600 pub navigation_provider: Arc<dyn NavigationProvider>,
603 pub shell_provider: Arc<dyn ShellProvider>,
605 pub html_parser_provider: Arc<dyn HtmlParserProvider>,
607 pub(crate) abort_signal: Option<AbortSignal>,
611}
612
613pub(crate) fn make_device(
614 viewport: &Viewport,
615 media_type: MediaType,
616 font_ctx: Arc<Mutex<FontContext>>,
617) -> Device {
618 let width = viewport.window_size.0 as f32 / viewport.scale();
619 let height = viewport.window_size.1 as f32 / viewport.scale();
620 let viewport_size = euclid::Size2D::new(width, height);
621 let device_size = euclid::Size2D::new(width, height) * viewport.scale();
622 let device_pixel_ratio = euclid::Scale::new(viewport.scale());
623
624 Device::new(
625 media_type,
626 selectors::matching::QuirksMode::NoQuirks,
627 viewport_size,
628 device_size,
629 device_pixel_ratio,
630 Box::new(BlitzFontMetricsProvider { font_ctx }),
631 ComputedValues::initial_values_with_font_override(Font::initial_values()),
632 match viewport.color_scheme {
633 ColorScheme::Light => PrefersColorScheme::Light,
634 ColorScheme::Dark => PrefersColorScheme::Dark,
635 },
636 PointerCapabilities::default(),
637 PointerCapabilities::default(),
638 )
639}
640
641fn incremental_layout_default() -> bool {
656 !matches!(
657 std::env::var("BLITZ_INCREMENTAL").ok().as_deref(),
658 Some("0" | "false" | "off")
659 )
660}
661
662impl BaseDocument {
663 pub fn new(config: DocumentConfig) -> Self {
665 static ID_GENERATOR: AtomicUsize = AtomicUsize::new(1);
666
667 let id = ID_GENERATOR.fetch_add(1, Ordering::SeqCst);
668 let teardown = config
669 .teardown
670 .unwrap_or_else(crate::release::Teardown::new);
671
672 let font_ctx = config
673 .font_ctx
674 .map(|mut font_ctx| {
675 font_ctx.source_cache.make_shared();
676 font_ctx
678 })
679 .unwrap_or_else(|| {
680 use parley::fontique::{Collection, CollectionOptions, SourceCache};
681 let mut font_ctx = FontContext {
682 source_cache: SourceCache::new_shared(),
683 collection: Collection::new(CollectionOptions {
684 shared: false,
685 system_fonts: cfg!(all(
686 feature = "system-fonts",
687 not(target_arch = "wasm32")
688 )),
689 }),
690 };
691 font_ctx
692 .collection
693 .register_fonts(Blob::new(Arc::new(crate::BULLET_FONT) as _), None);
694 font_ctx
695 });
696 let font_ctx = Arc::new(Mutex::new(font_ctx));
697 if let Some(fallback) = config.family_fallback {
698 crate::font_fallback::set(fallback);
699 }
700
701 style_config::set_pref!("layout.grid.enabled", true);
703 style_config::set_pref!("layout.unimplemented", true);
704 style_config::set_pref!("layout.columns.enabled", true);
705 style_config::set_pref!("layout.css.basic-shape-shape.enabled", true);
706 style_config::set_pref!("layout.threads", -1);
707
708 let viewport = config.viewport.unwrap_or_default();
709 let media_type = config.media_type.unwrap_or_else(MediaType::screen);
710 let device = make_device(&viewport, media_type.clone(), font_ctx.clone());
711 let stylist = Stylist::new(device, QuirksMode::NoQuirks);
712 let snapshots = SnapshotMap::new();
713 let nodes = Box::new(NodeTree::new());
714 let guard = SharedRwLock::new();
715 let nodes_to_id = HashMap::new();
716
717 let base_url = config
718 .base_url
719 .and_then(|url| DocumentUrl::from_str(&url).ok())
720 .unwrap_or_default();
721
722 let net_provider = config
723 .net_provider
724 .unwrap_or_else(|| Arc::new(DummyNetProvider));
725 let navigation_provider = config
726 .navigation_provider
727 .unwrap_or_else(|| Arc::new(DummyNavigationProvider));
728 let shell_provider = config
729 .shell_provider
730 .unwrap_or_else(|| Arc::new(DummyShellProvider));
731 let html_parser_provider = config
732 .html_parser_provider
733 .unwrap_or_else(|| Arc::new(DummyHtmlParserProvider));
734
735 let wake_shell = Arc::clone(&shell_provider);
736 let rx = EventQueue::new(move || wake_shell.request_redraw());
737 let tx = rx.target();
738
739 let mut doc = Self {
740 node_drop_depth: 0,
741 mutator_depth: 0,
742 mutator_redraw_pending: false,
743 details_toggles: Vec::new(),
744 viewport_generation: 0,
745 style_revision: 0,
746 layout_revision: 0,
747 template_contents_owner: None,
748 dropped_node_ids: FxHashSet::default(),
749 dropped_node_parents: FxHashSet::default(),
750 dropped_reference_owners: FxHashSet::default(),
751 hoisted_fixed_parents: HashMap::new(),
752 hoisted_fixed_indices: HashMap::new(),
753 abspos_placements: Vec::new(),
754 abspos_candidates: Vec::new(),
755 abspos_candidate_ids: HashSet::new(),
756 abspos_inputs: HashMap::new(),
757 abspos_written: HashSet::new(),
758 abspos_candidates_dirty: true,
759 abspos_layout_dirty: true,
760 abspos_normal_layout: false,
761 fixed_nodes: Vec::new(),
762 fixed_scroll_offset: crate::Point::ZERO,
763 sticky_nodes: Vec::new(),
764 sticky_offsets: HashMap::new(),
765 hoisted_clip_hosts: HashSet::new(),
766 hoisted_position_hosts: HashSet::new(),
767 id,
768 tx,
769 rx,
770
771 guard,
772 nodes,
773 root_node_id: NodeId::default(),
774 stylist,
775 animations: DocumentAnimationSet::default(),
776 last_resolve_animation_time: 0.0,
777 snapshots,
778 nodes_to_id,
779 viewport,
780 media_type,
781 style_threading: config.style_threading,
782 incremental_layout: config
783 .incremental
784 .unwrap_or_else(incremental_layout_default),
785 subdocument_depth: config.subdocument_depth,
786 devtool_settings: DevtoolSettings::default(),
787 viewport_scroll: crate::Point::ZERO,
788 mutation_log: None,
789 inline_handler_changes: Vec::new(),
790 live_ranges: Vec::new(),
791 dom_selection: None,
792 url: base_url,
793 ua_stylesheets: HashMap::new(),
794 nodes_to_stylesheet: BTreeMap::new(),
795 document_sheet_nodes: HashSet::new(),
796 #[cfg(feature = "shadow-dom")]
797 shadow_sheet_roots: HashMap::new(),
798 font_ctx,
799 #[cfg(feature = "parallel-construct")]
800 thread_font_contexts: ThreadLocal::new(),
801 layout_ctx: parley::LayoutContext::new(),
802
803 hover_node_id: None,
804 hover_hit_node_id: None,
805 hover_node_is_text: false,
806 last_client_pointer_position: None,
807 semantic_hover_node_id: None,
808 focus_node_id: None,
809 active_node_id: None,
810 mousedown_node_id: None,
811 has_active_animations: false,
812 subdoc_animation_pacing: AnimationPacing::Idle,
813 has_canvas: false,
814 sub_document_nodes: HashSet::new(),
815 stash_policy: Default::default(),
816 teardown: Arc::clone(&teardown),
817 _dom_lease: teardown.lease(),
818 page_visible: false,
819 page_frozen: false,
820 page_input_blocked: false,
821 page_retiring: false,
822 was_discarded: false,
823 paint_revision: std::cell::Cell::new(0),
824 stash_paint: false,
825 restore_scroll: None,
826 #[cfg(feature = "custom-widget")]
827 retired_resources: Vec::new(),
828 iframe_loads: HashMap::new(),
829
830 #[cfg(feature = "custom-widget")]
831 custom_widget_nodes: HashSet::new(),
832 #[cfg(feature = "custom-widget")]
833 pending_resource_deallocations: Vec::new(),
834
835 #[cfg(feature = "shadow-dom")]
836 custom_element_registry: crate::node::CustomElementRegistry::new(),
837 #[cfg(feature = "shadow-dom")]
838 shadow_host_nodes: HashSet::new(),
839 #[cfg(feature = "shadow-dom")]
840 dirty_shadow_hosts: HashSet::new(),
841 #[cfg(feature = "shadow-dom")]
842 pending_slot_changes: Vec::new(),
843 #[cfg(feature = "shadow-dom")]
844 signaled_slots: HashSet::new(),
845 #[cfg(feature = "shadow-dom")]
846 adopted_stylesheets: HashMap::new(),
847 #[cfg(feature = "shadow-dom")]
848 custom_element_nodes: HashSet::new(),
849 #[cfg(feature = "shadow-dom")]
850 declarative_template_shadows: HashMap::new(),
851
852 deferred_construction_nodes: Vec::new(),
853 paint_damage: Default::default(),
854 script_custom_elements: Default::default(),
855 image_cache: HashMap::new(),
856 pending_images: HashMap::new(),
857 pending_critical_resources: HashSet::new(),
858 web_fonts: Vec::new(),
859 font_epoch: 0,
860 document_referrer: String::new(),
861 document_content_type: String::from("text/html"),
862 document_window_focused: false,
863 controls_to_form: HashMap::new(),
864 net_provider,
865 navigation_provider,
866 shell_provider,
867 html_parser_provider,
868 abort_signal: config.abort_signal,
869 last_mousedown_time: None,
870 mousedown_position: taffy::Point::ZERO,
871 click_count: 0,
872 drag_mode: DragMode::None,
873 hovered_scrollbar: None,
874 scrollbar_activity: HashMap::new(),
875 scroll_animation: ScrollAnimationState::None,
876 text_selection: TextSelection::default(),
877 };
878
879 doc.root_node_id = doc.create_node(NodeData::Document(Box::default()));
881 doc.root_node_mut().flags.insert(NodeFlags::IS_IN_DOCUMENT);
882
883 match config.ua_stylesheets {
884 Some(stylesheets) => {
885 for ss in &stylesheets {
886 doc.add_user_agent_stylesheet(ss);
887 }
888 }
889 None => doc.add_user_agent_stylesheet(DEFAULT_CSS),
890 }
891
892 let stylo_element_data = StyloElementData {
894 styles: ElementStyles {
895 primary: Some(
896 ComputedValues::initial_values_with_font_override(Font::initial_values())
897 .to_arc(),
898 ),
899 ..Default::default()
900 },
901 ..Default::default()
902 };
903 let stylo_data = doc.root_node_mut().stylo_element_data_mut();
904 *stylo_data.ensure_init_mut() = stylo_element_data;
905
906 doc
907 }
908
909 pub fn set_net_provider(&mut self, net_provider: Arc<dyn NetProvider>) {
911 self.net_provider = net_provider;
912 }
913
914 pub fn set_navigation_provider(&mut self, navigation_provider: Arc<dyn NavigationProvider>) {
916 self.navigation_provider = navigation_provider;
917 }
918
919 pub fn set_shell_provider(&mut self, shell_provider: Arc<dyn ShellProvider>) {
921 self.shell_provider = shell_provider;
922 }
923
924 pub fn set_html_parser_provider(&mut self, html_parser_provider: Arc<dyn HtmlParserProvider>) {
926 self.html_parser_provider = html_parser_provider;
927 }
928
929 pub fn set_base_url(&mut self, url: &str) {
931 self.url = DocumentUrl::from(Url::parse(url).unwrap());
932 }
933
934 pub fn guard(&self) -> &SharedRwLock {
935 &self.guard
936 }
937
938 pub fn tree(&self) -> &NodeTree {
939 &self.nodes
940 }
941
942 pub fn id(&self) -> usize {
943 self.id
944 }
945
946 pub(crate) fn build_request(&self, url: url::Url) -> Request {
949 crate::net::with_referrer(
950 crate::net::stamped_request(url, self.abort_signal.as_ref()),
951 self.url(),
952 )
953 }
954
955 pub fn favicon_url(&self) -> Option<String> {
956 self.tree().iter().find_map(|(_, node)| {
957 let data = &node.data;
958 if !data.is_element_with_tag_name(&local_name!("link")) {
959 return None;
960 }
961 let rel = data.attr(local_name!("rel"))?;
962 if !rel
963 .split_ascii_whitespace()
964 .any(|v| v.eq_ignore_ascii_case("icon"))
965 {
966 return None;
967 }
968 data.attr(local_name!("href")).map(|s| s.to_string())
969 })
970 }
971
972 pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
973 self.nodes.get(node_id)
974 }
975
976 pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
977 self.nodes.get_mut(node_id)
978 }
979
980 pub fn get_focussed_node_id(&self) -> Option<NodeId> {
981 self.focus_node_id
982 .or(self.try_root_element().map(|el| el.id))
983 }
984
985 pub fn mutate<'doc>(&'doc mut self) -> DocumentMutator<'doc> {
986 DocumentMutator::new(self)
987 }
988
989 pub fn handle_dom_event<F: FnMut(DomEvent)>(
990 &mut self,
991 event: &mut DomEvent,
992 dispatch_event: F,
993 ) {
994 handle_dom_event(self, event, dispatch_event)
995 }
996
997 pub fn as_any_mut(&mut self) -> &mut dyn Any {
998 self
999 }
1000
1001 pub fn label_bound_input_element(&self, label_node_id: NodeId) -> Option<&Node> {
1008 let label_element = self.nodes[label_node_id].element_data()?;
1009 if let Some(target_element_dom_id) = label_element.attr(local_name!("for")) {
1010 TreeTraverser::new(self)
1011 .filter_map(|id| {
1012 let node = self.get_node(id)?;
1013 let element_data = node.element_data()?;
1014 if element_data.name.local != local_name!("input") {
1015 return None;
1016 }
1017 let id = element_data.id.as_ref()?;
1018 if *id == *target_element_dom_id {
1019 Some(node)
1020 } else {
1021 None
1022 }
1023 })
1024 .next()
1025 } else {
1026 TreeTraverser::new_with_root(self, label_node_id)
1027 .filter_map(|child_id| {
1028 let node = self.get_node(child_id)?;
1029 let element_data = node.element_data()?;
1030 if element_data.name.local == local_name!("input") {
1031 Some(node)
1032 } else {
1033 None
1034 }
1035 })
1036 .next()
1037 }
1038 }
1039
1040 pub fn run_pre_click_activation(&mut self, target: NodeId) -> Option<PreClickActivation> {
1046 let node_id = crate::events::pointer::checkable_activation_target(self, target)?;
1047 let el = self.get_node(node_id)?.data.downcast_element()?;
1048 let is_radio = el.attr(local_name!("type")) == Some("radio");
1049
1050 if !is_radio {
1051 let previous = el.checkbox_input_checked()?;
1052 let el = self.get_node_mut(node_id)?.data.downcast_element_mut()?;
1053 Self::toggle_checkbox(el);
1054 return Some(PreClickActivation {
1055 previous: vec![(node_id, previous)],
1056 });
1057 }
1058
1059 let radio_set = el.attr(local_name!("name")).map(str::to_string);
1060 let Some(radio_set) = radio_set else {
1061 let previous = el.checkbox_input_checked()?;
1062 let el = self.get_node_mut(node_id)?.data.downcast_element_mut()?;
1063 *el.checkbox_input_checked_mut()? = true;
1064 return Some(PreClickActivation {
1065 previous: vec![(node_id, previous)],
1066 });
1067 };
1068
1069 let mut previous: Vec<(NodeId, bool)> = Vec::new();
1081 for (id, node) in self.nodes.iter_mut() {
1082 let Some(el) = node.data.downcast_element_mut() else {
1083 continue;
1084 };
1085 if el.attr(local_name!("name")) != Some(&*radio_set) {
1086 continue;
1087 }
1088 let Some(is_checked) = el.checkbox_input_checked_mut() else {
1089 continue;
1090 };
1091 previous.push((id, *is_checked));
1092 *is_checked = id == node_id;
1093 }
1094 Some(PreClickActivation { previous })
1095 }
1096
1097 pub fn undo_pre_click_activation(&mut self, activation: PreClickActivation) {
1100 for (node_id, was_checked) in activation.previous {
1101 let Some(node) = self.get_node_mut(node_id) else {
1102 continue;
1103 };
1104 let Some(el) = node.data.downcast_element_mut() else {
1105 continue;
1106 };
1107 if let Some(is_checked) = el.checkbox_input_checked_mut() {
1108 *is_checked = was_checked;
1109 }
1110 }
1111 }
1112
1113 pub fn toggle_checkbox(el: &mut ElementData) -> bool {
1114 let Some(is_checked) = el.checkbox_input_checked_mut() else {
1115 return false;
1116 };
1117 *is_checked = !*is_checked;
1118
1119 *is_checked
1120 }
1121
1122 pub fn toggle_radio(&mut self, radio_set_name: String, target_radio_id: NodeId) {
1123 for (i, node) in self.nodes.iter_mut() {
1124 if let Some(node_data) = node.data.downcast_element_mut() {
1125 if node_data.attr(local_name!("name")) == Some(&radio_set_name) {
1126 let was_clicked = i == target_radio_id;
1127 let Some(is_checked) = node_data.checkbox_input_checked_mut() else {
1128 continue;
1129 };
1130 *is_checked = was_clicked;
1131 }
1132 }
1133 }
1134 }
1135
1136 pub fn toggle_details_open(&mut self, details_id: NodeId) {
1140 use crate::qual_name;
1141
1142 let node = &self.nodes[details_id];
1143 if !node.data.is_element_with_tag_name(&local_name!("details")) {
1144 return;
1145 }
1146 let is_open = node.data.has_attr(local_name!("open"));
1147
1148 let mut mutator = self.mutate();
1152 if is_open {
1153 mutator.clear_attribute(details_id, qual_name!("open"));
1154 } else {
1155 mutator.set_attribute(details_id, qual_name!("open"), "");
1156 }
1157 drop(mutator);
1158
1159 self.shell_provider.request_redraw();
1160 }
1161
1162 pub fn set_style_property(&mut self, node_id: NodeId, name: &str, value: &str) {
1163 let node = &mut self.nodes[node_id];
1164 let did_change = node.element_data_mut().unwrap().set_style_property(
1165 name,
1166 value,
1167 &self.guard,
1168 self.url.url_extra_data(),
1169 );
1170 if did_change {
1171 node.mark_style_attr_updated();
1172 }
1173 }
1174
1175 pub fn remove_style_property(&mut self, node_id: NodeId, name: &str) {
1176 let node = &mut self.nodes[node_id];
1177 let did_change = node.element_data_mut().unwrap().remove_style_property(
1178 name,
1179 &self.guard,
1180 self.url.url_extra_data(),
1181 );
1182 if did_change {
1183 node.mark_style_attr_updated();
1184 }
1185 }
1186
1187 pub fn sub_document_node_ids(&self) -> Vec<NodeId> {
1188 self.sub_document_nodes.iter().copied().collect()
1189 }
1190
1191 pub fn set_navigation_metadata(&mut self, request: &Request, content_type: Option<&str>) {
1193 self.document_referrer = request
1194 .headers
1195 .get(blitz_traits::net::http::header::REFERER)
1196 .and_then(|value| value.to_str().ok())
1197 .and_then(|value| Url::parse(value).ok())
1198 .map(|mut url| {
1199 let _ = url.set_username("");
1200 let _ = url.set_password(None);
1201 url.set_fragment(None);
1202 url.to_string()
1203 })
1204 .unwrap_or_default();
1205 self.document_content_type = content_type
1206 .and_then(|value| value.split(';').next())
1207 .map(str::trim)
1208 .filter(|value| !value.is_empty())
1209 .unwrap_or("text/html")
1210 .to_ascii_lowercase();
1211 }
1212
1213 pub fn referrer(&self) -> &str {
1214 &self.document_referrer
1215 }
1216
1217 pub fn content_type(&self) -> &str {
1218 &self.document_content_type
1219 }
1220
1221 pub fn window_focused(&self) -> bool {
1222 self.document_window_focused
1223 }
1224
1225 pub fn set_window_focused(&mut self, focused: bool) {
1227 self.document_window_focused = focused;
1228 for id in self.sub_document_node_ids() {
1229 if let Some(child) = self
1230 .get_node_mut(id)
1231 .and_then(|node| node.element_data_mut())
1232 .and_then(|element| element.sub_doc_data_mut())
1233 {
1234 child.inner_mut().set_window_focused(focused);
1235 }
1236 }
1237 }
1238
1239 pub fn set_document_quirks_mode(&mut self, mode: u8) {
1241 if let NodeData::Document(data) = &mut self.root_node_mut().data {
1242 data.quirks_mode = mode;
1243 }
1244 }
1245
1246 pub fn compat_mode(&self) -> &'static str {
1247 if matches!(&self.root_node().data, NodeData::Document(data) if data.quirks_mode == 2) {
1248 "BackCompat"
1249 } else {
1250 "CSS1Compat"
1251 }
1252 }
1253
1254 pub fn begin_document_stream(&mut self) {
1255 self.reset_web_fonts();
1256 self.pending_critical_resources.clear();
1257 self.pending_images.clear();
1258 self.set_document_quirks_mode(0);
1259 }
1260
1261 pub fn set_sub_document(&mut self, node_id: NodeId, mut sub_document: Box<dyn Document>) {
1262 sub_document
1263 .inner_mut()
1264 .set_window_focused(self.document_window_focused);
1265 self.nodes[node_id]
1266 .element_data_mut()
1267 .unwrap()
1268 .set_sub_document(sub_document);
1269 self.sub_document_nodes.insert(node_id);
1270 }
1271
1272 pub fn prepare_teardown(&mut self) {
1273 if self.page_retiring {
1274 return;
1275 }
1276 self.page_retiring = true;
1277 if let Some(signal) = &self.abort_signal {
1278 blitz_traits::net::AbortController {
1279 signal: signal.clone(),
1280 }
1281 .abort();
1282 }
1283 self.page_visible = false;
1284 self.page_frozen = true;
1285 let nodes: Vec<_> = self.sub_document_nodes.iter().copied().collect();
1286 let mut children = Vec::new();
1287 for id in nodes {
1288 if let Some(receipt) = self.retire_sub_document(id) {
1289 children.push(receipt);
1290 }
1291 }
1292 #[cfg(feature = "custom-widget")]
1293 self.destroy_surfaces();
1294 for (_, load) in std::mem::take(&mut self.iframe_loads) {
1295 load.abort_controller.abort();
1296 }
1297 self.teardown.retire(children);
1298 }
1299
1300 pub fn take_sub_document(&mut self, node_id: NodeId) -> Option<Box<dyn Document>> {
1302 let element = self.nodes.get_mut(node_id)?.element_data_mut()?;
1303 let crate::node::SpecialElementData::SubDocument(document) = element.special_data.take()
1304 else {
1305 return None;
1306 };
1307 self.sub_document_nodes.remove(&node_id);
1308 if let Some(load) = self.iframe_loads.remove(&node_id) {
1309 load.abort_controller.abort();
1310 }
1311 Some(document)
1312 }
1313
1314 pub fn retire_sub_document(
1315 &mut self,
1316 node_id: NodeId,
1317 ) -> Option<Arc<crate::release::Teardown>> {
1318 let mut document = self.take_sub_document(node_id)?;
1319 document.prepare_teardown();
1320 #[cfg(feature = "custom-widget")]
1321 {
1322 let receipt = Arc::clone(&document.inner().teardown);
1323 let (resources, mut leases) = document.inner_mut().retire_render_resources();
1324 if !resources.is_empty() {
1325 leases.push(receipt.lease());
1326 self.retired_resources
1327 .push(crate::release::ResourceRetirement { resources, leases });
1328 }
1329 }
1330 Some(document.dispose())
1331 }
1332
1333 #[cfg(feature = "custom-widget")]
1334 fn retire_render_resources(
1335 &mut self,
1336 ) -> (
1337 Vec<anyrender::ResourceId>,
1338 Vec<crate::release::ReleaseLease>,
1339 ) {
1340 let mut resources = self.take_pending_resource_deallocations();
1341 let mut leases = Vec::new();
1342 for retirement in self.retired_resources.drain(..) {
1343 resources.extend(retirement.resources);
1344 leases.extend(retirement.leases);
1345 }
1346 for id in self.custom_widget_nodes.iter().copied() {
1347 if let Some(data) = self
1348 .nodes
1349 .get_mut(id)
1350 .and_then(|node| node.element_data_mut())
1351 .and_then(|el| el.custom_widget_data_mut())
1352 {
1353 resources.append(&mut data.active_resource_ids);
1354 }
1355 }
1356 for id in self.sub_document_nodes.iter().copied() {
1357 if let Some(document) = self.nodes.get_mut(id).and_then(|node| node.subdoc_mut()) {
1358 let (child_resources, child_leases) =
1359 document.inner_mut().retire_render_resources();
1360 resources.extend(child_resources);
1361 leases.extend(child_leases);
1362 }
1363 }
1364 (resources, leases)
1365 }
1366
1367 #[cfg(feature = "custom-widget")]
1368 pub fn take_retired_resources(&mut self) -> Vec<crate::release::ResourceRetirement> {
1369 std::mem::take(&mut self.retired_resources)
1370 }
1371
1372 pub fn remove_sub_document(&mut self, node_id: NodeId) {
1373 let _ = self.retire_sub_document(node_id);
1374 }
1375
1376 pub fn poll_subdocuments(&mut self, waker: Option<&Waker>) -> bool {
1382 self.poll_subdocuments_inner(waker, None)
1383 }
1384
1385 pub fn poll_subdocuments_for_settle(
1387 &mut self,
1388 waker: Option<&Waker>,
1389 run_timers: bool,
1390 ) -> bool {
1391 self.poll_subdocuments_inner(waker, Some(run_timers))
1392 }
1393
1394 fn poll_subdocuments_inner(
1395 &mut self,
1396 waker: Option<&Waker>,
1397 settle_timers: Option<bool>,
1398 ) -> bool {
1399 let mut has_changes = false;
1400 let node_ids: Vec<NodeId> = self.sub_document_nodes.iter().copied().collect();
1401 for node_id in node_ids {
1402 let Some(sub_doc) = self
1403 .nodes
1404 .get_mut(node_id)
1405 .and_then(|node| node.subdoc_mut())
1406 else {
1407 continue;
1408 };
1409 let task_context = waker.map(TaskContext::from_waker);
1410 has_changes |= match settle_timers {
1411 Some(run_timers) => sub_doc.poll_for_settle(task_context, run_timers),
1412 None => sub_doc.poll(task_context),
1413 };
1414 }
1415 has_changes
1416 }
1417
1418 #[cfg(feature = "custom-widget")]
1419 pub fn custom_widget_node_ids(&self) -> Vec<NodeId> {
1420 self.custom_widget_nodes.iter().copied().collect()
1421 }
1422
1423 #[cfg(feature = "custom-widget")]
1424 pub fn take_pending_resource_deallocations(&mut self) -> Vec<anyrender::ResourceId> {
1425 std::mem::take(&mut self.pending_resource_deallocations)
1426 }
1427
1428 #[cfg(feature = "custom-widget")]
1429 pub fn set_custom_widget(&mut self, node_id: NodeId, widget: Box<dyn crate::Widget>) {
1430 self.nodes[node_id]
1431 .element_data_mut()
1432 .unwrap()
1433 .set_custom_widget(widget);
1434 self.custom_widget_nodes.insert(node_id);
1435 }
1436
1437 #[cfg(feature = "custom-widget")]
1438 pub fn remove_custom_widget(&mut self, node_id: NodeId) {
1439 let resources_to_deallocate = self.nodes[node_id]
1440 .element_data_mut()
1441 .unwrap()
1442 .remove_custom_widget();
1443 self.pending_resource_deallocations
1444 .extend_from_slice(&resources_to_deallocate);
1445 self.custom_widget_nodes.remove(&node_id);
1446 }
1447
1448 #[cfg(feature = "shadow-dom")]
1452 pub fn custom_elements_mut(&mut self) -> &mut crate::node::CustomElementRegistry {
1453 &mut self.custom_element_registry
1454 }
1455
1456 #[cfg(feature = "shadow-dom")]
1459 pub fn define_custom_element(
1460 &mut self,
1461 name: markup5ever::LocalName,
1462 definition: crate::node::CustomElementDefinition,
1463 ) {
1464 self.custom_element_registry.define(name, definition);
1465 }
1466
1467 #[cfg(feature = "shadow-dom")]
1469 pub fn shadow_host_node_ids(&self) -> Vec<NodeId> {
1470 self.shadow_host_nodes.iter().copied().collect()
1471 }
1472
1473 #[cfg(feature = "shadow-dom")]
1475 pub fn shadow_root_id(&self, host_id: NodeId) -> Option<NodeId> {
1476 self.get_node(host_id)
1477 .and_then(|node| node.shadow_root_id())
1478 }
1479
1480 #[cfg(feature = "shadow-dom")]
1484 pub fn attach_shadow(&mut self, host_id: NodeId, mode: crate::node::ShadowRootMode) -> NodeId {
1485 if let Some(existing) = self.nodes[host_id].shadow_root_id() {
1486 return existing;
1487 }
1488
1489 let shadow_root_id = self.create_node(NodeData::ShadowRoot(
1490 crate::node::ShadowRootData::new(host_id, mode),
1491 ));
1492
1493 self.nodes[shadow_root_id].parent = Some(host_id);
1497 self.nodes[shadow_root_id].owner_document = self.nodes[host_id].owner_document;
1498 if self.nodes[host_id].flags.is_in_document() {
1499 self.nodes[shadow_root_id]
1500 .flags
1501 .insert(NodeFlags::IS_IN_DOCUMENT);
1502 }
1503
1504 self.nodes[host_id]
1505 .element_data_mut()
1506 .expect("Shadow host must be an element")
1507 .shadow_root = Some(shadow_root_id);
1508 self.shadow_host_nodes.insert(host_id);
1509 self.dirty_shadow_hosts.insert(host_id);
1510 self.nodes[host_id].set_restyle_hint(
1511 style::invalidation::element::restyle_hints::RestyleHint::restyle_subtree(),
1512 );
1513
1514 self.nodes[host_id].insert_damage(ALL_DAMAGE);
1516 self.nodes[host_id].mark_ancestors_dirty();
1517
1518 shadow_root_id
1519 }
1520
1521 #[cfg(feature = "shadow-dom")]
1526 pub fn element_can_be_shadow_host(element: &ElementData) -> bool {
1527 let tag = element.name.local.as_ref();
1528 element.name.ns == ns!(html)
1529 && (tag.contains('-')
1530 || matches!(
1531 tag,
1532 "article"
1533 | "aside"
1534 | "blockquote"
1535 | "body"
1536 | "div"
1537 | "footer"
1538 | "h1"
1539 | "h2"
1540 | "h3"
1541 | "h4"
1542 | "h5"
1543 | "h6"
1544 | "header"
1545 | "main"
1546 | "nav"
1547 | "p"
1548 | "section"
1549 | "span"
1550 ))
1551 }
1552
1553 #[cfg(feature = "shadow-dom")]
1559 pub fn try_attach_declarative_shadow(
1560 &mut self,
1561 host_id: NodeId,
1562 template_id: NodeId,
1563 mode: crate::node::ShadowRootMode,
1564 clonable: bool,
1565 serializable: bool,
1566 delegates_focus: bool,
1567 ) -> bool {
1568 let can_attach = self.nodes.get(host_id).is_some_and(|node| {
1569 node.element_data().is_some_and(|element| {
1570 Self::element_can_be_shadow_host(element) && element.shadow_root.is_none()
1571 })
1572 });
1573 if !can_attach {
1574 return false;
1575 }
1576 let root_id = self.attach_shadow(host_id, mode);
1577 if let Some(root) = self.nodes[root_id].shadow_root_data_mut() {
1578 root.declarative = true;
1579 root.clonable = clonable;
1580 root.serializable = serializable;
1581 root.delegates_focus = delegates_focus;
1582 }
1583 self.declarative_template_shadows
1584 .insert(template_id, root_id);
1585 true
1586 }
1587
1588 #[cfg(feature = "shadow-dom")]
1590 pub fn detach_shadow(&mut self, host_id: NodeId) {
1591 let shadow_root_id = self.nodes[host_id]
1592 .element_data_mut()
1593 .and_then(|el| el.shadow_root.take());
1594 if let Some(shadow_root_id) = shadow_root_id {
1595 self.drop_node_ignoring_parent(shadow_root_id);
1596 self.shadow_host_nodes.remove(&host_id);
1597 self.dirty_shadow_hosts.remove(&host_id);
1598 self.nodes[host_id].flattened_children = None;
1599 self.nodes[host_id].set_restyle_hint(
1600 style::invalidation::element::restyle_hints::RestyleHint::restyle_subtree(),
1601 );
1602 self.nodes[host_id].insert_damage(ALL_DAMAGE);
1603 self.nodes[host_id].mark_ancestors_dirty();
1604 }
1605 }
1606
1607 #[cfg(feature = "shadow-dom")]
1609 pub fn set_custom_element(
1610 &mut self,
1611 node_id: NodeId,
1612 controller: Box<dyn crate::node::CustomElement>,
1613 ) {
1614 use crate::node::{CustomElementData, SpecialElementData};
1615 self.nodes[node_id]
1616 .element_data_mut()
1617 .expect("Custom element host must be an element")
1618 .special_data = SpecialElementData::CustomElement(CustomElementData::new(controller));
1619 self.custom_element_nodes.insert(node_id);
1620 }
1621
1622 #[cfg(feature = "shadow-dom")]
1625 pub fn take_custom_element(
1626 &mut self,
1627 node_id: NodeId,
1628 ) -> Option<Box<dyn crate::node::CustomElement>> {
1629 use crate::node::SpecialElementData;
1630 self.custom_element_nodes.remove(&node_id);
1631 let element = self.nodes[node_id].element_data_mut()?;
1632 if matches!(element.special_data, SpecialElementData::CustomElement(_)) {
1633 if let SpecialElementData::CustomElement(mut data) = element.special_data.take() {
1634 return data.controller.take();
1635 }
1636 }
1637 None
1638 }
1639
1640 pub fn root_node(&self) -> &Node {
1641 &self.nodes[self.root_node_id]
1642 }
1643
1644 pub fn root_node_mut(&mut self) -> &mut Node {
1645 &mut self.nodes[self.root_node_id]
1646 }
1647
1648 pub fn set_paint_damage_tracking(&mut self, enabled: bool) {
1665 self.paint_damage.set_enabled(enabled);
1666 }
1667
1668 pub fn paint_damage_tracking(&self) -> bool {
1670 self.paint_damage.is_enabled()
1671 }
1672
1673 pub fn paint_damage(&self) -> &crate::paint_damage::PaintDamage {
1681 self.paint_damage.damage()
1682 }
1683
1684 pub fn try_root_element(&self) -> Option<&Node> {
1685 TDocument::as_node(&self.root_node()).first_element_child()
1686 }
1687
1688 pub fn root_element(&self) -> &Node {
1689 TDocument::as_node(&self.root_node())
1690 .first_element_child()
1691 .unwrap()
1692 .as_element()
1693 .unwrap()
1694 }
1695
1696 pub fn create_node(&mut self, node_data: NodeData) -> NodeId {
1697 let tree_ptr = self.nodes.as_mut() as *mut NodeTree;
1698 let guard = self.guard.clone();
1699 let is_document = matches!(node_data, NodeData::Document(_));
1700 let id = self
1701 .nodes
1702 .insert_with_key(|id| Node::new(tree_ptr, id, guard, node_data));
1703 if !is_document {
1704 self.nodes[id].owner_document = Some(self.root_node_id);
1705 }
1706 self.register_script_custom_element(id);
1707 if let Some(attributes) = self.nodes[id]
1708 .element_data()
1709 .and_then(|element| element.inline_event_attributes.as_deref())
1710 {
1711 self.inline_handler_changes.extend(
1712 attributes
1713 .iter()
1714 .map(|attr| (id, attr.name.clone(), Some(attr.value.clone()))),
1715 );
1716 }
1717 id
1718 }
1719
1720 pub fn add_to_document_id_map(&mut self, document_id: NodeId, id: &str, node_id: NodeId) {
1721 if document_id == self.root_node_id || id.is_empty() {
1722 return;
1723 }
1724 let Some(node) = self.get_node_mut(document_id) else {
1725 return;
1726 };
1727 let NodeData::Document(data) = &mut node.data else {
1728 return;
1729 };
1730 let candidates = data.ids.entry(id.to_owned()).or_default();
1731 if !candidates.contains(&node_id) {
1732 candidates.push(node_id);
1733 }
1734 }
1735
1736 pub fn remove_from_document_id_map(&mut self, document_id: NodeId, id: &str, node_id: NodeId) {
1737 if document_id == self.root_node_id {
1738 return;
1739 }
1740 let Some(node) = self.get_node_mut(document_id) else {
1741 return;
1742 };
1743 let NodeData::Document(data) = &mut node.data else {
1744 return;
1745 };
1746 if let Some(candidates) = data.ids.get_mut(id) {
1747 candidates.retain(|candidate| *candidate != node_id);
1748 if candidates.is_empty() {
1749 data.ids.remove(id);
1750 }
1751 }
1752 }
1753
1754 pub fn get_element_by_id_in(&self, document_id: NodeId, id: &str) -> Option<NodeId> {
1757 if document_id == self.root_node_id {
1758 return self.get_element_by_id(id);
1759 }
1760 if id.is_empty() {
1761 return None;
1762 }
1763 let NodeData::Document(data) = &self.get_node(document_id)?.data else {
1764 return None;
1765 };
1766 data.ids
1767 .get(id)?
1768 .iter()
1769 .filter_map(|candidate| {
1770 let mut current = *candidate;
1771 let mut path = Vec::new();
1772 while current != document_id {
1773 let node = self.get_node(current)?;
1774 let parent_id = node.parent?;
1775 let parent = self.get_node(parent_id)?;
1776 path.push(parent.index_of_child(current)?);
1777 current = parent_id;
1778 }
1779 path.reverse();
1780 Some((path, *candidate))
1781 })
1782 .min_by(|left, right| left.0.cmp(&right.0))
1783 .map(|(_, node_id)| node_id)
1784 }
1785
1786 fn collect_live_reference_owners(
1798 nodes: &NodeTree,
1799 hoisted_fixed_parents: &HashMap<NodeId, NodeId>,
1800 removed: &FxHashSet<NodeId>,
1801 node_id: NodeId,
1802 candidates: &mut FxHashSet<NodeId>,
1803 ) {
1804 fn push_survivor(
1805 removed: &FxHashSet<NodeId>,
1806 candidates: &mut FxHashSet<NodeId>,
1807 id: NodeId,
1808 ) {
1809 if !removed.contains(&id) {
1810 candidates.insert(id);
1811 }
1812 }
1813
1814 let (removed_is_text, dom_parent, layout_parent, seeds) = {
1815 let Some(node) = nodes.get(node_id) else {
1816 return;
1817 };
1818 let mut seeds: SmallVec<[NodeId; 8]> = SmallVec::new();
1819 {
1820 let borrowed = node.layout_children.borrow();
1821 if let Some(children) = borrowed.as_ref() {
1822 seeds.extend(children.iter().copied());
1823 }
1824 }
1825 seeds.extend(node.children.iter().copied());
1826 seeds.extend(node.anonymous_blocks.iter().copied());
1827 seeds.extend(node.before());
1828 seeds.extend(node.after());
1829 #[cfg(feature = "shadow-dom")]
1830 {
1831 seeds.extend(node.assigned_slot);
1832 seeds.extend(
1833 node.element_data()
1834 .and_then(|element| element.assigned_slot),
1835 );
1836 if let Some(assigned) = node.assigned_nodes.as_ref() {
1837 seeds.extend(assigned.iter().copied());
1838 }
1839 if let Some(root) = node.shadow_root_data() {
1840 seeds.push(root.host);
1841 }
1842 }
1843 (
1844 node.is_text_node(),
1845 node.parent,
1846 node.layout_parent.get(),
1847 seeds,
1848 )
1849 };
1850
1851 for parent_id in [dom_parent, layout_parent].into_iter().flatten() {
1852 let Some(parent) = nodes.get(parent_id) else {
1853 continue;
1854 };
1855 let anons: SmallVec<[NodeId; 4]> = parent.anonymous_blocks.iter().copied().collect();
1856 for anon in anons {
1857 let Some(anon_node) = nodes.get(anon) else {
1858 continue;
1859 };
1860 let listed = anon_node
1861 .children
1862 .iter()
1863 .any(|child| removed.contains(child))
1864 || anon_node
1865 .layout_children
1866 .borrow()
1867 .as_ref()
1868 .is_some_and(|children| {
1869 children.iter().any(|child| removed.contains(child))
1870 });
1871 if listed {
1872 push_survivor(removed, candidates, anon);
1873 }
1874 }
1875 }
1876 for id in seeds {
1877 push_survivor(removed, candidates, id);
1878 }
1879
1880 let mut stack: SmallVec<[NodeId; 16]> = SmallVec::new();
1881 if let Some(id) = dom_parent {
1882 if !removed.contains(&id) {
1883 stack.push(id);
1884 }
1885 }
1886 if let Some(id) = layout_parent {
1887 if !removed.contains(&id) {
1888 stack.push(id);
1889 }
1890 }
1891 if let Some(origin) = hoisted_fixed_parents.get(&node_id).copied() {
1892 push_survivor(removed, candidates, origin);
1895 if !removed.contains(&origin) {
1896 stack.push(origin);
1897 }
1898 }
1899
1900 let mut seen: SmallVec<[NodeId; 16]> = SmallVec::new();
1901 seen.push(node_id);
1902 while let Some(id) = stack.pop() {
1903 if seen.contains(&id) {
1904 continue;
1905 }
1906 seen.push(id);
1907 let Some(ancestor) = nodes.get(id) else {
1908 continue;
1909 };
1910 let stacking = ancestor.stacking_context.as_ref().is_some_and(|context| {
1911 context.children.iter().any(|child| {
1912 removed.contains(&child.node_id)
1913 || child
1914 .clip_ancestors
1915 .iter()
1916 .any(|clip| removed.contains(clip))
1917 })
1918 });
1919 let inline_or_list = ancestor.element_data().is_some_and(|element| {
1920 element.inline_layout_data.is_some() || element.list_item_data.is_some()
1921 });
1922 let table_root = !removed_is_text && ancestor.flags.is_table_root();
1924 #[cfg(feature = "shadow-dom")]
1925 let shadow_hit = {
1926 let mut hit = ancestor
1927 .flattened_children
1928 .as_ref()
1929 .is_some_and(|children| children.iter().any(|child| removed.contains(child)))
1930 || ancestor.assigned_nodes.as_ref().is_some_and(|assigned| {
1931 assigned.iter().any(|child| removed.contains(child))
1932 });
1933 if let Some(root) = ancestor.shadow_root_data() {
1934 let host = root.host;
1935 hit = hit
1936 || root.slots.iter().any(|slot| removed.contains(slot))
1937 || root
1938 .slottables
1939 .iter()
1940 .any(|slottable| removed.contains(slottable))
1941 || root
1942 .stylesheet_nodes
1943 .iter()
1944 .any(|sheet| removed.contains(sheet));
1945 stack.push(host);
1946 }
1947 hit
1948 };
1949 #[cfg(not(feature = "shadow-dom"))]
1950 let shadow_hit = false;
1951 if stacking || inline_or_list || table_root || shadow_hit {
1952 push_survivor(removed, candidates, id);
1953 }
1954 if let Some(parent) = ancestor.parent {
1955 stack.push(parent);
1956 }
1957 if let Some(parent) = ancestor.layout_parent.get() {
1958 stack.push(parent);
1959 }
1960 }
1961 }
1962
1963 fn collect_stacking_context_owners(
1967 nodes: &NodeTree,
1968 position_hosts: &HashSet<NodeId>,
1969 clip_hosts: &HashSet<NodeId>,
1970 removed: &FxHashSet<NodeId>,
1971 candidates: &mut FxHashSet<NodeId>,
1972 ) {
1973 for host in position_hosts.iter().chain(clip_hosts.iter()).copied() {
1974 if removed.contains(&host) {
1975 continue;
1976 }
1977 let Some(node) = nodes.get(host) else {
1978 continue;
1979 };
1980 let Some(context) = node.stacking_context.as_ref() else {
1981 continue;
1982 };
1983 let references = context.children.iter().any(|child| {
1984 removed.contains(&child.node_id)
1985 || child
1986 .clip_ancestors
1987 .iter()
1988 .any(|clip| removed.contains(clip))
1989 });
1990 if references {
1991 candidates.insert(host);
1992 }
1993 }
1994 }
1995
1996 pub(crate) fn invalidate_node_references(
2007 &mut self,
2008 removed: &FxHashSet<NodeId>,
2009 mut parents: FxHashSet<NodeId>,
2010 extra_owners: FxHashSet<NodeId>,
2011 ) {
2012 if removed.is_empty() {
2013 return;
2014 }
2015
2016 fn layout_references_removed(
2017 layout: &parley::Layout<TextBrush>,
2018 removed: &FxHashSet<NodeId>,
2019 ) -> bool {
2020 layout
2021 .inline_boxes()
2022 .iter()
2023 .any(|ibox| removed.contains(&NodeId::from_u64(ibox.id)))
2024 || layout.styles().iter().any(|style| {
2025 removed.contains(&style.brush.id)
2026 || style
2027 .brush
2028 .text_node
2029 .is_some_and(|id| removed.contains(&id))
2030 })
2031 }
2032
2033 for (&id, &origin) in &self.hoisted_fixed_parents {
2037 if removed.contains(&id) || removed.contains(&origin) {
2038 if let Some(node) = self.nodes.get(id) {
2039 parents.extend(node.layout_parent.get());
2040 }
2041 }
2042 }
2043
2044 let mut candidates: FxHashSet<NodeId> = FxHashSet::with_capacity_and_hasher(
2045 parents.len() + extra_owners.len() + removed.len(),
2046 Default::default(),
2047 );
2048 for &id in &parents {
2049 if !removed.contains(&id) {
2050 candidates.insert(id);
2051 }
2052 }
2053 for id in extra_owners {
2054 if !removed.contains(&id) {
2055 candidates.insert(id);
2056 }
2057 }
2058 for &id in removed {
2059 let detach = self
2060 .nodes
2061 .get(id)
2062 .and_then(|node| node.layout_parent.get())
2063 .is_some_and(|parent| !removed.contains(&parent));
2064 if detach {
2065 candidates.insert(id);
2066 }
2067 if self.nodes.contains_key(id) {
2068 Self::collect_live_reference_owners(
2069 &self.nodes,
2070 &self.hoisted_fixed_parents,
2071 removed,
2072 id,
2073 &mut candidates,
2074 );
2075 }
2076 }
2077 Self::collect_stacking_context_owners(
2078 &self.nodes,
2079 &self.hoisted_position_hosts,
2080 &self.hoisted_clip_hosts,
2081 removed,
2082 &mut candidates,
2083 );
2084
2085 self.fixed_nodes.retain(|id| !removed.contains(id));
2086 self.sticky_nodes.retain(|id| !removed.contains(id));
2087 self.sticky_offsets.retain(|id, _| !removed.contains(id));
2088 self.hoisted_fixed_parents
2089 .retain(|id, origin| !removed.contains(id) && !removed.contains(origin));
2090 let fixed_parents = &self.hoisted_fixed_parents;
2091 self.hoisted_fixed_indices
2092 .retain(|id, _| fixed_parents.contains_key(id));
2093 self.hoisted_clip_hosts.retain(|id| !removed.contains(id));
2094 self.hoisted_position_hosts
2095 .retain(|id| !removed.contains(id));
2096
2097 self.abspos_placements.clear();
2101 self.abspos_candidates.clear();
2102 self.abspos_candidate_ids.clear();
2103 self.abspos_inputs.clear();
2104 self.abspos_written.clear();
2105 self.abspos_candidates_dirty = true;
2106 self.abspos_layout_dirty = true;
2107
2108 let mut dirty = FxHashSet::default();
2109 for id in candidates {
2110 let Some(node) = self.nodes.get_mut(id) else {
2111 continue;
2112 };
2113 if removed.contains(&id) {
2116 if node
2117 .layout_parent
2118 .get()
2119 .is_some_and(|parent| !removed.contains(&parent))
2120 {
2121 node.layout_parent.set(None);
2122 }
2123 continue;
2124 }
2125
2126 let mut reconstruct = parents.contains(&id);
2127 reconstruct |= node
2128 .layout_children
2129 .get_mut()
2130 .as_ref()
2131 .is_some_and(|children| children.iter().any(|id| removed.contains(id)));
2132 let mut repaint = node
2133 .paint_children
2134 .get_mut()
2135 .as_ref()
2136 .is_some_and(|children| children.iter().any(|id| removed.contains(id)));
2137
2138 let old_anonymous_count = node.anonymous_blocks.len();
2139 node.anonymous_blocks.retain(|id| !removed.contains(id));
2140 reconstruct |= old_anonymous_count != node.anonymous_blocks.len();
2141 if node.is_anonymous() {
2142 let old_child_count = node.children.len();
2143 node.children.retain(|id| !removed.contains(id));
2144 reconstruct |= old_child_count != node.children.len();
2145 }
2146 if node
2147 .layout_parent
2148 .get()
2149 .is_some_and(|parent| removed.contains(&parent))
2150 {
2151 node.layout_parent.set(None);
2152 reconstruct = true;
2153 }
2154
2155 if let Some(context) = node.stacking_context.as_mut() {
2156 let old_count = context.children.len();
2157 context.children.retain(|child| {
2158 !removed.contains(&child.node_id)
2159 && !child.clip_ancestors.iter().any(|id| removed.contains(id))
2160 });
2161 if old_count != context.children.len() {
2162 context.sort();
2165 context.content_area = taffy::Rect::ZERO;
2166 repaint = true;
2167 }
2168 }
2169
2170 #[cfg(feature = "shadow-dom")]
2171 {
2172 for children in [
2173 node.flattened_children.as_mut(),
2174 node.assigned_nodes.as_mut(),
2175 ]
2176 .into_iter()
2177 .flatten()
2178 {
2179 let old_count = children.len();
2180 children.retain(|id| !removed.contains(id));
2181 reconstruct |= old_count != children.len();
2182 }
2183 if node.assigned_slot.is_some_and(|id| removed.contains(&id)) {
2184 node.assigned_slot = None;
2185 reconstruct = true;
2186 }
2187 if let Some(root) = node.shadow_root_data_mut() {
2188 let old_count = root.slots.len() + root.slottables.len();
2189 root.slots.retain(|id| !removed.contains(id));
2190 root.slottables.retain(|id| !removed.contains(id));
2191 root.stylesheet_nodes.retain(|id| !removed.contains(id));
2192 reconstruct |= old_count != root.slots.len() + root.slottables.len();
2193 }
2194 }
2195
2196 let mut table_removed = false;
2197 if let Some(element) = node.data.downcast_element_mut() {
2198 for pseudo in [&mut element.before, &mut element.after] {
2199 if pseudo.is_some_and(|id| removed.contains(&id)) {
2200 *pseudo = None;
2201 reconstruct = true;
2202 }
2203 }
2204 if element
2205 .inline_layout_data
2206 .as_ref()
2207 .is_some_and(|inline| layout_references_removed(&inline.layout, removed))
2208 {
2209 element.inline_layout_data = None;
2210 reconstruct = true;
2211 }
2212 if element
2213 .list_item_data
2214 .as_ref()
2215 .is_some_and(|item| match &item.position {
2216 crate::node::ListItemLayoutPosition::Outside(layout) => {
2217 layout_references_removed(layout, removed)
2218 }
2219 crate::node::ListItemLayoutPosition::Inside => false,
2220 })
2221 {
2222 element.list_item_data = None;
2223 reconstruct = true;
2224 }
2225 if let SpecialElementData::TableRoot(context) = &element.special_data {
2226 table_removed = context.references_removed_nodes(removed);
2227 }
2228 if table_removed {
2229 element.special_data = SpecialElementData::None;
2230 reconstruct = true;
2231 }
2232 #[cfg(feature = "shadow-dom")]
2233 if element
2234 .assigned_slot
2235 .is_some_and(|id| removed.contains(&id))
2236 {
2237 element.assigned_slot = None;
2238 reconstruct = true;
2239 }
2240 }
2241 if table_removed {
2242 node.flags.remove(NodeFlags::IS_TABLE_ROOT);
2243 }
2244
2245 if reconstruct || repaint {
2246 node.layout_children.get_mut().take();
2247 node.paint_children.get_mut().take();
2248 if let Some(element) = node.data.downcast_element_mut() {
2249 element.inline_layout_data = None;
2250 }
2251 dirty.insert(id);
2252 }
2253 }
2254
2255 let mut stack: Vec<NodeId> = dirty.iter().copied().collect();
2259 let mut visited = FxHashSet::default();
2260 while let Some(id) = stack.pop() {
2261 if !visited.insert(id) || removed.contains(&id) {
2262 continue;
2263 }
2264 let Some(node) = self.nodes.get_mut(id) else {
2265 continue;
2266 };
2267 stack.extend(node.parent);
2268 stack.extend(node.layout_parent.get());
2269 #[cfg(feature = "shadow-dom")]
2270 {
2271 stack.extend(node.assigned_slot);
2272 if let Some(root) = node.shadow_root_data() {
2273 stack.push(root.host);
2274 }
2275 }
2276 node.set_dirty_descendants();
2277 if node.stylo_element_data_opt().is_some() {
2278 node.insert_damage(if dirty.contains(&id) {
2279 ALL_DAMAGE
2280 } else {
2281 crate::layout::damage::ONLY_RELAYOUT
2282 | style::selector_parser::RestyleDamage::REPAINT
2283 });
2284 node.cache_mut().clear();
2285 }
2286 if let Some(inline) = node
2287 .data
2288 .downcast_element_mut()
2289 .and_then(|element| element.inline_layout_data.as_mut())
2290 {
2291 inline.content_widths = None;
2292 }
2293 }
2294
2295 #[cfg(feature = "shadow-dom")]
2296 {
2297 let nodes = &self.nodes;
2298 self.shadow_host_nodes.retain(|id| nodes.contains_key(*id));
2299 self.dirty_shadow_hosts.retain(|id| nodes.contains_key(*id));
2300 self.pending_slot_changes
2301 .retain(|id| nodes.contains_key(*id));
2302 self.signaled_slots.retain(|id| nodes.contains_key(*id));
2303 self.dirty_shadow_hosts
2304 .extend(self.shadow_host_nodes.iter().copied());
2305 }
2306
2307 self.controls_to_form
2308 .retain(|control, form| !removed.contains(control) && !removed.contains(form));
2309 if dirty.iter().any(|id| {
2312 self.nodes
2313 .get(*id)
2314 .is_some_and(|node| node.flags.is_in_document())
2315 }) {
2316 if self.mutator_depth == 0 {
2317 self.shell_provider.request_redraw();
2318 } else {
2319 self.mutator_redraw_pending = true;
2320 }
2321 }
2322
2323 }
2326
2327 fn purge_dropped_node_references(&mut self) {
2328 if self.node_drop_depth != 0 || self.dropped_node_ids.is_empty() {
2329 return;
2330 }
2331 let removed = std::mem::take(&mut self.dropped_node_ids);
2332 let parents = std::mem::take(&mut self.dropped_node_parents);
2333 let owners = std::mem::take(&mut self.dropped_reference_owners);
2334 self.invalidate_node_references(&removed, parents, owners);
2335 }
2336
2337 pub(crate) fn remove_node_from_tree(&mut self, node_id: NodeId) -> Option<Node> {
2340 self.clear_interaction_state_for_removed_node(node_id);
2341 let indexed = self.get_node(node_id).and_then(|node| {
2342 Some((
2343 node.owner_document?,
2344 node.element_data()?.id.as_ref()?.to_string(),
2345 ))
2346 });
2347 if let Some((document_id, id)) = indexed {
2348 self.remove_from_document_id_map(document_id, &id, node_id);
2349 }
2350 self.forget_script_custom_element(node_id);
2351 if self.nodes.contains_key(node_id) {
2352 self.dropped_node_ids.insert(node_id);
2353 let mut owners = FxHashSet::default();
2354 Self::collect_live_reference_owners(
2355 &self.nodes,
2356 &self.hoisted_fixed_parents,
2357 &self.dropped_node_ids,
2358 node_id,
2359 &mut owners,
2360 );
2361 self.dropped_reference_owners.extend(owners);
2362 }
2363 let node = self.nodes.remove(node_id);
2364 if let Some(node) = &node {
2365 self.dropped_node_parents.extend(
2366 [node.parent, node.layout_parent.get()]
2367 .into_iter()
2368 .flatten(),
2369 );
2370 self.purge_dropped_node_references();
2371 }
2372 node
2373 }
2374
2375 fn nearest_surviving_element_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
2380 let mut current = self.get_node(node_id)?.parent;
2381 while let Some(id) = current {
2382 let node = self.get_node(id)?;
2383 if node.is_element() && node.flags.is_in_document() {
2384 return Some(id);
2385 }
2386 current = node.parent;
2387 }
2388 None
2389 }
2390
2391 pub(crate) fn clear_interaction_state_for_removed_node(&mut self, node_id: NodeId) {
2412 if !self.nodes.contains_key(node_id) {
2413 return;
2414 }
2415
2416 if self.hover_node_id == Some(node_id) {
2417 self.hover_node_id = self.nearest_surviving_element_ancestor(node_id);
2418 self.hover_node_is_text = false;
2419 }
2420 if self.hover_hit_node_id == Some(node_id) {
2421 self.hover_hit_node_id = None;
2422 }
2423 if self.semantic_hover_node_id == Some(node_id) {
2424 self.semantic_hover_node_id = None;
2425 }
2426 if self.active_node_id == Some(node_id) {
2427 self.active_node_id = self.nearest_surviving_element_ancestor(node_id);
2428 }
2429 if self.focus_node_id == Some(node_id) {
2430 let shell_provider = self.shell_provider.clone();
2431 self.nodes[node_id].blur(shell_provider);
2432 self.focus_node_id = None;
2433 }
2434 if self.mousedown_node_id == Some(node_id) {
2435 self.mousedown_node_id = None;
2436 }
2437 if self.text_selection.anchor.node_or_parent == Some(node_id)
2438 || self.text_selection.focus.node_or_parent == Some(node_id)
2439 {
2440 self.text_selection.clear();
2441 }
2442 if self
2443 .hovered_scrollbar
2444 .is_some_and(|scrollbar| scrollbar.node_id == node_id)
2445 {
2446 self.hovered_scrollbar = None;
2447 }
2448 let drag_references_node = match &self.drag_mode {
2449 DragMode::Panning(state) => state.target == node_id,
2450 DragMode::ScrollbarDrag(state) => state.scrollbar.node_id == node_id,
2451 DragMode::Selecting | DragMode::None => false,
2452 };
2453 if drag_references_node {
2454 self.drag_mode = DragMode::None;
2455 }
2456 if matches!(
2457 &self.scroll_animation,
2458 ScrollAnimationState::Fling(state) if state.target == node_id
2459 ) {
2460 self.scroll_animation = ScrollAnimationState::None;
2461 }
2462 self.scrollbar_activity.remove(&node_id);
2463
2464 self.controls_to_form.remove(&node_id);
2469 }
2470
2471 pub(crate) fn drop_node_ignoring_parent(&mut self, node_id: NodeId) -> Option<Node> {
2472 self.drop_node_ignoring_parent_with(node_id, &mut |_| {})
2473 }
2474
2475 pub(crate) fn drop_node_ignoring_parent_with(
2478 &mut self,
2479 node_id: NodeId,
2480 on_drop: &mut dyn FnMut(NodeId),
2481 ) -> Option<Node> {
2482 self.node_drop_depth += 1;
2483 let mut node = self.remove_node_from_tree(node_id);
2484 if let Some(node) = &mut node {
2485 on_drop(node_id);
2486 if let Some(before) = node.before() {
2487 self.drop_node_ignoring_parent_with(before, on_drop);
2488 }
2489 if let Some(after) = node.after() {
2490 self.drop_node_ignoring_parent_with(after, on_drop);
2491 }
2492
2493 for &child in &node.children {
2494 self.drop_node_ignoring_parent_with(child, on_drop);
2495 }
2496 if let Some(contents) = node
2497 .element_data()
2498 .and_then(|element| element.template_contents)
2499 {
2500 self.drop_node_ignoring_parent_with(contents, on_drop);
2501 }
2502
2503 for &anon_id in &node.anonymous_blocks {
2506 self.deallocate_anonymous_block(anon_id);
2507 }
2508
2509 #[cfg(feature = "shadow-dom")]
2512 if let Some(shadow_root_id) = node.shadow_root_id() {
2513 self.shadow_host_nodes.remove(&node_id);
2514 self.dirty_shadow_hosts.remove(&node_id);
2515 self.custom_element_nodes.remove(&node_id);
2516 self.drop_node_ignoring_parent_with(shadow_root_id, on_drop);
2517 }
2518 }
2519 self.node_drop_depth -= 1;
2520 self.purge_dropped_node_references();
2521 node
2522 }
2523
2524 pub(crate) fn deallocate_anonymous_block(&mut self, anon_id: NodeId) {
2527 if !self.nodes.contains_key(anon_id) {
2530 return;
2531 }
2532
2533 self.node_drop_depth += 1;
2535 let nested = std::mem::take(&mut self.nodes[anon_id].anonymous_blocks);
2536 for nested_id in nested {
2537 self.deallocate_anonymous_block(nested_id);
2538 }
2539
2540 self.remove_node_from_tree(anon_id);
2541 self.node_drop_depth -= 1;
2542 self.purge_dropped_node_references();
2543 }
2544
2545 pub fn create_text_node(&mut self, text: &str) -> NodeId {
2546 let content = text.to_string();
2547 let data = NodeData::Text(TextNodeData::new(content));
2548 self.create_node(data)
2549 }
2550
2551 pub fn deep_clone_node(&mut self, node_id: NodeId) -> NodeId {
2552 let node = &self.nodes[node_id];
2554 let mut data = node.data.clone();
2555
2556 match &mut data {
2557 NodeData::Element(elem) | NodeData::AnonymousBlock(elem) => {
2558 if let Some(arc) = elem.style_attribute.as_mut() {
2559 let read_guard = self.guard().read();
2560 let block = arc.read_with(&read_guard);
2561 *arc = ServoArc::new(self.guard().wrap(block.clone()));
2562 }
2563 }
2564 _ => {}
2565 }
2566
2567 let children = node.children.clone();
2568 #[cfg(feature = "shadow-dom")]
2569 let shadow_source = node.shadow_root_id();
2570 let template_contents = node
2571 .element_data()
2572 .and_then(|element| element.template_contents);
2573 let owner_document = node.owner_document;
2574 let markup = node.markup.clone();
2575 let inert_script = node
2576 .flags
2577 .contains(crate::node::NodeFlags::IS_PARSER_INERT_SCRIPT);
2578
2579 let new_node_id = self.create_node(data);
2581 self.inherit_stash_classification(node_id, new_node_id);
2582 self.nodes[new_node_id].owner_document = owner_document;
2583 self.nodes[new_node_id].markup = markup;
2584 self.nodes[new_node_id]
2585 .flags
2586 .set(crate::node::NodeFlags::IS_PARSER_INERT_SCRIPT, inert_script);
2587 if let Some(document_id) = owner_document {
2588 let id = self.nodes[new_node_id]
2589 .element_data()
2590 .and_then(|element| element.id.as_ref())
2591 .map(ToString::to_string);
2592 if let Some(id) = id {
2593 self.add_to_document_id_map(document_id, &id, new_node_id);
2594 }
2595 }
2596 if let Some(contents) = template_contents {
2597 let cloned_contents = self.deep_clone_node(contents);
2598 self.nodes[new_node_id]
2599 .element_data_mut()
2600 .expect("template clone must be an element")
2601 .template_contents = Some(cloned_contents);
2602 }
2603
2604 let new_children: ThinVec<NodeId> = children
2606 .into_iter()
2607 .map(|child_id| self.deep_clone_node(child_id))
2608 .collect();
2609 for &child_id in &new_children {
2610 self.nodes[child_id].parent = Some(new_node_id);
2611 self.attach_script_custom_element_subtree(child_id, new_node_id);
2612 }
2613 self.nodes[new_node_id].children = new_children;
2614
2615 #[cfg(feature = "shadow-dom")]
2618 if let Some(source_root) = shadow_source {
2619 let clonable = self
2620 .nodes
2621 .get(source_root)
2622 .and_then(|node| node.shadow_root_data())
2623 .is_some_and(|root| root.clonable);
2624 if clonable {
2625 let mode = self.nodes[source_root].shadow_root_data().unwrap().mode;
2626 let serializable = self.nodes[source_root]
2627 .shadow_root_data()
2628 .unwrap()
2629 .serializable;
2630 let delegates_focus = self.nodes[source_root]
2631 .shadow_root_data()
2632 .unwrap()
2633 .delegates_focus;
2634 let declarative = self.nodes[source_root]
2635 .shadow_root_data()
2636 .unwrap()
2637 .declarative;
2638 let shadow_children = self.nodes[source_root].children.clone();
2639 let new_root = self.attach_shadow(new_node_id, mode);
2640 if let Some(root) = self.nodes[new_root].shadow_root_data_mut() {
2641 root.clonable = true;
2642 root.serializable = serializable;
2643 root.delegates_focus = delegates_focus;
2644 root.declarative = declarative;
2645 }
2646 let cloned_shadow_children: ThinVec<NodeId> = shadow_children
2647 .into_iter()
2648 .map(|child_id| self.deep_clone_node(child_id))
2649 .collect();
2650 for &child_id in &cloned_shadow_children {
2651 self.nodes[child_id].parent = Some(new_root);
2652 self.attach_script_custom_element_subtree(child_id, new_root);
2653 }
2654 self.nodes[new_root].children = cloned_shadow_children;
2655 }
2656 }
2657
2658 new_node_id
2659 }
2660
2661 pub(crate) fn remove_and_drop_pe(&mut self, node_id: NodeId) -> Option<Node> {
2662 fn remove_pe_ignoring_parent(doc: &mut BaseDocument, node_id: NodeId) -> Option<Node> {
2663 let mut node = doc.remove_node_from_tree(node_id);
2664 if let Some(node) = &mut node {
2665 for &child in &node.children {
2666 remove_pe_ignoring_parent(doc, child);
2667 }
2668 for &anon_id in &node.anonymous_blocks {
2669 doc.deallocate_anonymous_block(anon_id);
2670 }
2671 }
2672 node
2673 }
2674
2675 self.node_drop_depth += 1;
2676 let node = remove_pe_ignoring_parent(self, node_id);
2677
2678 if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
2680 let parent = &mut self.nodes[parent_id];
2681 parent.children.retain(|id| *id != node_id);
2682 }
2683
2684 self.node_drop_depth -= 1;
2685 self.purge_dropped_node_references();
2686 node
2687 }
2688
2689 pub(crate) fn resolve_url(&self, raw: &str) -> url::Url {
2690 self.url.resolve_relative(raw).unwrap_or_else(|| {
2691 panic!(
2692 "to be able to resolve {raw} with the base_url: {:?}",
2693 *self.url
2694 )
2695 })
2696 }
2697
2698 pub fn navigate_to_url(&self, raw: &str) -> bool {
2706 let Some(url) = self.url.resolve_relative(raw) else {
2707 return false;
2708 };
2709 self.navigation_provider
2710 .navigate_to(blitz_traits::navigation::NavigationOptions::new(
2711 url,
2712 None,
2713 self.id(),
2714 ));
2715 true
2716 }
2717
2718 pub fn current_url(&self) -> String {
2720 self.url.to_string()
2721 }
2722
2723 pub fn print_tree(&self) {
2724 crate::util::walk_tree(0, self.root_node());
2725 }
2726
2727 pub fn print_subtree(&self, node_id: NodeId) {
2728 crate::util::walk_tree(0, &self.nodes[node_id]);
2729 }
2730
2731 pub fn reload_resource_by_href(&mut self, href_to_reload: &str) {
2732 for &node_id in self.nodes_to_stylesheet.keys() {
2733 let node = &self.nodes[node_id];
2734 let Some(element) = node.element_data() else {
2735 continue;
2736 };
2737
2738 if element.name.local == local_name!("link") {
2739 if let Some(href) = element.attr(local_name!("href")) {
2740 if href == href_to_reload {
2742 let resolved_href = self.resolve_url(href);
2743 self.net_provider.fetch(
2744 self.id(),
2745 self.build_request(resolved_href.clone())
2746 .destination(RequestDestination::Style),
2747 ResourceHandler::boxed(
2748 self.tx.clone(),
2749 self.id,
2750 Some(node_id),
2751 self.shell_provider.clone(),
2752 StylesheetHandler {
2753 source_url: resolved_href,
2754 guard: self.guard.clone(),
2755 net_provider: self.net_provider.clone(),
2756 abort_signal: self.abort_signal.clone(),
2757 },
2758 ),
2759 );
2760 }
2761 }
2762 }
2763 }
2764 }
2765
2766 pub fn process_style_element(&mut self, target_id: NodeId) {
2767 let css = self.nodes[target_id].text_content();
2768 let css = html_escape::decode_html_entities(&css);
2769 let sheet = self.make_stylesheet(&css, Origin::Author);
2770 self.add_stylesheet_for_node(sheet, target_id);
2771 }
2772
2773 pub fn remove_user_agent_stylesheet(&mut self, contents: &str) {
2774 if let Some(sheet) = self.ua_stylesheets.remove(contents) {
2775 self.stylist.remove_stylesheet(sheet, &self.guard.read());
2776 }
2777 }
2778
2779 pub fn url(&self) -> &url::Url {
2781 &self.url
2782 }
2783
2784 pub fn author_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
2787 self.nodes_to_stylesheet.values()
2788 }
2789
2790 pub fn useragent_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
2792 self.ua_stylesheets.values()
2793 }
2794
2795 pub fn add_user_agent_stylesheet(&mut self, css: &str) {
2796 let sheet = self.make_stylesheet(css, Origin::UserAgent);
2797 self.ua_stylesheets.insert(css.to_string(), sheet.clone());
2798 self.stylist.append_stylesheet(sheet, &self.guard.read());
2799 }
2800
2801 pub fn make_stylesheet(&self, css: impl AsRef<str>, origin: Origin) -> DocumentStyleSheet {
2802 let data = Stylesheet::from_str(
2803 css.as_ref(),
2804 self.url.url_extra_data(),
2805 origin,
2806 ServoArc::new(self.guard.wrap(MediaList::empty())),
2807 self.guard.clone(),
2808 Some(&StylesheetLoader {
2809 tx: self.tx.clone(),
2810 doc_id: self.id,
2811 net_provider: self.net_provider.clone(),
2812 shell_provider: self.shell_provider.clone(),
2813 abort_signal: self.abort_signal.clone(),
2814 }),
2815 None,
2816 QuirksMode::NoQuirks,
2817 AllowImportRules::Yes,
2818 );
2819
2820 DocumentStyleSheet(ServoArc::new(data))
2821 }
2822
2823 pub fn upsert_stylesheet_for_node(&mut self, node_id: NodeId) {
2824 let raw_styles = self.nodes[node_id].text_content();
2825 let sheet = self.make_stylesheet(raw_styles, Origin::Author);
2826 self.add_stylesheet_for_node(sheet, node_id);
2827 }
2828
2829 pub fn add_stylesheet_for_node(&mut self, stylesheet: DocumentStyleSheet, node_id: NodeId) {
2830 self.platform_initial_sheet_media(&stylesheet, node_id);
2831 #[cfg(feature = "shadow-dom")]
2832 if let Some(root_id) = self.containing_shadow_root(node_id) {
2833 if self.document_sheet_nodes.remove(&node_id)
2836 && let Some(old) = self.nodes_to_stylesheet.get(&node_id).cloned()
2837 {
2838 self.stylist.remove_stylesheet(old, &self.guard.read());
2839 }
2840 self.install_shadow_stylesheet(root_id, node_id, stylesheet);
2841 return;
2842 }
2843
2844 let old = self.nodes_to_stylesheet.insert(node_id, stylesheet.clone());
2845
2846 if let Some(old) = old
2847 && self.document_sheet_nodes.contains(&node_id)
2848 {
2849 self.stylist.remove_stylesheet(old, &self.guard.read())
2850 }
2851 self.document_sheet_nodes.insert(node_id);
2852
2853 crate::net::fetch_font_face(
2855 self.tx.clone(),
2856 self.id,
2857 Some(node_id),
2858 &stylesheet.0,
2859 &self.net_provider,
2860 &self.shell_provider,
2861 &self.guard.read(),
2862 self.abort_signal.as_ref(),
2863 &mut self.web_fonts,
2864 self.font_epoch,
2865 );
2866
2867 let element = &mut self.nodes[node_id].element_data_mut().unwrap();
2869 element.special_data = SpecialElementData::Stylesheet(stylesheet.clone());
2870
2871 let insertion_point = self
2875 .nodes_to_stylesheet
2876 .range((Bound::Excluded(node_id), Bound::Unbounded))
2877 .find(|(id, _)| self.document_sheet_nodes.contains(id))
2878 .map(|(_, sheet)| sheet);
2879
2880 if let Some(insertion_point) = insertion_point {
2881 self.stylist.insert_stylesheet_before(
2882 stylesheet,
2883 insertion_point.clone(),
2884 &self.guard.read(),
2885 )
2886 } else {
2887 self.stylist
2888 .append_stylesheet(stylesheet, &self.guard.read())
2889 }
2890 }
2891
2892 pub fn handle_messages(&mut self) {
2893 for msg in self.rx.take_pending() {
2894 let msg =
2895 Arc::try_unwrap(msg).unwrap_or_else(|_| panic!("document event has one consumer"));
2896 self.handle_message(msg);
2897 }
2898 }
2899
2900 pub fn handle_message(&mut self, msg: DocumentEvent) {
2901 match msg {
2902 DocumentEvent::ResourceLoad(resource) => self.load_resource(resource),
2903 DocumentEvent::NavigateIframe { node_id, url } => self.navigate_iframe(node_id, url),
2904 }
2905 }
2906
2907 pub fn has_pending_critical_resources(&self) -> bool {
2909 !self.pending_critical_resources.is_empty()
2910 }
2911
2912 pub fn pending_image_count(&self) -> usize {
2919 self.pending_images.len()
2920 }
2921
2922 pub fn load_resource(&mut self, res: ResourceLoadResponse) {
2923 self.pending_critical_resources.remove(&res.request_id);
2924
2925 let resource = match res.result {
2926 Ok(resource) => resource,
2927 Err(err) => {
2928 if let Some(url) = res.resolved_url.as_ref() {
2929 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
2930 #[cfg(feature = "tracing")]
2931 tracing::warn!(
2932 url = url.as_str(),
2933 waiting_nodes = waiting_nodes.len(),
2934 error = err.as_str(),
2935 "Resource load failed"
2936 );
2937 #[cfg(not(feature = "tracing"))]
2938 let _ = (waiting_nodes, err);
2939 } else {
2940 #[cfg(feature = "tracing")]
2941 tracing::warn!(error = err.as_str(), "Resource load failed (no url)");
2942 #[cfg(not(feature = "tracing"))]
2943 let _ = err;
2944 }
2945 return;
2946 }
2947 };
2948
2949 let (resource, font_ticket) = match resource {
2950 Resource::TrackedFont(bytes, overrides, ticket) => {
2951 if ticket.css && ticket.epoch != self.font_epoch {
2952 ticket.finish(false);
2953 return;
2954 }
2955 (Resource::Font(bytes, overrides), Some(ticket))
2956 }
2957 resource => (resource, None),
2958 };
2959 if matches!(&resource, Resource::Css(_) | Resource::Font(_, _))
2960 && res.node_id.is_some_and(|id| {
2961 self.get_node(id)
2962 .is_none_or(|node| !node.flags.is_in_document())
2963 })
2964 {
2965 if let Some(ticket) = font_ticket {
2966 ticket.finish(false);
2967 }
2968 return;
2969 }
2970
2971 match resource {
2972 Resource::Css(css) => {
2973 let node_id = res.node_id.unwrap();
2974 self.add_stylesheet_for_node(css, node_id);
2975 }
2976 Resource::ImportSheet(import_rule, sheet) => {
2977 {
2986 let mut guard = self.guard.write();
2987 import_rule.write_with(&mut guard).stylesheet =
2988 style::stylesheets::import_rule::ImportSheet::Sheet(sheet.clone());
2989 }
2990
2991 crate::net::fetch_font_face(
2995 self.tx.clone(),
2996 self.id,
2997 res.node_id,
2998 &sheet,
2999 &self.net_provider,
3000 &self.shell_provider,
3001 &self.guard.read(),
3002 self.abort_signal.as_ref(),
3003 &mut self.web_fonts,
3004 self.font_epoch,
3005 );
3006 }
3007 Resource::Image(_kind, width, height, image_data) => {
3008 let image = ImageData::Raster(RasterImageData::new(width, height, image_data));
3010
3011 let Some(url) = res.resolved_url.as_ref() else {
3012 return;
3013 };
3014
3015 self.apply_loaded_image(url, image);
3016 }
3017 #[cfg(feature = "svg")]
3018 Resource::Svg(_kind, svg) => {
3019 let image = ImageData::Svg(svg);
3021
3022 let Some(url) = res.resolved_url.as_ref() else {
3023 return;
3024 };
3025
3026 self.apply_loaded_image(url, image);
3027 }
3028 Resource::DocumentSrc(html) => {
3029 let Some(node_id) = res.node_id else {
3030 return;
3031 };
3032 self.apply_iframe_html(node_id, res.request_id, res.resolved_url, &html);
3033 }
3034 Resource::Font(bytes, overrides) => {
3035 let font = Blob::new(Arc::new(bytes));
3036
3037 let weight_override = overrides.weight.map(parley::fontique::FontWeight::new);
3043 let info_override = parley::fontique::FontInfoOverride {
3044 family_name: overrides.family_name.as_deref(),
3045 weight: weight_override,
3046 style: overrides.style,
3047 ..Default::default()
3048 };
3049
3050 let mut global_font_ctx = self.font_ctx.lock().unwrap();
3052 let registered = global_font_ctx
3053 .collection
3054 .register_fonts(font.clone(), Some(info_override));
3055 let registered = !registered.is_empty();
3056
3057 #[cfg(feature = "parallel-construct")]
3058 {
3059 rayon::broadcast(|_ctx| {
3060 let mut font_ctx = self
3061 .thread_font_contexts
3062 .get_or(|| RefCell::new(Box::new(global_font_ctx.clone())))
3063 .borrow_mut();
3064 font_ctx
3065 .collection
3066 .register_fonts(font.clone(), Some(info_override));
3067 });
3068 }
3069 drop(global_font_ctx);
3070
3071 self.invalidate_inline_contexts();
3073 if let Some(ticket) = font_ticket {
3074 ticket.finish(registered);
3075 }
3076 }
3077 Resource::TrackedFont(..) => unreachable!("tracked font was normalized"),
3078 Resource::None => {
3079 }
3081 }
3082 }
3083
3084 fn apply_loaded_image(&mut self, url: &str, image: ImageData) {
3087 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
3089
3090 #[cfg(feature = "tracing")]
3091 tracing::info!(
3092 "Image {url} loaded, applying to {} nodes",
3093 waiting_nodes.len()
3094 );
3095
3096 self.image_cache.insert(url.to_string(), image.clone());
3098
3099 for (node_id, image_type) in waiting_nodes {
3101 let Some(node) = self.get_node_mut(node_id) else {
3102 continue;
3103 };
3104
3105 match image_type {
3106 ImageType::Image => {
3107 node.element_data_mut().unwrap().special_data =
3108 SpecialElementData::Image(Box::new(image.clone()));
3109
3110 node.cache_mut().clear();
3112 node.insert_damage(ALL_DAMAGE);
3113 }
3114 ImageType::Background(idx) | ImageType::Mask(idx) => {
3115 let layer_image = node.element_data_mut().and_then(|el| {
3116 let images = match image_type {
3117 ImageType::Background(_) => &mut el.background_images,
3118 ImageType::Mask(_) => &mut el.mask_images,
3119 ImageType::Image => unreachable!(),
3120 };
3121 images.get_mut(idx)
3122 });
3123 if let Some(Some(layer_image)) = layer_image {
3124 layer_image.status = Status::Ok;
3125 layer_image.image = image.clone();
3126 }
3127 }
3128 }
3129 }
3130 }
3131
3132 pub fn snapshot_node(&mut self, node_id: NodeId) {
3133 let node = &mut self.nodes[node_id];
3134
3135 let has_been_styled = node.primary_styles().is_some();
3140 if !has_been_styled {
3141 return;
3142 }
3143
3144 let opaque_node_id = TNode::opaque(&&*node);
3145 node.set_has_snapshot(true);
3146 node.snapshot_handled()
3147 .store(false, std::sync::atomic::Ordering::SeqCst);
3148
3149 if let Some(_existing_snapshot) = self.snapshots.get_mut(&opaque_node_id) {
3151 } else {
3154 let attrs: Option<Vec<_>> = node.attrs().map(|attrs| {
3155 attrs
3156 .iter()
3157 .map(|attr| {
3158 let ident = AttrIdentifier {
3159 local_name: GenericAtomIdent(attr.name.local.clone()),
3160 name: GenericAtomIdent(attr.name.local.clone()),
3161 namespace: GenericAtomIdent(attr.name.ns.clone()),
3162 prefix: None,
3163 };
3164
3165 let value = if attr.name.local == local_name!("id") {
3166 AttrValue::Atom(Atom::from(&*attr.value))
3167 } else if attr.name.local == local_name!("class") {
3168 let classes = attr
3169 .value
3170 .split_ascii_whitespace()
3171 .map(Atom::from)
3172 .collect();
3173 AttrValue::TokenList(OnceLock::from(attr.value.to_string()), classes)
3179 } else {
3180 AttrValue::String(attr.value.to_string())
3181 };
3182
3183 (ident, value)
3184 })
3185 .collect()
3186 });
3187
3188 let changed_attrs = attrs
3189 .as_ref()
3190 .map(|attrs| attrs.iter().map(|attr| attr.0.name.clone()).collect())
3191 .unwrap_or_default();
3192
3193 self.snapshots.insert(
3194 opaque_node_id,
3195 ServoElementSnapshot {
3196 state: Some(*node.element_state()),
3197 attrs,
3198 changed_attrs,
3199 class_changed: true,
3200 id_changed: true,
3201 other_attributes_changed: true,
3202 },
3203 );
3204 }
3205 }
3206
3207 pub(crate) fn snapshot_attribute(&mut self, node_id: NodeId, name: &markup5ever::LocalName) {
3209 self.snapshot_node(node_id);
3210 let opaque_node_id = TNode::opaque(&&self.nodes[node_id]);
3211 if let Some(snapshot) = self.snapshots.get_mut(&opaque_node_id) {
3212 let changed_attr = GenericAtomIdent(name.clone());
3213 if !snapshot.changed_attrs.contains(&changed_attr) {
3214 snapshot.changed_attrs.extend(std::iter::once(changed_attr));
3215 }
3216 }
3217 }
3218
3219 pub fn snapshot_node_and(&mut self, node_id: NodeId, cb: impl FnOnce(&mut Node)) {
3226 if !self.nodes.contains_key(node_id) {
3227 return;
3228 }
3229 self.snapshot_node(node_id);
3230 cb(&mut self.nodes[node_id]);
3231 }
3232
3233 pub fn hit(&self, x: f32, y: f32) -> Option<HitResult> {
3235 self.hit_with_scrollbar(x, y).0
3236 }
3237
3238 pub fn nearest_non_anonymous_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
3251 let mut node = self.get_node(node_id)?;
3255 loop {
3256 let parent = match node.parent {
3257 Some(parent_id) => self.get_node(parent_id)?,
3258 None => return Some(node.id),
3259 };
3260 if !node.is_anonymous() && !parent.is_anonymous() {
3261 return Some(node.id);
3262 }
3263 node = parent;
3264 }
3265 }
3266
3267 pub fn focus_next_node(&mut self) -> Option<NodeId> {
3268 let focussed_node_id = self.get_focussed_node_id()?;
3269 let id = self.next_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
3270 self.set_focus_to(id);
3271 Some(id)
3272 }
3273
3274 pub fn focus_prev_node(&mut self) -> Option<NodeId> {
3276 let focussed_node_id = self.get_focussed_node_id()?;
3277 let id = self.prev_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
3278 self.set_focus_to(id);
3279 Some(id)
3280 }
3281
3282 pub fn clear_focus(&mut self) {
3284 if let Some(id) = self.focus_node_id {
3285 let shell_provider = self.shell_provider.clone();
3286 self.snapshot_node_and(id, |node| node.blur(shell_provider));
3287 self.focus_node_id = None;
3288 }
3289 }
3290
3291 pub fn set_mousedown_node_id(&mut self, node_id: Option<NodeId>) {
3292 self.mousedown_node_id = node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
3293 }
3294 pub fn set_focus_to(&mut self, focus_node_id: NodeId) -> bool {
3295 let Some(focus_node_id) = self.nearest_non_anonymous_ancestor(focus_node_id) else {
3296 return false;
3297 };
3298 if Some(focus_node_id) == self.focus_node_id {
3299 return false;
3300 }
3301
3302 #[cfg(feature = "tracing")]
3303 tracing::info!("Focussed node {focus_node_id}");
3304
3305 let shell_provider = self.shell_provider.clone();
3306
3307 if let Some(id) = self.focus_node_id {
3309 self.snapshot_node_and(id, |node| node.blur(shell_provider.clone()));
3310 }
3311
3312 self.snapshot_node_and(focus_node_id, |node| node.focus(shell_provider));
3314
3315 self.focus_node_id = Some(focus_node_id);
3316
3317 true
3318 }
3319
3320 pub fn active_node(&mut self) -> bool {
3321 let Some(hover_node_id) = self.get_hover_node_id() else {
3322 return false;
3323 };
3324
3325 if let Some(active_node_id) = self.active_node_id {
3326 if active_node_id == hover_node_id {
3327 return true;
3328 }
3329 self.unactive_node();
3330 }
3331
3332 debug_assert!(
3334 self.get_node(hover_node_id)
3335 .is_some_and(|node| !node.is_anonymous()),
3336 "interaction state must reference DOM nodes, not layout-generated nodes"
3337 );
3338 let active_node_id = Some(hover_node_id);
3339
3340 let node_path = self.maybe_node_layout_ancestors(active_node_id);
3341 for &id in node_path.iter() {
3342 self.snapshot_node_and(id, |node| node.active());
3343 }
3344
3345 self.active_node_id = active_node_id;
3346
3347 true
3348 }
3349
3350 pub fn unactive_node(&mut self) -> bool {
3351 let Some(active_node_id) = self.active_node_id.take() else {
3352 return false;
3353 };
3354
3355 let node_path = self.maybe_node_layout_ancestors(Some(active_node_id));
3356 for &id in node_path.iter() {
3357 self.snapshot_node_and(id, |node| node.unactive());
3358 }
3359
3360 true
3361 }
3362
3363 pub fn hovered_scrollbar(&self) -> Option<crate::node::ScrollbarRef> {
3365 self.hovered_scrollbar
3366 }
3367
3368 pub fn scrollbar_drag_target(&self) -> Option<crate::node::ScrollbarRef> {
3370 match &self.drag_mode {
3371 DragMode::ScrollbarDrag(state) => Some(state.scrollbar),
3372 _ => None,
3373 }
3374 }
3375
3376 pub fn scrollbar_opacity(&self, node_id: NodeId) -> f32 {
3381 let interacting = |scrollbar: &crate::node::ScrollbarRef| scrollbar.node_id == node_id;
3382 if self.hovered_scrollbar.as_ref().is_some_and(interacting)
3383 || self
3384 .scrollbar_drag_target()
3385 .as_ref()
3386 .is_some_and(interacting)
3387 {
3388 return 1.0;
3389 }
3390 self.scrollbar_activity.get(&node_id).map_or(1.0, |last| {
3391 crate::node::scrollbar::opacity_at(last.elapsed())
3392 })
3393 }
3394
3395 pub(crate) fn show_scrollbars(&mut self, node_id: NodeId) {
3398 if cfg!(feature = "scrollbars") {
3399 self.scrollbar_activity.insert(node_id, Instant::now());
3400 }
3401 }
3402
3403 fn scrollbars_animating(&self) -> bool {
3406 use crate::node::scrollbar::{FADE_DELAY, FADE_DURATION};
3407 self.scrollbar_activity
3408 .values()
3409 .any(|last| last.elapsed() < FADE_DELAY + FADE_DURATION)
3410 }
3411
3412 pub(crate) fn hit_with_scrollbar(
3416 &self,
3417 x: f32,
3418 y: f32,
3419 ) -> (Option<HitResult>, Option<crate::node::ScrollbarRef>) {
3420 if TDocument::as_node(&self.root_node())
3421 .first_element_child()
3422 .is_none()
3423 {
3424 #[cfg(feature = "tracing")]
3425 tracing::warn!("No DOM - not resolving hit test");
3426 return (None, None);
3427 }
3428 let mut scrollbar = None;
3429 let hit = self
3430 .root_element()
3431 .hit_inner(x, y, self.viewport().scale_f64(), &mut scrollbar);
3432 (hit, scrollbar)
3433 }
3434
3435 pub fn set_hover_to(&mut self, x: f32, y: f32) -> bool {
3436 self.semantic_hover_node_id = None;
3437 self.last_client_pointer_position = Some(taffy::Point {
3441 x: x - self.viewport_scroll.x as f32,
3442 y: y - self.viewport_scroll.y as f32,
3443 });
3444
3445 let (hit, hovered_scrollbar) = self.hit_with_scrollbar(x, y);
3446 let hovered_scrollbar =
3449 hovered_scrollbar.filter(|scrollbar| self.scrollbar_opacity(scrollbar.node_id) > 0.0);
3450 let scrollbar_changed = hovered_scrollbar != self.hovered_scrollbar;
3454 if scrollbar_changed {
3455 for scrollbar in [self.hovered_scrollbar, hovered_scrollbar]
3458 .into_iter()
3459 .flatten()
3460 {
3461 self.show_scrollbars(scrollbar.node_id);
3462 }
3463 }
3464 self.hovered_scrollbar = hovered_scrollbar;
3465
3466 let hit_node_id = hit.map(|hit| hit.node_id);
3471 let hover_node_id = hit_node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
3472 let new_is_text = hit.map(|hit| hit.is_text).unwrap_or(false);
3473
3474 self.apply_hover_target(hit_node_id, hover_node_id, new_is_text, scrollbar_changed)
3475 }
3476
3477 pub fn set_hover_to_node(&mut self, node_id: NodeId, x: f32, y: f32) -> bool {
3485 self.semantic_hover_node_id = Some(node_id);
3486 self.last_client_pointer_position = Some(taffy::Point {
3487 x: x - self.viewport_scroll.x as f32,
3488 y: y - self.viewport_scroll.y as f32,
3489 });
3490
3491 let hovered_scrollbar = self.hovered_scrollbar.take();
3492 let scrollbar_changed = hovered_scrollbar.is_some();
3493 if let Some(scrollbar) = hovered_scrollbar {
3494 self.show_scrollbars(scrollbar.node_id);
3495 }
3496 let hover_node_id = self.nearest_non_anonymous_ancestor(node_id);
3497 self.apply_hover_target(Some(node_id), hover_node_id, false, scrollbar_changed)
3498 }
3499
3500 fn apply_hover_target(
3501 &mut self,
3502 hit_node_id: Option<NodeId>,
3503 hover_node_id: Option<NodeId>,
3504 new_is_text: bool,
3505 scrollbar_changed: bool,
3506 ) -> bool {
3507 let hit_changed =
3508 hit_node_id != self.hover_hit_node_id || new_is_text != self.hover_node_is_text;
3509 self.hover_hit_node_id = hit_node_id;
3510 self.hover_node_is_text = new_is_text;
3511
3512 if hover_node_id == self.hover_node_id {
3514 if hit_changed {
3515 self.shell_provider.set_cursor(self.get_cursor());
3519 }
3520 return scrollbar_changed;
3521 }
3522
3523 let old_node_path = self.maybe_node_layout_ancestors(self.hover_node_id);
3524 let new_node_path = self.maybe_node_layout_ancestors(hover_node_id);
3525 let same_count = old_node_path
3526 .iter()
3527 .zip(&new_node_path)
3528 .take_while(|(o, n)| o == n)
3529 .count();
3530 for &id in old_node_path.iter().skip(same_count) {
3531 self.snapshot_node_and(id, |node| node.unhover());
3532 }
3533 for &id in new_node_path.iter().skip(same_count) {
3534 self.snapshot_node_and(id, |node| node.hover());
3535 }
3536
3537 self.hover_node_id = hover_node_id;
3538
3539 self.shell_provider.set_cursor(self.get_cursor());
3541
3542 self.shell_provider.request_redraw();
3544
3545 true
3546 }
3547
3548 pub fn clear_hover(&mut self) -> bool {
3549 self.last_client_pointer_position = None;
3552 self.semantic_hover_node_id = None;
3553 self.hover_hit_node_id = None;
3554
3555 let Some(hover_node_id) = self.hover_node_id else {
3556 return false;
3557 };
3558
3559 let old_node_path = self.maybe_node_layout_ancestors(Some(hover_node_id));
3560 for &id in old_node_path.iter() {
3561 self.snapshot_node_and(id, |node| node.unhover());
3562 }
3563
3564 self.hover_node_id = None;
3565 self.hover_node_is_text = false;
3566
3567 self.shell_provider.set_cursor(self.get_cursor());
3569
3570 self.shell_provider.request_redraw();
3572
3573 true
3574 }
3575
3576 pub fn refresh_hover(&mut self) -> bool {
3582 if let Some(node_id) = self.semantic_hover_node_id {
3583 if self.get_node(node_id).is_some() {
3584 let hover_node_id = self.nearest_non_anonymous_ancestor(node_id);
3585 return self.apply_hover_target(Some(node_id), hover_node_id, false, false);
3586 }
3587 self.semantic_hover_node_id = None;
3588 }
3589 let Some(pos) = self.last_client_pointer_position else {
3590 return false;
3591 };
3592 let x = pos.x + self.viewport_scroll.x as f32;
3593 let y = pos.y + self.viewport_scroll.y as f32;
3594 self.set_hover_to(x, y)
3595 }
3596
3597 pub fn get_hover_node_id(&self) -> Option<NodeId> {
3598 self.hover_node_id
3599 }
3600
3601 pub fn get_mousedown_node_id(&self) -> Option<NodeId> {
3602 self.mousedown_node_id
3603 }
3604
3605 pub fn set_viewport(&mut self, viewport: Viewport) {
3606 let scale_has_changed = viewport.scale_f64() != self.viewport.scale_f64();
3607 self.viewport = viewport;
3608 self.set_stylist_device(make_device(
3609 &self.viewport,
3610 self.media_type.clone(),
3611 self.font_ctx.clone(),
3612 ));
3613 self.scroll_viewport_by(0.0, 0.0); if scale_has_changed {
3616 self.invalidate_inline_contexts();
3617 self.shell_provider.request_redraw();
3618 }
3619 }
3620
3621 pub fn media_type(&self) -> &MediaType {
3623 &self.media_type
3624 }
3625
3626 pub fn set_media_type(&mut self, media_type: MediaType) {
3629 if self.media_type == media_type {
3630 return;
3631 }
3632 self.media_type = media_type;
3633 self.set_stylist_device(make_device(
3634 &self.viewport,
3635 self.media_type.clone(),
3636 self.font_ctx.clone(),
3637 ));
3638 }
3639
3640 pub fn viewport(&self) -> &Viewport {
3641 &self.viewport
3642 }
3643
3644 pub fn viewport_mut(&mut self) -> ViewportMut<'_> {
3645 ViewportMut::new(self)
3646 }
3647
3648 pub fn zoom_by(&mut self, increment: f32) {
3649 let mut viewport = self.viewport.clone();
3650 viewport.zoom_by(increment);
3651 self.set_viewport(viewport);
3652 }
3653
3654 pub fn zoom_to(&mut self, zoom: f32) {
3655 let mut viewport = self.viewport.clone();
3656 viewport.set_zoom(zoom);
3657 self.set_viewport(viewport);
3658 }
3659
3660 pub fn get_viewport(&self) -> Viewport {
3661 self.viewport.clone()
3662 }
3663
3664 pub fn incremental_layout(&self) -> bool {
3666 self.incremental_layout
3667 }
3668
3669 pub fn set_incremental_layout(&mut self, enabled: bool) {
3671 self.incremental_layout = enabled;
3672 }
3673
3674 pub fn devtools(&self) -> &DevtoolSettings {
3675 &self.devtool_settings
3676 }
3677
3678 pub fn devtools_mut(&mut self) -> &mut DevtoolSettings {
3679 &mut self.devtool_settings
3680 }
3681
3682 pub fn subdoc(&self, node_id: NodeId) -> Option<&dyn Document> {
3683 self.get_node(node_id)
3684 .and_then(|node| node.element_data())
3685 .and_then(|el| el.sub_doc_data())
3686 }
3687
3688 pub fn subdoc_mut(&mut self, node_id: NodeId) -> Option<&mut dyn Document> {
3689 self.get_node_mut(node_id)
3690 .and_then(|node| node.element_data_mut())
3691 .and_then(|el| el.sub_doc_data_mut())
3692 }
3693
3694 pub fn is_animating(&self) -> bool {
3695 #[cfg(feature = "custom-widget")]
3696 let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
3697 self.nodes[node_id]
3698 .element_data()
3699 .and_then(|el| el.custom_widget_data())
3700 .is_some_and(|data| data.widget.requires_redraw())
3701 });
3702 #[cfg(not(feature = "custom-widget"))]
3703 let custom_widget_is_animating = false;
3704
3705 let animating = self.has_canvas
3706 | self.has_active_animations
3707 | (self.subdoc_animation_pacing != AnimationPacing::Idle)
3708 | custom_widget_is_animating
3709 | (self.scroll_animation != ScrollAnimationState::None)
3710 | self.scrollbars_animating();
3711
3712 if animating && crate::debug::animation_reasons_enabled() {
3713 crate::debug::report_animation_reasons(
3714 self.id(),
3715 self.has_canvas,
3716 self.has_active_animations,
3717 self.subdoc_animation_pacing != AnimationPacing::Idle,
3718 custom_widget_is_animating,
3719 self.scroll_animation != ScrollAnimationState::None,
3720 self.scrollbars_animating(),
3721 self.animating_node_names().as_deref(),
3722 );
3723 }
3724
3725 animating
3726 }
3727
3728 pub fn animation_pacing(&self) -> AnimationPacing {
3733 let focused_text_input = self.focus_node_id.is_some_and(|node_id| {
3734 self.nodes
3735 .get(node_id)
3736 .and_then(|node| node.element_data())
3737 .is_some_and(|element| element.text_input_data().is_some())
3738 });
3739 #[cfg(feature = "custom-widget")]
3740 let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
3741 self.nodes[node_id]
3742 .element_data()
3743 .and_then(|el| el.custom_widget_data())
3744 .is_some_and(|data| data.widget.requires_redraw())
3745 });
3746 #[cfg(not(feature = "custom-widget"))]
3747 let custom_widget_is_animating = false;
3748
3749 if self.has_canvas
3750 || custom_widget_is_animating
3751 || self.scroll_animation != ScrollAnimationState::None
3752 || self.scrollbars_animating()
3753 {
3754 AnimationPacing::Interactive
3755 } else if self.has_active_animations {
3756 const SLOW_ANIMATION_SECONDS: f64 = 2.0;
3757 let sets = self.animations.sets.read();
3758 let has_fast_animation_or_transition = sets.values().any(|set| {
3759 set.transitions.iter().any(|transition| {
3760 matches!(
3761 transition.state,
3762 AnimationState::Pending | AnimationState::Running
3763 )
3764 }) || set.animations.iter().any(|animation| {
3765 matches!(
3766 animation.state,
3767 AnimationState::Pending | AnimationState::Running
3768 ) && animation.duration < SLOW_ANIMATION_SECONDS
3769 })
3770 });
3771 if has_fast_animation_or_transition {
3772 AnimationPacing::Interactive
3773 } else {
3774 AnimationPacing::SlowCss
3775 }
3776 } else if focused_text_input {
3777 AnimationPacing::Caret
3778 } else if self.subdoc_animation_pacing != AnimationPacing::Idle {
3779 self.subdoc_animation_pacing
3780 } else {
3781 AnimationPacing::Idle
3782 }
3783 }
3784
3785 fn animating_node_names(&self) -> Option<String> {
3792 if !self.has_active_animations {
3793 return None;
3794 }
3795 let sets = self.animations.sets.read();
3796 let mut described: Vec<String> = sets
3797 .iter()
3798 .filter(|(_, state)| state.needs_animation_ticks())
3799 .filter_map(|(key, state)| {
3800 let node_id = NodeId::from_u64(key.node.id() as u64);
3801 let node = self.nodes.get(node_id)?;
3802 let element = node.element_data()?;
3803 let name = element
3804 .attr(local_name!("id"))
3805 .map(|id| format!("#{id}"))
3806 .or_else(|| {
3807 element
3808 .attr(local_name!("class"))
3809 .and_then(|c| c.split_ascii_whitespace().next())
3810 .map(|c| format!(".{c}"))
3811 })
3812 .unwrap_or_else(|| element.name.local.to_string());
3813 Some(format!(
3814 "{name}(anim={},trans={},in_doc={})",
3815 state.animations.len(),
3816 state.transitions.len(),
3817 node.flags.is_in_document(),
3818 ))
3819 })
3820 .collect();
3821 described.sort();
3822 described.truncate(12);
3823 Some(described.join(" "))
3824 }
3825
3826 pub fn set_stylist_device(&mut self, device: Device) {
3828 self.viewport_generation = self.viewport_generation.wrapping_add(1);
3829 let previous_viewport = self.stylist.device().au_viewport_size();
3832 let next_viewport = device.au_viewport_size();
3833
3834 let root_styles = self
3840 .try_root_element()
3841 .and_then(|root| root.primary_styles());
3842 if let Some(root_style) = root_styles.as_deref() {
3843 device.set_root_style(root_style);
3844
3845 let font = root_style.get_font();
3846 let font_size = font.clone_font_size().computed_size();
3847 device.set_root_font_size(root_style.effective_zoom.unzoom(font_size.px()));
3848
3849 let line_height = device
3850 .calc_line_height(font, root_style.writing_mode, None)
3851 .0;
3852 device.set_root_line_height(root_style.effective_zoom.unzoom(line_height.px()));
3853 }
3854 drop(root_styles);
3855
3856 let origins = {
3857 let guard = &self.guard;
3858 let guards = StylesheetGuards {
3859 author: &guard.read(),
3860 ua_or_user: &guard.read(),
3861 };
3862 self.stylist.set_device(device, &guards)
3863 };
3864 self.stylist.force_stylesheet_origins_dirty(origins);
3865 #[cfg(feature = "shadow-dom")]
3866 for host_id in self.shadow_host_node_ids() {
3867 if let Some(root_id) = self.shadow_root_id(host_id) {
3868 self.invalidate_shadow_styles(root_id);
3869 }
3870 }
3871
3872 if let Some(root_id) = self.try_root_element().map(|root| root.id) {
3875 self.nodes[root_id].set_restyle_hint(RestyleHint::recascade_subtree());
3876 self.shell_provider.request_redraw();
3877 }
3878 if previous_viewport != next_viewport {
3879 self.abspos_layout_dirty = true;
3880 }
3881 }
3882
3883 pub fn stylist_device(&mut self) -> &Device {
3884 self.stylist.device()
3885 }
3886
3887 pub fn get_cursor(&self) -> Option<CursorIcon> {
3895 let node_id = self
3900 .hover_hit_node_id
3901 .filter(|&id| self.nodes.contains_key(id))
3902 .or(self.get_hover_node_id());
3903 let Some(node_id) = node_id else {
3904 return Some(CursorIcon::Default);
3905 };
3906 let node = &self.nodes[node_id];
3907
3908 if let Some(subdoc) = node.subdoc().map(|doc| doc.inner()) {
3909 if subdoc.hover_hit_node_id.is_some() || subdoc.get_hover_node_id().is_some() {
3915 return subdoc.get_cursor();
3916 }
3917 return Some(CursorIcon::Default);
3918 }
3919
3920 let Some(style) = node.primary_styles() else {
3921 return Some(CursorIcon::Default);
3922 };
3923 let user_select = style.clone_user_select();
3924 let keyword = style.clone_cursor().keyword;
3925
3926 if keyword != CursorKind::Auto {
3928 return stylo_to_cursor_icon(keyword);
3929 }
3930
3931 if node
3933 .element_data()
3934 .is_some_and(|e| e.text_input_data().is_some())
3935 {
3936 return Some(CursorIcon::Text);
3937 }
3938
3939 let mut maybe_node = Some(node);
3941 while let Some(node) = maybe_node {
3942 if node.is_link() {
3943 return Some(CursorIcon::Pointer);
3944 }
3945
3946 maybe_node = node.layout_parent.get().map(|node_id| node.with(node_id));
3947 }
3948
3949 if self.hover_node_is_text {
3951 return Some(match user_select {
3952 UserSelect::Text | UserSelect::All | UserSelect::Auto => CursorIcon::Text,
3953 UserSelect::None => CursorIcon::Default,
3954 });
3955 }
3956
3957 Some(CursorIcon::Default)
3959 }
3960
3961 pub fn scroll_node_by<F: FnMut(DomEvent)>(
3962 &mut self,
3963 node_id: NodeId,
3964 x: f64,
3965 y: f64,
3966 dispatch_event: F,
3967 ) {
3968 self.scroll_node_by_has_changed(node_id, x, y, dispatch_event);
3969 }
3970
3971 pub fn scroll_node_by_has_changed<F: FnMut(DomEvent)>(
3981 &mut self,
3982 node_id: NodeId,
3983 x: f64,
3984 y: f64,
3985 dispatch_event: F,
3986 ) -> bool {
3987 let has_changed = self.scroll_node_by_inner(node_id, x, y, dispatch_event);
3988 if has_changed {
3989 self.resolve_positioned_scroll();
3990 }
3991 has_changed
3992 }
3993
3994 fn scroll_node_by_inner<F: FnMut(DomEvent)>(
3995 &mut self,
3996 node_id: NodeId,
3997 x: f64,
3998 y: f64,
3999 mut dispatch_event: F,
4000 ) -> bool {
4001 if self.try_root_element().is_some_and(|el| el.id == node_id) {
4006 let has_changed = self.scroll_viewport_by_has_changed(x, y);
4007 if has_changed {
4008 let layout = *self.root_element().final_layout();
4009 let scale = self.viewport.scale() as f64;
4010 let event = BlitzScrollEvent {
4011 scroll_top: self.viewport_scroll.y,
4012 scroll_left: self.viewport_scroll.x,
4013 scroll_width: layout.size.width.max(layout.content_size.width) as i32,
4014 scroll_height: layout.size.height.max(layout.content_size.height) as i32,
4015 client_width: (self.viewport.window_size.0 as f64 / scale) as i32,
4016 client_height: (self.viewport.window_size.1 as f64 / scale) as i32,
4017 };
4018 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
4019 }
4020 return has_changed;
4021 }
4022
4023 let Some(node) = self.nodes.get_mut(node_id) else {
4024 return false;
4025 };
4026
4027 if node
4031 .element_data()
4032 .is_some_and(|el| el.text_input_data().is_some())
4033 {
4034 let parent = node.parent;
4035 let content_box_width = node.final_layout().content_box_width();
4036 let content_box_height = node.final_layout().content_box_height();
4037 let input = node
4038 .element_data_mut()
4039 .and_then(|el| el.text_input_data_mut())
4040 .unwrap();
4041
4042 let (bubble_x, bubble_y) = if input.is_multiline {
4043 (
4044 x,
4045 input.scroll_by(y as f32, content_box_width, content_box_height) as f64,
4046 )
4047 } else {
4048 (
4049 input.scroll_by(x as f32, content_box_width, content_box_height) as f64,
4050 y,
4051 )
4052 };
4053
4054 let has_changed = bubble_x != x || bubble_y != y;
4055
4056 if bubble_x != 0.0 || bubble_y != 0.0 {
4057 let bubbled = if let Some(parent) = parent {
4058 self.scroll_node_by_inner(parent, bubble_x, bubble_y, dispatch_event)
4059 } else {
4060 self.scroll_viewport_by_has_changed(bubble_x, bubble_y)
4061 };
4062 return bubbled | has_changed;
4063 }
4064
4065 return has_changed;
4066 }
4067
4068 let (can_x_scroll, can_y_scroll) = node
4069 .primary_styles()
4070 .map(|styles| {
4071 (
4072 matches!(styles.clone_overflow_x(), Overflow::Scroll | Overflow::Auto),
4073 matches!(styles.clone_overflow_y(), Overflow::Scroll | Overflow::Auto),
4074 )
4075 })
4076 .unwrap_or((false, false));
4077
4078 let initial = *node.scroll_offset();
4079 let new_x = node.scroll_offset().x - x;
4080 let new_y = node.scroll_offset().y - y;
4081
4082 let mut bubble_x = 0.0;
4083 let mut bubble_y = 0.0;
4084
4085 let scroll_width = node.final_layout().scroll_width() as f64;
4086 let scroll_height = node.final_layout().scroll_height() as f64;
4087
4088 if let Some(mut sub_doc) = node.subdoc_mut().map(|doc| doc.inner_mut()) {
4090 let has_changed = if let Some(hover_node_id) = sub_doc.get_hover_node_id() {
4091 sub_doc.scroll_node_by_has_changed(hover_node_id, x, y, dispatch_event)
4092 } else {
4093 sub_doc.scroll_viewport_by_has_changed(x, y)
4094 };
4095
4096 return has_changed;
4098 }
4099
4100 if !can_x_scroll {
4102 bubble_x = x
4103 } else if new_x < 0.0 {
4104 bubble_x = -new_x;
4105 node.scroll_offset_mut().x = 0.0;
4106 } else if new_x > scroll_width {
4107 bubble_x = scroll_width - new_x;
4108 node.scroll_offset_mut().x = scroll_width;
4109 } else {
4110 node.scroll_offset_mut().x = new_x;
4111 }
4112
4113 if !can_y_scroll {
4114 bubble_y = y
4115 } else if new_y < 0.0 {
4116 bubble_y = -new_y;
4117 node.scroll_offset_mut().y = 0.0;
4118 } else if new_y > scroll_height {
4119 bubble_y = scroll_height - new_y;
4120 node.scroll_offset_mut().y = scroll_height;
4121 } else {
4122 node.scroll_offset_mut().y = new_y;
4123 }
4124
4125 let has_changed = *node.scroll_offset() != initial;
4126
4127 if has_changed {
4128 let layout = *node.final_layout();
4129 let event = BlitzScrollEvent {
4130 scroll_top: node.scroll_offset().y,
4131 scroll_left: node.scroll_offset().x,
4132 scroll_width: layout.scroll_width() as i32,
4133 scroll_height: layout.scroll_height() as i32,
4134 client_width: layout.size.width as i32,
4135 client_height: layout.size.height as i32,
4136 };
4137
4138 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
4139 }
4140
4141 let parent = node.parent;
4142 if has_changed {
4143 self.show_scrollbars(node_id);
4144 }
4145
4146 if bubble_x != 0.0 || bubble_y != 0.0 {
4147 if let Some(parent) = parent {
4148 return self.scroll_node_by_inner(parent, bubble_x, bubble_y, dispatch_event)
4149 | has_changed;
4150 } else {
4151 return self.scroll_viewport_by_has_changed(bubble_x, bubble_y) | has_changed;
4152 }
4153 }
4154
4155 has_changed
4156 }
4157
4158 pub fn scroll_viewport_by(&mut self, x: f64, y: f64) {
4159 self.scroll_viewport_by_has_changed(x, y);
4160 }
4161
4162 pub fn scroll_viewport_by_has_changed(&mut self, x: f64, y: f64) -> bool {
4164 let (content_width, content_height) = match self.try_root_element() {
4169 Some(root) => {
4170 let root_layout = root.final_layout();
4171 (
4172 root_layout.size.width.max(root_layout.content_size.width) as f64,
4173 root_layout.size.height.max(root_layout.content_size.height) as f64,
4174 )
4175 }
4176 None => (0.0, 0.0),
4177 };
4178 let new_scroll = (self.viewport_scroll.x - x, self.viewport_scroll.y - y);
4179 let window_width = self.viewport.window_size.0 as f64 / self.viewport.scale() as f64;
4180 let window_height = self.viewport.window_size.1 as f64 / self.viewport.scale() as f64;
4181
4182 let initial = self.viewport_scroll;
4183 self.viewport_scroll.x =
4184 f64::max(0.0, f64::min(new_scroll.0, content_width - window_width));
4185 self.viewport_scroll.y =
4186 f64::max(0.0, f64::min(new_scroll.1, content_height - window_height));
4187
4188 let has_changed = self.viewport_scroll != initial;
4189 if has_changed {
4190 self.resolve_positioned_scroll();
4195 }
4196 has_changed
4197 }
4198
4199 pub fn scroll_by(
4200 &mut self,
4201 anchor_node_id: Option<NodeId>,
4202 scroll_x: f64,
4203 scroll_y: f64,
4204 dispatch_event: &mut dyn FnMut(DomEvent),
4205 ) -> bool {
4206 if let Some(anchor_node_id) = anchor_node_id {
4207 self.scroll_node_by_has_changed(anchor_node_id, scroll_x, scroll_y, dispatch_event)
4208 } else {
4209 self.scroll_viewport_by_has_changed(scroll_x, scroll_y)
4210 }
4211 }
4212
4213 pub fn viewport_scroll(&self) -> crate::Point<f64> {
4214 self.viewport_scroll
4215 }
4216
4217 pub fn set_viewport_scroll(&mut self, scroll: crate::Point<f64>) {
4218 self.viewport_scroll = scroll;
4219 }
4220
4221 pub fn get_fragment_target(&self, fragment: &str) -> Option<NodeId> {
4226 if let Some(node_id) = self.get_element_by_id(fragment) {
4227 return Some(node_id);
4228 }
4229
4230 self.nodes.iter().find_map(|(id, node)| {
4232 let el = node.element_data()?;
4233 (el.name.local == local_name!("a") && el.attr(local_name!("name")) == Some(fragment))
4234 .then_some(id)
4235 })
4236 }
4237
4238 pub fn nearest_scroll_container(&self, node_id: NodeId) -> Option<NodeId> {
4249 let mut current = Some(node_id);
4250 for _ in 0..64 {
4251 let id = current?;
4252 let node = self.nodes.get(id)?;
4253 if node.style().overflow.x.is_scroll_container()
4254 || node.style().overflow.y.is_scroll_container()
4255 {
4256 return Some(id);
4257 }
4258 current = node.parent;
4259 }
4260 None
4261 }
4262
4263 pub fn scroll_nearest_container_by(&mut self, node_id: NodeId, x: f64, y: f64) -> bool {
4264 self.scroll_nearest_container_by_with_events(node_id, x, y, |_| {})
4265 }
4266
4267 pub fn scroll_nearest_container_by_with_events<F: FnMut(DomEvent)>(
4268 &mut self,
4269 node_id: NodeId,
4270 x: f64,
4271 y: f64,
4272 mut dispatch_event: F,
4273 ) -> bool {
4274 let mut current = Some(node_id);
4275 for _ in 0..64 {
4276 let Some(id) = current else { break };
4277 let Some(node) = self.nodes.get(id) else {
4278 break;
4279 };
4280 let scrolls = node.style().overflow.x.is_scroll_container()
4281 || node.style().overflow.y.is_scroll_container();
4282 if scrolls {
4283 self.scroll_node_by(id, x, y, &mut dispatch_event);
4284 return true;
4285 }
4286 current = node.parent;
4287 }
4288 self.scroll_viewport_by(x, y);
4289 false
4290 }
4291
4292 pub fn scroll_to_node(&mut self, node_id: NodeId) {
4293 self.scroll_to_node_with_events(node_id, |_| {});
4294 }
4295
4296 pub fn scroll_to_node_with_events<F: FnMut(DomEvent)>(
4297 &mut self,
4298 node_id: NodeId,
4299 dispatch_event: F,
4300 ) {
4301 self.scroll_to_node_aligned_with_events(node_id, false, dispatch_event);
4302 }
4303
4304 pub fn scroll_to_node_centered_with_events<F: FnMut(DomEvent)>(
4310 &mut self,
4311 node_id: NodeId,
4312 dispatch_event: F,
4313 ) {
4314 self.scroll_to_node_aligned_with_events(node_id, true, dispatch_event);
4315 }
4316
4317 fn scroll_to_node_aligned_with_events<F: FnMut(DomEvent)>(
4318 &mut self,
4319 node_id: NodeId,
4320 centered: bool,
4321 mut dispatch_event: F,
4322 ) {
4323 let mut scroll_target = Some(node_id);
4330 let node_id = loop {
4331 let Some(candidate) = scroll_target else {
4332 return;
4333 };
4334 let Some(node) = self.nodes.get(candidate) else {
4335 return;
4336 };
4337 if matches!(
4338 node.data,
4339 NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
4340 ) {
4341 break candidate;
4342 }
4343 scroll_target = node.layout_parent.get().or(node.parent);
4344 };
4345
4346 let mut chain = Vec::new();
4358 let mut current = self.nodes.get(node_id).and_then(|node| node.parent);
4359 while let Some(id) = current {
4360 let Some(node) = self.nodes.get(id) else {
4361 break;
4362 };
4363 let scrolls = node.style().overflow.x.is_scroll_container()
4364 || node.style().overflow.y.is_scroll_container();
4365 if scrolls {
4366 chain.push(id);
4367 }
4368 current = node.parent;
4369 }
4370
4371 for container in chain {
4375 let Some(node) = self.nodes.get(node_id) else {
4376 return;
4377 };
4378 let target = node.absolute_position(0.0, 0.0);
4379 let Some(scroller) = self.nodes.get(container) else {
4380 continue;
4381 };
4382 let box_ = scroller.absolute_position(0.0, 0.0);
4383 let layout = scroller.final_layout();
4384 let target_layout = node.final_layout();
4385 let (dx, dy) = if centered {
4389 (
4390 f64::from(
4391 box_.x + layout.size.width / 2.0
4392 - (target.x + target_layout.size.width / 2.0),
4393 ),
4394 f64::from(
4395 box_.y + layout.size.height / 2.0
4396 - (target.y + target_layout.size.height / 2.0),
4397 ),
4398 )
4399 } else {
4400 (f64::from(box_.x - target.x), f64::from(box_.y - target.y))
4401 };
4402 self.scroll_node_by(container, dx, dy, &mut dispatch_event);
4403 }
4404
4405 let Some(node) = self.nodes.get(node_id) else {
4408 return;
4409 };
4410 let target = node.absolute_position(0.0, 0.0);
4411 let current = self.viewport_scroll;
4412 let (desired_x, desired_y) = if centered {
4413 let target_layout = node.final_layout();
4414 let scale = self.viewport.scale();
4415 let viewport_width = self.viewport.window_size.0 as f32 / scale;
4416 let viewport_height = self.viewport.window_size.1 as f32 / scale;
4417 (
4418 f64::from(target.x + target_layout.size.width / 2.0 - viewport_width / 2.0)
4419 .max(0.0),
4420 f64::from(target.y + target_layout.size.height / 2.0 - viewport_height / 2.0)
4421 .max(0.0),
4422 )
4423 } else {
4424 (f64::from(target.x), f64::from(target.y))
4425 };
4426
4427 let dx = current.x - desired_x;
4430 let dy = current.y - desired_y;
4431 if let Some(root) = self.try_root_element().map(|element| element.id) {
4432 self.scroll_node_by(root, dx, dy, dispatch_event);
4433 } else {
4434 self.scroll_viewport_by(dx, dy);
4435 }
4436 }
4437
4438 pub fn scroll_to_fragment(&mut self, fragment: &str) -> bool {
4444 let decoded = percent_encoding::percent_decode_str(fragment)
4446 .decode_utf8_lossy()
4447 .into_owned();
4448
4449 if !decoded.is_empty() {
4450 if let Some(node_id) = self.get_fragment_target(&decoded) {
4451 self.scroll_to_node(node_id);
4452 return true;
4453 }
4454 }
4455
4456 if decoded.is_empty() || decoded.eq_ignore_ascii_case("top") {
4459 let current = self.viewport_scroll;
4460 self.scroll_viewport_by(current.x, current.y);
4461 return true;
4462 }
4463
4464 false
4465 }
4466
4467 pub fn get_svg_bbox(&self, node_id: NodeId) -> Option<BoundingRect> {
4470 #[cfg(feature = "svg")]
4471 {
4472 self.svg_user_bbox(node_id)
4473 }
4474 #[cfg(not(feature = "svg"))]
4475 {
4476 let _ = node_id;
4477 None
4478 }
4479 }
4480
4481 pub fn surface_on_screen(&self, id: NodeId) -> bool {
4483 use style::computed_values::visibility::T as Visibility;
4484 let Some(rect) = self.get_client_bounding_rect(id) else {
4485 return false;
4486 };
4487 let viewport = self.viewport();
4488 let scale = viewport.scale_f64();
4489 let mut x0 = rect.x.max(0.0);
4490 let mut y0 = rect.y.max(0.0);
4491 let mut x1 = (rect.x + rect.width).min(viewport.window_size.0 as f64 / scale);
4492 let mut y1 = (rect.y + rect.height).min(viewport.window_size.1 as f64 / scale);
4493 let mut cursor = Some(id);
4494 while let Some(node_id) = cursor {
4495 let Some(node) = self.get_node(node_id) else {
4496 return false;
4497 };
4498 if matches!(node.style().display, taffy::Display::None) {
4499 return false;
4500 }
4501 if let Some(styles) = node.primary_styles() {
4502 if styles.clone_visibility() != Visibility::Visible
4503 || styles.get_effects().opacity == 0.0
4504 {
4505 return false;
4506 }
4507 if node_id != id
4508 && let Some(clip) = self.get_client_bounding_rect(node_id)
4509 {
4510 if styles.get_box().overflow_x != Overflow::Visible {
4511 x0 = x0.max(clip.x);
4512 x1 = x1.min(clip.x + clip.width);
4513 }
4514 if styles.get_box().overflow_y != Overflow::Visible {
4515 y0 = y0.max(clip.y);
4516 y1 = y1.min(clip.y + clip.height);
4517 }
4518 }
4519 }
4520 cursor = node.parent;
4521 }
4522 x1 > x0 && y1 > y0
4523 }
4524
4525 pub fn get_client_bounding_rect(&self, node_id: NodeId) -> Option<BoundingRect> {
4527 #[cfg(feature = "svg")]
4528 if let Some(root) = self.svg_geometry_root(node_id) {
4529 return self.svg_client_rect(root, node_id);
4530 }
4531
4532 if let Some(rects) = self.inline_fragment_rects(node_id) {
4535 let rects: Vec<_> = rects
4536 .into_iter()
4537 .map(|rect| self.transformed_client_rect(node_id, rect))
4538 .collect();
4539 let x0 = rects.iter().map(|r| r.x).fold(f64::INFINITY, f64::min);
4540 let y0 = rects.iter().map(|r| r.y).fold(f64::INFINITY, f64::min);
4541 let x1 = rects
4542 .iter()
4543 .map(|r| r.x + r.width)
4544 .fold(f64::NEG_INFINITY, f64::max);
4545 let y1 = rects
4546 .iter()
4547 .map(|r| r.y + r.height)
4548 .fold(f64::NEG_INFINITY, f64::max);
4549 return match rects.is_empty() {
4550 true => None,
4551 false => Some(BoundingRect {
4552 x: x0,
4553 y: y0,
4554 width: x1 - x0,
4555 height: y1 - y0,
4556 }),
4557 };
4558 }
4559
4560 let node = self.get_node(node_id)?;
4561 if !matches!(
4562 node.data,
4563 NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
4564 ) {
4565 return None;
4566 }
4567 let pos = node.absolute_position(0.0, 0.0);
4568
4569 Some(self.transformed_client_rect(
4570 node_id,
4571 BoundingRect {
4572 x: pos.x as f64 - self.viewport_scroll.x,
4573 y: pos.y as f64 - self.viewport_scroll.y,
4574 width: node.unrounded_layout().size.width as f64,
4575 height: node.unrounded_layout().size.height as f64,
4576 },
4577 ))
4578 }
4579
4580 pub(crate) fn transformed_client_rect(
4583 &self,
4584 node_id: NodeId,
4585 rect: BoundingRect,
4586 ) -> BoundingRect {
4587 let mut matrix = kurbo::Affine::IDENTITY;
4588 let mut current = self.get_node(node_id);
4589 let scale = self.viewport.scale_f64();
4590 while let Some(node) = current {
4591 if matches!(
4592 node.data,
4593 NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
4594 ) && let Some(transform) = *node.transform()
4595 {
4596 let [a, b, c, d, e, f] = transform.as_coeffs();
4597 let css_transform = kurbo::Affine::new([a, b, c, d, e / scale, f / scale]);
4598 let origin = node.absolute_position(0.0, 0.0);
4599 let translate = kurbo::Affine::translate((
4600 origin.x as f64 - self.viewport_scroll.x,
4601 origin.y as f64 - self.viewport_scroll.y,
4602 ));
4603 matrix = translate * css_transform * translate.inverse() * matrix;
4604 }
4605 current = node
4606 .layout_parent
4607 .get()
4608 .or(node.parent)
4609 .and_then(|id| self.get_node(id));
4610 }
4611 let transformed = matrix.transform_rect_bbox(kurbo::Rect::new(
4612 rect.x,
4613 rect.y,
4614 rect.x + rect.width,
4615 rect.y + rect.height,
4616 ));
4617 BoundingRect {
4618 x: transformed.x0,
4619 y: transformed.y0,
4620 width: transformed.width(),
4621 height: transformed.height(),
4622 }
4623 }
4624
4625 pub fn node_client_rects(&self, node_id: NodeId) -> Vec<BoundingRect> {
4630 #[cfg(feature = "svg")]
4631 if self.svg_geometry_root(node_id).is_some() {
4632 return self.get_client_bounding_rect(node_id).into_iter().collect();
4633 }
4634 match self.inline_fragment_rects(node_id) {
4635 Some(rects) => rects
4636 .into_iter()
4637 .map(|rect| self.transformed_client_rect(node_id, rect))
4638 .collect(),
4639 None => self.get_client_bounding_rect(node_id).into_iter().collect(),
4640 }
4641 }
4642
4643 pub(crate) fn trace_escaped_inline_fragments(&self) {
4657 static TRACE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4658 if !*TRACE.get_or_init(|| std::env::var_os("BLITZ_TRACE_INLINE").is_some()) {
4659 return;
4660 }
4661 let mut reported = 0;
4662 for (id, node) in self.nodes.iter() {
4663 if !node.is_element() {
4664 continue;
4665 }
4666 let Some(rects) = self.inline_fragment_rects(id) else {
4667 continue;
4668 };
4669 let Some(root) = node.inline_root_ancestor() else {
4670 continue;
4671 };
4672 let root_layout = root.final_layout();
4673 let root_pos = root.absolute_position(0.0, 0.0);
4674 let root_right =
4675 root_pos.x as f64 + root_layout.size.width as f64 - self.viewport_scroll.x;
4676 for rect in &rects {
4677 if rect.x + rect.width > root_right + 1.0 {
4678 reported += 1;
4679 if reported <= 12 {
4680 eprintln!(
4681 "escaped-fragment node={id:?} rect=[{:.1},{:.1} {:.1}x{:.1}] \
4682root={:?} root_right={root_right:.1} root_w={:.1} lines={} layout_scale={:.2} vp_scale={:.2} layout_w={:.1}",
4683 rect.x,
4684 rect.y,
4685 rect.width,
4686 rect.height,
4687 root.id,
4688 root_layout.size.width,
4689 root.element_data()
4690 .and_then(|e| e.inline_layout_data.as_ref())
4691 .map(|i| i.layout.len())
4692 .unwrap_or(0),
4693 root.element_data()
4694 .and_then(|e| e.inline_layout_data.as_ref())
4695 .map(|i| i.layout.scale())
4696 .unwrap_or(0.0),
4697 self.viewport.scale(),
4698 root.element_data()
4699 .and_then(|e| e.inline_layout_data.as_ref())
4700 .map(|i| i.layout.width())
4701 .unwrap_or(0.0),
4702 );
4703 }
4704 break;
4705 }
4706 }
4707 }
4708 if reported > 0 {
4709 eprintln!("escaped-fragment total={reported}");
4710 }
4711
4712 let mut narrow = 0;
4717 for (id, node) in self.nodes.iter() {
4718 let Some(inline) = node
4719 .data
4720 .downcast_element()
4721 .and_then(|element| element.inline_layout_data.as_ref())
4722 else {
4723 continue;
4724 };
4725 let box_width = node.final_layout().size.width as f64 * self.viewport.scale() as f64;
4726 let broken_at = inline.layout.width() as f64;
4727 let full = inline.layout.calculate_content_widths().max as f64;
4730 if box_width > 40.0 && broken_at < box_width * 0.6 && full > box_width * 0.9 {
4731 narrow += 1;
4732 if narrow <= 12 {
4733 eprintln!(
4734 "narrow-break node={id:?} broken_at={broken_at:.1} box={box_width:.1} \
4735 max_content={full:.1} lines={} text={:?}",
4736 inline.layout.len(),
4737 inline.text.chars().take(40).collect::<String>(),
4738 );
4739 }
4740 }
4741 }
4742 if narrow > 0 {
4743 eprintln!("narrow-break total={narrow}");
4744 }
4745 }
4746
4747 pub fn inline_fragment_rects(&self, node_id: NodeId) -> Option<Vec<BoundingRect>> {
4748 use parley::PositionedLayoutItem;
4749
4750 let node = self.get_node(node_id)?;
4751
4752 let is_text = node.is_text_node();
4755 if !is_text {
4756 if !node.is_element() || node.flags.is_inline_root() {
4757 return None;
4758 }
4759 let display = node.primary_styles()?.clone_display();
4760 if !(display.outside() == DisplayOutside::Inline
4761 && display.inside() == DisplayInside::Flow)
4762 {
4763 return None;
4764 }
4765 }
4766
4767 let inline_root = node.inline_root_ancestor()?;
4768 let inline_layout = inline_root.element_data()?.inline_layout_data.as_ref()?;
4769 let layout = &inline_layout.layout;
4770 let scale = layout.scale() as f64;
4771
4772 let is_in_target = |mut id: NodeId| -> bool {
4775 loop {
4776 if id == node_id {
4777 return true;
4778 }
4779 if id == inline_root.id {
4780 return false;
4781 }
4782 match self.get_node(id).and_then(|n| n.parent) {
4783 Some(parent) => id = parent,
4784 None => return false,
4785 }
4786 }
4787 };
4788
4789 let root_layout = inline_root.final_layout();
4791 let root_pos = inline_root.absolute_position(0.0, 0.0);
4792 let origin_x = root_pos.x as f64
4793 + (root_layout.padding.left + root_layout.border.left) as f64
4794 - self.viewport_scroll.x;
4795 let origin_y = root_pos.y as f64
4796 + (root_layout.padding.top + root_layout.border.top) as f64
4797 - self.viewport_scroll.y;
4798
4799 let mut rects: Vec<BoundingRect> = Vec::new();
4800 for line in layout.lines() {
4801 let line_metrics = line.metrics();
4802 let mut line_rect: Option<(f64, f64, f64, f64)> = None;
4804 let mut add = |x0: f64, y0: f64, x1: f64, y1: f64| {
4805 line_rect = Some(match line_rect {
4806 Some((lx0, ly0, lx1, ly1)) => {
4807 (lx0.min(x0), ly0.min(y0), lx1.max(x1), ly1.max(y1))
4808 }
4809 None => (x0, y0, x1, y1),
4810 });
4811 };
4812
4813 for item in line.items() {
4814 match item {
4815 PositionedLayoutItem::GlyphRun(glyph_run) => {
4816 let brush = glyph_run.style().brush;
4817 let matches = if is_text {
4818 brush.text_node == Some(node_id)
4819 } else {
4820 is_in_target(brush.id)
4821 };
4822 if !matches {
4823 continue;
4824 }
4825 let x0 = glyph_run.offset() as f64;
4826 let x1 = x0 + glyph_run.advance() as f64;
4827 let y0 = line_metrics.block_min_coord as f64;
4833 let y1 = line_metrics.block_max_coord as f64;
4834 add(x0, y0, x1, y1);
4835 }
4836 PositionedLayoutItem::InlineBox(inline_box) => {
4837 if is_text || !is_in_target(NodeId::from_u64(inline_box.id)) {
4838 continue;
4839 }
4840 let x0 = inline_box.x as f64;
4841 let y0 = inline_box.y as f64;
4842 add(
4843 x0,
4844 y0,
4845 x0 + inline_box.width as f64,
4846 y0 + inline_box.height as f64,
4847 );
4848 }
4849 }
4850 }
4851
4852 if let Some((x0, y0, x1, y1)) = line_rect {
4853 rects.push(BoundingRect {
4854 x: origin_x + x0 / scale,
4855 y: origin_y + y0 / scale,
4856 width: (x1 - x0) / scale,
4857 height: (y1 - y0) / scale,
4858 });
4859 }
4860 }
4861
4862 Some(rects)
4863 }
4864
4865 pub fn find_title_node(&self) -> Option<&Node> {
4866 TreeTraverser::new(self)
4867 .find(|node_id| {
4868 let node = &self.nodes[*node_id];
4869 let Some(element) = node.element_data() else {
4870 return false;
4871 };
4872 if element.name.ns != ns!(html) || element.name.local != local_name!("title") {
4873 return false;
4874 }
4875 node.parent
4876 .and_then(|parent_id| self.nodes.get(parent_id))
4877 .and_then(Node::element_data)
4878 .is_some_and(|parent| {
4879 parent.name.ns == ns!(html) && parent.name.local == local_name!("head")
4880 })
4881 })
4882 .map(|node_id| &self.nodes[node_id])
4883 }
4884
4885 pub fn with_text_input(
4886 &mut self,
4887 node_id: NodeId,
4888 cb: impl FnOnce(PlainEditorDriver<TextBrush>),
4889 ) {
4890 let Some(node) = self.nodes.get_mut(node_id) else {
4891 return;
4892 };
4893
4894 if let Some(text_input) = node
4895 .element_data_mut()
4896 .and_then(|el| el.text_input_data_mut())
4897 {
4898 let mut font_ctx = self.font_ctx.lock().unwrap();
4899 let layout_ctx = &mut self.layout_ctx;
4900 let driver = text_input.editor.driver(&mut font_ctx, layout_ctx);
4901 cb(driver)
4902 }
4903 }
4904
4905 pub(crate) fn clamp_text_input_scroll(&mut self, node_id: NodeId) {
4908 let Some(node) = self.nodes.get_mut(node_id) else {
4909 return;
4910 };
4911
4912 let content_box_width = node.final_layout().content_box_width();
4913 let content_box_height = node.final_layout().content_box_height();
4914
4915 if let Some(text_input) = node
4916 .element_data_mut()
4917 .and_then(|el| el.text_input_data_mut())
4918 {
4919 text_input.clamp_scroll_offset(content_box_width, content_box_height);
4920 }
4921 }
4922
4923 pub(crate) fn compute_has_canvas(&self) -> bool {
4924 TreeTraverser::new(self).any(|node_id| {
4925 let node = &self.nodes[node_id];
4926 let Some(element) = node.element_data() else {
4927 return false;
4928 };
4929 if element.name.local == local_name!("canvas") && element.has_attr(local_name!("src")) {
4930 return true;
4931 }
4932
4933 false
4934 })
4935 }
4936
4937 pub fn find_text_position(&self, x: f32, y: f32) -> Option<(NodeId, usize)> {
4943 let hit = self.hit(x, y)?;
4944 let hit_node = self.get_node(hit.node_id)?;
4945 let inline_root = hit_node.inline_root_ancestor()?;
4946 let byte_offset = inline_root.text_offset_at_point(hit.x, hit.y)?;
4947 Some((inline_root.id, byte_offset))
4948 }
4949
4950 pub fn find_text_range(
4956 &self,
4957 x: f32,
4958 y: f32,
4959 granularity: TextGranularity,
4960 ) -> Option<(NodeId, usize, usize)> {
4961 let hit = self.hit(x, y)?;
4962 let hit_node = self.get_node(hit.node_id)?;
4963 let inline_root = hit_node.inline_root_ancestor()?;
4964 let range = inline_root.text_range_at_point(hit.x, hit.y, granularity)?;
4965 Some((inline_root.id, range.start, range.end))
4966 }
4967
4968 pub fn set_text_selection(
4970 &mut self,
4971 anchor_node: NodeId,
4972 anchor_offset: usize,
4973 focus_node: NodeId,
4974 focus_offset: usize,
4975 ) {
4976 self.dom_selection = None;
4977 self.text_selection =
4978 TextSelection::new(anchor_node, anchor_offset, focus_node, focus_offset);
4979
4980 if let (Some(parent), Some(idx)) = self.anonymous_block_location(anchor_node) {
4982 self.text_selection
4983 .anchor
4984 .set_anonymous(parent, idx, anchor_offset);
4985 }
4986 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
4987 self.text_selection
4988 .focus
4989 .set_anonymous(parent, idx, focus_offset);
4990 }
4991 }
4992
4993 fn anonymous_block_location(&self, node_id: NodeId) -> (Option<NodeId>, Option<usize>) {
4996 let Some(node) = self.get_node(node_id) else {
4997 return (None, None);
4998 };
4999
5000 if !node.is_anonymous() {
5001 return (None, None);
5002 }
5003
5004 let Some(parent_id) = node.parent else {
5005 return (None, None);
5006 };
5007
5008 let Some(parent) = self.get_node(parent_id) else {
5009 return (Some(parent_id), None);
5010 };
5011
5012 let layout_children = parent.layout_children.borrow();
5013 let Some(children) = layout_children.as_ref() else {
5014 return (Some(parent_id), None);
5015 };
5016
5017 let mut anon_index = 0;
5019 for &child_id in children.iter() {
5020 if child_id == node_id {
5021 return (Some(parent_id), Some(anon_index));
5022 }
5023 if self.get_node(child_id).is_some_and(|n| n.is_anonymous()) {
5024 anon_index += 1;
5025 }
5026 }
5027
5028 (Some(parent_id), None)
5029 }
5030
5031 pub fn clear_text_selection(&mut self) {
5033 self.dom_selection = None;
5034 self.text_selection.clear();
5035 }
5036
5037 pub fn update_selection_focus(&mut self, focus_node: NodeId, focus_offset: usize) {
5039 self.dom_selection = None;
5040 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
5042 self.text_selection
5043 .focus
5044 .set_anonymous(parent, idx, focus_offset);
5045 } else {
5046 self.text_selection.set_focus(focus_node, focus_offset);
5047 }
5048 }
5049
5050 pub fn extend_text_selection_to_point(&mut self, x: f32, y: f32) -> bool {
5053 if !self.text_selection.anchor.is_some() {
5054 return false;
5055 }
5056
5057 if let Some((node, offset)) = self.find_text_position(x, y) {
5058 self.update_selection_focus(node, offset);
5059 self.shell_provider.request_redraw();
5060 true
5061 } else {
5062 false
5063 }
5064 }
5065
5066 fn find_anonymous_block_by_index(
5068 &self,
5069 parent_id: NodeId,
5070 target_index: usize,
5071 ) -> Option<NodeId> {
5072 let parent = self.get_node(parent_id)?;
5073 let layout_children = parent.layout_children.borrow();
5074 let children = layout_children.as_ref()?;
5075
5076 children
5077 .iter()
5078 .filter(|&&child_id| self.get_node(child_id).is_some_and(|n| n.is_anonymous()))
5079 .nth(target_index)
5080 .copied()
5081 }
5082
5083 pub fn has_text_selection(&self) -> bool {
5085 self.dom_selection.as_ref().map_or_else(
5086 || self.text_selection.is_active(),
5087 |range| !range.bounds().collapsed(),
5088 )
5089 }
5090
5091 pub fn get_selected_text(&self) -> Option<String> {
5093 if let Some(range) = &self.dom_selection {
5094 let text = self.range_string(range.bounds());
5095 return (!text.is_empty()).then_some(text);
5096 }
5097 let ranges = self.get_text_selection_ranges();
5098 if ranges.is_empty() {
5099 return None;
5100 }
5101
5102 let mut result = String::new();
5103 for (node_id, start, end) in &ranges {
5104 let node = self.get_node(*node_id)?;
5105 let element_data = node.element_data()?;
5106 let inline_layout = element_data.inline_layout_data.as_ref()?;
5107
5108 if *end > inline_layout.text.len() {
5109 continue;
5110 }
5111
5112 if !result.is_empty() {
5113 result.push(' ');
5114 }
5115 result.push_str(&inline_layout.text[*start..*end]);
5116 }
5117
5118 if result.is_empty() {
5119 None
5120 } else {
5121 Some(result)
5122 }
5123 }
5124
5125 pub fn get_selected_html(&self) -> Option<String> {
5138 let ranges = self.get_text_selection_ranges();
5139 if ranges.is_empty() {
5140 return None;
5141 }
5142
5143 let mut result = String::new();
5144 for (node_id, start, end) in &ranges {
5145 let Some(node) = self.get_node(*node_id) else {
5146 continue;
5147 };
5148 let Some(element_data) = node.element_data() else {
5149 continue;
5150 };
5151 let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
5152 continue;
5153 };
5154 if *end > inline_layout.text.len() {
5155 continue;
5156 }
5157
5158 if !result.is_empty() {
5159 result.push(' ');
5160 }
5161
5162 let tag = element_data.name.local.as_ref();
5163 let fragment = escape_html(&inline_layout.text[*start..*end]);
5164 if matches!(
5169 tag,
5170 "b" | "strong" | "i" | "em" | "code" | "kbd" | "mark" | "s" | "u" | "sub" | "sup"
5171 ) {
5172 result.push_str(&format!("<{tag}>{fragment}</{tag}>"));
5173 } else {
5174 result.push_str(&fragment);
5175 }
5176 }
5177
5178 if result.is_empty() {
5179 None
5180 } else {
5181 Some(result)
5182 }
5183 }
5184
5185 pub fn get_text_selection_ranges(&self) -> Vec<(NodeId, usize, usize)> {
5188 if let Some(range) = &self.dom_selection {
5189 return self.range_layout_ranges(range.bounds());
5190 }
5191 let lookup = |parent_id, idx| self.find_anonymous_block_by_index(parent_id, idx);
5192
5193 let anchor_node = match self.text_selection.anchor.resolve_node_id(lookup) {
5194 Some(id) => id,
5195 None => return Vec::new(),
5196 };
5197 let focus_node = match self.text_selection.focus.resolve_node_id(lookup) {
5198 Some(id) => id,
5199 None => return Vec::new(),
5200 };
5201
5202 let node_is_in_doc = |node_id: NodeId| {
5205 self.nodes
5206 .get(node_id)
5207 .is_some_and(|node| node.flags.is_in_document())
5208 };
5209 if !node_is_in_doc(anchor_node) || !node_is_in_doc(focus_node) {
5210 return Vec::new();
5211 }
5212
5213 if anchor_node == focus_node {
5215 let start = self
5216 .text_selection
5217 .anchor
5218 .offset
5219 .min(self.text_selection.focus.offset);
5220 let end = self
5221 .text_selection
5222 .anchor
5223 .offset
5224 .max(self.text_selection.focus.offset);
5225
5226 if start == end {
5227 return Vec::new();
5228 }
5229 return vec![(anchor_node, start, end)];
5230 }
5231
5232 let inline_roots = self.collect_inline_roots_in_range(anchor_node, focus_node);
5234 if inline_roots.is_empty() {
5235 return Vec::new();
5236 }
5237
5238 let first_in_roots = inline_roots[0];
5241
5242 let (first_node, first_offset, last_node, last_offset) =
5243 if first_in_roots == anchor_node || (first_in_roots != focus_node) {
5244 (
5246 anchor_node,
5247 self.text_selection.anchor.offset,
5248 focus_node,
5249 self.text_selection.focus.offset,
5250 )
5251 } else {
5252 (
5254 focus_node,
5255 self.text_selection.focus.offset,
5256 anchor_node,
5257 self.text_selection.anchor.offset,
5258 )
5259 };
5260
5261 let mut ranges = Vec::with_capacity(inline_roots.len());
5262
5263 for &node_id in &inline_roots {
5264 let Some(node) = self.get_node(node_id) else {
5265 continue;
5266 };
5267 let Some(element_data) = node.element_data() else {
5268 continue;
5269 };
5270 let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
5271 continue;
5272 };
5273
5274 let text_len = inline_layout.text.len();
5275
5276 if node_id == first_node && node_id == last_node {
5277 let start = first_offset.min(last_offset);
5278 let end = first_offset.max(last_offset);
5279 if start < end && end <= text_len {
5280 ranges.push((node_id, start, end));
5281 }
5282 } else if node_id == first_node {
5283 if first_offset < text_len {
5284 ranges.push((node_id, first_offset, text_len));
5285 }
5286 } else if node_id == last_node {
5287 if last_offset > 0 && last_offset <= text_len {
5288 ranges.push((node_id, 0, last_offset));
5289 }
5290 } else if text_len > 0 {
5291 ranges.push((node_id, 0, text_len));
5292 }
5293 }
5294
5295 ranges
5296 }
5297}
5298
5299#[derive(Debug, Clone, Copy, PartialEq)]
5300pub struct BoundingRect {
5301 pub x: f64,
5302 pub y: f64,
5303 pub width: f64,
5304 pub height: f64,
5305}
5306
5307impl BaseDocument {
5310 pub(crate) fn record_details_open(
5313 &mut self,
5314 node_id: NodeId,
5315 name: &markup5ever::QualName,
5316 present: bool,
5317 ) {
5318 if name.local != local_name!("open") || !name.ns.is_empty() {
5319 return;
5320 }
5321 let Some(element) = self.nodes[node_id].element_data() else {
5322 return;
5323 };
5324 if element.name.local != local_name!("details") || element.name.ns != ns!(html) {
5325 return;
5326 }
5327 let was_open = element.attr(local_name!("open")).is_some();
5328 if was_open != present && !self.details_toggles.iter().any(|(id, _)| *id == node_id) {
5329 self.details_toggles.push((node_id, was_open));
5330 }
5331 }
5332
5333 pub(crate) fn template_contents_owner(&mut self) -> NodeId {
5335 if let Some(owner) = self.template_contents_owner {
5336 return owner;
5337 }
5338 let mut data = crate::node::DocumentData::default();
5339 data.content_type = "text/html";
5340 let owner = self.create_node(NodeData::Document(Box::new(data)));
5341 self.template_contents_owner = Some(owner);
5342 owner
5343 }
5344
5345 pub fn style_revision(&self) -> u64 {
5347 self.style_revision
5348 }
5349
5350 pub fn layout_revision(&self) -> u64 {
5352 self.layout_revision
5353 }
5354
5355 pub(crate) fn note_layout_resolved(&mut self) {
5356 self.layout_revision = self.layout_revision.wrapping_add(1);
5357 }
5358
5359 pub fn viewport_generation(&self) -> u64 {
5361 self.viewport_generation
5362 }
5363
5364 pub fn take_details_toggles(&mut self) -> Vec<(NodeId, bool)> {
5366 std::mem::take(&mut self.details_toggles)
5367 }
5368
5369 pub(crate) fn record_inline_handler_attribute(
5372 &mut self,
5373 node_id: NodeId,
5374 name: &crate::QualName,
5375 value: Option<&str>,
5376 ) {
5377 if name.ns != markup5ever::ns!() || !name.local.as_ref().starts_with("on") {
5378 return;
5379 }
5380 let Some(element) = self
5381 .nodes
5382 .get_mut(node_id)
5383 .and_then(|node| node.element_data_mut())
5384 else {
5385 return;
5386 };
5387 let value = value.map(crate::node::AttrAtom::from);
5388 if let Some(value) = &value {
5389 let attributes = element
5390 .inline_event_attributes
5391 .get_or_insert_with(|| Box::new(Vec::new()));
5392 if let Some(attribute) = attributes
5393 .iter_mut()
5394 .find(|attribute| attribute.name.local == name.local)
5395 {
5396 attribute.name = name.clone();
5397 attribute.value = value.clone();
5398 } else {
5399 attributes.push(crate::node::Attribute {
5400 name: name.clone(),
5401 value: value.clone(),
5402 });
5403 }
5404 } else {
5405 let Some(attributes) = element.inline_event_attributes.as_mut() else {
5406 return;
5407 };
5408 let Some(index) = attributes
5409 .iter()
5410 .position(|attribute| attribute.name.local == name.local)
5411 else {
5412 return;
5413 };
5414 attributes.remove(index);
5415 if attributes.is_empty() {
5416 element.inline_event_attributes = None;
5417 }
5418 }
5419 self.inline_handler_changes
5420 .push((node_id, name.clone(), value));
5421 }
5422
5423 pub fn take_inline_handler_changes(
5425 &mut self,
5426 ) -> Vec<(NodeId, crate::QualName, Option<crate::node::AttrAtom>)> {
5427 std::mem::take(&mut self.inline_handler_changes)
5428 }
5429
5430 pub fn set_recording_mutations(&mut self, recording: bool) {
5433 match (recording, self.mutation_log.is_some()) {
5434 (true, false) => self.mutation_log = Some(Vec::new()),
5435 (false, true) => self.mutation_log = None,
5436 _ => {}
5437 }
5438 }
5439
5440 pub fn is_recording_mutations(&self) -> bool {
5442 self.mutation_log.is_some()
5443 }
5444 pub fn has_pending_mutations(&self) -> bool {
5446 self.mutation_log
5447 .as_ref()
5448 .is_some_and(|records| !records.is_empty())
5449 }
5450
5451 pub fn take_mutations(&mut self) -> Vec<crate::DomMutation> {
5453 self.mutation_log
5454 .as_mut()
5455 .map(std::mem::take)
5456 .unwrap_or_default()
5457 }
5458
5459 pub(crate) fn record_mutation(&mut self, mutation: crate::DomMutation) {
5460 if let Some(log) = self.mutation_log.as_mut() {
5461 if log.len() < crate::mutation_record::MUTATION_LOG_CAP {
5462 log.push(mutation);
5463 }
5464 }
5465 }
5466
5467 pub(crate) fn sibling_context(
5470 &self,
5471 node_id: NodeId,
5472 ) -> Option<(NodeId, Option<NodeId>, Option<NodeId>)> {
5473 let parent_id = self.nodes.get(node_id)?.parent?;
5474 let siblings = &self.nodes.get(parent_id)?.children;
5475 let index = siblings.iter().position(|id| *id == node_id)?;
5476 let previous = index.checked_sub(1).map(|i| siblings[i]);
5477 let next = siblings.get(index + 1).copied();
5478 Some((parent_id, previous, next))
5479 }
5480}
5481
5482impl AsRef<BaseDocument> for BaseDocument {
5483 fn as_ref(&self) -> &BaseDocument {
5484 self
5485 }
5486}
5487
5488impl AsMut<BaseDocument> for BaseDocument {
5489 fn as_mut(&mut self) -> &mut BaseDocument {
5490 self
5491 }
5492}
5493
5494#[cfg(test)]
5495mod hover_state_tests {
5496 use super::*;
5497 use crate::{Attribute, qual_name};
5498 use blitz_traits::shell::ColorScheme;
5499
5500 fn make_doc() -> (BaseDocument, NodeId) {
5507 let mut doc = BaseDocument::new(DocumentConfig {
5508 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
5509 ..Default::default()
5510 });
5511 let root_id = doc.root_node().id;
5512 let style = |value: &str| Attribute {
5513 name: qual_name!("style"),
5514 value: value.into(),
5515 };
5516
5517 let mut mutator = doc.mutate();
5518 let html = mutator.create_element(qual_name!("html"), vec![]);
5519 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
5520 let container = mutator.create_element(qual_name!("div"), vec![style("width:300px")]);
5521 let text = mutator.create_text_node("some text");
5522 let block = mutator.create_element(qual_name!("div"), vec![style("height:50px")]);
5523 mutator.append_children(container, &[text, block]);
5524 mutator.append_children(body, &[container]);
5525 mutator.append_children(html, &[body]);
5526 mutator.append_children(root_id, &[html]);
5527 drop(mutator);
5528
5529 doc.resolve(0.0);
5530 (doc, container)
5531 }
5532
5533 fn text_has_size(doc: &BaseDocument, container: NodeId) -> bool {
5537 doc.nodes[container].final_layout().size.height > 50.0
5538 }
5539
5540 #[test]
5546 fn hovering_text_in_anonymous_block_reports_text_cursor() {
5547 let (mut doc, container) = make_doc();
5548 if !text_has_size(&doc, container) {
5549 eprintln!("skipping: no usable font (text measures 0x0)");
5550 return;
5551 }
5552
5553 doc.set_hover_to(5.0, 8.0);
5554 assert!(doc.hover_node_is_text, "expected a text hit");
5555 let hit_id = doc.hover_hit_node_id.expect("expected a hit node");
5556 assert!(
5557 doc.nodes[hit_id].is_anonymous(),
5558 "expected the hit node to be the anonymous inline root"
5559 );
5560 assert_eq!(
5561 doc.get_hover_node_id(),
5562 Some(container),
5563 "expected the stored hover target to be the containing element"
5564 );
5565 assert_eq!(doc.get_cursor(), Some(CursorIcon::Text));
5566 }
5567
5568 #[test]
5569 fn semantic_hover_keeps_the_resolved_node_instead_of_hit_testing_again() {
5570 let (mut doc, container) = make_doc();
5571
5572 doc.set_hover_to_node(container, 350.0, 250.0);
5576
5577 assert_eq!(doc.get_hover_node_id(), Some(container));
5578 assert_eq!(doc.hover_hit_node_id, Some(container));
5579
5580 doc.resolve(0.0);
5581 assert_eq!(
5582 doc.get_hover_node_id(),
5583 Some(container),
5584 "a resolve must not turn semantic identity back into a coordinate hit"
5585 );
5586 }
5587
5588 #[test]
5591 fn hovering_anonymous_block_whitespace_reports_default_cursor() {
5592 let (mut doc, container) = make_doc();
5593 if !text_has_size(&doc, container) {
5594 eprintln!("skipping: no usable font (text measures 0x0)");
5595 return;
5596 }
5597
5598 doc.set_hover_to(250.0, 8.0);
5599 assert!(!doc.hover_node_is_text);
5600 assert_eq!(doc.get_hover_node_id(), Some(container));
5601 assert_eq!(doc.get_cursor(), Some(CursorIcon::Default));
5602 }
5603}
5604
5605#[cfg(test)]
5606mod control_scroll_tests {
5607 use super::*;
5608 use crate::{Attribute, qual_name};
5609 use blitz_traits::shell::ColorScheme;
5610
5611 #[test]
5612 fn controlled_scroll_dispatches_the_dom_scroll_event() {
5613 let mut doc = BaseDocument::new(DocumentConfig {
5614 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
5615 ..Default::default()
5616 });
5617 let root_id = doc.root_node().id;
5618 let style = |value: &str| Attribute {
5619 name: qual_name!("style"),
5620 value: value.into(),
5621 };
5622
5623 let mut mutator = doc.mutate();
5624 let html = mutator.create_element(qual_name!("html"), vec![]);
5625 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
5626 let scroller = mutator.create_element(
5627 qual_name!("div"),
5628 vec![style("width:200px;height:100px;overflow-y:scroll")],
5629 );
5630 let spacer = mutator.create_element(qual_name!("div"), vec![style("height:400px")]);
5631 let target = mutator.create_element(qual_name!("button"), vec![style("height:40px")]);
5632 let target_text = mutator.create_text_node("Reveal me");
5633 mutator.append_children(target, &[target_text]);
5634 mutator.append_children(scroller, &[spacer, target]);
5635 mutator.append_children(body, &[scroller]);
5636 mutator.append_children(html, &[body]);
5637 mutator.append_children(root_id, &[html]);
5638 drop(mutator);
5639 doc.resolve(0.0);
5640
5641 doc.nodes[html].final_layout_mut().size.height = 300.0;
5645 doc.nodes[html].final_layout_mut().content_size.height = 600.0;
5646 doc.nodes[target].final_layout_mut().location.y = 400.0;
5647
5648 let mut events = Vec::new();
5649 doc.scroll_to_node_centered_with_events(target_text, |event| events.push(event));
5653
5654 assert!(
5655 (doc.viewport_scroll.y - 270.0).abs() < 0.1,
5656 "the 40px target should be centred in the 300px viewport"
5657 );
5658 assert!(
5659 events
5660 .iter()
5661 .any(|event| { event.target == html && event.name() == "scroll" })
5662 );
5663 }
5664}
5665
5666#[cfg(test)]
5667mod font_face_override_tests {
5668 use super::*;
5669 use crate::net::{FontFaceOverrides, Resource, ResourceLoadResponse};
5670
5671 #[test]
5687 fn font_face_overrides_alias_family_name() {
5688 const ALIAS: &str = "AliasedFamily";
5689
5690 let mut document = BaseDocument::new(DocumentConfig::default());
5691
5692 {
5694 let mut ctx = document.font_ctx.lock().unwrap();
5695 assert!(
5696 ctx.collection.family_id(ALIAS).is_none(),
5697 "alias must not exist before registration",
5698 );
5699 }
5700
5701 let response = ResourceLoadResponse {
5706 request_id: 0,
5707 node_id: None,
5708 resolved_url: Some(String::from("test://aliased-family")),
5709 result: Ok(Resource::Font(
5710 blitz_traits::net::Bytes::from_static(crate::BULLET_FONT),
5711 FontFaceOverrides {
5712 family_name: Some(String::from(ALIAS)),
5713 weight: Some(800.0),
5714 style: Some(parley::fontique::FontStyle::Italic),
5715 },
5716 )),
5717 };
5718 document.load_resource(response);
5719
5720 let mut ctx = document.font_ctx.lock().unwrap();
5723 let family_id = ctx
5724 .collection
5725 .family_id(ALIAS)
5726 .expect("CSS-declared family name should be registered as a family alias");
5727 let resolved_name = ctx
5728 .collection
5729 .family_name(family_id)
5730 .expect("family id should resolve back to a name");
5731 assert_eq!(
5732 resolved_name, ALIAS,
5733 "registered family should report the CSS-declared name, \
5734 not the font file's internal `name` table entry",
5735 );
5736 }
5737}
5738
5739#[cfg(test)]
5740mod clipboard_html_tests {
5741 use super::escape_html;
5742
5743 #[test]
5746 fn markup_characters_in_a_selection_are_escaped() {
5747 assert_eq!(
5748 escape_html("a < b && c > d"),
5749 "a < b && c > d"
5750 );
5751 }
5752
5753 #[test]
5756 fn an_ampersand_is_not_double_escaped() {
5757 assert_eq!(escape_html("<"), "&lt;");
5758 }
5759
5760 #[test]
5763 fn prose_is_untouched() {
5764 assert_eq!(escape_html("it's \"fine\""), "it's "fine"");
5765 }
5766}