1use crate::NodeTree;
2use crate::events::{DragMode, ScrollAnimationState, handle_dom_event};
3use crate::font_metrics::BlitzFontMetricsProvider;
4use crate::layout::construct::ConstructionTask;
5use crate::layout::damage::ALL_DAMAGE;
6use crate::mutator::ViewportMut;
7use crate::net::{
8 Resource, ResourceHandler, ResourceLoadResponse, StylesheetHandler, StylesheetLoader,
9};
10use crate::node::{
11 ImageData, NodeFlags, RasterImageData, SpecialElementData, Status, TextBrush, TextGranularity,
12};
13use crate::selection::TextSelection;
14use crate::stylo_to_cursor_icon::stylo_to_cursor_icon;
15use crate::traversal::TreeTraverser;
16use crate::url::DocumentUrl;
17use crate::util::ImageType;
18use crate::{
19 DEFAULT_CSS, DocumentConfig, DocumentMutator, DummyHtmlParserProvider, ElementData,
20 EventDriver, HtmlParserProvider, Node, NodeData, NoopEventHandler, StyleThreading,
21 TextNodeData,
22};
23use blitz_traits::devtools::DevtoolSettings;
24use blitz_traits::events::{BlitzScrollEvent, DomEvent, DomEventData, HitResult, UiEvent};
25use blitz_traits::navigation::{DummyNavigationProvider, NavigationProvider};
26use blitz_traits::net::{AbortSignal, DummyNetProvider, NetProvider, Request};
27use blitz_traits::node_id::NodeId;
28use blitz_traits::shell::{ColorScheme, DummyShellProvider, ShellProvider, Viewport};
29use cursor_icon::CursorIcon;
30use linebender_resource_handle::Blob;
31use markup5ever::{local_name, ns};
32use parley::{FontContext, PlainEditorDriver};
33use selectors::{Element, matching::QuirksMode};
34use smallvec::SmallVec;
35use std::any::Any;
36use std::cell::RefCell;
37use std::collections::{BTreeMap, Bound, HashMap, HashSet};
38use std::ops::{Deref, DerefMut};
39use std::rc::Rc;
40use std::str::FromStr;
41use std::sync::atomic::{AtomicUsize, Ordering};
42use std::sync::mpsc::{Receiver, Sender, channel};
43use std::sync::{Arc, Mutex, MutexGuard, OnceLock, RwLockReadGuard, RwLockWriteGuard};
44use std::task::{Context as TaskContext, Waker};
45use style::Atom;
46use style::animation::{AnimationState, DocumentAnimationSet};
47use style::attr::{AttrIdentifier, AttrValue};
48use style::data::{ElementData as StyloElementData, ElementStyles};
49use style::media_queries::MediaType;
50use style::properties::ComputedValues;
51use style::properties::style_structs::Font;
52use style::queries::values::PrefersColorScheme;
53use style::selector_parser::ServoElementSnapshot;
54use style::servo::media_features::PointerCapabilities;
55use style::servo_arc::Arc as ServoArc;
56use style::values::GenericAtomIdent;
57use style::values::computed::ui::CursorKind;
58use style::values::computed::{Overflow, UserSelect};
59use style::values::specified::box_::{DisplayInside, DisplayOutside};
60use style::{
61 device::Device,
62 dom::{TDocument, TNode},
63 media_queries::MediaList,
64 selector_parser::SnapshotMap,
65 shared_lock::{SharedRwLock, StylesheetGuards},
66 stylesheets::{AllowImportRules, DocumentStyleSheet, Origin, Stylesheet},
67 stylist::Stylist,
68};
69use thin_vec::ThinVec;
70use url::Url;
71use web_time::Instant;
72
73#[cfg(feature = "parallel-construct")]
74use thread_local::ThreadLocal;
75
76pub enum DocGuard<'a> {
77 Ref(&'a BaseDocument),
78 RefCell(std::cell::Ref<'a, BaseDocument>),
79 RwLock(RwLockReadGuard<'a, BaseDocument>),
80 Mutex(MutexGuard<'a, BaseDocument>),
81}
82
83impl Deref for DocGuard<'_> {
84 type Target = BaseDocument;
85 #[inline(always)]
86 fn deref(&self) -> &Self::Target {
87 match self {
88 Self::Ref(base_document) => base_document,
89 Self::RefCell(refcell_guard) => refcell_guard,
90 Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
91 Self::Mutex(mutex_guard) => mutex_guard,
92 }
93 }
94}
95
96pub enum DocGuardMut<'a> {
97 Ref(&'a mut BaseDocument),
98 RefCell(std::cell::RefMut<'a, BaseDocument>),
99 RwLock(RwLockWriteGuard<'a, BaseDocument>),
100 Mutex(MutexGuard<'a, BaseDocument>),
101}
102
103impl Deref for DocGuardMut<'_> {
104 type Target = BaseDocument;
105 #[inline(always)]
106 fn deref(&self) -> &Self::Target {
107 match self {
108 Self::Ref(base_document) => base_document,
109 Self::RefCell(refcell_guard) => refcell_guard,
110 Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
111 Self::Mutex(mutex_guard) => mutex_guard,
112 }
113 }
114}
115
116impl DerefMut for DocGuardMut<'_> {
117 #[inline(always)]
118 fn deref_mut(&mut self) -> &mut Self::Target {
119 match self {
120 Self::Ref(base_document) => base_document,
121 Self::RefCell(refcell_guard) => &mut *refcell_guard,
122 Self::RwLock(rw_lock_read_guard) => &mut *rw_lock_read_guard,
123 Self::Mutex(mutex_guard) => &mut *mutex_guard,
124 }
125 }
126}
127
128pub trait Document: Any + 'static {
131 fn inner(&self) -> DocGuard<'_>;
132 fn inner_mut(&mut self) -> DocGuardMut<'_>;
133
134 fn handle_ui_event(&mut self, event: UiEvent) {
136 let mut doc = self.inner_mut();
137 let mut driver = EventDriver::new(&mut *doc, NoopEventHandler);
138 driver.handle_ui_event(event);
139 }
140
141 fn poll(&mut self, task_context: Option<TaskContext>) -> bool {
143 let _ = task_context;
145 false
146 }
147
148 fn id(&self) -> usize {
150 self.inner().id
151 }
152}
153
154#[derive(Debug, Clone, PartialEq)]
157pub struct PreClickActivation {
158 previous: Vec<(NodeId, bool)>,
160}
161
162pub struct PlainDocument(pub BaseDocument);
163impl Document for PlainDocument {
164 fn inner(&self) -> DocGuard<'_> {
165 DocGuard::Ref(&self.0)
166 }
167 fn inner_mut(&mut self) -> DocGuardMut<'_> {
168 DocGuardMut::Ref(&mut self.0)
169 }
170}
171
172impl Document for BaseDocument {
173 fn inner(&self) -> DocGuard<'_> {
174 DocGuard::Ref(self)
175 }
176 fn inner_mut(&mut self) -> DocGuardMut<'_> {
177 DocGuardMut::Ref(self)
178 }
179}
180
181impl Document for Rc<RefCell<BaseDocument>> {
182 fn inner(&self) -> DocGuard<'_> {
183 DocGuard::RefCell(self.borrow())
184 }
185
186 fn inner_mut(&mut self) -> DocGuardMut<'_> {
187 DocGuardMut::RefCell(self.borrow_mut())
188 }
189}
190
191pub enum DocumentEvent {
192 ResourceLoad(ResourceLoadResponse),
193 NavigateIframe {
196 node_id: NodeId,
197 url: Url,
198 },
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
203pub enum AnimationPacing {
204 Idle,
205 Caret,
206 SlowCss,
207 Interactive,
208}
209
210pub struct BaseDocument {
211 id: usize,
213
214 pub(crate) url: DocumentUrl,
217 pub(crate) devtool_settings: DevtoolSettings,
219 pub(crate) viewport: Viewport,
221 pub(crate) viewport_scroll: crate::Point<f64>,
223 pub(crate) media_type: MediaType,
225 pub(crate) style_threading: StyleThreading,
227 pub(crate) incremental_layout: bool,
229 pub(crate) subdocument_depth: usize,
232
233 pub(crate) tx: Sender<DocumentEvent>,
235 pub(crate) rx: Option<Receiver<DocumentEvent>>,
237
238 pub(crate) nodes: Box<NodeTree>,
243
244 pub(crate) root_node_id: NodeId,
246
247 pub(crate) hoisted_fixed_parents: HashMap<NodeId, NodeId>,
257
258 pub(crate) hoisted_clip_hosts: Vec<NodeId>,
264
265 pub(crate) stylist: Stylist,
268 pub(crate) animations: DocumentAnimationSet,
269 pub(crate) last_resolve_animation_time: f64,
276 pub(crate) guard: SharedRwLock,
278 pub(crate) snapshots: SnapshotMap,
280
281 pub(crate) font_ctx: Arc<Mutex<parley::FontContext>>,
284 #[cfg(feature = "parallel-construct")]
285 pub(crate) thread_font_contexts: ThreadLocal<RefCell<Box<FontContext>>>,
287 pub(crate) layout_ctx: parley::LayoutContext<TextBrush>,
289
290 pub(crate) hover_node_id: Option<NodeId>,
294 pub(crate) hover_hit_node_id: Option<NodeId>,
298 pub(crate) hover_node_is_text: bool,
300 pub(crate) last_client_pointer_position: Option<taffy::Point<f32>>,
302 pub(crate) semantic_hover_node_id: Option<NodeId>,
308 pub(crate) focus_node_id: Option<NodeId>,
310 pub(crate) active_node_id: Option<NodeId>,
312 pub(crate) mousedown_node_id: Option<NodeId>,
314 pub(crate) last_mousedown_time: Option<Instant>,
316 pub(crate) mousedown_position: taffy::Point<f32>,
318 pub(crate) click_count: u16,
320 pub(crate) drag_mode: DragMode,
322 pub(crate) hovered_scrollbar: Option<crate::node::ScrollbarRef>,
324 pub(crate) scrollbar_activity: HashMap<NodeId, Instant>,
327 pub(crate) scroll_animation: ScrollAnimationState,
329
330 pub(crate) text_selection: TextSelection,
332
333 pub(crate) has_active_animations: bool,
336 pub(crate) has_canvas: bool,
338 pub(crate) subdoc_animation_pacing: AnimationPacing,
340
341 pub(crate) nodes_to_id: HashMap<String, SmallVec<[NodeId; 1]>>,
345 pub(crate) nodes_to_stylesheet: BTreeMap<NodeId, DocumentStyleSheet>,
347 pub(crate) ua_stylesheets: HashMap<String, DocumentStyleSheet>,
350 pub(crate) controls_to_form: HashMap<NodeId, NodeId>,
352 pub(crate) sub_document_nodes: HashSet<NodeId>,
354 pub(crate) iframe_loads: HashMap<NodeId, crate::iframe::IframeLoad>,
357 pub(crate) deferred_construction_nodes: Vec<ConstructionTask>,
359 pub(crate) paint_damage: crate::paint_damage::PaintDamageTracker,
365
366 #[cfg(feature = "custom-widget")]
368 pub(crate) custom_widget_nodes: HashSet<NodeId>,
369 #[cfg(feature = "custom-widget")]
371 pub(crate) pending_resource_deallocations: Vec<anyrender::ResourceId>,
372
373 #[cfg(feature = "shadow-dom")]
375 pub(crate) custom_element_registry: crate::node::CustomElementRegistry,
376 #[cfg(feature = "shadow-dom")]
378 pub(crate) shadow_host_nodes: HashSet<NodeId>,
379 #[cfg(feature = "shadow-dom")]
381 pub(crate) custom_element_nodes: HashSet<NodeId>,
382
383 pub(crate) image_cache: HashMap<String, ImageData>,
386
387 pub(crate) pending_images: HashMap<String, Vec<(NodeId, ImageType)>>,
391
392 pub(crate) pending_critical_resources: HashSet<usize>,
395
396 pub net_provider: Arc<dyn NetProvider>,
399 pub navigation_provider: Arc<dyn NavigationProvider>,
402 pub shell_provider: Arc<dyn ShellProvider>,
404 pub html_parser_provider: Arc<dyn HtmlParserProvider>,
406 pub(crate) abort_signal: Option<AbortSignal>,
410}
411
412pub(crate) fn make_device(
413 viewport: &Viewport,
414 media_type: MediaType,
415 font_ctx: Arc<Mutex<FontContext>>,
416) -> Device {
417 let width = viewport.window_size.0 as f32 / viewport.scale();
418 let height = viewport.window_size.1 as f32 / viewport.scale();
419 let viewport_size = euclid::Size2D::new(width, height);
420 let device_size = euclid::Size2D::new(width, height) * viewport.scale();
421 let device_pixel_ratio = euclid::Scale::new(viewport.scale());
422
423 Device::new(
424 media_type,
425 selectors::matching::QuirksMode::NoQuirks,
426 viewport_size,
427 device_size,
428 device_pixel_ratio,
429 Box::new(BlitzFontMetricsProvider { font_ctx }),
430 ComputedValues::initial_values_with_font_override(Font::initial_values()),
431 match viewport.color_scheme {
432 ColorScheme::Light => PrefersColorScheme::Light,
433 ColorScheme::Dark => PrefersColorScheme::Dark,
434 },
435 PointerCapabilities::default(),
436 PointerCapabilities::default(),
437 )
438}
439
440fn incremental_layout_default() -> bool {
455 !matches!(
456 std::env::var("BLITZ_INCREMENTAL").ok().as_deref(),
457 Some("0" | "false" | "off")
458 )
459}
460
461impl BaseDocument {
462 pub fn new(config: DocumentConfig) -> Self {
464 static ID_GENERATOR: AtomicUsize = AtomicUsize::new(1);
465
466 let id = ID_GENERATOR.fetch_add(1, Ordering::SeqCst);
467
468 let font_ctx = config
469 .font_ctx
470 .map(|mut font_ctx| {
471 font_ctx.source_cache.make_shared();
472 font_ctx
474 })
475 .unwrap_or_else(|| {
476 use parley::fontique::{Collection, CollectionOptions, SourceCache};
477 let mut font_ctx = FontContext {
478 source_cache: SourceCache::new_shared(),
479 collection: Collection::new(CollectionOptions {
480 shared: false,
481 system_fonts: cfg!(all(
482 feature = "system-fonts",
483 not(target_arch = "wasm32")
484 )),
485 }),
486 };
487 font_ctx
488 .collection
489 .register_fonts(Blob::new(Arc::new(crate::BULLET_FONT) as _), None);
490 font_ctx
491 });
492 let font_ctx = Arc::new(Mutex::new(font_ctx));
493
494 style_config::set_pref!("layout.grid.enabled", true);
496 style_config::set_pref!("layout.unimplemented", true);
497 style_config::set_pref!("layout.columns.enabled", true);
498 style_config::set_pref!("layout.css.basic-shape-shape.enabled", true);
499 style_config::set_pref!("layout.threads", -1);
500
501 let viewport = config.viewport.unwrap_or_default();
502 let media_type = config.media_type.unwrap_or_else(MediaType::screen);
503 let device = make_device(&viewport, media_type.clone(), font_ctx.clone());
504 let stylist = Stylist::new(device, QuirksMode::NoQuirks);
505 let snapshots = SnapshotMap::new();
506 let nodes = Box::new(NodeTree::new());
507 let guard = SharedRwLock::new();
508 let nodes_to_id = HashMap::new();
509
510 let base_url = config
511 .base_url
512 .and_then(|url| DocumentUrl::from_str(&url).ok())
513 .unwrap_or_default();
514
515 let net_provider = config
516 .net_provider
517 .unwrap_or_else(|| Arc::new(DummyNetProvider));
518 let navigation_provider = config
519 .navigation_provider
520 .unwrap_or_else(|| Arc::new(DummyNavigationProvider));
521 let shell_provider = config
522 .shell_provider
523 .unwrap_or_else(|| Arc::new(DummyShellProvider));
524 let html_parser_provider = config
525 .html_parser_provider
526 .unwrap_or_else(|| Arc::new(DummyHtmlParserProvider));
527
528 let (tx, rx) = channel();
529
530 let mut doc = Self {
531 hoisted_fixed_parents: HashMap::new(),
532 hoisted_clip_hosts: Vec::new(),
533 id,
534 tx,
535 rx: Some(rx),
536
537 guard,
538 nodes,
539 root_node_id: NodeId::default(),
540 stylist,
541 animations: DocumentAnimationSet::default(),
542 last_resolve_animation_time: 0.0,
543 snapshots,
544 nodes_to_id,
545 viewport,
546 media_type,
547 style_threading: config.style_threading,
548 incremental_layout: config
549 .incremental
550 .unwrap_or_else(incremental_layout_default),
551 subdocument_depth: config.subdocument_depth,
552 devtool_settings: DevtoolSettings::default(),
553 viewport_scroll: crate::Point::ZERO,
554 url: base_url,
555 ua_stylesheets: HashMap::new(),
556 nodes_to_stylesheet: BTreeMap::new(),
557 font_ctx,
558 #[cfg(feature = "parallel-construct")]
559 thread_font_contexts: ThreadLocal::new(),
560 layout_ctx: parley::LayoutContext::new(),
561
562 hover_node_id: None,
563 hover_hit_node_id: None,
564 hover_node_is_text: false,
565 last_client_pointer_position: None,
566 semantic_hover_node_id: None,
567 focus_node_id: None,
568 active_node_id: None,
569 mousedown_node_id: None,
570 has_active_animations: false,
571 subdoc_animation_pacing: AnimationPacing::Idle,
572 has_canvas: false,
573 sub_document_nodes: HashSet::new(),
574 iframe_loads: HashMap::new(),
575
576 #[cfg(feature = "custom-widget")]
577 custom_widget_nodes: HashSet::new(),
578 #[cfg(feature = "custom-widget")]
579 pending_resource_deallocations: Vec::new(),
580
581 #[cfg(feature = "shadow-dom")]
582 custom_element_registry: crate::node::CustomElementRegistry::new(),
583 #[cfg(feature = "shadow-dom")]
584 shadow_host_nodes: HashSet::new(),
585 #[cfg(feature = "shadow-dom")]
586 custom_element_nodes: HashSet::new(),
587
588 deferred_construction_nodes: Vec::new(),
589 paint_damage: Default::default(),
590 image_cache: HashMap::new(),
591 pending_images: HashMap::new(),
592 pending_critical_resources: HashSet::new(),
593 controls_to_form: HashMap::new(),
594 net_provider,
595 navigation_provider,
596 shell_provider,
597 html_parser_provider,
598 abort_signal: config.abort_signal,
599 last_mousedown_time: None,
600 mousedown_position: taffy::Point::ZERO,
601 click_count: 0,
602 drag_mode: DragMode::None,
603 hovered_scrollbar: None,
604 scrollbar_activity: HashMap::new(),
605 scroll_animation: ScrollAnimationState::None,
606 text_selection: TextSelection::default(),
607 };
608
609 doc.root_node_id = doc.create_node(NodeData::Document(Box::default()));
611 doc.root_node_mut().flags.insert(NodeFlags::IS_IN_DOCUMENT);
612
613 match config.ua_stylesheets {
614 Some(stylesheets) => {
615 for ss in &stylesheets {
616 doc.add_user_agent_stylesheet(ss);
617 }
618 }
619 None => doc.add_user_agent_stylesheet(DEFAULT_CSS),
620 }
621
622 let stylo_element_data = StyloElementData {
624 styles: ElementStyles {
625 primary: Some(
626 ComputedValues::initial_values_with_font_override(Font::initial_values())
627 .to_arc(),
628 ),
629 ..Default::default()
630 },
631 ..Default::default()
632 };
633 let stylo_data = doc.root_node_mut().stylo_element_data_mut();
634 *stylo_data.ensure_init_mut() = stylo_element_data;
635
636 doc
637 }
638
639 pub fn set_net_provider(&mut self, net_provider: Arc<dyn NetProvider>) {
641 self.net_provider = net_provider;
642 }
643
644 pub fn set_navigation_provider(&mut self, navigation_provider: Arc<dyn NavigationProvider>) {
646 self.navigation_provider = navigation_provider;
647 }
648
649 pub fn set_shell_provider(&mut self, shell_provider: Arc<dyn ShellProvider>) {
651 self.shell_provider = shell_provider;
652 }
653
654 pub fn set_html_parser_provider(&mut self, html_parser_provider: Arc<dyn HtmlParserProvider>) {
656 self.html_parser_provider = html_parser_provider;
657 }
658
659 pub fn set_base_url(&mut self, url: &str) {
661 self.url = DocumentUrl::from(Url::parse(url).unwrap());
662 }
663
664 pub fn guard(&self) -> &SharedRwLock {
665 &self.guard
666 }
667
668 pub fn tree(&self) -> &NodeTree {
669 &self.nodes
670 }
671
672 pub fn id(&self) -> usize {
673 self.id
674 }
675
676 pub(crate) fn build_request(&self, url: url::Url) -> Request {
679 crate::net::stamped_request(url, self.abort_signal.as_ref())
680 }
681
682 pub fn favicon_url(&self) -> Option<String> {
683 self.tree().iter().find_map(|(_, node)| {
684 let data = &node.data;
685 if !data.is_element_with_tag_name(&local_name!("link")) {
686 return None;
687 }
688 let rel = data.attr(local_name!("rel"))?;
689 if !rel
690 .split_ascii_whitespace()
691 .any(|v| v.eq_ignore_ascii_case("icon"))
692 {
693 return None;
694 }
695 data.attr(local_name!("href")).map(|s| s.to_string())
696 })
697 }
698
699 pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
700 self.nodes.get(node_id)
701 }
702
703 pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
704 self.nodes.get_mut(node_id)
705 }
706
707 pub fn get_focussed_node_id(&self) -> Option<NodeId> {
708 self.focus_node_id
709 .or(self.try_root_element().map(|el| el.id))
710 }
711
712 pub fn mutate<'doc>(&'doc mut self) -> DocumentMutator<'doc> {
713 DocumentMutator::new(self)
714 }
715
716 pub fn handle_dom_event<F: FnMut(DomEvent)>(
717 &mut self,
718 event: &mut DomEvent,
719 dispatch_event: F,
720 ) {
721 handle_dom_event(self, event, dispatch_event)
722 }
723
724 pub fn as_any_mut(&mut self) -> &mut dyn Any {
725 self
726 }
727
728 pub fn label_bound_input_element(&self, label_node_id: NodeId) -> Option<&Node> {
735 let label_element = self.nodes[label_node_id].element_data()?;
736 if let Some(target_element_dom_id) = label_element.attr(local_name!("for")) {
737 TreeTraverser::new(self)
738 .filter_map(|id| {
739 let node = self.get_node(id)?;
740 let element_data = node.element_data()?;
741 if element_data.name.local != local_name!("input") {
742 return None;
743 }
744 let id = element_data.id.as_ref()?;
745 if *id == *target_element_dom_id {
746 Some(node)
747 } else {
748 None
749 }
750 })
751 .next()
752 } else {
753 TreeTraverser::new_with_root(self, label_node_id)
754 .filter_map(|child_id| {
755 let node = self.get_node(child_id)?;
756 let element_data = node.element_data()?;
757 if element_data.name.local == local_name!("input") {
758 Some(node)
759 } else {
760 None
761 }
762 })
763 .next()
764 }
765 }
766
767 pub fn run_pre_click_activation(&mut self, target: NodeId) -> Option<PreClickActivation> {
773 let node_id = crate::events::pointer::checkable_activation_target(self, target)?;
774 let el = self.get_node(node_id)?.data.downcast_element()?;
775 let is_radio = el.attr(local_name!("type")) == Some("radio");
776
777 if !is_radio {
778 let previous = el.checkbox_input_checked()?;
779 let el = self.get_node_mut(node_id)?.data.downcast_element_mut()?;
780 Self::toggle_checkbox(el);
781 return Some(PreClickActivation {
782 previous: vec![(node_id, previous)],
783 });
784 }
785
786 let radio_set = el.attr(local_name!("name")).map(str::to_string);
787 let Some(radio_set) = radio_set else {
788 let previous = el.checkbox_input_checked()?;
789 let el = self.get_node_mut(node_id)?.data.downcast_element_mut()?;
790 *el.checkbox_input_checked_mut()? = true;
791 return Some(PreClickActivation {
792 previous: vec![(node_id, previous)],
793 });
794 };
795
796 let mut previous: Vec<(NodeId, bool)> = Vec::new();
808 for (id, node) in self.nodes.iter_mut() {
809 let Some(el) = node.data.downcast_element_mut() else {
810 continue;
811 };
812 if el.attr(local_name!("name")) != Some(&*radio_set) {
813 continue;
814 }
815 let Some(is_checked) = el.checkbox_input_checked_mut() else {
816 continue;
817 };
818 previous.push((id, *is_checked));
819 *is_checked = id == node_id;
820 }
821 Some(PreClickActivation { previous })
822 }
823
824 pub fn undo_pre_click_activation(&mut self, activation: PreClickActivation) {
827 for (node_id, was_checked) in activation.previous {
828 let Some(node) = self.get_node_mut(node_id) else {
829 continue;
830 };
831 let Some(el) = node.data.downcast_element_mut() else {
832 continue;
833 };
834 if let Some(is_checked) = el.checkbox_input_checked_mut() {
835 *is_checked = was_checked;
836 }
837 }
838 }
839
840 pub fn toggle_checkbox(el: &mut ElementData) -> bool {
841 let Some(is_checked) = el.checkbox_input_checked_mut() else {
842 return false;
843 };
844 *is_checked = !*is_checked;
845
846 *is_checked
847 }
848
849 pub fn toggle_radio(&mut self, radio_set_name: String, target_radio_id: NodeId) {
850 for (i, node) in self.nodes.iter_mut() {
851 if let Some(node_data) = node.data.downcast_element_mut() {
852 if node_data.attr(local_name!("name")) == Some(&radio_set_name) {
853 let was_clicked = i == target_radio_id;
854 let Some(is_checked) = node_data.checkbox_input_checked_mut() else {
855 continue;
856 };
857 *is_checked = was_clicked;
858 }
859 }
860 }
861 }
862
863 pub fn toggle_details_open(&mut self, details_id: NodeId) {
867 use crate::qual_name;
868
869 let node = &self.nodes[details_id];
870 if !node.data.is_element_with_tag_name(&local_name!("details")) {
871 return;
872 }
873 let is_open = node.data.has_attr(local_name!("open"));
874
875 let mut mutator = self.mutate();
879 if is_open {
880 mutator.clear_attribute(details_id, qual_name!("open"));
881 } else {
882 mutator.set_attribute(details_id, qual_name!("open"), "");
883 }
884 drop(mutator);
885
886 self.shell_provider.request_redraw();
887 }
888
889 pub fn set_style_property(&mut self, node_id: NodeId, name: &str, value: &str) {
890 let node = &mut self.nodes[node_id];
891 let did_change = node.element_data_mut().unwrap().set_style_property(
892 name,
893 value,
894 &self.guard,
895 self.url.url_extra_data(),
896 );
897 if did_change {
898 node.mark_style_attr_updated();
899 }
900 }
901
902 pub fn remove_style_property(&mut self, node_id: NodeId, name: &str) {
903 let node = &mut self.nodes[node_id];
904 let did_change = node.element_data_mut().unwrap().remove_style_property(
905 name,
906 &self.guard,
907 self.url.url_extra_data(),
908 );
909 if did_change {
910 node.mark_style_attr_updated();
911 }
912 }
913
914 pub fn sub_document_node_ids(&self) -> Vec<NodeId> {
915 self.sub_document_nodes.iter().copied().collect()
916 }
917
918 pub fn set_sub_document(&mut self, node_id: NodeId, sub_document: Box<dyn Document>) {
919 self.nodes[node_id]
920 .element_data_mut()
921 .unwrap()
922 .set_sub_document(sub_document);
923 self.sub_document_nodes.insert(node_id);
924 }
925
926 pub fn remove_sub_document(&mut self, node_id: NodeId) {
927 self.nodes[node_id]
928 .element_data_mut()
929 .unwrap()
930 .remove_sub_document();
931 self.sub_document_nodes.remove(&node_id);
932 if let Some(load) = self.iframe_loads.remove(&node_id) {
933 load.abort_controller.abort();
934 }
935 }
936
937 pub fn poll_subdocuments(&mut self, waker: Option<&Waker>) -> bool {
943 let mut has_changes = false;
944 let node_ids: Vec<NodeId> = self.sub_document_nodes.iter().copied().collect();
945 for node_id in node_ids {
946 let Some(sub_doc) = self
947 .nodes
948 .get_mut(node_id)
949 .and_then(|node| node.subdoc_mut())
950 else {
951 continue;
952 };
953 let task_context = waker.map(TaskContext::from_waker);
954 has_changes |= sub_doc.poll(task_context);
955 }
956 has_changes
957 }
958
959 #[cfg(feature = "custom-widget")]
960 pub fn custom_widget_node_ids(&self) -> Vec<NodeId> {
961 self.custom_widget_nodes.iter().copied().collect()
962 }
963
964 #[cfg(feature = "custom-widget")]
965 pub fn take_pending_resource_deallocations(&mut self) -> Vec<anyrender::ResourceId> {
966 std::mem::take(&mut self.pending_resource_deallocations)
967 }
968
969 #[cfg(feature = "custom-widget")]
970 pub fn set_custom_widget(&mut self, node_id: NodeId, widget: Box<dyn crate::Widget>) {
971 self.nodes[node_id]
972 .element_data_mut()
973 .unwrap()
974 .set_custom_widget(widget);
975 self.custom_widget_nodes.insert(node_id);
976 }
977
978 #[cfg(feature = "custom-widget")]
979 pub fn remove_custom_widget(&mut self, node_id: NodeId) {
980 let resources_to_deallocate = self.nodes[node_id]
981 .element_data_mut()
982 .unwrap()
983 .remove_custom_widget();
984 self.pending_resource_deallocations
985 .extend_from_slice(&resources_to_deallocate);
986 self.custom_widget_nodes.remove(&node_id);
987 }
988
989 #[cfg(feature = "shadow-dom")]
993 pub fn custom_elements_mut(&mut self) -> &mut crate::node::CustomElementRegistry {
994 &mut self.custom_element_registry
995 }
996
997 #[cfg(feature = "shadow-dom")]
1000 pub fn define_custom_element(
1001 &mut self,
1002 name: markup5ever::LocalName,
1003 definition: crate::node::CustomElementDefinition,
1004 ) {
1005 self.custom_element_registry.define(name, definition);
1006 }
1007
1008 #[cfg(feature = "shadow-dom")]
1010 pub fn shadow_host_node_ids(&self) -> Vec<NodeId> {
1011 self.shadow_host_nodes.iter().copied().collect()
1012 }
1013
1014 #[cfg(feature = "shadow-dom")]
1016 pub fn shadow_root_id(&self, host_id: NodeId) -> Option<NodeId> {
1017 self.get_node(host_id)
1018 .and_then(|node| node.shadow_root_id())
1019 }
1020
1021 #[cfg(feature = "shadow-dom")]
1025 pub fn attach_shadow(&mut self, host_id: NodeId, mode: crate::node::ShadowRootMode) -> NodeId {
1026 if let Some(existing) = self.nodes[host_id].shadow_root_id() {
1027 return existing;
1028 }
1029
1030 let shadow_root_id = self.create_node(NodeData::ShadowRoot(
1031 crate::node::ShadowRootData::new(host_id, mode),
1032 ));
1033
1034 self.nodes[shadow_root_id].parent = Some(host_id);
1038 if self.nodes[host_id].flags.is_in_document() {
1039 self.nodes[shadow_root_id]
1040 .flags
1041 .insert(NodeFlags::IS_IN_DOCUMENT);
1042 }
1043
1044 self.nodes[host_id]
1045 .element_data_mut()
1046 .expect("Shadow host must be an element")
1047 .shadow_root = Some(shadow_root_id);
1048 self.shadow_host_nodes.insert(host_id);
1049
1050 self.nodes[host_id].insert_damage(ALL_DAMAGE);
1052 self.nodes[host_id].mark_ancestors_dirty();
1053
1054 shadow_root_id
1055 }
1056
1057 #[cfg(feature = "shadow-dom")]
1059 pub fn detach_shadow(&mut self, host_id: NodeId) {
1060 let shadow_root_id = self.nodes[host_id]
1061 .element_data_mut()
1062 .and_then(|el| el.shadow_root.take());
1063 if let Some(shadow_root_id) = shadow_root_id {
1064 self.drop_node_ignoring_parent(shadow_root_id);
1065 self.shadow_host_nodes.remove(&host_id);
1066 self.nodes[host_id].insert_damage(ALL_DAMAGE);
1067 self.nodes[host_id].mark_ancestors_dirty();
1068 }
1069 }
1070
1071 #[cfg(feature = "shadow-dom")]
1073 pub fn set_custom_element(
1074 &mut self,
1075 node_id: NodeId,
1076 controller: Box<dyn crate::node::CustomElement>,
1077 ) {
1078 use crate::node::{CustomElementData, SpecialElementData};
1079 self.nodes[node_id]
1080 .element_data_mut()
1081 .expect("Custom element host must be an element")
1082 .special_data = SpecialElementData::CustomElement(CustomElementData::new(controller));
1083 self.custom_element_nodes.insert(node_id);
1084 }
1085
1086 #[cfg(feature = "shadow-dom")]
1089 pub fn take_custom_element(
1090 &mut self,
1091 node_id: NodeId,
1092 ) -> Option<Box<dyn crate::node::CustomElement>> {
1093 use crate::node::SpecialElementData;
1094 self.custom_element_nodes.remove(&node_id);
1095 let element = self.nodes[node_id].element_data_mut()?;
1096 if matches!(element.special_data, SpecialElementData::CustomElement(_)) {
1097 if let SpecialElementData::CustomElement(mut data) = element.special_data.take() {
1098 return data.controller.take();
1099 }
1100 }
1101 None
1102 }
1103
1104 pub fn root_node(&self) -> &Node {
1105 &self.nodes[self.root_node_id]
1106 }
1107
1108 pub fn root_node_mut(&mut self) -> &mut Node {
1109 &mut self.nodes[self.root_node_id]
1110 }
1111
1112 pub fn set_paint_damage_tracking(&mut self, enabled: bool) {
1129 self.paint_damage.set_enabled(enabled);
1130 }
1131
1132 pub fn paint_damage_tracking(&self) -> bool {
1134 self.paint_damage.is_enabled()
1135 }
1136
1137 pub fn paint_damage(&self) -> &crate::paint_damage::PaintDamage {
1145 self.paint_damage.damage()
1146 }
1147
1148 pub fn try_root_element(&self) -> Option<&Node> {
1149 TDocument::as_node(&self.root_node()).first_element_child()
1150 }
1151
1152 pub fn root_element(&self) -> &Node {
1153 TDocument::as_node(&self.root_node())
1154 .first_element_child()
1155 .unwrap()
1156 .as_element()
1157 .unwrap()
1158 }
1159
1160 pub fn create_node(&mut self, node_data: NodeData) -> NodeId {
1161 let tree_ptr = self.nodes.as_mut() as *mut NodeTree;
1162 let guard = self.guard.clone();
1163
1164 self.nodes
1165 .insert_with_key(|id| Node::new(tree_ptr, id, guard, node_data))
1166 }
1167
1168 pub(crate) fn remove_node_from_tree(&mut self, node_id: NodeId) -> Option<Node> {
1172 self.clear_interaction_state_for_removed_node(node_id);
1173 self.nodes.remove(node_id)
1174 }
1175
1176 fn nearest_surviving_element_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
1181 let mut current = self.get_node(node_id)?.parent;
1182 while let Some(id) = current {
1183 let node = self.get_node(id)?;
1184 if node.is_element() && node.flags.is_in_document() {
1185 return Some(id);
1186 }
1187 current = node.parent;
1188 }
1189 None
1190 }
1191
1192 pub(crate) fn clear_interaction_state_for_removed_node(&mut self, node_id: NodeId) {
1213 if !self.nodes.contains_key(node_id) {
1214 return;
1215 }
1216
1217 if self.hover_node_id == Some(node_id) {
1218 self.hover_node_id = self.nearest_surviving_element_ancestor(node_id);
1219 self.hover_node_is_text = false;
1220 }
1221 if self.hover_hit_node_id == Some(node_id) {
1222 self.hover_hit_node_id = None;
1223 }
1224 if self.active_node_id == Some(node_id) {
1225 self.active_node_id = self.nearest_surviving_element_ancestor(node_id);
1226 }
1227 if self.focus_node_id == Some(node_id) {
1228 let shell_provider = self.shell_provider.clone();
1229 self.nodes[node_id].blur(shell_provider);
1230 self.focus_node_id = None;
1231 }
1232 if self.mousedown_node_id == Some(node_id) {
1233 self.mousedown_node_id = None;
1234 }
1235 if self.text_selection.anchor.node_or_parent == Some(node_id)
1236 || self.text_selection.focus.node_or_parent == Some(node_id)
1237 {
1238 self.text_selection.clear();
1239 }
1240 if self
1241 .hovered_scrollbar
1242 .is_some_and(|scrollbar| scrollbar.node_id == node_id)
1243 {
1244 self.hovered_scrollbar = None;
1245 }
1246 let drag_references_node = match &self.drag_mode {
1247 DragMode::Panning(state) => state.target == node_id,
1248 DragMode::ScrollbarDrag(state) => state.scrollbar.node_id == node_id,
1249 DragMode::Selecting | DragMode::None => false,
1250 };
1251 if drag_references_node {
1252 self.drag_mode = DragMode::None;
1253 }
1254 self.scrollbar_activity.remove(&node_id);
1255
1256 self.controls_to_form.remove(&node_id);
1261 }
1262
1263 pub(crate) fn drop_node_ignoring_parent(&mut self, node_id: NodeId) -> Option<Node> {
1264 self.drop_node_ignoring_parent_with(node_id, &mut |_| {})
1265 }
1266
1267 pub(crate) fn drop_node_ignoring_parent_with(
1270 &mut self,
1271 node_id: NodeId,
1272 on_drop: &mut dyn FnMut(NodeId),
1273 ) -> Option<Node> {
1274 let mut node = self.remove_node_from_tree(node_id);
1275 if let Some(node) = &mut node {
1276 on_drop(node_id);
1277 if let Some(before) = node.before() {
1278 self.drop_node_ignoring_parent_with(before, on_drop);
1279 }
1280 if let Some(after) = node.after() {
1281 self.drop_node_ignoring_parent_with(after, on_drop);
1282 }
1283
1284 for &child in &node.children {
1285 self.drop_node_ignoring_parent_with(child, on_drop);
1286 }
1287
1288 for &anon_id in &node.anonymous_blocks {
1291 self.deallocate_anonymous_block(anon_id);
1292 }
1293
1294 #[cfg(feature = "shadow-dom")]
1297 if let Some(shadow_root_id) = node.shadow_root_id() {
1298 self.shadow_host_nodes.remove(&node_id);
1299 self.custom_element_nodes.remove(&node_id);
1300 self.drop_node_ignoring_parent(shadow_root_id);
1301 }
1302 }
1303 node
1304 }
1305
1306 pub(crate) fn deallocate_anonymous_block(&mut self, anon_id: NodeId) {
1309 if !self.nodes.contains_key(anon_id) {
1312 return;
1313 }
1314
1315 let nested = std::mem::take(&mut self.nodes[anon_id].anonymous_blocks);
1317 for nested_id in nested {
1318 self.deallocate_anonymous_block(nested_id);
1319 }
1320
1321 self.remove_node_from_tree(anon_id);
1322 }
1323
1324 pub fn create_text_node(&mut self, text: &str) -> NodeId {
1325 let content = text.to_string();
1326 let data = NodeData::Text(TextNodeData::new(content));
1327 self.create_node(data)
1328 }
1329
1330 pub fn deep_clone_node(&mut self, node_id: NodeId) -> NodeId {
1331 let node = &self.nodes[node_id];
1333 let mut data = node.data.clone();
1334
1335 match &mut data {
1336 NodeData::Element(elem) | NodeData::AnonymousBlock(elem) => {
1337 if let Some(arc) = elem.style_attribute.as_mut() {
1338 let read_guard = self.guard().read();
1339 let block = arc.read_with(&read_guard);
1340 *arc = ServoArc::new(self.guard().wrap(block.clone()));
1341 }
1342 }
1343 _ => {}
1344 }
1345
1346 let children = node.children.clone();
1347
1348 let new_node_id = self.create_node(data);
1350
1351 let new_children: ThinVec<NodeId> = children
1353 .into_iter()
1354 .map(|child_id| self.deep_clone_node(child_id))
1355 .collect();
1356 for &child_id in &new_children {
1357 self.nodes[child_id].parent = Some(new_node_id);
1358 }
1359 self.nodes[new_node_id].children = new_children;
1360
1361 new_node_id
1362 }
1363
1364 pub(crate) fn remove_and_drop_pe(&mut self, node_id: NodeId) -> Option<Node> {
1365 fn remove_pe_ignoring_parent(doc: &mut BaseDocument, node_id: NodeId) -> Option<Node> {
1366 let mut node = doc.remove_node_from_tree(node_id);
1367 if let Some(node) = &mut node {
1368 for &child in &node.children {
1369 remove_pe_ignoring_parent(doc, child);
1370 }
1371 for &anon_id in &node.anonymous_blocks {
1372 doc.deallocate_anonymous_block(anon_id);
1373 }
1374 }
1375 node
1376 }
1377
1378 let node = remove_pe_ignoring_parent(self, node_id);
1379
1380 if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
1382 let parent = &mut self.nodes[parent_id];
1383 parent.children.retain(|id| *id != node_id);
1384 }
1385
1386 node
1387 }
1388
1389 pub(crate) fn resolve_url(&self, raw: &str) -> url::Url {
1390 self.url.resolve_relative(raw).unwrap_or_else(|| {
1391 panic!(
1392 "to be able to resolve {raw} with the base_url: {:?}",
1393 *self.url
1394 )
1395 })
1396 }
1397
1398 pub fn navigate_to_url(&self, raw: &str) -> bool {
1406 let Some(url) = self.url.resolve_relative(raw) else {
1407 return false;
1408 };
1409 self.navigation_provider
1410 .navigate_to(blitz_traits::navigation::NavigationOptions::new(
1411 url,
1412 None,
1413 self.id(),
1414 ));
1415 true
1416 }
1417
1418 pub fn current_url(&self) -> String {
1420 self.url.to_string()
1421 }
1422
1423 pub fn print_tree(&self) {
1424 crate::util::walk_tree(0, self.root_node());
1425 }
1426
1427 pub fn print_subtree(&self, node_id: NodeId) {
1428 crate::util::walk_tree(0, &self.nodes[node_id]);
1429 }
1430
1431 pub fn reload_resource_by_href(&mut self, href_to_reload: &str) {
1432 for &node_id in self.nodes_to_stylesheet.keys() {
1433 let node = &self.nodes[node_id];
1434 let Some(element) = node.element_data() else {
1435 continue;
1436 };
1437
1438 if element.name.local == local_name!("link") {
1439 if let Some(href) = element.attr(local_name!("href")) {
1440 if href == href_to_reload {
1442 let resolved_href = self.resolve_url(href);
1443 self.net_provider.fetch(
1444 self.id(),
1445 self.build_request(resolved_href.clone()),
1446 ResourceHandler::boxed(
1447 self.tx.clone(),
1448 self.id,
1449 Some(node_id),
1450 self.shell_provider.clone(),
1451 StylesheetHandler {
1452 source_url: resolved_href,
1453 guard: self.guard.clone(),
1454 net_provider: self.net_provider.clone(),
1455 abort_signal: self.abort_signal.clone(),
1456 },
1457 ),
1458 );
1459 }
1460 }
1461 }
1462 }
1463 }
1464
1465 pub fn process_style_element(&mut self, target_id: NodeId) {
1466 let css = self.nodes[target_id].text_content();
1467 let css = html_escape::decode_html_entities(&css);
1468 let sheet = self.make_stylesheet(&css, Origin::Author);
1469 self.add_stylesheet_for_node(sheet, target_id);
1470 }
1471
1472 pub fn remove_user_agent_stylesheet(&mut self, contents: &str) {
1473 if let Some(sheet) = self.ua_stylesheets.remove(contents) {
1474 self.stylist.remove_stylesheet(sheet, &self.guard.read());
1475 }
1476 }
1477
1478 pub fn url(&self) -> &url::Url {
1480 &self.url
1481 }
1482
1483 pub fn author_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
1486 self.nodes_to_stylesheet.values()
1487 }
1488
1489 pub fn useragent_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
1491 self.ua_stylesheets.values()
1492 }
1493
1494 pub fn add_user_agent_stylesheet(&mut self, css: &str) {
1495 let sheet = self.make_stylesheet(css, Origin::UserAgent);
1496 self.ua_stylesheets.insert(css.to_string(), sheet.clone());
1497 self.stylist.append_stylesheet(sheet, &self.guard.read());
1498 }
1499
1500 pub fn make_stylesheet(&self, css: impl AsRef<str>, origin: Origin) -> DocumentStyleSheet {
1501 let data = Stylesheet::from_str(
1502 css.as_ref(),
1503 self.url.url_extra_data(),
1504 origin,
1505 ServoArc::new(self.guard.wrap(MediaList::empty())),
1506 self.guard.clone(),
1507 Some(&StylesheetLoader {
1508 tx: self.tx.clone(),
1509 doc_id: self.id,
1510 net_provider: self.net_provider.clone(),
1511 shell_provider: self.shell_provider.clone(),
1512 abort_signal: self.abort_signal.clone(),
1513 }),
1514 None,
1515 QuirksMode::NoQuirks,
1516 AllowImportRules::Yes,
1517 );
1518
1519 DocumentStyleSheet(ServoArc::new(data))
1520 }
1521
1522 pub fn upsert_stylesheet_for_node(&mut self, node_id: NodeId) {
1523 let raw_styles = self.nodes[node_id].text_content();
1524 let sheet = self.make_stylesheet(raw_styles, Origin::Author);
1525 self.add_stylesheet_for_node(sheet, node_id);
1526 }
1527
1528 pub fn add_stylesheet_for_node(&mut self, stylesheet: DocumentStyleSheet, node_id: NodeId) {
1529 let old = self.nodes_to_stylesheet.insert(node_id, stylesheet.clone());
1530
1531 if let Some(old) = old {
1532 self.stylist.remove_stylesheet(old, &self.guard.read())
1533 }
1534
1535 crate::net::fetch_font_face(
1537 self.tx.clone(),
1538 self.id,
1539 Some(node_id),
1540 &stylesheet.0,
1541 &self.net_provider,
1542 &self.shell_provider,
1543 &self.guard.read(),
1544 self.abort_signal.as_ref(),
1545 );
1546
1547 let element = &mut self.nodes[node_id].element_data_mut().unwrap();
1549 element.special_data = SpecialElementData::Stylesheet(stylesheet.clone());
1550
1551 let insertion_point = self
1553 .nodes_to_stylesheet
1554 .range((Bound::Excluded(node_id), Bound::Unbounded))
1555 .next()
1556 .map(|(_, sheet)| sheet);
1557
1558 if let Some(insertion_point) = insertion_point {
1559 self.stylist.insert_stylesheet_before(
1560 stylesheet,
1561 insertion_point.clone(),
1562 &self.guard.read(),
1563 )
1564 } else {
1565 self.stylist
1566 .append_stylesheet(stylesheet, &self.guard.read())
1567 }
1568 }
1569
1570 pub fn handle_messages(&mut self) {
1571 let rx = self.rx.take().unwrap();
1574
1575 while let Ok(msg) = rx.try_recv() {
1576 self.handle_message(msg);
1577 }
1578
1579 self.rx = Some(rx);
1581 }
1582
1583 pub fn handle_message(&mut self, msg: DocumentEvent) {
1584 match msg {
1585 DocumentEvent::ResourceLoad(resource) => self.load_resource(resource),
1586 DocumentEvent::NavigateIframe { node_id, url } => self.navigate_iframe(node_id, url),
1587 }
1588 }
1589
1590 pub fn has_pending_critical_resources(&self) -> bool {
1592 !self.pending_critical_resources.is_empty()
1593 }
1594
1595 pub fn pending_image_count(&self) -> usize {
1602 self.pending_images.len()
1603 }
1604
1605 pub fn load_resource(&mut self, res: ResourceLoadResponse) {
1606 self.pending_critical_resources.remove(&res.request_id);
1607
1608 let resource = match res.result {
1609 Ok(resource) => resource,
1610 Err(err) => {
1611 if let Some(url) = res.resolved_url.as_ref() {
1612 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1613 #[cfg(feature = "tracing")]
1614 tracing::warn!(
1615 url = url.as_str(),
1616 waiting_nodes = waiting_nodes.len(),
1617 error = err.as_str(),
1618 "Resource load failed"
1619 );
1620 #[cfg(not(feature = "tracing"))]
1621 let _ = (waiting_nodes, err);
1622 } else {
1623 #[cfg(feature = "tracing")]
1624 tracing::warn!(error = err.as_str(), "Resource load failed (no url)");
1625 #[cfg(not(feature = "tracing"))]
1626 let _ = err;
1627 }
1628 return;
1629 }
1630 };
1631
1632 match resource {
1633 Resource::Css(css) => {
1634 let node_id = res.node_id.unwrap();
1635 self.add_stylesheet_for_node(css, node_id);
1636 }
1637 Resource::ImportSheet(import_rule, sheet) => {
1638 {
1647 let mut guard = self.guard.write();
1648 import_rule.write_with(&mut guard).stylesheet =
1649 style::stylesheets::import_rule::ImportSheet::Sheet(sheet.clone());
1650 }
1651
1652 crate::net::fetch_font_face(
1656 self.tx.clone(),
1657 self.id,
1658 res.node_id,
1659 &sheet,
1660 &self.net_provider,
1661 &self.shell_provider,
1662 &self.guard.read(),
1663 self.abort_signal.as_ref(),
1664 );
1665 }
1666 Resource::Image(_kind, width, height, image_data) => {
1667 let image = ImageData::Raster(RasterImageData::new(width, height, image_data));
1669
1670 let Some(url) = res.resolved_url.as_ref() else {
1671 return;
1672 };
1673
1674 self.apply_loaded_image(url, image);
1675 }
1676 #[cfg(feature = "svg")]
1677 Resource::Svg(_kind, svg) => {
1678 let image = ImageData::Svg(svg);
1680
1681 let Some(url) = res.resolved_url.as_ref() else {
1682 return;
1683 };
1684
1685 self.apply_loaded_image(url, image);
1686 }
1687 Resource::DocumentSrc(html) => {
1688 let Some(node_id) = res.node_id else {
1689 return;
1690 };
1691 self.apply_iframe_html(node_id, res.request_id, res.resolved_url, &html);
1692 }
1693 Resource::Font(bytes, overrides) => {
1694 let font = Blob::new(Arc::new(bytes));
1695
1696 let weight_override = overrides.weight.map(parley::fontique::FontWeight::new);
1702 let info_override = parley::fontique::FontInfoOverride {
1703 family_name: overrides.family_name.as_deref(),
1704 weight: weight_override,
1705 style: overrides.style,
1706 ..Default::default()
1707 };
1708
1709 let mut global_font_ctx = self.font_ctx.lock().unwrap();
1711 global_font_ctx
1712 .collection
1713 .register_fonts(font.clone(), Some(info_override));
1714
1715 #[cfg(feature = "parallel-construct")]
1716 {
1717 rayon::broadcast(|_ctx| {
1718 let mut font_ctx = self
1719 .thread_font_contexts
1720 .get_or(|| RefCell::new(Box::new(global_font_ctx.clone())))
1721 .borrow_mut();
1722 font_ctx
1723 .collection
1724 .register_fonts(font.clone(), Some(info_override));
1725 });
1726 }
1727 drop(global_font_ctx);
1728
1729 self.invalidate_inline_contexts();
1731 }
1732 Resource::None => {
1733 }
1735 }
1736 }
1737
1738 fn apply_loaded_image(&mut self, url: &str, image: ImageData) {
1741 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1743
1744 #[cfg(feature = "tracing")]
1745 tracing::info!(
1746 "Image {url} loaded, applying to {} nodes",
1747 waiting_nodes.len()
1748 );
1749
1750 self.image_cache.insert(url.to_string(), image.clone());
1752
1753 for (node_id, image_type) in waiting_nodes {
1755 let Some(node) = self.get_node_mut(node_id) else {
1756 continue;
1757 };
1758
1759 match image_type {
1760 ImageType::Image => {
1761 node.element_data_mut().unwrap().special_data =
1762 SpecialElementData::Image(Box::new(image.clone()));
1763
1764 node.cache_mut().clear();
1766 node.insert_damage(ALL_DAMAGE);
1767 }
1768 ImageType::Background(idx) | ImageType::Mask(idx) => {
1769 let layer_image = node.element_data_mut().and_then(|el| {
1770 let images = match image_type {
1771 ImageType::Background(_) => &mut el.background_images,
1772 ImageType::Mask(_) => &mut el.mask_images,
1773 ImageType::Image => unreachable!(),
1774 };
1775 images.get_mut(idx)
1776 });
1777 if let Some(Some(layer_image)) = layer_image {
1778 layer_image.status = Status::Ok;
1779 layer_image.image = image.clone();
1780 }
1781 }
1782 }
1783 }
1784 }
1785
1786 pub fn snapshot_node(&mut self, node_id: NodeId) {
1787 let node = &mut self.nodes[node_id];
1788
1789 let has_been_styled = node.primary_styles().is_some();
1794 if !has_been_styled {
1795 return;
1796 }
1797
1798 let opaque_node_id = TNode::opaque(&&*node);
1799 node.set_has_snapshot(true);
1800 node.snapshot_handled()
1801 .store(false, std::sync::atomic::Ordering::SeqCst);
1802
1803 if let Some(_existing_snapshot) = self.snapshots.get_mut(&opaque_node_id) {
1805 } else {
1808 let attrs: Option<Vec<_>> = node.attrs().map(|attrs| {
1809 attrs
1810 .iter()
1811 .map(|attr| {
1812 let ident = AttrIdentifier {
1813 local_name: GenericAtomIdent(attr.name.local.clone()),
1814 name: GenericAtomIdent(attr.name.local.clone()),
1815 namespace: GenericAtomIdent(attr.name.ns.clone()),
1816 prefix: None,
1817 };
1818
1819 let value = if attr.name.local == local_name!("id") {
1820 AttrValue::Atom(Atom::from(&*attr.value))
1821 } else if attr.name.local == local_name!("class") {
1822 let classes = attr
1823 .value
1824 .split_ascii_whitespace()
1825 .map(Atom::from)
1826 .collect();
1827 AttrValue::TokenList(OnceLock::from(attr.value.to_string()), classes)
1833 } else {
1834 AttrValue::String(attr.value.to_string())
1835 };
1836
1837 (ident, value)
1838 })
1839 .collect()
1840 });
1841
1842 let changed_attrs = attrs
1843 .as_ref()
1844 .map(|attrs| attrs.iter().map(|attr| attr.0.name.clone()).collect())
1845 .unwrap_or_default();
1846
1847 self.snapshots.insert(
1848 opaque_node_id,
1849 ServoElementSnapshot {
1850 state: Some(*node.element_state()),
1851 attrs,
1852 changed_attrs,
1853 class_changed: true,
1854 id_changed: true,
1855 other_attributes_changed: true,
1856 },
1857 );
1858 }
1859 }
1860
1861 pub fn snapshot_node_and(&mut self, node_id: NodeId, cb: impl FnOnce(&mut Node)) {
1868 if !self.nodes.contains_key(node_id) {
1869 return;
1870 }
1871 self.snapshot_node(node_id);
1872 cb(&mut self.nodes[node_id]);
1873 }
1874
1875 pub fn hit(&self, x: f32, y: f32) -> Option<HitResult> {
1877 self.hit_with_scrollbar(x, y).0
1878 }
1879
1880 pub fn nearest_non_anonymous_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
1893 let mut node = self.get_node(node_id)?;
1897 loop {
1898 let parent = match node.parent {
1899 Some(parent_id) => self.get_node(parent_id)?,
1900 None => return Some(node.id),
1901 };
1902 if !node.is_anonymous() && !parent.is_anonymous() {
1903 return Some(node.id);
1904 }
1905 node = parent;
1906 }
1907 }
1908
1909 pub fn focus_next_node(&mut self) -> Option<NodeId> {
1910 let focussed_node_id = self.get_focussed_node_id()?;
1911 let id = self.next_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
1912 self.set_focus_to(id);
1913 Some(id)
1914 }
1915
1916 pub fn focus_prev_node(&mut self) -> Option<NodeId> {
1918 let focussed_node_id = self.get_focussed_node_id()?;
1919 let id = self.prev_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
1920 self.set_focus_to(id);
1921 Some(id)
1922 }
1923
1924 pub fn clear_focus(&mut self) {
1926 if let Some(id) = self.focus_node_id {
1927 let shell_provider = self.shell_provider.clone();
1928 self.snapshot_node_and(id, |node| node.blur(shell_provider));
1929 self.focus_node_id = None;
1930 }
1931 }
1932
1933 pub fn set_mousedown_node_id(&mut self, node_id: Option<NodeId>) {
1934 self.mousedown_node_id = node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
1935 }
1936 pub fn set_focus_to(&mut self, focus_node_id: NodeId) -> bool {
1937 let Some(focus_node_id) = self.nearest_non_anonymous_ancestor(focus_node_id) else {
1938 return false;
1939 };
1940 if Some(focus_node_id) == self.focus_node_id {
1941 return false;
1942 }
1943
1944 #[cfg(feature = "tracing")]
1945 tracing::info!("Focussed node {focus_node_id}");
1946
1947 let shell_provider = self.shell_provider.clone();
1948
1949 if let Some(id) = self.focus_node_id {
1951 self.snapshot_node_and(id, |node| node.blur(shell_provider.clone()));
1952 }
1953
1954 self.snapshot_node_and(focus_node_id, |node| node.focus(shell_provider));
1956
1957 self.focus_node_id = Some(focus_node_id);
1958
1959 true
1960 }
1961
1962 pub fn active_node(&mut self) -> bool {
1963 let Some(hover_node_id) = self.get_hover_node_id() else {
1964 return false;
1965 };
1966
1967 if let Some(active_node_id) = self.active_node_id {
1968 if active_node_id == hover_node_id {
1969 return true;
1970 }
1971 self.unactive_node();
1972 }
1973
1974 debug_assert!(
1976 self.get_node(hover_node_id)
1977 .is_some_and(|node| !node.is_anonymous()),
1978 "interaction state must reference DOM nodes, not layout-generated nodes"
1979 );
1980 let active_node_id = Some(hover_node_id);
1981
1982 let node_path = self.maybe_node_layout_ancestors(active_node_id);
1983 for &id in node_path.iter() {
1984 self.snapshot_node_and(id, |node| node.active());
1985 }
1986
1987 self.active_node_id = active_node_id;
1988
1989 true
1990 }
1991
1992 pub fn unactive_node(&mut self) -> bool {
1993 let Some(active_node_id) = self.active_node_id.take() else {
1994 return false;
1995 };
1996
1997 let node_path = self.maybe_node_layout_ancestors(Some(active_node_id));
1998 for &id in node_path.iter() {
1999 self.snapshot_node_and(id, |node| node.unactive());
2000 }
2001
2002 true
2003 }
2004
2005 pub fn hovered_scrollbar(&self) -> Option<crate::node::ScrollbarRef> {
2007 self.hovered_scrollbar
2008 }
2009
2010 pub fn scrollbar_drag_target(&self) -> Option<crate::node::ScrollbarRef> {
2012 match &self.drag_mode {
2013 DragMode::ScrollbarDrag(state) => Some(state.scrollbar),
2014 _ => None,
2015 }
2016 }
2017
2018 pub fn scrollbar_opacity(&self, node_id: NodeId) -> f32 {
2023 let interacting = |scrollbar: &crate::node::ScrollbarRef| scrollbar.node_id == node_id;
2024 if self.hovered_scrollbar.as_ref().is_some_and(interacting)
2025 || self
2026 .scrollbar_drag_target()
2027 .as_ref()
2028 .is_some_and(interacting)
2029 {
2030 return 1.0;
2031 }
2032 self.scrollbar_activity.get(&node_id).map_or(1.0, |last| {
2033 crate::node::scrollbar::opacity_at(last.elapsed())
2034 })
2035 }
2036
2037 pub(crate) fn show_scrollbars(&mut self, node_id: NodeId) {
2040 if cfg!(feature = "scrollbars") {
2041 self.scrollbar_activity.insert(node_id, Instant::now());
2042 }
2043 }
2044
2045 fn scrollbars_animating(&self) -> bool {
2048 use crate::node::scrollbar::{FADE_DELAY, FADE_DURATION};
2049 self.scrollbar_activity
2050 .values()
2051 .any(|last| last.elapsed() < FADE_DELAY + FADE_DURATION)
2052 }
2053
2054 pub(crate) fn hit_with_scrollbar(
2058 &self,
2059 x: f32,
2060 y: f32,
2061 ) -> (Option<HitResult>, Option<crate::node::ScrollbarRef>) {
2062 if TDocument::as_node(&self.root_node())
2063 .first_element_child()
2064 .is_none()
2065 {
2066 #[cfg(feature = "tracing")]
2067 tracing::warn!("No DOM - not resolving hit test");
2068 return (None, None);
2069 }
2070 let mut scrollbar = None;
2071 let hit = self
2072 .root_element()
2073 .hit_inner(x, y, self.viewport().scale_f64(), &mut scrollbar);
2074 (hit, scrollbar)
2075 }
2076
2077 pub fn set_hover_to(&mut self, x: f32, y: f32) -> bool {
2078 self.semantic_hover_node_id = None;
2079 self.last_client_pointer_position = Some(taffy::Point {
2083 x: x - self.viewport_scroll.x as f32,
2084 y: y - self.viewport_scroll.y as f32,
2085 });
2086
2087 let (hit, hovered_scrollbar) = self.hit_with_scrollbar(x, y);
2088 let hovered_scrollbar =
2091 hovered_scrollbar.filter(|scrollbar| self.scrollbar_opacity(scrollbar.node_id) > 0.0);
2092 let scrollbar_changed = hovered_scrollbar != self.hovered_scrollbar;
2096 if scrollbar_changed {
2097 for scrollbar in [self.hovered_scrollbar, hovered_scrollbar]
2100 .into_iter()
2101 .flatten()
2102 {
2103 self.show_scrollbars(scrollbar.node_id);
2104 }
2105 }
2106 self.hovered_scrollbar = hovered_scrollbar;
2107
2108 let hit_node_id = hit.map(|hit| hit.node_id);
2113 let hover_node_id = hit_node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
2114 let new_is_text = hit.map(|hit| hit.is_text).unwrap_or(false);
2115
2116 self.apply_hover_target(hit_node_id, hover_node_id, new_is_text, scrollbar_changed)
2117 }
2118
2119 pub fn set_hover_to_node(&mut self, node_id: NodeId, x: f32, y: f32) -> bool {
2127 self.semantic_hover_node_id = Some(node_id);
2128 self.last_client_pointer_position = Some(taffy::Point {
2129 x: x - self.viewport_scroll.x as f32,
2130 y: y - self.viewport_scroll.y as f32,
2131 });
2132
2133 let hovered_scrollbar = self.hovered_scrollbar.take();
2134 let scrollbar_changed = hovered_scrollbar.is_some();
2135 if let Some(scrollbar) = hovered_scrollbar {
2136 self.show_scrollbars(scrollbar.node_id);
2137 }
2138 let hover_node_id = self.nearest_non_anonymous_ancestor(node_id);
2139 self.apply_hover_target(Some(node_id), hover_node_id, false, scrollbar_changed)
2140 }
2141
2142 fn apply_hover_target(
2143 &mut self,
2144 hit_node_id: Option<NodeId>,
2145 hover_node_id: Option<NodeId>,
2146 new_is_text: bool,
2147 scrollbar_changed: bool,
2148 ) -> bool {
2149 let hit_changed =
2150 hit_node_id != self.hover_hit_node_id || new_is_text != self.hover_node_is_text;
2151 self.hover_hit_node_id = hit_node_id;
2152 self.hover_node_is_text = new_is_text;
2153
2154 if hover_node_id == self.hover_node_id {
2156 if hit_changed {
2157 self.shell_provider.set_cursor(self.get_cursor());
2161 }
2162 return scrollbar_changed;
2163 }
2164
2165 let old_node_path = self.maybe_node_layout_ancestors(self.hover_node_id);
2166 let new_node_path = self.maybe_node_layout_ancestors(hover_node_id);
2167 let same_count = old_node_path
2168 .iter()
2169 .zip(&new_node_path)
2170 .take_while(|(o, n)| o == n)
2171 .count();
2172 for &id in old_node_path.iter().skip(same_count) {
2173 self.snapshot_node_and(id, |node| node.unhover());
2174 }
2175 for &id in new_node_path.iter().skip(same_count) {
2176 self.snapshot_node_and(id, |node| node.hover());
2177 }
2178
2179 self.hover_node_id = hover_node_id;
2180
2181 self.shell_provider.set_cursor(self.get_cursor());
2183
2184 self.shell_provider.request_redraw();
2186
2187 true
2188 }
2189
2190 pub fn clear_hover(&mut self) -> bool {
2191 self.last_client_pointer_position = None;
2194 self.semantic_hover_node_id = None;
2195 self.hover_hit_node_id = None;
2196
2197 let Some(hover_node_id) = self.hover_node_id else {
2198 return false;
2199 };
2200
2201 let old_node_path = self.maybe_node_layout_ancestors(Some(hover_node_id));
2202 for &id in old_node_path.iter() {
2203 self.snapshot_node_and(id, |node| node.unhover());
2204 }
2205
2206 self.hover_node_id = None;
2207 self.hover_node_is_text = false;
2208
2209 self.shell_provider.set_cursor(self.get_cursor());
2211
2212 self.shell_provider.request_redraw();
2214
2215 true
2216 }
2217
2218 pub fn refresh_hover(&mut self) -> bool {
2224 if let Some(node_id) = self.semantic_hover_node_id {
2225 if self.get_node(node_id).is_some() {
2226 let hover_node_id = self.nearest_non_anonymous_ancestor(node_id);
2227 return self.apply_hover_target(Some(node_id), hover_node_id, false, false);
2228 }
2229 self.semantic_hover_node_id = None;
2230 }
2231 let Some(pos) = self.last_client_pointer_position else {
2232 return false;
2233 };
2234 let x = pos.x + self.viewport_scroll.x as f32;
2235 let y = pos.y + self.viewport_scroll.y as f32;
2236 self.set_hover_to(x, y)
2237 }
2238
2239 pub fn get_hover_node_id(&self) -> Option<NodeId> {
2240 self.hover_node_id
2241 }
2242
2243 pub fn get_mousedown_node_id(&self) -> Option<NodeId> {
2244 self.mousedown_node_id
2245 }
2246
2247 pub fn set_viewport(&mut self, viewport: Viewport) {
2248 let scale_has_changed = viewport.scale_f64() != self.viewport.scale_f64();
2249 self.viewport = viewport;
2250 self.set_stylist_device(make_device(
2251 &self.viewport,
2252 self.media_type.clone(),
2253 self.font_ctx.clone(),
2254 ));
2255 self.scroll_viewport_by(0.0, 0.0); if scale_has_changed {
2258 self.invalidate_inline_contexts();
2259 self.shell_provider.request_redraw();
2260 }
2261 }
2262
2263 pub fn media_type(&self) -> &MediaType {
2265 &self.media_type
2266 }
2267
2268 pub fn set_media_type(&mut self, media_type: MediaType) {
2271 if self.media_type == media_type {
2272 return;
2273 }
2274 self.media_type = media_type;
2275 self.set_stylist_device(make_device(
2276 &self.viewport,
2277 self.media_type.clone(),
2278 self.font_ctx.clone(),
2279 ));
2280 }
2281
2282 pub fn viewport(&self) -> &Viewport {
2283 &self.viewport
2284 }
2285
2286 pub fn viewport_mut(&mut self) -> ViewportMut<'_> {
2287 ViewportMut::new(self)
2288 }
2289
2290 pub fn zoom_by(&mut self, increment: f32) {
2291 *self.viewport.zoom_mut() += increment;
2292 self.set_viewport(self.viewport.clone());
2293 }
2294
2295 pub fn zoom_to(&mut self, zoom: f32) {
2296 *self.viewport.zoom_mut() = zoom;
2297 self.set_viewport(self.viewport.clone());
2298 }
2299
2300 pub fn get_viewport(&self) -> Viewport {
2301 self.viewport.clone()
2302 }
2303
2304 pub fn incremental_layout(&self) -> bool {
2306 self.incremental_layout
2307 }
2308
2309 pub fn set_incremental_layout(&mut self, enabled: bool) {
2311 self.incremental_layout = enabled;
2312 }
2313
2314 pub fn devtools(&self) -> &DevtoolSettings {
2315 &self.devtool_settings
2316 }
2317
2318 pub fn devtools_mut(&mut self) -> &mut DevtoolSettings {
2319 &mut self.devtool_settings
2320 }
2321
2322 pub fn subdoc(&self, node_id: NodeId) -> Option<&dyn Document> {
2323 self.get_node(node_id)
2324 .and_then(|node| node.element_data())
2325 .and_then(|el| el.sub_doc_data())
2326 }
2327
2328 pub fn subdoc_mut(&mut self, node_id: NodeId) -> Option<&mut dyn Document> {
2329 self.get_node_mut(node_id)
2330 .and_then(|node| node.element_data_mut())
2331 .and_then(|el| el.sub_doc_data_mut())
2332 }
2333
2334 pub fn is_animating(&self) -> bool {
2335 #[cfg(feature = "custom-widget")]
2336 let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
2337 self.nodes[node_id]
2338 .element_data()
2339 .and_then(|el| el.custom_widget_data())
2340 .is_some_and(|data| data.widget.requires_redraw())
2341 });
2342 #[cfg(not(feature = "custom-widget"))]
2343 let custom_widget_is_animating = false;
2344
2345 let animating = self.has_canvas
2346 | self.has_active_animations
2347 | (self.subdoc_animation_pacing != AnimationPacing::Idle)
2348 | custom_widget_is_animating
2349 | (self.scroll_animation != ScrollAnimationState::None)
2350 | self.scrollbars_animating();
2351
2352 if animating && crate::debug::animation_reasons_enabled() {
2353 crate::debug::report_animation_reasons(
2354 self.id(),
2355 self.has_canvas,
2356 self.has_active_animations,
2357 self.subdoc_animation_pacing != AnimationPacing::Idle,
2358 custom_widget_is_animating,
2359 self.scroll_animation != ScrollAnimationState::None,
2360 self.scrollbars_animating(),
2361 self.animating_node_names().as_deref(),
2362 );
2363 }
2364
2365 animating
2366 }
2367
2368 pub fn animation_pacing(&self) -> AnimationPacing {
2373 let focused_text_input = self.focus_node_id.is_some_and(|node_id| {
2374 self.nodes
2375 .get(node_id)
2376 .and_then(|node| node.element_data())
2377 .is_some_and(|element| element.text_input_data().is_some())
2378 });
2379 #[cfg(feature = "custom-widget")]
2380 let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
2381 self.nodes[node_id]
2382 .element_data()
2383 .and_then(|el| el.custom_widget_data())
2384 .is_some_and(|data| data.widget.requires_redraw())
2385 });
2386 #[cfg(not(feature = "custom-widget"))]
2387 let custom_widget_is_animating = false;
2388
2389 if self.has_canvas
2390 || custom_widget_is_animating
2391 || self.scroll_animation != ScrollAnimationState::None
2392 || self.scrollbars_animating()
2393 {
2394 AnimationPacing::Interactive
2395 } else if self.has_active_animations {
2396 const SLOW_ANIMATION_SECONDS: f64 = 2.0;
2397 let sets = self.animations.sets.read();
2398 let has_fast_animation_or_transition = sets.values().any(|set| {
2399 set.transitions.iter().any(|transition| {
2400 matches!(
2401 transition.state,
2402 AnimationState::Pending | AnimationState::Running
2403 )
2404 }) || set.animations.iter().any(|animation| {
2405 matches!(
2406 animation.state,
2407 AnimationState::Pending | AnimationState::Running
2408 ) && animation.duration < SLOW_ANIMATION_SECONDS
2409 })
2410 });
2411 if has_fast_animation_or_transition {
2412 AnimationPacing::Interactive
2413 } else {
2414 AnimationPacing::SlowCss
2415 }
2416 } else if focused_text_input {
2417 AnimationPacing::Caret
2418 } else if self.subdoc_animation_pacing != AnimationPacing::Idle {
2419 self.subdoc_animation_pacing
2420 } else {
2421 AnimationPacing::Idle
2422 }
2423 }
2424
2425 fn animating_node_names(&self) -> Option<String> {
2432 if !self.has_active_animations {
2433 return None;
2434 }
2435 let sets = self.animations.sets.read();
2436 let mut described: Vec<String> = sets
2437 .iter()
2438 .filter(|(_, state)| state.needs_animation_ticks())
2439 .filter_map(|(key, state)| {
2440 let node_id = NodeId::from_u64(key.node.id() as u64);
2441 let node = self.nodes.get(node_id)?;
2442 let element = node.element_data()?;
2443 let name = element
2444 .attr(local_name!("id"))
2445 .map(|id| format!("#{id}"))
2446 .or_else(|| {
2447 element
2448 .attr(local_name!("class"))
2449 .and_then(|c| c.split_ascii_whitespace().next())
2450 .map(|c| format!(".{c}"))
2451 })
2452 .unwrap_or_else(|| element.name.local.to_string());
2453 Some(format!(
2454 "{name}(anim={},trans={},in_doc={})",
2455 state.animations.len(),
2456 state.transitions.len(),
2457 node.flags.is_in_document(),
2458 ))
2459 })
2460 .collect();
2461 described.sort();
2462 described.truncate(12);
2463 Some(described.join(" "))
2464 }
2465
2466 pub fn set_stylist_device(&mut self, device: Device) {
2468 let root_styles = self
2474 .try_root_element()
2475 .and_then(|root| root.primary_styles());
2476 if let Some(root_style) = root_styles.as_deref() {
2477 device.set_root_style(root_style);
2478
2479 let font = root_style.get_font();
2480 let font_size = font.clone_font_size().computed_size();
2481 device.set_root_font_size(root_style.effective_zoom.unzoom(font_size.px()));
2482
2483 let line_height = device
2484 .calc_line_height(font, root_style.writing_mode, None)
2485 .0;
2486 device.set_root_line_height(root_style.effective_zoom.unzoom(line_height.px()));
2487 }
2488 drop(root_styles);
2489
2490 let origins = {
2491 let guard = &self.guard;
2492 let guards = StylesheetGuards {
2493 author: &guard.read(),
2494 ua_or_user: &guard.read(),
2495 };
2496 self.stylist.set_device(device, &guards)
2497 };
2498 self.stylist.force_stylesheet_origins_dirty(origins);
2499 }
2500
2501 pub fn stylist_device(&mut self) -> &Device {
2502 self.stylist.device()
2503 }
2504
2505 pub fn get_cursor(&self) -> Option<CursorIcon> {
2513 let node_id = self
2518 .hover_hit_node_id
2519 .filter(|&id| self.nodes.contains_key(id))
2520 .or(self.get_hover_node_id());
2521 let Some(node_id) = node_id else {
2522 return Some(CursorIcon::Default);
2523 };
2524 let node = &self.nodes[node_id];
2525
2526 if let Some(subdoc) = node.subdoc().map(|doc| doc.inner()) {
2527 if subdoc.hover_hit_node_id.is_some() || subdoc.get_hover_node_id().is_some() {
2533 return subdoc.get_cursor();
2534 }
2535 return Some(CursorIcon::Default);
2536 }
2537
2538 let Some(style) = node.primary_styles() else {
2539 return Some(CursorIcon::Default);
2540 };
2541 let user_select = style.clone_user_select();
2542 let keyword = style.clone_cursor().keyword;
2543
2544 if keyword != CursorKind::Auto {
2546 return stylo_to_cursor_icon(keyword);
2547 }
2548
2549 if node
2551 .element_data()
2552 .is_some_and(|e| e.text_input_data().is_some())
2553 {
2554 return Some(CursorIcon::Text);
2555 }
2556
2557 let mut maybe_node = Some(node);
2559 while let Some(node) = maybe_node {
2560 if node.is_link() {
2561 return Some(CursorIcon::Pointer);
2562 }
2563
2564 maybe_node = node.layout_parent.get().map(|node_id| node.with(node_id));
2565 }
2566
2567 if self.hover_node_is_text {
2569 return Some(match user_select {
2570 UserSelect::Text | UserSelect::All | UserSelect::Auto => CursorIcon::Text,
2571 UserSelect::None => CursorIcon::Default,
2572 });
2573 }
2574
2575 Some(CursorIcon::Default)
2577 }
2578
2579 pub fn scroll_node_by<F: FnMut(DomEvent)>(
2580 &mut self,
2581 node_id: NodeId,
2582 x: f64,
2583 y: f64,
2584 dispatch_event: F,
2585 ) {
2586 self.scroll_node_by_has_changed(node_id, x, y, dispatch_event);
2587 }
2588
2589 pub fn scroll_node_by_has_changed<F: FnMut(DomEvent)>(
2593 &mut self,
2594 node_id: NodeId,
2595 x: f64,
2596 y: f64,
2597 mut dispatch_event: F,
2598 ) -> bool {
2599 if self.try_root_element().is_some_and(|el| el.id == node_id) {
2604 let has_changed = self.scroll_viewport_by_has_changed(x, y);
2605 if has_changed {
2606 let layout = *self.root_element().final_layout();
2607 let scale = self.viewport.scale() as f64;
2608 let event = BlitzScrollEvent {
2609 scroll_top: self.viewport_scroll.y,
2610 scroll_left: self.viewport_scroll.x,
2611 scroll_width: layout.size.width.max(layout.content_size.width) as i32,
2612 scroll_height: layout.size.height.max(layout.content_size.height) as i32,
2613 client_width: (self.viewport.window_size.0 as f64 / scale) as i32,
2614 client_height: (self.viewport.window_size.1 as f64 / scale) as i32,
2615 };
2616 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
2617 }
2618 return has_changed;
2619 }
2620
2621 let Some(node) = self.nodes.get_mut(node_id) else {
2622 return false;
2623 };
2624
2625 if node
2629 .element_data()
2630 .is_some_and(|el| el.text_input_data().is_some())
2631 {
2632 let parent = node.parent;
2633 let content_box_width = node.final_layout().content_box_width();
2634 let content_box_height = node.final_layout().content_box_height();
2635 let input = node
2636 .element_data_mut()
2637 .and_then(|el| el.text_input_data_mut())
2638 .unwrap();
2639
2640 let (bubble_x, bubble_y) = if input.is_multiline {
2641 (
2642 x,
2643 input.scroll_by(y as f32, content_box_width, content_box_height) as f64,
2644 )
2645 } else {
2646 (
2647 input.scroll_by(x as f32, content_box_width, content_box_height) as f64,
2648 y,
2649 )
2650 };
2651
2652 let has_changed = bubble_x != x || bubble_y != y;
2653
2654 if bubble_x != 0.0 || bubble_y != 0.0 {
2655 let bubbled = if let Some(parent) = parent {
2656 self.scroll_node_by_has_changed(parent, bubble_x, bubble_y, dispatch_event)
2657 } else {
2658 self.scroll_viewport_by_has_changed(bubble_x, bubble_y)
2659 };
2660 return bubbled | has_changed;
2661 }
2662
2663 return has_changed;
2664 }
2665
2666 let (can_x_scroll, can_y_scroll) = node
2667 .primary_styles()
2668 .map(|styles| {
2669 (
2670 matches!(styles.clone_overflow_x(), Overflow::Scroll | Overflow::Auto),
2671 matches!(styles.clone_overflow_y(), Overflow::Scroll | Overflow::Auto),
2672 )
2673 })
2674 .unwrap_or((false, false));
2675
2676 let initial = *node.scroll_offset();
2677 let new_x = node.scroll_offset().x - x;
2678 let new_y = node.scroll_offset().y - y;
2679
2680 let mut bubble_x = 0.0;
2681 let mut bubble_y = 0.0;
2682
2683 let scroll_width = node.final_layout().scroll_width() as f64;
2684 let scroll_height = node.final_layout().scroll_height() as f64;
2685
2686 if let Some(mut sub_doc) = node.subdoc_mut().map(|doc| doc.inner_mut()) {
2688 let has_changed = if let Some(hover_node_id) = sub_doc.get_hover_node_id() {
2689 sub_doc.scroll_node_by_has_changed(hover_node_id, x, y, dispatch_event)
2690 } else {
2691 sub_doc.scroll_viewport_by_has_changed(x, y)
2692 };
2693
2694 return has_changed;
2696 }
2697
2698 if !can_x_scroll {
2700 bubble_x = x
2701 } else if new_x < 0.0 {
2702 bubble_x = -new_x;
2703 node.scroll_offset_mut().x = 0.0;
2704 } else if new_x > scroll_width {
2705 bubble_x = scroll_width - new_x;
2706 node.scroll_offset_mut().x = scroll_width;
2707 } else {
2708 node.scroll_offset_mut().x = new_x;
2709 }
2710
2711 if !can_y_scroll {
2712 bubble_y = y
2713 } else if new_y < 0.0 {
2714 bubble_y = -new_y;
2715 node.scroll_offset_mut().y = 0.0;
2716 } else if new_y > scroll_height {
2717 bubble_y = scroll_height - new_y;
2718 node.scroll_offset_mut().y = scroll_height;
2719 } else {
2720 node.scroll_offset_mut().y = new_y;
2721 }
2722
2723 let has_changed = *node.scroll_offset() != initial;
2724
2725 if has_changed {
2726 let layout = *node.final_layout();
2727 let event = BlitzScrollEvent {
2728 scroll_top: node.scroll_offset().y,
2729 scroll_left: node.scroll_offset().x,
2730 scroll_width: layout.scroll_width() as i32,
2731 scroll_height: layout.scroll_height() as i32,
2732 client_width: layout.size.width as i32,
2733 client_height: layout.size.height as i32,
2734 };
2735
2736 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
2737 }
2738
2739 let parent = node.parent;
2740 if has_changed {
2741 self.show_scrollbars(node_id);
2742 }
2743
2744 if bubble_x != 0.0 || bubble_y != 0.0 {
2745 if let Some(parent) = parent {
2746 return self.scroll_node_by_has_changed(parent, bubble_x, bubble_y, dispatch_event)
2747 | has_changed;
2748 } else {
2749 return self.scroll_viewport_by_has_changed(bubble_x, bubble_y) | has_changed;
2750 }
2751 }
2752
2753 has_changed
2754 }
2755
2756 pub fn scroll_viewport_by(&mut self, x: f64, y: f64) {
2757 self.scroll_viewport_by_has_changed(x, y);
2758 }
2759
2760 pub fn scroll_viewport_by_has_changed(&mut self, x: f64, y: f64) -> bool {
2762 let (content_width, content_height) = match self.try_root_element() {
2767 Some(root) => {
2768 let root_layout = root.final_layout();
2769 (
2770 root_layout.size.width.max(root_layout.content_size.width) as f64,
2771 root_layout.size.height.max(root_layout.content_size.height) as f64,
2772 )
2773 }
2774 None => (0.0, 0.0),
2775 };
2776 let new_scroll = (self.viewport_scroll.x - x, self.viewport_scroll.y - y);
2777 let window_width = self.viewport.window_size.0 as f64 / self.viewport.scale() as f64;
2778 let window_height = self.viewport.window_size.1 as f64 / self.viewport.scale() as f64;
2779
2780 let initial = self.viewport_scroll;
2781 self.viewport_scroll.x =
2782 f64::max(0.0, f64::min(new_scroll.0, content_width - window_width));
2783 self.viewport_scroll.y =
2784 f64::max(0.0, f64::min(new_scroll.1, content_height - window_height));
2785
2786 self.viewport_scroll != initial
2787 }
2788
2789 pub fn scroll_by(
2790 &mut self,
2791 anchor_node_id: Option<NodeId>,
2792 scroll_x: f64,
2793 scroll_y: f64,
2794 dispatch_event: &mut dyn FnMut(DomEvent),
2795 ) -> bool {
2796 if let Some(anchor_node_id) = anchor_node_id {
2797 self.scroll_node_by_has_changed(anchor_node_id, scroll_x, scroll_y, dispatch_event)
2798 } else {
2799 self.scroll_viewport_by_has_changed(scroll_x, scroll_y)
2800 }
2801 }
2802
2803 pub fn viewport_scroll(&self) -> crate::Point<f64> {
2804 self.viewport_scroll
2805 }
2806
2807 pub fn set_viewport_scroll(&mut self, scroll: crate::Point<f64>) {
2808 self.viewport_scroll = scroll;
2809 }
2810
2811 pub fn get_fragment_target(&self, fragment: &str) -> Option<NodeId> {
2816 if let Some(node_id) = self.get_element_by_id(fragment) {
2817 return Some(node_id);
2818 }
2819
2820 self.nodes.iter().find_map(|(id, node)| {
2822 let el = node.element_data()?;
2823 (el.name.local == local_name!("a") && el.attr(local_name!("name")) == Some(fragment))
2824 .then_some(id)
2825 })
2826 }
2827
2828 pub fn nearest_scroll_container(&self, node_id: NodeId) -> Option<NodeId> {
2839 let mut current = Some(node_id);
2840 for _ in 0..64 {
2841 let id = current?;
2842 let node = self.nodes.get(id)?;
2843 if node.style().overflow.x.is_scroll_container()
2844 || node.style().overflow.y.is_scroll_container()
2845 {
2846 return Some(id);
2847 }
2848 current = node.parent;
2849 }
2850 None
2851 }
2852
2853 pub fn scroll_nearest_container_by(&mut self, node_id: NodeId, x: f64, y: f64) -> bool {
2854 self.scroll_nearest_container_by_with_events(node_id, x, y, |_| {})
2855 }
2856
2857 pub fn scroll_nearest_container_by_with_events<F: FnMut(DomEvent)>(
2858 &mut self,
2859 node_id: NodeId,
2860 x: f64,
2861 y: f64,
2862 mut dispatch_event: F,
2863 ) -> bool {
2864 let mut current = Some(node_id);
2865 for _ in 0..64 {
2866 let Some(id) = current else { break };
2867 let Some(node) = self.nodes.get(id) else {
2868 break;
2869 };
2870 let scrolls = node.style().overflow.x.is_scroll_container()
2871 || node.style().overflow.y.is_scroll_container();
2872 if scrolls {
2873 self.scroll_node_by(id, x, y, &mut dispatch_event);
2874 return true;
2875 }
2876 current = node.parent;
2877 }
2878 self.scroll_viewport_by(x, y);
2879 false
2880 }
2881
2882 pub fn scroll_to_node(&mut self, node_id: NodeId) {
2883 self.scroll_to_node_with_events(node_id, |_| {});
2884 }
2885
2886 pub fn scroll_to_node_with_events<F: FnMut(DomEvent)>(
2887 &mut self,
2888 node_id: NodeId,
2889 mut dispatch_event: F,
2890 ) {
2891 let mut chain = Vec::new();
2903 let mut current = self.nodes.get(node_id).and_then(|node| node.parent);
2904 while let Some(id) = current {
2905 let Some(node) = self.nodes.get(id) else {
2906 break;
2907 };
2908 let scrolls = node.style().overflow.x.is_scroll_container()
2909 || node.style().overflow.y.is_scroll_container();
2910 if scrolls {
2911 chain.push(id);
2912 }
2913 current = node.parent;
2914 }
2915
2916 for container in chain {
2920 let Some(node) = self.nodes.get(node_id) else {
2921 return;
2922 };
2923 let target = node.absolute_position(0.0, 0.0);
2924 let Some(scroller) = self.nodes.get(container) else {
2925 continue;
2926 };
2927 let box_ = scroller.absolute_position(0.0, 0.0);
2928 let layout = scroller.final_layout();
2929 let dx = f64::from(box_.x - target.x);
2933 let dy = f64::from(box_.y - target.y);
2934 let _ = layout;
2935 self.scroll_node_by(container, dx, dy, &mut dispatch_event);
2936 }
2937
2938 let Some(node) = self.nodes.get(node_id) else {
2941 return;
2942 };
2943 let target = node.absolute_position(0.0, 0.0);
2944 let current = self.viewport_scroll;
2945
2946 let dx = current.x - target.x as f64;
2949 let dy = current.y - target.y as f64;
2950 if let Some(root) = self.try_root_element().map(|element| element.id) {
2951 self.scroll_node_by(root, dx, dy, dispatch_event);
2952 } else {
2953 self.scroll_viewport_by(dx, dy);
2954 }
2955 }
2956
2957 pub fn scroll_to_fragment(&mut self, fragment: &str) -> bool {
2963 let decoded = percent_encoding::percent_decode_str(fragment)
2965 .decode_utf8_lossy()
2966 .into_owned();
2967
2968 if !decoded.is_empty() {
2969 if let Some(node_id) = self.get_fragment_target(&decoded) {
2970 self.scroll_to_node(node_id);
2971 return true;
2972 }
2973 }
2974
2975 if decoded.is_empty() || decoded.eq_ignore_ascii_case("top") {
2978 let current = self.viewport_scroll;
2979 self.scroll_viewport_by(current.x, current.y);
2980 return true;
2981 }
2982
2983 false
2984 }
2985
2986 pub fn get_client_bounding_rect(&self, node_id: NodeId) -> Option<BoundingRect> {
2988 if let Some(rects) = self.inline_fragment_rects(node_id) {
2991 let x0 = rects.iter().map(|r| r.x).fold(f64::INFINITY, f64::min);
2992 let y0 = rects.iter().map(|r| r.y).fold(f64::INFINITY, f64::min);
2993 let x1 = rects
2994 .iter()
2995 .map(|r| r.x + r.width)
2996 .fold(f64::NEG_INFINITY, f64::max);
2997 let y1 = rects
2998 .iter()
2999 .map(|r| r.y + r.height)
3000 .fold(f64::NEG_INFINITY, f64::max);
3001 return match rects.is_empty() {
3002 true => None,
3003 false => Some(BoundingRect {
3004 x: x0,
3005 y: y0,
3006 width: x1 - x0,
3007 height: y1 - y0,
3008 }),
3009 };
3010 }
3011
3012 let node = self.get_node(node_id)?;
3013 if !matches!(
3014 node.data,
3015 NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
3016 ) {
3017 return None;
3018 }
3019 let pos = node.absolute_position(0.0, 0.0);
3020
3021 Some(BoundingRect {
3022 x: pos.x as f64 - self.viewport_scroll.x,
3023 y: pos.y as f64 - self.viewport_scroll.y,
3024 width: node.unrounded_layout().size.width as f64,
3025 height: node.unrounded_layout().size.height as f64,
3026 })
3027 }
3028
3029 pub fn node_client_rects(&self, node_id: NodeId) -> Vec<BoundingRect> {
3034 match self.inline_fragment_rects(node_id) {
3035 Some(rects) => rects,
3036 None => self.get_client_bounding_rect(node_id).into_iter().collect(),
3037 }
3038 }
3039
3040 pub(crate) fn trace_escaped_inline_fragments(&self) {
3054 static TRACE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3055 if !*TRACE.get_or_init(|| std::env::var_os("BLITZ_TRACE_INLINE").is_some()) {
3056 return;
3057 }
3058 let mut reported = 0;
3059 for (id, node) in self.nodes.iter() {
3060 if !node.is_element() {
3061 continue;
3062 }
3063 let Some(rects) = self.inline_fragment_rects(id) else {
3064 continue;
3065 };
3066 let Some(root) = node.inline_root_ancestor() else {
3067 continue;
3068 };
3069 let root_layout = root.final_layout();
3070 let root_pos = root.absolute_position(0.0, 0.0);
3071 let root_right =
3072 root_pos.x as f64 + root_layout.size.width as f64 - self.viewport_scroll.x;
3073 for rect in &rects {
3074 if rect.x + rect.width > root_right + 1.0 {
3075 reported += 1;
3076 if reported <= 12 {
3077 eprintln!(
3078 "escaped-fragment node={id:?} rect=[{:.1},{:.1} {:.1}x{:.1}] \
3079root={:?} root_right={root_right:.1} root_w={:.1} lines={} layout_scale={:.2} vp_scale={:.2} layout_w={:.1}",
3080 rect.x,
3081 rect.y,
3082 rect.width,
3083 rect.height,
3084 root.id,
3085 root_layout.size.width,
3086 root.element_data()
3087 .and_then(|e| e.inline_layout_data.as_ref())
3088 .map(|i| i.layout.len())
3089 .unwrap_or(0),
3090 root.element_data()
3091 .and_then(|e| e.inline_layout_data.as_ref())
3092 .map(|i| i.layout.scale())
3093 .unwrap_or(0.0),
3094 self.viewport.scale(),
3095 root.element_data()
3096 .and_then(|e| e.inline_layout_data.as_ref())
3097 .map(|i| i.layout.width())
3098 .unwrap_or(0.0),
3099 );
3100 }
3101 break;
3102 }
3103 }
3104 }
3105 if reported > 0 {
3106 eprintln!("escaped-fragment total={reported}");
3107 }
3108
3109 let mut narrow = 0;
3114 for (id, node) in self.nodes.iter() {
3115 let Some(inline) = node
3116 .data
3117 .downcast_element()
3118 .and_then(|element| element.inline_layout_data.as_ref())
3119 else {
3120 continue;
3121 };
3122 let box_width = node.final_layout().size.width as f64 * self.viewport.scale() as f64;
3123 let broken_at = inline.layout.width() as f64;
3124 let full = inline.layout.calculate_content_widths().max as f64;
3127 if box_width > 40.0 && broken_at < box_width * 0.6 && full > box_width * 0.9 {
3128 narrow += 1;
3129 if narrow <= 12 {
3130 eprintln!(
3131 "narrow-break node={id:?} broken_at={broken_at:.1} box={box_width:.1} \
3132 max_content={full:.1} lines={} text={:?}",
3133 inline.layout.len(),
3134 inline.text.chars().take(40).collect::<String>(),
3135 );
3136 }
3137 }
3138 }
3139 if narrow > 0 {
3140 eprintln!("narrow-break total={narrow}");
3141 }
3142 }
3143
3144 pub fn inline_fragment_rects(&self, node_id: NodeId) -> Option<Vec<BoundingRect>> {
3145 use parley::PositionedLayoutItem;
3146
3147 let node = self.get_node(node_id)?;
3148
3149 if !node.is_element() || node.flags.is_inline_root() {
3152 return None;
3153 }
3154 let display = node.primary_styles()?.clone_display();
3155 if !(display.outside() == DisplayOutside::Inline && display.inside() == DisplayInside::Flow)
3156 {
3157 return None;
3158 }
3159
3160 let inline_root = node.inline_root_ancestor()?;
3161 let inline_layout = inline_root.element_data()?.inline_layout_data.as_ref()?;
3162 let layout = &inline_layout.layout;
3163 let scale = layout.scale() as f64;
3164
3165 let is_in_target = |mut id: NodeId| -> bool {
3168 loop {
3169 if id == node_id {
3170 return true;
3171 }
3172 if id == inline_root.id {
3173 return false;
3174 }
3175 match self.get_node(id).and_then(|n| n.parent) {
3176 Some(parent) => id = parent,
3177 None => return false,
3178 }
3179 }
3180 };
3181
3182 let root_layout = inline_root.final_layout();
3184 let root_pos = inline_root.absolute_position(0.0, 0.0);
3185 let origin_x = root_pos.x as f64
3186 + (root_layout.padding.left + root_layout.border.left) as f64
3187 - self.viewport_scroll.x;
3188 let origin_y = root_pos.y as f64
3189 + (root_layout.padding.top + root_layout.border.top) as f64
3190 - self.viewport_scroll.y;
3191
3192 let mut rects: Vec<BoundingRect> = Vec::new();
3193 for line in layout.lines() {
3194 let line_metrics = line.metrics();
3195 let mut line_rect: Option<(f64, f64, f64, f64)> = None;
3197 let mut add = |x0: f64, y0: f64, x1: f64, y1: f64| {
3198 line_rect = Some(match line_rect {
3199 Some((lx0, ly0, lx1, ly1)) => {
3200 (lx0.min(x0), ly0.min(y0), lx1.max(x1), ly1.max(y1))
3201 }
3202 None => (x0, y0, x1, y1),
3203 });
3204 };
3205
3206 for item in line.items() {
3207 match item {
3208 PositionedLayoutItem::GlyphRun(glyph_run) => {
3209 if !is_in_target(glyph_run.style().brush.id) {
3210 continue;
3211 }
3212 let x0 = glyph_run.offset() as f64;
3213 let x1 = x0 + glyph_run.advance() as f64;
3214 let y0 = line_metrics.block_min_coord as f64;
3220 let y1 = line_metrics.block_max_coord as f64;
3221 add(x0, y0, x1, y1);
3222 }
3223 PositionedLayoutItem::InlineBox(inline_box) => {
3224 if !is_in_target(NodeId::from_u64(inline_box.id)) {
3225 continue;
3226 }
3227 let x0 = inline_box.x as f64;
3228 let y0 = inline_box.y as f64;
3229 add(
3230 x0,
3231 y0,
3232 x0 + inline_box.width as f64,
3233 y0 + inline_box.height as f64,
3234 );
3235 }
3236 }
3237 }
3238
3239 if let Some((x0, y0, x1, y1)) = line_rect {
3240 rects.push(BoundingRect {
3241 x: origin_x + x0 / scale,
3242 y: origin_y + y0 / scale,
3243 width: (x1 - x0) / scale,
3244 height: (y1 - y0) / scale,
3245 });
3246 }
3247 }
3248
3249 Some(rects)
3250 }
3251
3252 pub fn find_title_node(&self) -> Option<&Node> {
3253 TreeTraverser::new(self)
3254 .find(|node_id| {
3255 let node = &self.nodes[*node_id];
3256 let Some(element) = node.element_data() else {
3257 return false;
3258 };
3259 if element.name.ns != ns!(html) || element.name.local != local_name!("title") {
3260 return false;
3261 }
3262 node.parent
3263 .and_then(|parent_id| self.nodes.get(parent_id))
3264 .and_then(Node::element_data)
3265 .is_some_and(|parent| {
3266 parent.name.ns == ns!(html) && parent.name.local == local_name!("head")
3267 })
3268 })
3269 .map(|node_id| &self.nodes[node_id])
3270 }
3271
3272 pub fn with_text_input(
3273 &mut self,
3274 node_id: NodeId,
3275 cb: impl FnOnce(PlainEditorDriver<TextBrush>),
3276 ) {
3277 let Some(node) = self.nodes.get_mut(node_id) else {
3278 return;
3279 };
3280
3281 if let Some(text_input) = node
3282 .element_data_mut()
3283 .and_then(|el| el.text_input_data_mut())
3284 {
3285 let mut font_ctx = self.font_ctx.lock().unwrap();
3286 let layout_ctx = &mut self.layout_ctx;
3287 let driver = text_input.editor.driver(&mut font_ctx, layout_ctx);
3288 cb(driver)
3289 }
3290 }
3291
3292 pub(crate) fn clamp_text_input_scroll(&mut self, node_id: NodeId) {
3295 let Some(node) = self.nodes.get_mut(node_id) else {
3296 return;
3297 };
3298
3299 let content_box_width = node.final_layout().content_box_width();
3300 let content_box_height = node.final_layout().content_box_height();
3301
3302 if let Some(text_input) = node
3303 .element_data_mut()
3304 .and_then(|el| el.text_input_data_mut())
3305 {
3306 text_input.clamp_scroll_offset(content_box_width, content_box_height);
3307 }
3308 }
3309
3310 pub(crate) fn compute_has_canvas(&self) -> bool {
3311 TreeTraverser::new(self).any(|node_id| {
3312 let node = &self.nodes[node_id];
3313 let Some(element) = node.element_data() else {
3314 return false;
3315 };
3316 if element.name.local == local_name!("canvas") && element.has_attr(local_name!("src")) {
3317 return true;
3318 }
3319
3320 false
3321 })
3322 }
3323
3324 pub fn find_text_position(&self, x: f32, y: f32) -> Option<(NodeId, usize)> {
3330 let hit = self.hit(x, y)?;
3331 let hit_node = self.get_node(hit.node_id)?;
3332 let inline_root = hit_node.inline_root_ancestor()?;
3333 let byte_offset = inline_root.text_offset_at_point(hit.x, hit.y)?;
3334 Some((inline_root.id, byte_offset))
3335 }
3336
3337 pub fn find_text_range(
3343 &self,
3344 x: f32,
3345 y: f32,
3346 granularity: TextGranularity,
3347 ) -> Option<(NodeId, usize, usize)> {
3348 let hit = self.hit(x, y)?;
3349 let hit_node = self.get_node(hit.node_id)?;
3350 let inline_root = hit_node.inline_root_ancestor()?;
3351 let range = inline_root.text_range_at_point(hit.x, hit.y, granularity)?;
3352 Some((inline_root.id, range.start, range.end))
3353 }
3354
3355 pub fn set_text_selection(
3357 &mut self,
3358 anchor_node: NodeId,
3359 anchor_offset: usize,
3360 focus_node: NodeId,
3361 focus_offset: usize,
3362 ) {
3363 self.text_selection =
3364 TextSelection::new(anchor_node, anchor_offset, focus_node, focus_offset);
3365
3366 if let (Some(parent), Some(idx)) = self.anonymous_block_location(anchor_node) {
3368 self.text_selection
3369 .anchor
3370 .set_anonymous(parent, idx, anchor_offset);
3371 }
3372 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
3373 self.text_selection
3374 .focus
3375 .set_anonymous(parent, idx, focus_offset);
3376 }
3377 }
3378
3379 fn anonymous_block_location(&self, node_id: NodeId) -> (Option<NodeId>, Option<usize>) {
3382 let Some(node) = self.get_node(node_id) else {
3383 return (None, None);
3384 };
3385
3386 if !node.is_anonymous() {
3387 return (None, None);
3388 }
3389
3390 let Some(parent_id) = node.parent else {
3391 return (None, None);
3392 };
3393
3394 let Some(parent) = self.get_node(parent_id) else {
3395 return (Some(parent_id), None);
3396 };
3397
3398 let layout_children = parent.layout_children.borrow();
3399 let Some(children) = layout_children.as_ref() else {
3400 return (Some(parent_id), None);
3401 };
3402
3403 let mut anon_index = 0;
3405 for &child_id in children.iter() {
3406 if child_id == node_id {
3407 return (Some(parent_id), Some(anon_index));
3408 }
3409 if self.get_node(child_id).is_some_and(|n| n.is_anonymous()) {
3410 anon_index += 1;
3411 }
3412 }
3413
3414 (Some(parent_id), None)
3415 }
3416
3417 pub fn clear_text_selection(&mut self) {
3419 self.text_selection.clear();
3420 }
3421
3422 pub fn update_selection_focus(&mut self, focus_node: NodeId, focus_offset: usize) {
3424 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
3426 self.text_selection
3427 .focus
3428 .set_anonymous(parent, idx, focus_offset);
3429 } else {
3430 self.text_selection.set_focus(focus_node, focus_offset);
3431 }
3432 }
3433
3434 pub fn extend_text_selection_to_point(&mut self, x: f32, y: f32) -> bool {
3437 if !self.text_selection.anchor.is_some() {
3438 return false;
3439 }
3440
3441 if let Some((node, offset)) = self.find_text_position(x, y) {
3442 self.update_selection_focus(node, offset);
3443 self.shell_provider.request_redraw();
3444 true
3445 } else {
3446 false
3447 }
3448 }
3449
3450 fn find_anonymous_block_by_index(
3452 &self,
3453 parent_id: NodeId,
3454 target_index: usize,
3455 ) -> Option<NodeId> {
3456 let parent = self.get_node(parent_id)?;
3457 let layout_children = parent.layout_children.borrow();
3458 let children = layout_children.as_ref()?;
3459
3460 children
3461 .iter()
3462 .filter(|&&child_id| self.get_node(child_id).is_some_and(|n| n.is_anonymous()))
3463 .nth(target_index)
3464 .copied()
3465 }
3466
3467 pub fn has_text_selection(&self) -> bool {
3469 self.text_selection.is_active()
3470 }
3471
3472 pub fn get_selected_text(&self) -> Option<String> {
3474 let ranges = self.get_text_selection_ranges();
3475 if ranges.is_empty() {
3476 return None;
3477 }
3478
3479 let mut result = String::new();
3480 for (node_id, start, end) in &ranges {
3481 let node = self.get_node(*node_id)?;
3482 let element_data = node.element_data()?;
3483 let inline_layout = element_data.inline_layout_data.as_ref()?;
3484
3485 if *end > inline_layout.text.len() {
3486 continue;
3487 }
3488
3489 if !result.is_empty() {
3490 result.push(' ');
3491 }
3492 result.push_str(&inline_layout.text[*start..*end]);
3493 }
3494
3495 if result.is_empty() {
3496 None
3497 } else {
3498 Some(result)
3499 }
3500 }
3501
3502 pub fn get_text_selection_ranges(&self) -> Vec<(NodeId, usize, usize)> {
3505 let lookup = |parent_id, idx| self.find_anonymous_block_by_index(parent_id, idx);
3506
3507 let anchor_node = match self.text_selection.anchor.resolve_node_id(lookup) {
3508 Some(id) => id,
3509 None => return Vec::new(),
3510 };
3511 let focus_node = match self.text_selection.focus.resolve_node_id(lookup) {
3512 Some(id) => id,
3513 None => return Vec::new(),
3514 };
3515
3516 let node_is_in_doc = |node_id: NodeId| {
3519 self.nodes
3520 .get(node_id)
3521 .is_some_and(|node| node.flags.is_in_document())
3522 };
3523 if !node_is_in_doc(anchor_node) || !node_is_in_doc(focus_node) {
3524 return Vec::new();
3525 }
3526
3527 if anchor_node == focus_node {
3529 let start = self
3530 .text_selection
3531 .anchor
3532 .offset
3533 .min(self.text_selection.focus.offset);
3534 let end = self
3535 .text_selection
3536 .anchor
3537 .offset
3538 .max(self.text_selection.focus.offset);
3539
3540 if start == end {
3541 return Vec::new();
3542 }
3543 return vec![(anchor_node, start, end)];
3544 }
3545
3546 let inline_roots = self.collect_inline_roots_in_range(anchor_node, focus_node);
3548 if inline_roots.is_empty() {
3549 return Vec::new();
3550 }
3551
3552 let first_in_roots = inline_roots[0];
3555
3556 let (first_node, first_offset, last_node, last_offset) =
3557 if first_in_roots == anchor_node || (first_in_roots != focus_node) {
3558 (
3560 anchor_node,
3561 self.text_selection.anchor.offset,
3562 focus_node,
3563 self.text_selection.focus.offset,
3564 )
3565 } else {
3566 (
3568 focus_node,
3569 self.text_selection.focus.offset,
3570 anchor_node,
3571 self.text_selection.anchor.offset,
3572 )
3573 };
3574
3575 let mut ranges = Vec::with_capacity(inline_roots.len());
3576
3577 for &node_id in &inline_roots {
3578 let Some(node) = self.get_node(node_id) else {
3579 continue;
3580 };
3581 let Some(element_data) = node.element_data() else {
3582 continue;
3583 };
3584 let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
3585 continue;
3586 };
3587
3588 let text_len = inline_layout.text.len();
3589
3590 if node_id == first_node && node_id == last_node {
3591 let start = first_offset.min(last_offset);
3592 let end = first_offset.max(last_offset);
3593 if start < end && end <= text_len {
3594 ranges.push((node_id, start, end));
3595 }
3596 } else if node_id == first_node {
3597 if first_offset < text_len {
3598 ranges.push((node_id, first_offset, text_len));
3599 }
3600 } else if node_id == last_node {
3601 if last_offset > 0 && last_offset <= text_len {
3602 ranges.push((node_id, 0, last_offset));
3603 }
3604 } else if text_len > 0 {
3605 ranges.push((node_id, 0, text_len));
3606 }
3607 }
3608
3609 ranges
3610 }
3611}
3612
3613#[derive(Debug, Clone, Copy, PartialEq)]
3614pub struct BoundingRect {
3615 pub x: f64,
3616 pub y: f64,
3617 pub width: f64,
3618 pub height: f64,
3619}
3620
3621impl AsRef<BaseDocument> for BaseDocument {
3622 fn as_ref(&self) -> &BaseDocument {
3623 self
3624 }
3625}
3626
3627impl AsMut<BaseDocument> for BaseDocument {
3628 fn as_mut(&mut self) -> &mut BaseDocument {
3629 self
3630 }
3631}
3632
3633#[cfg(test)]
3634mod hover_state_tests {
3635 use super::*;
3636 use crate::{Attribute, qual_name};
3637 use blitz_traits::shell::ColorScheme;
3638
3639 fn make_doc() -> (BaseDocument, NodeId) {
3646 let mut doc = BaseDocument::new(DocumentConfig {
3647 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3648 ..Default::default()
3649 });
3650 let root_id = doc.root_node().id;
3651 let style = |value: &str| Attribute {
3652 name: qual_name!("style"),
3653 value: value.into(),
3654 };
3655
3656 let mut mutator = doc.mutate();
3657 let html = mutator.create_element(qual_name!("html"), vec![]);
3658 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
3659 let container = mutator.create_element(qual_name!("div"), vec![style("width:300px")]);
3660 let text = mutator.create_text_node("some text");
3661 let block = mutator.create_element(qual_name!("div"), vec![style("height:50px")]);
3662 mutator.append_children(container, &[text, block]);
3663 mutator.append_children(body, &[container]);
3664 mutator.append_children(html, &[body]);
3665 mutator.append_children(root_id, &[html]);
3666 drop(mutator);
3667
3668 doc.resolve(0.0);
3669 (doc, container)
3670 }
3671
3672 fn text_has_size(doc: &BaseDocument, container: NodeId) -> bool {
3676 doc.nodes[container].final_layout().size.height > 50.0
3677 }
3678
3679 #[test]
3685 fn hovering_text_in_anonymous_block_reports_text_cursor() {
3686 let (mut doc, container) = make_doc();
3687 if !text_has_size(&doc, container) {
3688 eprintln!("skipping: no usable font (text measures 0x0)");
3689 return;
3690 }
3691
3692 doc.set_hover_to(5.0, 8.0);
3693 assert!(doc.hover_node_is_text, "expected a text hit");
3694 let hit_id = doc.hover_hit_node_id.expect("expected a hit node");
3695 assert!(
3696 doc.nodes[hit_id].is_anonymous(),
3697 "expected the hit node to be the anonymous inline root"
3698 );
3699 assert_eq!(
3700 doc.get_hover_node_id(),
3701 Some(container),
3702 "expected the stored hover target to be the containing element"
3703 );
3704 assert_eq!(doc.get_cursor(), Some(CursorIcon::Text));
3705 }
3706
3707 #[test]
3708 fn semantic_hover_keeps_the_resolved_node_instead_of_hit_testing_again() {
3709 let (mut doc, container) = make_doc();
3710
3711 doc.set_hover_to_node(container, 350.0, 250.0);
3715
3716 assert_eq!(doc.get_hover_node_id(), Some(container));
3717 assert_eq!(doc.hover_hit_node_id, Some(container));
3718
3719 doc.resolve(0.0);
3720 assert_eq!(
3721 doc.get_hover_node_id(),
3722 Some(container),
3723 "a resolve must not turn semantic identity back into a coordinate hit"
3724 );
3725 }
3726
3727 #[test]
3730 fn hovering_anonymous_block_whitespace_reports_default_cursor() {
3731 let (mut doc, container) = make_doc();
3732 if !text_has_size(&doc, container) {
3733 eprintln!("skipping: no usable font (text measures 0x0)");
3734 return;
3735 }
3736
3737 doc.set_hover_to(250.0, 8.0);
3738 assert!(!doc.hover_node_is_text);
3739 assert_eq!(doc.get_hover_node_id(), Some(container));
3740 assert_eq!(doc.get_cursor(), Some(CursorIcon::Default));
3741 }
3742}
3743
3744#[cfg(test)]
3745mod control_scroll_tests {
3746 use super::*;
3747 use crate::{Attribute, qual_name};
3748 use blitz_traits::shell::ColorScheme;
3749
3750 #[test]
3751 fn controlled_scroll_dispatches_the_dom_scroll_event() {
3752 let mut doc = BaseDocument::new(DocumentConfig {
3753 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3754 ..Default::default()
3755 });
3756 let root_id = doc.root_node().id;
3757 let style = |value: &str| Attribute {
3758 name: qual_name!("style"),
3759 value: value.into(),
3760 };
3761
3762 let mut mutator = doc.mutate();
3763 let html = mutator.create_element(qual_name!("html"), vec![]);
3764 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
3765 let scroller = mutator.create_element(
3766 qual_name!("div"),
3767 vec![style("width:200px;height:100px;overflow-y:scroll")],
3768 );
3769 let spacer = mutator.create_element(qual_name!("div"), vec![style("height:400px")]);
3770 let target = mutator.create_element(qual_name!("button"), vec![style("height:40px")]);
3771 mutator.append_children(scroller, &[spacer, target]);
3772 mutator.append_children(body, &[scroller]);
3773 mutator.append_children(html, &[body]);
3774 mutator.append_children(root_id, &[html]);
3775 drop(mutator);
3776 doc.resolve(0.0);
3777
3778 doc.nodes[html].final_layout_mut().size.height = 300.0;
3782 doc.nodes[html].final_layout_mut().content_size.height = 600.0;
3783 doc.nodes[target].final_layout_mut().location.y = 400.0;
3784
3785 let mut events = Vec::new();
3786 doc.scroll_to_node_with_events(target, |event| events.push(event));
3787
3788 assert!(doc.viewport_scroll.y > 0.0);
3789 assert!(
3790 events
3791 .iter()
3792 .any(|event| { event.target == html && event.name() == "scroll" })
3793 );
3794 }
3795}
3796
3797#[cfg(test)]
3798mod font_face_override_tests {
3799 use super::*;
3800 use crate::net::{FontFaceOverrides, Resource, ResourceLoadResponse};
3801
3802 #[test]
3818 fn font_face_overrides_alias_family_name() {
3819 const ALIAS: &str = "AliasedFamily";
3820
3821 let mut document = BaseDocument::new(DocumentConfig::default());
3822
3823 {
3825 let mut ctx = document.font_ctx.lock().unwrap();
3826 assert!(
3827 ctx.collection.family_id(ALIAS).is_none(),
3828 "alias must not exist before registration",
3829 );
3830 }
3831
3832 let response = ResourceLoadResponse {
3837 request_id: 0,
3838 node_id: None,
3839 resolved_url: Some(String::from("test://aliased-family")),
3840 result: Ok(Resource::Font(
3841 blitz_traits::net::Bytes::from_static(crate::BULLET_FONT),
3842 FontFaceOverrides {
3843 family_name: Some(String::from(ALIAS)),
3844 weight: Some(800.0),
3845 style: Some(parley::fontique::FontStyle::Italic),
3846 },
3847 )),
3848 };
3849 document.load_resource(response);
3850
3851 let mut ctx = document.font_ctx.lock().unwrap();
3854 let family_id = ctx
3855 .collection
3856 .family_id(ALIAS)
3857 .expect("CSS-declared family name should be registered as a family alias");
3858 let resolved_name = ctx
3859 .collection
3860 .family_name(family_id)
3861 .expect("family id should resolve back to a name");
3862 assert_eq!(
3863 resolved_name, ALIAS,
3864 "registered family should report the CSS-declared name, \
3865 not the font file's internal `name` table entry",
3866 );
3867 }
3868}