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
1257 pub(crate) fn drop_node_ignoring_parent(&mut self, node_id: NodeId) -> Option<Node> {
1258 self.drop_node_ignoring_parent_with(node_id, &mut |_| {})
1259 }
1260
1261 pub(crate) fn drop_node_ignoring_parent_with(
1264 &mut self,
1265 node_id: NodeId,
1266 on_drop: &mut dyn FnMut(NodeId),
1267 ) -> Option<Node> {
1268 let mut node = self.remove_node_from_tree(node_id);
1269 if let Some(node) = &mut node {
1270 on_drop(node_id);
1271 if let Some(before) = node.before() {
1272 self.drop_node_ignoring_parent_with(before, on_drop);
1273 }
1274 if let Some(after) = node.after() {
1275 self.drop_node_ignoring_parent_with(after, on_drop);
1276 }
1277
1278 for &child in &node.children {
1279 self.drop_node_ignoring_parent_with(child, on_drop);
1280 }
1281
1282 for &anon_id in &node.anonymous_blocks {
1285 self.deallocate_anonymous_block(anon_id);
1286 }
1287
1288 #[cfg(feature = "shadow-dom")]
1291 if let Some(shadow_root_id) = node.shadow_root_id() {
1292 self.shadow_host_nodes.remove(&node_id);
1293 self.custom_element_nodes.remove(&node_id);
1294 self.drop_node_ignoring_parent(shadow_root_id);
1295 }
1296 }
1297 node
1298 }
1299
1300 pub(crate) fn deallocate_anonymous_block(&mut self, anon_id: NodeId) {
1303 if !self.nodes.contains_key(anon_id) {
1306 return;
1307 }
1308
1309 let nested = std::mem::take(&mut self.nodes[anon_id].anonymous_blocks);
1311 for nested_id in nested {
1312 self.deallocate_anonymous_block(nested_id);
1313 }
1314
1315 self.remove_node_from_tree(anon_id);
1316 }
1317
1318 pub fn create_text_node(&mut self, text: &str) -> NodeId {
1319 let content = text.to_string();
1320 let data = NodeData::Text(TextNodeData::new(content));
1321 self.create_node(data)
1322 }
1323
1324 pub fn deep_clone_node(&mut self, node_id: NodeId) -> NodeId {
1325 let node = &self.nodes[node_id];
1327 let mut data = node.data.clone();
1328
1329 match &mut data {
1330 NodeData::Element(elem) | NodeData::AnonymousBlock(elem) => {
1331 if let Some(arc) = elem.style_attribute.as_mut() {
1332 let read_guard = self.guard().read();
1333 let block = arc.read_with(&read_guard);
1334 *arc = ServoArc::new(self.guard().wrap(block.clone()));
1335 }
1336 }
1337 _ => {}
1338 }
1339
1340 let children = node.children.clone();
1341
1342 let new_node_id = self.create_node(data);
1344
1345 let new_children: ThinVec<NodeId> = children
1347 .into_iter()
1348 .map(|child_id| self.deep_clone_node(child_id))
1349 .collect();
1350 for &child_id in &new_children {
1351 self.nodes[child_id].parent = Some(new_node_id);
1352 }
1353 self.nodes[new_node_id].children = new_children;
1354
1355 new_node_id
1356 }
1357
1358 pub(crate) fn remove_and_drop_pe(&mut self, node_id: NodeId) -> Option<Node> {
1359 fn remove_pe_ignoring_parent(doc: &mut BaseDocument, node_id: NodeId) -> Option<Node> {
1360 let mut node = doc.remove_node_from_tree(node_id);
1361 if let Some(node) = &mut node {
1362 for &child in &node.children {
1363 remove_pe_ignoring_parent(doc, child);
1364 }
1365 for &anon_id in &node.anonymous_blocks {
1366 doc.deallocate_anonymous_block(anon_id);
1367 }
1368 }
1369 node
1370 }
1371
1372 let node = remove_pe_ignoring_parent(self, node_id);
1373
1374 if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
1376 let parent = &mut self.nodes[parent_id];
1377 parent.children.retain(|id| *id != node_id);
1378 }
1379
1380 node
1381 }
1382
1383 pub(crate) fn resolve_url(&self, raw: &str) -> url::Url {
1384 self.url.resolve_relative(raw).unwrap_or_else(|| {
1385 panic!(
1386 "to be able to resolve {raw} with the base_url: {:?}",
1387 *self.url
1388 )
1389 })
1390 }
1391
1392 pub fn print_tree(&self) {
1393 crate::util::walk_tree(0, self.root_node());
1394 }
1395
1396 pub fn print_subtree(&self, node_id: NodeId) {
1397 crate::util::walk_tree(0, &self.nodes[node_id]);
1398 }
1399
1400 pub fn reload_resource_by_href(&mut self, href_to_reload: &str) {
1401 for &node_id in self.nodes_to_stylesheet.keys() {
1402 let node = &self.nodes[node_id];
1403 let Some(element) = node.element_data() else {
1404 continue;
1405 };
1406
1407 if element.name.local == local_name!("link") {
1408 if let Some(href) = element.attr(local_name!("href")) {
1409 if href == href_to_reload {
1411 let resolved_href = self.resolve_url(href);
1412 self.net_provider.fetch(
1413 self.id(),
1414 self.build_request(resolved_href.clone()),
1415 ResourceHandler::boxed(
1416 self.tx.clone(),
1417 self.id,
1418 Some(node_id),
1419 self.shell_provider.clone(),
1420 StylesheetHandler {
1421 source_url: resolved_href,
1422 guard: self.guard.clone(),
1423 net_provider: self.net_provider.clone(),
1424 abort_signal: self.abort_signal.clone(),
1425 },
1426 ),
1427 );
1428 }
1429 }
1430 }
1431 }
1432 }
1433
1434 pub fn process_style_element(&mut self, target_id: NodeId) {
1435 let css = self.nodes[target_id].text_content();
1436 let css = html_escape::decode_html_entities(&css);
1437 let sheet = self.make_stylesheet(&css, Origin::Author);
1438 self.add_stylesheet_for_node(sheet, target_id);
1439 }
1440
1441 pub fn remove_user_agent_stylesheet(&mut self, contents: &str) {
1442 if let Some(sheet) = self.ua_stylesheets.remove(contents) {
1443 self.stylist.remove_stylesheet(sheet, &self.guard.read());
1444 }
1445 }
1446
1447 pub fn url(&self) -> &url::Url {
1449 &self.url
1450 }
1451
1452 pub fn author_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
1455 self.nodes_to_stylesheet.values()
1456 }
1457
1458 pub fn useragent_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
1460 self.ua_stylesheets.values()
1461 }
1462
1463 pub fn add_user_agent_stylesheet(&mut self, css: &str) {
1464 let sheet = self.make_stylesheet(css, Origin::UserAgent);
1465 self.ua_stylesheets.insert(css.to_string(), sheet.clone());
1466 self.stylist.append_stylesheet(sheet, &self.guard.read());
1467 }
1468
1469 pub fn make_stylesheet(&self, css: impl AsRef<str>, origin: Origin) -> DocumentStyleSheet {
1470 let data = Stylesheet::from_str(
1471 css.as_ref(),
1472 self.url.url_extra_data(),
1473 origin,
1474 ServoArc::new(self.guard.wrap(MediaList::empty())),
1475 self.guard.clone(),
1476 Some(&StylesheetLoader {
1477 tx: self.tx.clone(),
1478 doc_id: self.id,
1479 net_provider: self.net_provider.clone(),
1480 shell_provider: self.shell_provider.clone(),
1481 abort_signal: self.abort_signal.clone(),
1482 }),
1483 None,
1484 QuirksMode::NoQuirks,
1485 AllowImportRules::Yes,
1486 );
1487
1488 DocumentStyleSheet(ServoArc::new(data))
1489 }
1490
1491 pub fn upsert_stylesheet_for_node(&mut self, node_id: NodeId) {
1492 let raw_styles = self.nodes[node_id].text_content();
1493 let sheet = self.make_stylesheet(raw_styles, Origin::Author);
1494 self.add_stylesheet_for_node(sheet, node_id);
1495 }
1496
1497 pub fn add_stylesheet_for_node(&mut self, stylesheet: DocumentStyleSheet, node_id: NodeId) {
1498 let old = self.nodes_to_stylesheet.insert(node_id, stylesheet.clone());
1499
1500 if let Some(old) = old {
1501 self.stylist.remove_stylesheet(old, &self.guard.read())
1502 }
1503
1504 crate::net::fetch_font_face(
1506 self.tx.clone(),
1507 self.id,
1508 Some(node_id),
1509 &stylesheet.0,
1510 &self.net_provider,
1511 &self.shell_provider,
1512 &self.guard.read(),
1513 self.abort_signal.as_ref(),
1514 );
1515
1516 let element = &mut self.nodes[node_id].element_data_mut().unwrap();
1518 element.special_data = SpecialElementData::Stylesheet(stylesheet.clone());
1519
1520 let insertion_point = self
1522 .nodes_to_stylesheet
1523 .range((Bound::Excluded(node_id), Bound::Unbounded))
1524 .next()
1525 .map(|(_, sheet)| sheet);
1526
1527 if let Some(insertion_point) = insertion_point {
1528 self.stylist.insert_stylesheet_before(
1529 stylesheet,
1530 insertion_point.clone(),
1531 &self.guard.read(),
1532 )
1533 } else {
1534 self.stylist
1535 .append_stylesheet(stylesheet, &self.guard.read())
1536 }
1537 }
1538
1539 pub fn handle_messages(&mut self) {
1540 let rx = self.rx.take().unwrap();
1543
1544 while let Ok(msg) = rx.try_recv() {
1545 self.handle_message(msg);
1546 }
1547
1548 self.rx = Some(rx);
1550 }
1551
1552 pub fn handle_message(&mut self, msg: DocumentEvent) {
1553 match msg {
1554 DocumentEvent::ResourceLoad(resource) => self.load_resource(resource),
1555 DocumentEvent::NavigateIframe { node_id, url } => self.navigate_iframe(node_id, url),
1556 }
1557 }
1558
1559 pub fn has_pending_critical_resources(&self) -> bool {
1561 !self.pending_critical_resources.is_empty()
1562 }
1563
1564 pub fn pending_image_count(&self) -> usize {
1571 self.pending_images.len()
1572 }
1573
1574 pub fn load_resource(&mut self, res: ResourceLoadResponse) {
1575 self.pending_critical_resources.remove(&res.request_id);
1576
1577 let resource = match res.result {
1578 Ok(resource) => resource,
1579 Err(err) => {
1580 if let Some(url) = res.resolved_url.as_ref() {
1581 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1582 #[cfg(feature = "tracing")]
1583 tracing::warn!(
1584 url = url.as_str(),
1585 waiting_nodes = waiting_nodes.len(),
1586 error = err.as_str(),
1587 "Resource load failed"
1588 );
1589 #[cfg(not(feature = "tracing"))]
1590 let _ = (waiting_nodes, err);
1591 } else {
1592 #[cfg(feature = "tracing")]
1593 tracing::warn!(error = err.as_str(), "Resource load failed (no url)");
1594 #[cfg(not(feature = "tracing"))]
1595 let _ = err;
1596 }
1597 return;
1598 }
1599 };
1600
1601 match resource {
1602 Resource::Css(css) => {
1603 let node_id = res.node_id.unwrap();
1604 self.add_stylesheet_for_node(css, node_id);
1605 }
1606 Resource::ImportSheet(import_rule, sheet) => {
1607 {
1616 let mut guard = self.guard.write();
1617 import_rule.write_with(&mut guard).stylesheet =
1618 style::stylesheets::import_rule::ImportSheet::Sheet(sheet.clone());
1619 }
1620
1621 crate::net::fetch_font_face(
1625 self.tx.clone(),
1626 self.id,
1627 res.node_id,
1628 &sheet,
1629 &self.net_provider,
1630 &self.shell_provider,
1631 &self.guard.read(),
1632 self.abort_signal.as_ref(),
1633 );
1634 }
1635 Resource::Image(_kind, width, height, image_data) => {
1636 let image = ImageData::Raster(RasterImageData::new(width, height, image_data));
1638
1639 let Some(url) = res.resolved_url.as_ref() else {
1640 return;
1641 };
1642
1643 self.apply_loaded_image(url, image);
1644 }
1645 #[cfg(feature = "svg")]
1646 Resource::Svg(_kind, svg) => {
1647 let image = ImageData::Svg(svg);
1649
1650 let Some(url) = res.resolved_url.as_ref() else {
1651 return;
1652 };
1653
1654 self.apply_loaded_image(url, image);
1655 }
1656 Resource::DocumentSrc(html) => {
1657 let Some(node_id) = res.node_id else {
1658 return;
1659 };
1660 self.apply_iframe_html(node_id, res.request_id, res.resolved_url, &html);
1661 }
1662 Resource::Font(bytes, overrides) => {
1663 let font = Blob::new(Arc::new(bytes));
1664
1665 let weight_override = overrides.weight.map(parley::fontique::FontWeight::new);
1671 let info_override = parley::fontique::FontInfoOverride {
1672 family_name: overrides.family_name.as_deref(),
1673 weight: weight_override,
1674 style: overrides.style,
1675 ..Default::default()
1676 };
1677
1678 let mut global_font_ctx = self.font_ctx.lock().unwrap();
1680 global_font_ctx
1681 .collection
1682 .register_fonts(font.clone(), Some(info_override));
1683
1684 #[cfg(feature = "parallel-construct")]
1685 {
1686 rayon::broadcast(|_ctx| {
1687 let mut font_ctx = self
1688 .thread_font_contexts
1689 .get_or(|| RefCell::new(Box::new(global_font_ctx.clone())))
1690 .borrow_mut();
1691 font_ctx
1692 .collection
1693 .register_fonts(font.clone(), Some(info_override));
1694 });
1695 }
1696 drop(global_font_ctx);
1697
1698 self.invalidate_inline_contexts();
1700 }
1701 Resource::None => {
1702 }
1704 }
1705 }
1706
1707 fn apply_loaded_image(&mut self, url: &str, image: ImageData) {
1710 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1712
1713 #[cfg(feature = "tracing")]
1714 tracing::info!(
1715 "Image {url} loaded, applying to {} nodes",
1716 waiting_nodes.len()
1717 );
1718
1719 self.image_cache.insert(url.to_string(), image.clone());
1721
1722 for (node_id, image_type) in waiting_nodes {
1724 let Some(node) = self.get_node_mut(node_id) else {
1725 continue;
1726 };
1727
1728 match image_type {
1729 ImageType::Image => {
1730 node.element_data_mut().unwrap().special_data =
1731 SpecialElementData::Image(Box::new(image.clone()));
1732
1733 node.cache_mut().clear();
1735 node.insert_damage(ALL_DAMAGE);
1736 }
1737 ImageType::Background(idx) | ImageType::Mask(idx) => {
1738 let layer_image = node.element_data_mut().and_then(|el| {
1739 let images = match image_type {
1740 ImageType::Background(_) => &mut el.background_images,
1741 ImageType::Mask(_) => &mut el.mask_images,
1742 ImageType::Image => unreachable!(),
1743 };
1744 images.get_mut(idx)
1745 });
1746 if let Some(Some(layer_image)) = layer_image {
1747 layer_image.status = Status::Ok;
1748 layer_image.image = image.clone();
1749 }
1750 }
1751 }
1752 }
1753 }
1754
1755 pub fn snapshot_node(&mut self, node_id: NodeId) {
1756 let node = &mut self.nodes[node_id];
1757
1758 let has_been_styled = node.primary_styles().is_some();
1763 if !has_been_styled {
1764 return;
1765 }
1766
1767 let opaque_node_id = TNode::opaque(&&*node);
1768 node.set_has_snapshot(true);
1769 node.snapshot_handled()
1770 .store(false, std::sync::atomic::Ordering::SeqCst);
1771
1772 if let Some(_existing_snapshot) = self.snapshots.get_mut(&opaque_node_id) {
1774 } else {
1777 let attrs: Option<Vec<_>> = node.attrs().map(|attrs| {
1778 attrs
1779 .iter()
1780 .map(|attr| {
1781 let ident = AttrIdentifier {
1782 local_name: GenericAtomIdent(attr.name.local.clone()),
1783 name: GenericAtomIdent(attr.name.local.clone()),
1784 namespace: GenericAtomIdent(attr.name.ns.clone()),
1785 prefix: None,
1786 };
1787
1788 let value = if attr.name.local == local_name!("id") {
1789 AttrValue::Atom(Atom::from(&*attr.value))
1790 } else if attr.name.local == local_name!("class") {
1791 let classes = attr
1792 .value
1793 .split_ascii_whitespace()
1794 .map(Atom::from)
1795 .collect();
1796 AttrValue::TokenList(OnceLock::from(attr.value.to_string()), classes)
1802 } else {
1803 AttrValue::String(attr.value.to_string())
1804 };
1805
1806 (ident, value)
1807 })
1808 .collect()
1809 });
1810
1811 let changed_attrs = attrs
1812 .as_ref()
1813 .map(|attrs| attrs.iter().map(|attr| attr.0.name.clone()).collect())
1814 .unwrap_or_default();
1815
1816 self.snapshots.insert(
1817 opaque_node_id,
1818 ServoElementSnapshot {
1819 state: Some(*node.element_state()),
1820 attrs,
1821 changed_attrs,
1822 class_changed: true,
1823 id_changed: true,
1824 other_attributes_changed: true,
1825 },
1826 );
1827 }
1828 }
1829
1830 pub fn snapshot_node_and(&mut self, node_id: NodeId, cb: impl FnOnce(&mut Node)) {
1837 if !self.nodes.contains_key(node_id) {
1838 return;
1839 }
1840 self.snapshot_node(node_id);
1841 cb(&mut self.nodes[node_id]);
1842 }
1843
1844 pub fn hit(&self, x: f32, y: f32) -> Option<HitResult> {
1846 self.hit_with_scrollbar(x, y).0
1847 }
1848
1849 pub fn nearest_non_anonymous_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
1862 let mut node = self.get_node(node_id)?;
1866 loop {
1867 let parent = match node.parent {
1868 Some(parent_id) => self.get_node(parent_id)?,
1869 None => return Some(node.id),
1870 };
1871 if !node.is_anonymous() && !parent.is_anonymous() {
1872 return Some(node.id);
1873 }
1874 node = parent;
1875 }
1876 }
1877
1878 pub fn focus_next_node(&mut self) -> Option<NodeId> {
1879 let focussed_node_id = self.get_focussed_node_id()?;
1880 let id = self.next_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
1881 self.set_focus_to(id);
1882 Some(id)
1883 }
1884
1885 pub fn focus_prev_node(&mut self) -> Option<NodeId> {
1887 let focussed_node_id = self.get_focussed_node_id()?;
1888 let id = self.prev_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
1889 self.set_focus_to(id);
1890 Some(id)
1891 }
1892
1893 pub fn clear_focus(&mut self) {
1895 if let Some(id) = self.focus_node_id {
1896 let shell_provider = self.shell_provider.clone();
1897 self.snapshot_node_and(id, |node| node.blur(shell_provider));
1898 self.focus_node_id = None;
1899 }
1900 }
1901
1902 pub fn set_mousedown_node_id(&mut self, node_id: Option<NodeId>) {
1903 self.mousedown_node_id = node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
1904 }
1905 pub fn set_focus_to(&mut self, focus_node_id: NodeId) -> bool {
1906 let Some(focus_node_id) = self.nearest_non_anonymous_ancestor(focus_node_id) else {
1907 return false;
1908 };
1909 if Some(focus_node_id) == self.focus_node_id {
1910 return false;
1911 }
1912
1913 #[cfg(feature = "tracing")]
1914 tracing::info!("Focussed node {focus_node_id}");
1915
1916 let shell_provider = self.shell_provider.clone();
1917
1918 if let Some(id) = self.focus_node_id {
1920 self.snapshot_node_and(id, |node| node.blur(shell_provider.clone()));
1921 }
1922
1923 self.snapshot_node_and(focus_node_id, |node| node.focus(shell_provider));
1925
1926 self.focus_node_id = Some(focus_node_id);
1927
1928 true
1929 }
1930
1931 pub fn active_node(&mut self) -> bool {
1932 let Some(hover_node_id) = self.get_hover_node_id() else {
1933 return false;
1934 };
1935
1936 if let Some(active_node_id) = self.active_node_id {
1937 if active_node_id == hover_node_id {
1938 return true;
1939 }
1940 self.unactive_node();
1941 }
1942
1943 debug_assert!(
1945 self.get_node(hover_node_id)
1946 .is_some_and(|node| !node.is_anonymous()),
1947 "interaction state must reference DOM nodes, not layout-generated nodes"
1948 );
1949 let active_node_id = Some(hover_node_id);
1950
1951 let node_path = self.maybe_node_layout_ancestors(active_node_id);
1952 for &id in node_path.iter() {
1953 self.snapshot_node_and(id, |node| node.active());
1954 }
1955
1956 self.active_node_id = active_node_id;
1957
1958 true
1959 }
1960
1961 pub fn unactive_node(&mut self) -> bool {
1962 let Some(active_node_id) = self.active_node_id.take() else {
1963 return false;
1964 };
1965
1966 let node_path = self.maybe_node_layout_ancestors(Some(active_node_id));
1967 for &id in node_path.iter() {
1968 self.snapshot_node_and(id, |node| node.unactive());
1969 }
1970
1971 true
1972 }
1973
1974 pub fn hovered_scrollbar(&self) -> Option<crate::node::ScrollbarRef> {
1976 self.hovered_scrollbar
1977 }
1978
1979 pub fn scrollbar_drag_target(&self) -> Option<crate::node::ScrollbarRef> {
1981 match &self.drag_mode {
1982 DragMode::ScrollbarDrag(state) => Some(state.scrollbar),
1983 _ => None,
1984 }
1985 }
1986
1987 pub fn scrollbar_opacity(&self, node_id: NodeId) -> f32 {
1992 let interacting = |scrollbar: &crate::node::ScrollbarRef| scrollbar.node_id == node_id;
1993 if self.hovered_scrollbar.as_ref().is_some_and(interacting)
1994 || self
1995 .scrollbar_drag_target()
1996 .as_ref()
1997 .is_some_and(interacting)
1998 {
1999 return 1.0;
2000 }
2001 self.scrollbar_activity.get(&node_id).map_or(1.0, |last| {
2002 crate::node::scrollbar::opacity_at(last.elapsed())
2003 })
2004 }
2005
2006 pub(crate) fn show_scrollbars(&mut self, node_id: NodeId) {
2009 if cfg!(feature = "scrollbars") {
2010 self.scrollbar_activity.insert(node_id, Instant::now());
2011 }
2012 }
2013
2014 fn scrollbars_animating(&self) -> bool {
2017 use crate::node::scrollbar::{FADE_DELAY, FADE_DURATION};
2018 self.scrollbar_activity
2019 .values()
2020 .any(|last| last.elapsed() < FADE_DELAY + FADE_DURATION)
2021 }
2022
2023 pub(crate) fn hit_with_scrollbar(
2027 &self,
2028 x: f32,
2029 y: f32,
2030 ) -> (Option<HitResult>, Option<crate::node::ScrollbarRef>) {
2031 if TDocument::as_node(&self.root_node())
2032 .first_element_child()
2033 .is_none()
2034 {
2035 #[cfg(feature = "tracing")]
2036 tracing::warn!("No DOM - not resolving hit test");
2037 return (None, None);
2038 }
2039 let mut scrollbar = None;
2040 let hit = self
2041 .root_element()
2042 .hit_inner(x, y, self.viewport().scale_f64(), &mut scrollbar);
2043 (hit, scrollbar)
2044 }
2045
2046 pub fn set_hover_to(&mut self, x: f32, y: f32) -> bool {
2047 self.semantic_hover_node_id = None;
2048 self.last_client_pointer_position = Some(taffy::Point {
2052 x: x - self.viewport_scroll.x as f32,
2053 y: y - self.viewport_scroll.y as f32,
2054 });
2055
2056 let (hit, hovered_scrollbar) = self.hit_with_scrollbar(x, y);
2057 let hovered_scrollbar =
2060 hovered_scrollbar.filter(|scrollbar| self.scrollbar_opacity(scrollbar.node_id) > 0.0);
2061 let scrollbar_changed = hovered_scrollbar != self.hovered_scrollbar;
2065 if scrollbar_changed {
2066 for scrollbar in [self.hovered_scrollbar, hovered_scrollbar]
2069 .into_iter()
2070 .flatten()
2071 {
2072 self.show_scrollbars(scrollbar.node_id);
2073 }
2074 }
2075 self.hovered_scrollbar = hovered_scrollbar;
2076
2077 let hit_node_id = hit.map(|hit| hit.node_id);
2082 let hover_node_id = hit_node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
2083 let new_is_text = hit.map(|hit| hit.is_text).unwrap_or(false);
2084
2085 self.apply_hover_target(hit_node_id, hover_node_id, new_is_text, scrollbar_changed)
2086 }
2087
2088 pub fn set_hover_to_node(&mut self, node_id: NodeId, x: f32, y: f32) -> bool {
2096 self.semantic_hover_node_id = Some(node_id);
2097 self.last_client_pointer_position = Some(taffy::Point {
2098 x: x - self.viewport_scroll.x as f32,
2099 y: y - self.viewport_scroll.y as f32,
2100 });
2101
2102 let hovered_scrollbar = self.hovered_scrollbar.take();
2103 let scrollbar_changed = hovered_scrollbar.is_some();
2104 if let Some(scrollbar) = hovered_scrollbar {
2105 self.show_scrollbars(scrollbar.node_id);
2106 }
2107 let hover_node_id = self.nearest_non_anonymous_ancestor(node_id);
2108 self.apply_hover_target(Some(node_id), hover_node_id, false, scrollbar_changed)
2109 }
2110
2111 fn apply_hover_target(
2112 &mut self,
2113 hit_node_id: Option<NodeId>,
2114 hover_node_id: Option<NodeId>,
2115 new_is_text: bool,
2116 scrollbar_changed: bool,
2117 ) -> bool {
2118 let hit_changed =
2119 hit_node_id != self.hover_hit_node_id || new_is_text != self.hover_node_is_text;
2120 self.hover_hit_node_id = hit_node_id;
2121 self.hover_node_is_text = new_is_text;
2122
2123 if hover_node_id == self.hover_node_id {
2125 if hit_changed {
2126 self.shell_provider.set_cursor(self.get_cursor());
2130 }
2131 return scrollbar_changed;
2132 }
2133
2134 let old_node_path = self.maybe_node_layout_ancestors(self.hover_node_id);
2135 let new_node_path = self.maybe_node_layout_ancestors(hover_node_id);
2136 let same_count = old_node_path
2137 .iter()
2138 .zip(&new_node_path)
2139 .take_while(|(o, n)| o == n)
2140 .count();
2141 for &id in old_node_path.iter().skip(same_count) {
2142 self.snapshot_node_and(id, |node| node.unhover());
2143 }
2144 for &id in new_node_path.iter().skip(same_count) {
2145 self.snapshot_node_and(id, |node| node.hover());
2146 }
2147
2148 self.hover_node_id = hover_node_id;
2149
2150 self.shell_provider.set_cursor(self.get_cursor());
2152
2153 self.shell_provider.request_redraw();
2155
2156 true
2157 }
2158
2159 pub fn clear_hover(&mut self) -> bool {
2160 self.last_client_pointer_position = None;
2163 self.semantic_hover_node_id = None;
2164 self.hover_hit_node_id = None;
2165
2166 let Some(hover_node_id) = self.hover_node_id else {
2167 return false;
2168 };
2169
2170 let old_node_path = self.maybe_node_layout_ancestors(Some(hover_node_id));
2171 for &id in old_node_path.iter() {
2172 self.snapshot_node_and(id, |node| node.unhover());
2173 }
2174
2175 self.hover_node_id = None;
2176 self.hover_node_is_text = false;
2177
2178 self.shell_provider.set_cursor(self.get_cursor());
2180
2181 self.shell_provider.request_redraw();
2183
2184 true
2185 }
2186
2187 pub fn refresh_hover(&mut self) -> bool {
2193 if let Some(node_id) = self.semantic_hover_node_id {
2194 if self.get_node(node_id).is_some() {
2195 let hover_node_id = self.nearest_non_anonymous_ancestor(node_id);
2196 return self.apply_hover_target(Some(node_id), hover_node_id, false, false);
2197 }
2198 self.semantic_hover_node_id = None;
2199 }
2200 let Some(pos) = self.last_client_pointer_position else {
2201 return false;
2202 };
2203 let x = pos.x + self.viewport_scroll.x as f32;
2204 let y = pos.y + self.viewport_scroll.y as f32;
2205 self.set_hover_to(x, y)
2206 }
2207
2208 pub fn get_hover_node_id(&self) -> Option<NodeId> {
2209 self.hover_node_id
2210 }
2211
2212 pub fn get_mousedown_node_id(&self) -> Option<NodeId> {
2213 self.mousedown_node_id
2214 }
2215
2216 pub fn set_viewport(&mut self, viewport: Viewport) {
2217 let scale_has_changed = viewport.scale_f64() != self.viewport.scale_f64();
2218 self.viewport = viewport;
2219 self.set_stylist_device(make_device(
2220 &self.viewport,
2221 self.media_type.clone(),
2222 self.font_ctx.clone(),
2223 ));
2224 self.scroll_viewport_by(0.0, 0.0); if scale_has_changed {
2227 self.invalidate_inline_contexts();
2228 self.shell_provider.request_redraw();
2229 }
2230 }
2231
2232 pub fn media_type(&self) -> &MediaType {
2234 &self.media_type
2235 }
2236
2237 pub fn set_media_type(&mut self, media_type: MediaType) {
2240 if self.media_type == media_type {
2241 return;
2242 }
2243 self.media_type = media_type;
2244 self.set_stylist_device(make_device(
2245 &self.viewport,
2246 self.media_type.clone(),
2247 self.font_ctx.clone(),
2248 ));
2249 }
2250
2251 pub fn viewport(&self) -> &Viewport {
2252 &self.viewport
2253 }
2254
2255 pub fn viewport_mut(&mut self) -> ViewportMut<'_> {
2256 ViewportMut::new(self)
2257 }
2258
2259 pub fn zoom_by(&mut self, increment: f32) {
2260 *self.viewport.zoom_mut() += increment;
2261 self.set_viewport(self.viewport.clone());
2262 }
2263
2264 pub fn zoom_to(&mut self, zoom: f32) {
2265 *self.viewport.zoom_mut() = zoom;
2266 self.set_viewport(self.viewport.clone());
2267 }
2268
2269 pub fn get_viewport(&self) -> Viewport {
2270 self.viewport.clone()
2271 }
2272
2273 pub fn incremental_layout(&self) -> bool {
2275 self.incremental_layout
2276 }
2277
2278 pub fn set_incremental_layout(&mut self, enabled: bool) {
2280 self.incremental_layout = enabled;
2281 }
2282
2283 pub fn devtools(&self) -> &DevtoolSettings {
2284 &self.devtool_settings
2285 }
2286
2287 pub fn devtools_mut(&mut self) -> &mut DevtoolSettings {
2288 &mut self.devtool_settings
2289 }
2290
2291 pub fn subdoc(&self, node_id: NodeId) -> Option<&dyn Document> {
2292 self.get_node(node_id)
2293 .and_then(|node| node.element_data())
2294 .and_then(|el| el.sub_doc_data())
2295 }
2296
2297 pub fn subdoc_mut(&mut self, node_id: NodeId) -> Option<&mut dyn Document> {
2298 self.get_node_mut(node_id)
2299 .and_then(|node| node.element_data_mut())
2300 .and_then(|el| el.sub_doc_data_mut())
2301 }
2302
2303 pub fn is_animating(&self) -> bool {
2304 #[cfg(feature = "custom-widget")]
2305 let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
2306 self.nodes[node_id]
2307 .element_data()
2308 .and_then(|el| el.custom_widget_data())
2309 .is_some_and(|data| data.widget.requires_redraw())
2310 });
2311 #[cfg(not(feature = "custom-widget"))]
2312 let custom_widget_is_animating = false;
2313
2314 let animating = self.has_canvas
2315 | self.has_active_animations
2316 | (self.subdoc_animation_pacing != AnimationPacing::Idle)
2317 | custom_widget_is_animating
2318 | (self.scroll_animation != ScrollAnimationState::None)
2319 | self.scrollbars_animating();
2320
2321 if animating && crate::debug::animation_reasons_enabled() {
2322 crate::debug::report_animation_reasons(
2323 self.id(),
2324 self.has_canvas,
2325 self.has_active_animations,
2326 self.subdoc_animation_pacing != AnimationPacing::Idle,
2327 custom_widget_is_animating,
2328 self.scroll_animation != ScrollAnimationState::None,
2329 self.scrollbars_animating(),
2330 self.animating_node_names().as_deref(),
2331 );
2332 }
2333
2334 animating
2335 }
2336
2337 pub fn animation_pacing(&self) -> AnimationPacing {
2342 let focused_text_input = self.focus_node_id.is_some_and(|node_id| {
2343 self.nodes
2344 .get(node_id)
2345 .and_then(|node| node.element_data())
2346 .is_some_and(|element| element.text_input_data().is_some())
2347 });
2348 #[cfg(feature = "custom-widget")]
2349 let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
2350 self.nodes[node_id]
2351 .element_data()
2352 .and_then(|el| el.custom_widget_data())
2353 .is_some_and(|data| data.widget.requires_redraw())
2354 });
2355 #[cfg(not(feature = "custom-widget"))]
2356 let custom_widget_is_animating = false;
2357
2358 if self.has_canvas
2359 || custom_widget_is_animating
2360 || self.scroll_animation != ScrollAnimationState::None
2361 || self.scrollbars_animating()
2362 {
2363 AnimationPacing::Interactive
2364 } else if self.has_active_animations {
2365 const SLOW_ANIMATION_SECONDS: f64 = 2.0;
2366 let sets = self.animations.sets.read();
2367 let has_fast_animation_or_transition = sets.values().any(|set| {
2368 set.transitions.iter().any(|transition| {
2369 matches!(
2370 transition.state,
2371 AnimationState::Pending | AnimationState::Running
2372 )
2373 }) || set.animations.iter().any(|animation| {
2374 matches!(
2375 animation.state,
2376 AnimationState::Pending | AnimationState::Running
2377 ) && animation.duration < SLOW_ANIMATION_SECONDS
2378 })
2379 });
2380 if has_fast_animation_or_transition {
2381 AnimationPacing::Interactive
2382 } else {
2383 AnimationPacing::SlowCss
2384 }
2385 } else if focused_text_input {
2386 AnimationPacing::Caret
2387 } else if self.subdoc_animation_pacing != AnimationPacing::Idle {
2388 self.subdoc_animation_pacing
2389 } else {
2390 AnimationPacing::Idle
2391 }
2392 }
2393
2394 fn animating_node_names(&self) -> Option<String> {
2401 if !self.has_active_animations {
2402 return None;
2403 }
2404 let sets = self.animations.sets.read();
2405 let mut described: Vec<String> = sets
2406 .iter()
2407 .filter(|(_, state)| state.needs_animation_ticks())
2408 .filter_map(|(key, state)| {
2409 let node_id = NodeId::from_u64(key.node.id() as u64);
2410 let node = self.nodes.get(node_id)?;
2411 let element = node.element_data()?;
2412 let name = element
2413 .attr(local_name!("id"))
2414 .map(|id| format!("#{id}"))
2415 .or_else(|| {
2416 element
2417 .attr(local_name!("class"))
2418 .and_then(|c| c.split_ascii_whitespace().next())
2419 .map(|c| format!(".{c}"))
2420 })
2421 .unwrap_or_else(|| element.name.local.to_string());
2422 Some(format!(
2423 "{name}(anim={},trans={},in_doc={})",
2424 state.animations.len(),
2425 state.transitions.len(),
2426 node.flags.is_in_document(),
2427 ))
2428 })
2429 .collect();
2430 described.sort();
2431 described.truncate(12);
2432 Some(described.join(" "))
2433 }
2434
2435 pub fn set_stylist_device(&mut self, device: Device) {
2437 let root_styles = self
2443 .try_root_element()
2444 .and_then(|root| root.primary_styles());
2445 if let Some(root_style) = root_styles.as_deref() {
2446 device.set_root_style(root_style);
2447
2448 let font = root_style.get_font();
2449 let font_size = font.clone_font_size().computed_size();
2450 device.set_root_font_size(root_style.effective_zoom.unzoom(font_size.px()));
2451
2452 let line_height = device
2453 .calc_line_height(font, root_style.writing_mode, None)
2454 .0;
2455 device.set_root_line_height(root_style.effective_zoom.unzoom(line_height.px()));
2456 }
2457 drop(root_styles);
2458
2459 let origins = {
2460 let guard = &self.guard;
2461 let guards = StylesheetGuards {
2462 author: &guard.read(),
2463 ua_or_user: &guard.read(),
2464 };
2465 self.stylist.set_device(device, &guards)
2466 };
2467 self.stylist.force_stylesheet_origins_dirty(origins);
2468 }
2469
2470 pub fn stylist_device(&mut self) -> &Device {
2471 self.stylist.device()
2472 }
2473
2474 pub fn get_cursor(&self) -> Option<CursorIcon> {
2482 let node_id = self
2487 .hover_hit_node_id
2488 .filter(|&id| self.nodes.contains_key(id))
2489 .or(self.get_hover_node_id());
2490 let Some(node_id) = node_id else {
2491 return Some(CursorIcon::Default);
2492 };
2493 let node = &self.nodes[node_id];
2494
2495 if let Some(subdoc) = node.subdoc().map(|doc| doc.inner()) {
2496 if subdoc.hover_hit_node_id.is_some() || subdoc.get_hover_node_id().is_some() {
2502 return subdoc.get_cursor();
2503 }
2504 return Some(CursorIcon::Default);
2505 }
2506
2507 let Some(style) = node.primary_styles() else {
2508 return Some(CursorIcon::Default);
2509 };
2510 let user_select = style.clone_user_select();
2511 let keyword = style.clone_cursor().keyword;
2512
2513 if keyword != CursorKind::Auto {
2515 return stylo_to_cursor_icon(keyword);
2516 }
2517
2518 if node
2520 .element_data()
2521 .is_some_and(|e| e.text_input_data().is_some())
2522 {
2523 return Some(CursorIcon::Text);
2524 }
2525
2526 let mut maybe_node = Some(node);
2528 while let Some(node) = maybe_node {
2529 if node.is_link() {
2530 return Some(CursorIcon::Pointer);
2531 }
2532
2533 maybe_node = node.layout_parent.get().map(|node_id| node.with(node_id));
2534 }
2535
2536 if self.hover_node_is_text {
2538 return Some(match user_select {
2539 UserSelect::Text | UserSelect::All | UserSelect::Auto => CursorIcon::Text,
2540 UserSelect::None => CursorIcon::Default,
2541 });
2542 }
2543
2544 Some(CursorIcon::Default)
2546 }
2547
2548 pub fn scroll_node_by<F: FnMut(DomEvent)>(
2549 &mut self,
2550 node_id: NodeId,
2551 x: f64,
2552 y: f64,
2553 dispatch_event: F,
2554 ) {
2555 self.scroll_node_by_has_changed(node_id, x, y, dispatch_event);
2556 }
2557
2558 pub fn scroll_node_by_has_changed<F: FnMut(DomEvent)>(
2562 &mut self,
2563 node_id: NodeId,
2564 x: f64,
2565 y: f64,
2566 mut dispatch_event: F,
2567 ) -> bool {
2568 if self.try_root_element().is_some_and(|el| el.id == node_id) {
2573 let has_changed = self.scroll_viewport_by_has_changed(x, y);
2574 if has_changed {
2575 let layout = *self.root_element().final_layout();
2576 let scale = self.viewport.scale() as f64;
2577 let event = BlitzScrollEvent {
2578 scroll_top: self.viewport_scroll.y,
2579 scroll_left: self.viewport_scroll.x,
2580 scroll_width: layout.size.width.max(layout.content_size.width) as i32,
2581 scroll_height: layout.size.height.max(layout.content_size.height) as i32,
2582 client_width: (self.viewport.window_size.0 as f64 / scale) as i32,
2583 client_height: (self.viewport.window_size.1 as f64 / scale) as i32,
2584 };
2585 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
2586 }
2587 return has_changed;
2588 }
2589
2590 let Some(node) = self.nodes.get_mut(node_id) else {
2591 return false;
2592 };
2593
2594 if node
2598 .element_data()
2599 .is_some_and(|el| el.text_input_data().is_some())
2600 {
2601 let parent = node.parent;
2602 let content_box_width = node.final_layout().content_box_width();
2603 let content_box_height = node.final_layout().content_box_height();
2604 let input = node
2605 .element_data_mut()
2606 .and_then(|el| el.text_input_data_mut())
2607 .unwrap();
2608
2609 let (bubble_x, bubble_y) = if input.is_multiline {
2610 (
2611 x,
2612 input.scroll_by(y as f32, content_box_width, content_box_height) as f64,
2613 )
2614 } else {
2615 (
2616 input.scroll_by(x as f32, content_box_width, content_box_height) as f64,
2617 y,
2618 )
2619 };
2620
2621 let has_changed = bubble_x != x || bubble_y != y;
2622
2623 if bubble_x != 0.0 || bubble_y != 0.0 {
2624 let bubbled = if let Some(parent) = parent {
2625 self.scroll_node_by_has_changed(parent, bubble_x, bubble_y, dispatch_event)
2626 } else {
2627 self.scroll_viewport_by_has_changed(bubble_x, bubble_y)
2628 };
2629 return bubbled | has_changed;
2630 }
2631
2632 return has_changed;
2633 }
2634
2635 let (can_x_scroll, can_y_scroll) = node
2636 .primary_styles()
2637 .map(|styles| {
2638 (
2639 matches!(styles.clone_overflow_x(), Overflow::Scroll | Overflow::Auto),
2640 matches!(styles.clone_overflow_y(), Overflow::Scroll | Overflow::Auto),
2641 )
2642 })
2643 .unwrap_or((false, false));
2644
2645 let initial = *node.scroll_offset();
2646 let new_x = node.scroll_offset().x - x;
2647 let new_y = node.scroll_offset().y - y;
2648
2649 let mut bubble_x = 0.0;
2650 let mut bubble_y = 0.0;
2651
2652 let scroll_width = node.final_layout().scroll_width() as f64;
2653 let scroll_height = node.final_layout().scroll_height() as f64;
2654
2655 if let Some(mut sub_doc) = node.subdoc_mut().map(|doc| doc.inner_mut()) {
2657 let has_changed = if let Some(hover_node_id) = sub_doc.get_hover_node_id() {
2658 sub_doc.scroll_node_by_has_changed(hover_node_id, x, y, dispatch_event)
2659 } else {
2660 sub_doc.scroll_viewport_by_has_changed(x, y)
2661 };
2662
2663 return has_changed;
2665 }
2666
2667 if !can_x_scroll {
2669 bubble_x = x
2670 } else if new_x < 0.0 {
2671 bubble_x = -new_x;
2672 node.scroll_offset_mut().x = 0.0;
2673 } else if new_x > scroll_width {
2674 bubble_x = scroll_width - new_x;
2675 node.scroll_offset_mut().x = scroll_width;
2676 } else {
2677 node.scroll_offset_mut().x = new_x;
2678 }
2679
2680 if !can_y_scroll {
2681 bubble_y = y
2682 } else if new_y < 0.0 {
2683 bubble_y = -new_y;
2684 node.scroll_offset_mut().y = 0.0;
2685 } else if new_y > scroll_height {
2686 bubble_y = scroll_height - new_y;
2687 node.scroll_offset_mut().y = scroll_height;
2688 } else {
2689 node.scroll_offset_mut().y = new_y;
2690 }
2691
2692 let has_changed = *node.scroll_offset() != initial;
2693
2694 if has_changed {
2695 let layout = *node.final_layout();
2696 let event = BlitzScrollEvent {
2697 scroll_top: node.scroll_offset().y,
2698 scroll_left: node.scroll_offset().x,
2699 scroll_width: layout.scroll_width() as i32,
2700 scroll_height: layout.scroll_height() as i32,
2701 client_width: layout.size.width as i32,
2702 client_height: layout.size.height as i32,
2703 };
2704
2705 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
2706 }
2707
2708 let parent = node.parent;
2709 if has_changed {
2710 self.show_scrollbars(node_id);
2711 }
2712
2713 if bubble_x != 0.0 || bubble_y != 0.0 {
2714 if let Some(parent) = parent {
2715 return self.scroll_node_by_has_changed(parent, bubble_x, bubble_y, dispatch_event)
2716 | has_changed;
2717 } else {
2718 return self.scroll_viewport_by_has_changed(bubble_x, bubble_y) | has_changed;
2719 }
2720 }
2721
2722 has_changed
2723 }
2724
2725 pub fn scroll_viewport_by(&mut self, x: f64, y: f64) {
2726 self.scroll_viewport_by_has_changed(x, y);
2727 }
2728
2729 pub fn scroll_viewport_by_has_changed(&mut self, x: f64, y: f64) -> bool {
2731 let (content_width, content_height) = match self.try_root_element() {
2736 Some(root) => {
2737 let root_layout = root.final_layout();
2738 (
2739 root_layout.size.width.max(root_layout.content_size.width) as f64,
2740 root_layout.size.height.max(root_layout.content_size.height) as f64,
2741 )
2742 }
2743 None => (0.0, 0.0),
2744 };
2745 let new_scroll = (self.viewport_scroll.x - x, self.viewport_scroll.y - y);
2746 let window_width = self.viewport.window_size.0 as f64 / self.viewport.scale() as f64;
2747 let window_height = self.viewport.window_size.1 as f64 / self.viewport.scale() as f64;
2748
2749 let initial = self.viewport_scroll;
2750 self.viewport_scroll.x =
2751 f64::max(0.0, f64::min(new_scroll.0, content_width - window_width));
2752 self.viewport_scroll.y =
2753 f64::max(0.0, f64::min(new_scroll.1, content_height - window_height));
2754
2755 self.viewport_scroll != initial
2756 }
2757
2758 pub fn scroll_by(
2759 &mut self,
2760 anchor_node_id: Option<NodeId>,
2761 scroll_x: f64,
2762 scroll_y: f64,
2763 dispatch_event: &mut dyn FnMut(DomEvent),
2764 ) -> bool {
2765 if let Some(anchor_node_id) = anchor_node_id {
2766 self.scroll_node_by_has_changed(anchor_node_id, scroll_x, scroll_y, dispatch_event)
2767 } else {
2768 self.scroll_viewport_by_has_changed(scroll_x, scroll_y)
2769 }
2770 }
2771
2772 pub fn viewport_scroll(&self) -> crate::Point<f64> {
2773 self.viewport_scroll
2774 }
2775
2776 pub fn set_viewport_scroll(&mut self, scroll: crate::Point<f64>) {
2777 self.viewport_scroll = scroll;
2778 }
2779
2780 pub fn get_fragment_target(&self, fragment: &str) -> Option<NodeId> {
2785 if let Some(node_id) = self.get_element_by_id(fragment) {
2786 return Some(node_id);
2787 }
2788
2789 self.nodes.iter().find_map(|(id, node)| {
2791 let el = node.element_data()?;
2792 (el.name.local == local_name!("a") && el.attr(local_name!("name")) == Some(fragment))
2793 .then_some(id)
2794 })
2795 }
2796
2797 pub fn nearest_scroll_container(&self, node_id: NodeId) -> Option<NodeId> {
2808 let mut current = Some(node_id);
2809 for _ in 0..64 {
2810 let id = current?;
2811 let node = self.nodes.get(id)?;
2812 if node.style().overflow.x.is_scroll_container()
2813 || node.style().overflow.y.is_scroll_container()
2814 {
2815 return Some(id);
2816 }
2817 current = node.parent;
2818 }
2819 None
2820 }
2821
2822 pub fn scroll_nearest_container_by(&mut self, node_id: NodeId, x: f64, y: f64) -> bool {
2823 self.scroll_nearest_container_by_with_events(node_id, x, y, |_| {})
2824 }
2825
2826 pub fn scroll_nearest_container_by_with_events<F: FnMut(DomEvent)>(
2827 &mut self,
2828 node_id: NodeId,
2829 x: f64,
2830 y: f64,
2831 mut dispatch_event: F,
2832 ) -> bool {
2833 let mut current = Some(node_id);
2834 for _ in 0..64 {
2835 let Some(id) = current else { break };
2836 let Some(node) = self.nodes.get(id) else {
2837 break;
2838 };
2839 let scrolls = node.style().overflow.x.is_scroll_container()
2840 || node.style().overflow.y.is_scroll_container();
2841 if scrolls {
2842 self.scroll_node_by(id, x, y, &mut dispatch_event);
2843 return true;
2844 }
2845 current = node.parent;
2846 }
2847 self.scroll_viewport_by(x, y);
2848 false
2849 }
2850
2851 pub fn scroll_to_node(&mut self, node_id: NodeId) {
2852 self.scroll_to_node_with_events(node_id, |_| {});
2853 }
2854
2855 pub fn scroll_to_node_with_events<F: FnMut(DomEvent)>(
2856 &mut self,
2857 node_id: NodeId,
2858 mut dispatch_event: F,
2859 ) {
2860 let mut chain = Vec::new();
2872 let mut current = self.nodes.get(node_id).and_then(|node| node.parent);
2873 while let Some(id) = current {
2874 let Some(node) = self.nodes.get(id) else {
2875 break;
2876 };
2877 let scrolls = node.style().overflow.x.is_scroll_container()
2878 || node.style().overflow.y.is_scroll_container();
2879 if scrolls {
2880 chain.push(id);
2881 }
2882 current = node.parent;
2883 }
2884
2885 for container in chain {
2889 let Some(node) = self.nodes.get(node_id) else {
2890 return;
2891 };
2892 let target = node.absolute_position(0.0, 0.0);
2893 let Some(scroller) = self.nodes.get(container) else {
2894 continue;
2895 };
2896 let box_ = scroller.absolute_position(0.0, 0.0);
2897 let layout = scroller.final_layout();
2898 let dx = f64::from(box_.x - target.x);
2902 let dy = f64::from(box_.y - target.y);
2903 let _ = layout;
2904 self.scroll_node_by(container, dx, dy, &mut dispatch_event);
2905 }
2906
2907 let Some(node) = self.nodes.get(node_id) else {
2910 return;
2911 };
2912 let target = node.absolute_position(0.0, 0.0);
2913 let current = self.viewport_scroll;
2914
2915 let dx = current.x - target.x as f64;
2918 let dy = current.y - target.y as f64;
2919 if let Some(root) = self.try_root_element().map(|element| element.id) {
2920 self.scroll_node_by(root, dx, dy, dispatch_event);
2921 } else {
2922 self.scroll_viewport_by(dx, dy);
2923 }
2924 }
2925
2926 pub fn scroll_to_fragment(&mut self, fragment: &str) -> bool {
2932 let decoded = percent_encoding::percent_decode_str(fragment)
2934 .decode_utf8_lossy()
2935 .into_owned();
2936
2937 if !decoded.is_empty() {
2938 if let Some(node_id) = self.get_fragment_target(&decoded) {
2939 self.scroll_to_node(node_id);
2940 return true;
2941 }
2942 }
2943
2944 if decoded.is_empty() || decoded.eq_ignore_ascii_case("top") {
2947 let current = self.viewport_scroll;
2948 self.scroll_viewport_by(current.x, current.y);
2949 return true;
2950 }
2951
2952 false
2953 }
2954
2955 pub fn get_client_bounding_rect(&self, node_id: NodeId) -> Option<BoundingRect> {
2957 if let Some(rects) = self.inline_fragment_rects(node_id) {
2960 let x0 = rects.iter().map(|r| r.x).fold(f64::INFINITY, f64::min);
2961 let y0 = rects.iter().map(|r| r.y).fold(f64::INFINITY, f64::min);
2962 let x1 = rects
2963 .iter()
2964 .map(|r| r.x + r.width)
2965 .fold(f64::NEG_INFINITY, f64::max);
2966 let y1 = rects
2967 .iter()
2968 .map(|r| r.y + r.height)
2969 .fold(f64::NEG_INFINITY, f64::max);
2970 return match rects.is_empty() {
2971 true => None,
2972 false => Some(BoundingRect {
2973 x: x0,
2974 y: y0,
2975 width: x1 - x0,
2976 height: y1 - y0,
2977 }),
2978 };
2979 }
2980
2981 let node = self.get_node(node_id)?;
2982 if !matches!(
2983 node.data,
2984 NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
2985 ) {
2986 return None;
2987 }
2988 let pos = node.absolute_position(0.0, 0.0);
2989
2990 Some(BoundingRect {
2991 x: pos.x as f64 - self.viewport_scroll.x,
2992 y: pos.y as f64 - self.viewport_scroll.y,
2993 width: node.unrounded_layout().size.width as f64,
2994 height: node.unrounded_layout().size.height as f64,
2995 })
2996 }
2997
2998 pub fn node_client_rects(&self, node_id: NodeId) -> Vec<BoundingRect> {
3003 match self.inline_fragment_rects(node_id) {
3004 Some(rects) => rects,
3005 None => self.get_client_bounding_rect(node_id).into_iter().collect(),
3006 }
3007 }
3008
3009 pub(crate) fn trace_escaped_inline_fragments(&self) {
3023 static TRACE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3024 if !*TRACE.get_or_init(|| std::env::var_os("BLITZ_TRACE_INLINE").is_some()) {
3025 return;
3026 }
3027 let mut reported = 0;
3028 for (id, node) in self.nodes.iter() {
3029 if !node.is_element() {
3030 continue;
3031 }
3032 let Some(rects) = self.inline_fragment_rects(id) else {
3033 continue;
3034 };
3035 let Some(root) = node.inline_root_ancestor() else {
3036 continue;
3037 };
3038 let root_layout = root.final_layout();
3039 let root_pos = root.absolute_position(0.0, 0.0);
3040 let root_right =
3041 root_pos.x as f64 + root_layout.size.width as f64 - self.viewport_scroll.x;
3042 for rect in &rects {
3043 if rect.x + rect.width > root_right + 1.0 {
3044 reported += 1;
3045 if reported <= 12 {
3046 eprintln!(
3047 "escaped-fragment node={id:?} rect=[{:.1},{:.1} {:.1}x{:.1}] \
3048root={:?} root_right={root_right:.1} root_w={:.1} lines={} layout_scale={:.2} vp_scale={:.2} layout_w={:.1}",
3049 rect.x,
3050 rect.y,
3051 rect.width,
3052 rect.height,
3053 root.id,
3054 root_layout.size.width,
3055 root.element_data()
3056 .and_then(|e| e.inline_layout_data.as_ref())
3057 .map(|i| i.layout.len())
3058 .unwrap_or(0),
3059 root.element_data()
3060 .and_then(|e| e.inline_layout_data.as_ref())
3061 .map(|i| i.layout.scale())
3062 .unwrap_or(0.0),
3063 self.viewport.scale(),
3064 root.element_data()
3065 .and_then(|e| e.inline_layout_data.as_ref())
3066 .map(|i| i.layout.width())
3067 .unwrap_or(0.0),
3068 );
3069 }
3070 break;
3071 }
3072 }
3073 }
3074 if reported > 0 {
3075 eprintln!("escaped-fragment total={reported}");
3076 }
3077
3078 let mut narrow = 0;
3083 for (id, node) in self.nodes.iter() {
3084 let Some(inline) = node
3085 .data
3086 .downcast_element()
3087 .and_then(|element| element.inline_layout_data.as_ref())
3088 else {
3089 continue;
3090 };
3091 let box_width = node.final_layout().size.width as f64 * self.viewport.scale() as f64;
3092 let broken_at = inline.layout.width() as f64;
3093 let full = inline.layout.calculate_content_widths().max as f64;
3096 if box_width > 40.0 && broken_at < box_width * 0.6 && full > box_width * 0.9 {
3097 narrow += 1;
3098 if narrow <= 12 {
3099 eprintln!(
3100 "narrow-break node={id:?} broken_at={broken_at:.1} box={box_width:.1} \
3101 max_content={full:.1} lines={} text={:?}",
3102 inline.layout.len(),
3103 inline.text.chars().take(40).collect::<String>(),
3104 );
3105 }
3106 }
3107 }
3108 if narrow > 0 {
3109 eprintln!("narrow-break total={narrow}");
3110 }
3111 }
3112
3113 pub fn inline_fragment_rects(&self, node_id: NodeId) -> Option<Vec<BoundingRect>> {
3114 use parley::PositionedLayoutItem;
3115
3116 let node = self.get_node(node_id)?;
3117
3118 if !node.is_element() || node.flags.is_inline_root() {
3121 return None;
3122 }
3123 let display = node.primary_styles()?.clone_display();
3124 if !(display.outside() == DisplayOutside::Inline && display.inside() == DisplayInside::Flow)
3125 {
3126 return None;
3127 }
3128
3129 let inline_root = node.inline_root_ancestor()?;
3130 let inline_layout = inline_root.element_data()?.inline_layout_data.as_ref()?;
3131 let layout = &inline_layout.layout;
3132 let scale = layout.scale() as f64;
3133
3134 let is_in_target = |mut id: NodeId| -> bool {
3137 loop {
3138 if id == node_id {
3139 return true;
3140 }
3141 if id == inline_root.id {
3142 return false;
3143 }
3144 match self.get_node(id).and_then(|n| n.parent) {
3145 Some(parent) => id = parent,
3146 None => return false,
3147 }
3148 }
3149 };
3150
3151 let root_layout = inline_root.final_layout();
3153 let root_pos = inline_root.absolute_position(0.0, 0.0);
3154 let origin_x = root_pos.x as f64
3155 + (root_layout.padding.left + root_layout.border.left) as f64
3156 - self.viewport_scroll.x;
3157 let origin_y = root_pos.y as f64
3158 + (root_layout.padding.top + root_layout.border.top) as f64
3159 - self.viewport_scroll.y;
3160
3161 let mut rects: Vec<BoundingRect> = Vec::new();
3162 for line in layout.lines() {
3163 let line_metrics = line.metrics();
3164 let mut line_rect: Option<(f64, f64, f64, f64)> = None;
3166 let mut add = |x0: f64, y0: f64, x1: f64, y1: f64| {
3167 line_rect = Some(match line_rect {
3168 Some((lx0, ly0, lx1, ly1)) => {
3169 (lx0.min(x0), ly0.min(y0), lx1.max(x1), ly1.max(y1))
3170 }
3171 None => (x0, y0, x1, y1),
3172 });
3173 };
3174
3175 for item in line.items() {
3176 match item {
3177 PositionedLayoutItem::GlyphRun(glyph_run) => {
3178 if !is_in_target(glyph_run.style().brush.id) {
3179 continue;
3180 }
3181 let x0 = glyph_run.offset() as f64;
3182 let x1 = x0 + glyph_run.advance() as f64;
3183 let y0 = line_metrics.block_min_coord as f64;
3189 let y1 = line_metrics.block_max_coord as f64;
3190 add(x0, y0, x1, y1);
3191 }
3192 PositionedLayoutItem::InlineBox(inline_box) => {
3193 if !is_in_target(NodeId::from_u64(inline_box.id)) {
3194 continue;
3195 }
3196 let x0 = inline_box.x as f64;
3197 let y0 = inline_box.y as f64;
3198 add(
3199 x0,
3200 y0,
3201 x0 + inline_box.width as f64,
3202 y0 + inline_box.height as f64,
3203 );
3204 }
3205 }
3206 }
3207
3208 if let Some((x0, y0, x1, y1)) = line_rect {
3209 rects.push(BoundingRect {
3210 x: origin_x + x0 / scale,
3211 y: origin_y + y0 / scale,
3212 width: (x1 - x0) / scale,
3213 height: (y1 - y0) / scale,
3214 });
3215 }
3216 }
3217
3218 Some(rects)
3219 }
3220
3221 pub fn find_title_node(&self) -> Option<&Node> {
3222 TreeTraverser::new(self)
3223 .find(|node_id| {
3224 let node = &self.nodes[*node_id];
3225 let Some(element) = node.element_data() else {
3226 return false;
3227 };
3228 if element.name.ns != ns!(html) || element.name.local != local_name!("title") {
3229 return false;
3230 }
3231 node.parent
3232 .and_then(|parent_id| self.nodes.get(parent_id))
3233 .and_then(Node::element_data)
3234 .is_some_and(|parent| {
3235 parent.name.ns == ns!(html) && parent.name.local == local_name!("head")
3236 })
3237 })
3238 .map(|node_id| &self.nodes[node_id])
3239 }
3240
3241 pub fn with_text_input(
3242 &mut self,
3243 node_id: NodeId,
3244 cb: impl FnOnce(PlainEditorDriver<TextBrush>),
3245 ) {
3246 let Some(node) = self.nodes.get_mut(node_id) else {
3247 return;
3248 };
3249
3250 if let Some(text_input) = node
3251 .element_data_mut()
3252 .and_then(|el| el.text_input_data_mut())
3253 {
3254 let mut font_ctx = self.font_ctx.lock().unwrap();
3255 let layout_ctx = &mut self.layout_ctx;
3256 let driver = text_input.editor.driver(&mut font_ctx, layout_ctx);
3257 cb(driver)
3258 }
3259 }
3260
3261 pub(crate) fn clamp_text_input_scroll(&mut self, node_id: NodeId) {
3264 let Some(node) = self.nodes.get_mut(node_id) else {
3265 return;
3266 };
3267
3268 let content_box_width = node.final_layout().content_box_width();
3269 let content_box_height = node.final_layout().content_box_height();
3270
3271 if let Some(text_input) = node
3272 .element_data_mut()
3273 .and_then(|el| el.text_input_data_mut())
3274 {
3275 text_input.clamp_scroll_offset(content_box_width, content_box_height);
3276 }
3277 }
3278
3279 pub(crate) fn compute_has_canvas(&self) -> bool {
3280 TreeTraverser::new(self).any(|node_id| {
3281 let node = &self.nodes[node_id];
3282 let Some(element) = node.element_data() else {
3283 return false;
3284 };
3285 if element.name.local == local_name!("canvas") && element.has_attr(local_name!("src")) {
3286 return true;
3287 }
3288
3289 false
3290 })
3291 }
3292
3293 pub fn find_text_position(&self, x: f32, y: f32) -> Option<(NodeId, usize)> {
3299 let hit = self.hit(x, y)?;
3300 let hit_node = self.get_node(hit.node_id)?;
3301 let inline_root = hit_node.inline_root_ancestor()?;
3302 let byte_offset = inline_root.text_offset_at_point(hit.x, hit.y)?;
3303 Some((inline_root.id, byte_offset))
3304 }
3305
3306 pub fn find_text_range(
3312 &self,
3313 x: f32,
3314 y: f32,
3315 granularity: TextGranularity,
3316 ) -> Option<(NodeId, usize, usize)> {
3317 let hit = self.hit(x, y)?;
3318 let hit_node = self.get_node(hit.node_id)?;
3319 let inline_root = hit_node.inline_root_ancestor()?;
3320 let range = inline_root.text_range_at_point(hit.x, hit.y, granularity)?;
3321 Some((inline_root.id, range.start, range.end))
3322 }
3323
3324 pub fn set_text_selection(
3326 &mut self,
3327 anchor_node: NodeId,
3328 anchor_offset: usize,
3329 focus_node: NodeId,
3330 focus_offset: usize,
3331 ) {
3332 self.text_selection =
3333 TextSelection::new(anchor_node, anchor_offset, focus_node, focus_offset);
3334
3335 if let (Some(parent), Some(idx)) = self.anonymous_block_location(anchor_node) {
3337 self.text_selection
3338 .anchor
3339 .set_anonymous(parent, idx, anchor_offset);
3340 }
3341 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
3342 self.text_selection
3343 .focus
3344 .set_anonymous(parent, idx, focus_offset);
3345 }
3346 }
3347
3348 fn anonymous_block_location(&self, node_id: NodeId) -> (Option<NodeId>, Option<usize>) {
3351 let Some(node) = self.get_node(node_id) else {
3352 return (None, None);
3353 };
3354
3355 if !node.is_anonymous() {
3356 return (None, None);
3357 }
3358
3359 let Some(parent_id) = node.parent else {
3360 return (None, None);
3361 };
3362
3363 let Some(parent) = self.get_node(parent_id) else {
3364 return (Some(parent_id), None);
3365 };
3366
3367 let layout_children = parent.layout_children.borrow();
3368 let Some(children) = layout_children.as_ref() else {
3369 return (Some(parent_id), None);
3370 };
3371
3372 let mut anon_index = 0;
3374 for &child_id in children.iter() {
3375 if child_id == node_id {
3376 return (Some(parent_id), Some(anon_index));
3377 }
3378 if self.get_node(child_id).is_some_and(|n| n.is_anonymous()) {
3379 anon_index += 1;
3380 }
3381 }
3382
3383 (Some(parent_id), None)
3384 }
3385
3386 pub fn clear_text_selection(&mut self) {
3388 self.text_selection.clear();
3389 }
3390
3391 pub fn update_selection_focus(&mut self, focus_node: NodeId, focus_offset: usize) {
3393 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
3395 self.text_selection
3396 .focus
3397 .set_anonymous(parent, idx, focus_offset);
3398 } else {
3399 self.text_selection.set_focus(focus_node, focus_offset);
3400 }
3401 }
3402
3403 pub fn extend_text_selection_to_point(&mut self, x: f32, y: f32) -> bool {
3406 if !self.text_selection.anchor.is_some() {
3407 return false;
3408 }
3409
3410 if let Some((node, offset)) = self.find_text_position(x, y) {
3411 self.update_selection_focus(node, offset);
3412 self.shell_provider.request_redraw();
3413 true
3414 } else {
3415 false
3416 }
3417 }
3418
3419 fn find_anonymous_block_by_index(
3421 &self,
3422 parent_id: NodeId,
3423 target_index: usize,
3424 ) -> Option<NodeId> {
3425 let parent = self.get_node(parent_id)?;
3426 let layout_children = parent.layout_children.borrow();
3427 let children = layout_children.as_ref()?;
3428
3429 children
3430 .iter()
3431 .filter(|&&child_id| self.get_node(child_id).is_some_and(|n| n.is_anonymous()))
3432 .nth(target_index)
3433 .copied()
3434 }
3435
3436 pub fn has_text_selection(&self) -> bool {
3438 self.text_selection.is_active()
3439 }
3440
3441 pub fn get_selected_text(&self) -> Option<String> {
3443 let ranges = self.get_text_selection_ranges();
3444 if ranges.is_empty() {
3445 return None;
3446 }
3447
3448 let mut result = String::new();
3449 for (node_id, start, end) in &ranges {
3450 let node = self.get_node(*node_id)?;
3451 let element_data = node.element_data()?;
3452 let inline_layout = element_data.inline_layout_data.as_ref()?;
3453
3454 if *end > inline_layout.text.len() {
3455 continue;
3456 }
3457
3458 if !result.is_empty() {
3459 result.push(' ');
3460 }
3461 result.push_str(&inline_layout.text[*start..*end]);
3462 }
3463
3464 if result.is_empty() {
3465 None
3466 } else {
3467 Some(result)
3468 }
3469 }
3470
3471 pub fn get_text_selection_ranges(&self) -> Vec<(NodeId, usize, usize)> {
3474 let lookup = |parent_id, idx| self.find_anonymous_block_by_index(parent_id, idx);
3475
3476 let anchor_node = match self.text_selection.anchor.resolve_node_id(lookup) {
3477 Some(id) => id,
3478 None => return Vec::new(),
3479 };
3480 let focus_node = match self.text_selection.focus.resolve_node_id(lookup) {
3481 Some(id) => id,
3482 None => return Vec::new(),
3483 };
3484
3485 let node_is_in_doc = |node_id: NodeId| {
3488 self.nodes
3489 .get(node_id)
3490 .is_some_and(|node| node.flags.is_in_document())
3491 };
3492 if !node_is_in_doc(anchor_node) || !node_is_in_doc(focus_node) {
3493 return Vec::new();
3494 }
3495
3496 if anchor_node == focus_node {
3498 let start = self
3499 .text_selection
3500 .anchor
3501 .offset
3502 .min(self.text_selection.focus.offset);
3503 let end = self
3504 .text_selection
3505 .anchor
3506 .offset
3507 .max(self.text_selection.focus.offset);
3508
3509 if start == end {
3510 return Vec::new();
3511 }
3512 return vec![(anchor_node, start, end)];
3513 }
3514
3515 let inline_roots = self.collect_inline_roots_in_range(anchor_node, focus_node);
3517 if inline_roots.is_empty() {
3518 return Vec::new();
3519 }
3520
3521 let first_in_roots = inline_roots[0];
3524
3525 let (first_node, first_offset, last_node, last_offset) =
3526 if first_in_roots == anchor_node || (first_in_roots != focus_node) {
3527 (
3529 anchor_node,
3530 self.text_selection.anchor.offset,
3531 focus_node,
3532 self.text_selection.focus.offset,
3533 )
3534 } else {
3535 (
3537 focus_node,
3538 self.text_selection.focus.offset,
3539 anchor_node,
3540 self.text_selection.anchor.offset,
3541 )
3542 };
3543
3544 let mut ranges = Vec::with_capacity(inline_roots.len());
3545
3546 for &node_id in &inline_roots {
3547 let Some(node) = self.get_node(node_id) else {
3548 continue;
3549 };
3550 let Some(element_data) = node.element_data() else {
3551 continue;
3552 };
3553 let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
3554 continue;
3555 };
3556
3557 let text_len = inline_layout.text.len();
3558
3559 if node_id == first_node && node_id == last_node {
3560 let start = first_offset.min(last_offset);
3561 let end = first_offset.max(last_offset);
3562 if start < end && end <= text_len {
3563 ranges.push((node_id, start, end));
3564 }
3565 } else if node_id == first_node {
3566 if first_offset < text_len {
3567 ranges.push((node_id, first_offset, text_len));
3568 }
3569 } else if node_id == last_node {
3570 if last_offset > 0 && last_offset <= text_len {
3571 ranges.push((node_id, 0, last_offset));
3572 }
3573 } else if text_len > 0 {
3574 ranges.push((node_id, 0, text_len));
3575 }
3576 }
3577
3578 ranges
3579 }
3580}
3581
3582#[derive(Debug, Clone, Copy, PartialEq)]
3583pub struct BoundingRect {
3584 pub x: f64,
3585 pub y: f64,
3586 pub width: f64,
3587 pub height: f64,
3588}
3589
3590impl AsRef<BaseDocument> for BaseDocument {
3591 fn as_ref(&self) -> &BaseDocument {
3592 self
3593 }
3594}
3595
3596impl AsMut<BaseDocument> for BaseDocument {
3597 fn as_mut(&mut self) -> &mut BaseDocument {
3598 self
3599 }
3600}
3601
3602#[cfg(test)]
3603mod hover_state_tests {
3604 use super::*;
3605 use crate::{Attribute, qual_name};
3606 use blitz_traits::shell::ColorScheme;
3607
3608 fn make_doc() -> (BaseDocument, NodeId) {
3615 let mut doc = BaseDocument::new(DocumentConfig {
3616 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3617 ..Default::default()
3618 });
3619 let root_id = doc.root_node().id;
3620 let style = |value: &str| Attribute {
3621 name: qual_name!("style"),
3622 value: value.into(),
3623 };
3624
3625 let mut mutator = doc.mutate();
3626 let html = mutator.create_element(qual_name!("html"), vec![]);
3627 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
3628 let container = mutator.create_element(qual_name!("div"), vec![style("width:300px")]);
3629 let text = mutator.create_text_node("some text");
3630 let block = mutator.create_element(qual_name!("div"), vec![style("height:50px")]);
3631 mutator.append_children(container, &[text, block]);
3632 mutator.append_children(body, &[container]);
3633 mutator.append_children(html, &[body]);
3634 mutator.append_children(root_id, &[html]);
3635 drop(mutator);
3636
3637 doc.resolve(0.0);
3638 (doc, container)
3639 }
3640
3641 fn text_has_size(doc: &BaseDocument, container: NodeId) -> bool {
3645 doc.nodes[container].final_layout().size.height > 50.0
3646 }
3647
3648 #[test]
3654 fn hovering_text_in_anonymous_block_reports_text_cursor() {
3655 let (mut doc, container) = make_doc();
3656 if !text_has_size(&doc, container) {
3657 eprintln!("skipping: no usable font (text measures 0x0)");
3658 return;
3659 }
3660
3661 doc.set_hover_to(5.0, 8.0);
3662 assert!(doc.hover_node_is_text, "expected a text hit");
3663 let hit_id = doc.hover_hit_node_id.expect("expected a hit node");
3664 assert!(
3665 doc.nodes[hit_id].is_anonymous(),
3666 "expected the hit node to be the anonymous inline root"
3667 );
3668 assert_eq!(
3669 doc.get_hover_node_id(),
3670 Some(container),
3671 "expected the stored hover target to be the containing element"
3672 );
3673 assert_eq!(doc.get_cursor(), Some(CursorIcon::Text));
3674 }
3675
3676 #[test]
3677 fn semantic_hover_keeps_the_resolved_node_instead_of_hit_testing_again() {
3678 let (mut doc, container) = make_doc();
3679
3680 doc.set_hover_to_node(container, 350.0, 250.0);
3684
3685 assert_eq!(doc.get_hover_node_id(), Some(container));
3686 assert_eq!(doc.hover_hit_node_id, Some(container));
3687
3688 doc.resolve(0.0);
3689 assert_eq!(
3690 doc.get_hover_node_id(),
3691 Some(container),
3692 "a resolve must not turn semantic identity back into a coordinate hit"
3693 );
3694 }
3695
3696 #[test]
3699 fn hovering_anonymous_block_whitespace_reports_default_cursor() {
3700 let (mut doc, container) = make_doc();
3701 if !text_has_size(&doc, container) {
3702 eprintln!("skipping: no usable font (text measures 0x0)");
3703 return;
3704 }
3705
3706 doc.set_hover_to(250.0, 8.0);
3707 assert!(!doc.hover_node_is_text);
3708 assert_eq!(doc.get_hover_node_id(), Some(container));
3709 assert_eq!(doc.get_cursor(), Some(CursorIcon::Default));
3710 }
3711}
3712
3713#[cfg(test)]
3714mod control_scroll_tests {
3715 use super::*;
3716 use crate::{Attribute, qual_name};
3717 use blitz_traits::shell::ColorScheme;
3718
3719 #[test]
3720 fn controlled_scroll_dispatches_the_dom_scroll_event() {
3721 let mut doc = BaseDocument::new(DocumentConfig {
3722 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3723 ..Default::default()
3724 });
3725 let root_id = doc.root_node().id;
3726 let style = |value: &str| Attribute {
3727 name: qual_name!("style"),
3728 value: value.into(),
3729 };
3730
3731 let mut mutator = doc.mutate();
3732 let html = mutator.create_element(qual_name!("html"), vec![]);
3733 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
3734 let scroller = mutator.create_element(
3735 qual_name!("div"),
3736 vec![style("width:200px;height:100px;overflow-y:scroll")],
3737 );
3738 let spacer = mutator.create_element(qual_name!("div"), vec![style("height:400px")]);
3739 let target = mutator.create_element(qual_name!("button"), vec![style("height:40px")]);
3740 mutator.append_children(scroller, &[spacer, target]);
3741 mutator.append_children(body, &[scroller]);
3742 mutator.append_children(html, &[body]);
3743 mutator.append_children(root_id, &[html]);
3744 drop(mutator);
3745 doc.resolve(0.0);
3746
3747 doc.nodes[html].final_layout_mut().size.height = 300.0;
3751 doc.nodes[html].final_layout_mut().content_size.height = 600.0;
3752 doc.nodes[target].final_layout_mut().location.y = 400.0;
3753
3754 let mut events = Vec::new();
3755 doc.scroll_to_node_with_events(target, |event| events.push(event));
3756
3757 assert!(doc.viewport_scroll.y > 0.0);
3758 assert!(
3759 events
3760 .iter()
3761 .any(|event| { event.target == html && event.name() == "scroll" })
3762 );
3763 }
3764}
3765
3766#[cfg(test)]
3767mod font_face_override_tests {
3768 use super::*;
3769 use crate::net::{FontFaceOverrides, Resource, ResourceLoadResponse};
3770
3771 #[test]
3787 fn font_face_overrides_alias_family_name() {
3788 const ALIAS: &str = "AliasedFamily";
3789
3790 let mut document = BaseDocument::new(DocumentConfig::default());
3791
3792 {
3794 let mut ctx = document.font_ctx.lock().unwrap();
3795 assert!(
3796 ctx.collection.family_id(ALIAS).is_none(),
3797 "alias must not exist before registration",
3798 );
3799 }
3800
3801 let response = ResourceLoadResponse {
3806 request_id: 0,
3807 node_id: None,
3808 resolved_url: Some(String::from("test://aliased-family")),
3809 result: Ok(Resource::Font(
3810 blitz_traits::net::Bytes::from_static(crate::BULLET_FONT),
3811 FontFaceOverrides {
3812 family_name: Some(String::from(ALIAS)),
3813 weight: Some(800.0),
3814 style: Some(parley::fontique::FontStyle::Italic),
3815 },
3816 )),
3817 };
3818 document.load_resource(response);
3819
3820 let mut ctx = document.font_ctx.lock().unwrap();
3823 let family_id = ctx
3824 .collection
3825 .family_id(ALIAS)
3826 .expect("CSS-declared family name should be registered as a family alias");
3827 let resolved_name = ctx
3828 .collection
3829 .family_name(family_id)
3830 .expect("family id should resolve back to a name");
3831 assert_eq!(
3832 resolved_name, ALIAS,
3833 "registered family should report the CSS-declared name, \
3834 not the font file's internal `name` table entry",
3835 );
3836 }
3837}