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
162fn escape_html(text: &str) -> String {
168 let mut out = String::with_capacity(text.len());
169 for ch in text.chars() {
170 match ch {
171 '&' => out.push_str("&"),
172 '<' => out.push_str("<"),
173 '>' => out.push_str(">"),
174 '"' => out.push_str("""),
175 _ => out.push(ch),
176 }
177 }
178 out
179}
180
181pub struct PlainDocument(pub BaseDocument);
182impl Document for PlainDocument {
183 fn inner(&self) -> DocGuard<'_> {
184 DocGuard::Ref(&self.0)
185 }
186 fn inner_mut(&mut self) -> DocGuardMut<'_> {
187 DocGuardMut::Ref(&mut self.0)
188 }
189}
190
191impl Document for BaseDocument {
192 fn inner(&self) -> DocGuard<'_> {
193 DocGuard::Ref(self)
194 }
195 fn inner_mut(&mut self) -> DocGuardMut<'_> {
196 DocGuardMut::Ref(self)
197 }
198}
199
200impl Document for Rc<RefCell<BaseDocument>> {
201 fn inner(&self) -> DocGuard<'_> {
202 DocGuard::RefCell(self.borrow())
203 }
204
205 fn inner_mut(&mut self) -> DocGuardMut<'_> {
206 DocGuardMut::RefCell(self.borrow_mut())
207 }
208}
209
210pub enum DocumentEvent {
211 ResourceLoad(ResourceLoadResponse),
212 NavigateIframe {
215 node_id: NodeId,
216 url: Url,
217 },
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
222pub enum AnimationPacing {
223 Idle,
224 Caret,
225 SlowCss,
226 Interactive,
227}
228
229pub struct BaseDocument {
230 id: usize,
232
233 pub(crate) url: DocumentUrl,
236 pub(crate) devtool_settings: DevtoolSettings,
238 pub(crate) viewport: Viewport,
240 pub(crate) viewport_scroll: crate::Point<f64>,
242 pub(crate) mutation_log: Option<Vec<crate::DomMutation>>,
245 pub(crate) media_type: MediaType,
247 pub(crate) style_threading: StyleThreading,
249 pub(crate) incremental_layout: bool,
251 pub(crate) subdocument_depth: usize,
254
255 pub(crate) tx: Sender<DocumentEvent>,
257 pub(crate) rx: Option<Receiver<DocumentEvent>>,
259
260 pub(crate) nodes: Box<NodeTree>,
265
266 pub(crate) root_node_id: NodeId,
268
269 pub(crate) hoisted_fixed_parents: HashMap<NodeId, NodeId>,
279
280 pub(crate) fixed_nodes: Vec<NodeId>,
286
287 pub(crate) fixed_scroll_offset: crate::Point<f64>,
294
295 pub(crate) sticky_nodes: Vec<NodeId>,
303
304 pub(crate) sticky_offsets: HashMap<NodeId, taffy::Point<f32>>,
314
315 pub(crate) hoisted_clip_hosts: Vec<NodeId>,
321
322 pub(crate) stylist: Stylist,
325 pub(crate) animations: DocumentAnimationSet,
326 pub(crate) last_resolve_animation_time: f64,
333 pub(crate) guard: SharedRwLock,
335 pub(crate) snapshots: SnapshotMap,
337
338 pub(crate) font_ctx: Arc<Mutex<parley::FontContext>>,
341 #[cfg(feature = "parallel-construct")]
342 pub(crate) thread_font_contexts: ThreadLocal<RefCell<Box<FontContext>>>,
344 pub(crate) layout_ctx: parley::LayoutContext<TextBrush>,
346
347 pub(crate) hover_node_id: Option<NodeId>,
351 pub(crate) hover_hit_node_id: Option<NodeId>,
355 pub(crate) hover_node_is_text: bool,
357 pub(crate) last_client_pointer_position: Option<taffy::Point<f32>>,
359 pub(crate) semantic_hover_node_id: Option<NodeId>,
365 pub(crate) focus_node_id: Option<NodeId>,
367 pub(crate) active_node_id: Option<NodeId>,
369 pub(crate) mousedown_node_id: Option<NodeId>,
371 pub(crate) last_mousedown_time: Option<Instant>,
373 pub(crate) mousedown_position: taffy::Point<f32>,
375 pub(crate) click_count: u16,
377 pub(crate) drag_mode: DragMode,
379 pub(crate) hovered_scrollbar: Option<crate::node::ScrollbarRef>,
381 pub(crate) scrollbar_activity: HashMap<NodeId, Instant>,
384 pub(crate) scroll_animation: ScrollAnimationState,
386
387 pub(crate) text_selection: TextSelection,
389
390 pub(crate) has_active_animations: bool,
393 pub(crate) has_canvas: bool,
395 pub(crate) subdoc_animation_pacing: AnimationPacing,
397
398 pub(crate) nodes_to_id: HashMap<String, SmallVec<[NodeId; 1]>>,
402 pub(crate) nodes_to_stylesheet: BTreeMap<NodeId, DocumentStyleSheet>,
404 pub(crate) ua_stylesheets: HashMap<String, DocumentStyleSheet>,
407 pub(crate) controls_to_form: HashMap<NodeId, NodeId>,
409 pub(crate) sub_document_nodes: HashSet<NodeId>,
411 pub(crate) iframe_loads: HashMap<NodeId, crate::iframe::IframeLoad>,
414 pub(crate) deferred_construction_nodes: Vec<ConstructionTask>,
416 pub(crate) paint_damage: crate::paint_damage::PaintDamageTracker,
422
423 #[cfg(feature = "custom-widget")]
425 pub(crate) custom_widget_nodes: HashSet<NodeId>,
426 #[cfg(feature = "custom-widget")]
428 pub(crate) pending_resource_deallocations: Vec<anyrender::ResourceId>,
429
430 #[cfg(feature = "shadow-dom")]
432 pub(crate) custom_element_registry: crate::node::CustomElementRegistry,
433 #[cfg(feature = "shadow-dom")]
435 pub(crate) shadow_host_nodes: HashSet<NodeId>,
436 #[cfg(feature = "shadow-dom")]
438 pub(crate) custom_element_nodes: HashSet<NodeId>,
439
440 pub(crate) image_cache: HashMap<String, ImageData>,
443
444 pub(crate) pending_images: HashMap<String, Vec<(NodeId, ImageType)>>,
448
449 pub(crate) pending_critical_resources: HashSet<usize>,
452
453 pub net_provider: Arc<dyn NetProvider>,
456 pub navigation_provider: Arc<dyn NavigationProvider>,
459 pub shell_provider: Arc<dyn ShellProvider>,
461 pub html_parser_provider: Arc<dyn HtmlParserProvider>,
463 pub(crate) abort_signal: Option<AbortSignal>,
467}
468
469pub(crate) fn make_device(
470 viewport: &Viewport,
471 media_type: MediaType,
472 font_ctx: Arc<Mutex<FontContext>>,
473) -> Device {
474 let width = viewport.window_size.0 as f32 / viewport.scale();
475 let height = viewport.window_size.1 as f32 / viewport.scale();
476 let viewport_size = euclid::Size2D::new(width, height);
477 let device_size = euclid::Size2D::new(width, height) * viewport.scale();
478 let device_pixel_ratio = euclid::Scale::new(viewport.scale());
479
480 Device::new(
481 media_type,
482 selectors::matching::QuirksMode::NoQuirks,
483 viewport_size,
484 device_size,
485 device_pixel_ratio,
486 Box::new(BlitzFontMetricsProvider { font_ctx }),
487 ComputedValues::initial_values_with_font_override(Font::initial_values()),
488 match viewport.color_scheme {
489 ColorScheme::Light => PrefersColorScheme::Light,
490 ColorScheme::Dark => PrefersColorScheme::Dark,
491 },
492 PointerCapabilities::default(),
493 PointerCapabilities::default(),
494 )
495}
496
497fn incremental_layout_default() -> bool {
512 !matches!(
513 std::env::var("BLITZ_INCREMENTAL").ok().as_deref(),
514 Some("0" | "false" | "off")
515 )
516}
517
518impl BaseDocument {
519 pub fn new(config: DocumentConfig) -> Self {
521 static ID_GENERATOR: AtomicUsize = AtomicUsize::new(1);
522
523 let id = ID_GENERATOR.fetch_add(1, Ordering::SeqCst);
524
525 let font_ctx = config
526 .font_ctx
527 .map(|mut font_ctx| {
528 font_ctx.source_cache.make_shared();
529 font_ctx
531 })
532 .unwrap_or_else(|| {
533 use parley::fontique::{Collection, CollectionOptions, SourceCache};
534 let mut font_ctx = FontContext {
535 source_cache: SourceCache::new_shared(),
536 collection: Collection::new(CollectionOptions {
537 shared: false,
538 system_fonts: cfg!(all(
539 feature = "system-fonts",
540 not(target_arch = "wasm32")
541 )),
542 }),
543 };
544 font_ctx
545 .collection
546 .register_fonts(Blob::new(Arc::new(crate::BULLET_FONT) as _), None);
547 font_ctx
548 });
549 let font_ctx = Arc::new(Mutex::new(font_ctx));
550
551 style_config::set_pref!("layout.grid.enabled", true);
553 style_config::set_pref!("layout.unimplemented", true);
554 style_config::set_pref!("layout.columns.enabled", true);
555 style_config::set_pref!("layout.css.basic-shape-shape.enabled", true);
556 style_config::set_pref!("layout.threads", -1);
557
558 let viewport = config.viewport.unwrap_or_default();
559 let media_type = config.media_type.unwrap_or_else(MediaType::screen);
560 let device = make_device(&viewport, media_type.clone(), font_ctx.clone());
561 let stylist = Stylist::new(device, QuirksMode::NoQuirks);
562 let snapshots = SnapshotMap::new();
563 let nodes = Box::new(NodeTree::new());
564 let guard = SharedRwLock::new();
565 let nodes_to_id = HashMap::new();
566
567 let base_url = config
568 .base_url
569 .and_then(|url| DocumentUrl::from_str(&url).ok())
570 .unwrap_or_default();
571
572 let net_provider = config
573 .net_provider
574 .unwrap_or_else(|| Arc::new(DummyNetProvider));
575 let navigation_provider = config
576 .navigation_provider
577 .unwrap_or_else(|| Arc::new(DummyNavigationProvider));
578 let shell_provider = config
579 .shell_provider
580 .unwrap_or_else(|| Arc::new(DummyShellProvider));
581 let html_parser_provider = config
582 .html_parser_provider
583 .unwrap_or_else(|| Arc::new(DummyHtmlParserProvider));
584
585 let (tx, rx) = channel();
586
587 let mut doc = Self {
588 hoisted_fixed_parents: HashMap::new(),
589 fixed_nodes: Vec::new(),
590 fixed_scroll_offset: crate::Point::ZERO,
591 sticky_nodes: Vec::new(),
592 sticky_offsets: HashMap::new(),
593 hoisted_clip_hosts: Vec::new(),
594 id,
595 tx,
596 rx: Some(rx),
597
598 guard,
599 nodes,
600 root_node_id: NodeId::default(),
601 stylist,
602 animations: DocumentAnimationSet::default(),
603 last_resolve_animation_time: 0.0,
604 snapshots,
605 nodes_to_id,
606 viewport,
607 media_type,
608 style_threading: config.style_threading,
609 incremental_layout: config
610 .incremental
611 .unwrap_or_else(incremental_layout_default),
612 subdocument_depth: config.subdocument_depth,
613 devtool_settings: DevtoolSettings::default(),
614 viewport_scroll: crate::Point::ZERO,
615 mutation_log: None,
616 url: base_url,
617 ua_stylesheets: HashMap::new(),
618 nodes_to_stylesheet: BTreeMap::new(),
619 font_ctx,
620 #[cfg(feature = "parallel-construct")]
621 thread_font_contexts: ThreadLocal::new(),
622 layout_ctx: parley::LayoutContext::new(),
623
624 hover_node_id: None,
625 hover_hit_node_id: None,
626 hover_node_is_text: false,
627 last_client_pointer_position: None,
628 semantic_hover_node_id: None,
629 focus_node_id: None,
630 active_node_id: None,
631 mousedown_node_id: None,
632 has_active_animations: false,
633 subdoc_animation_pacing: AnimationPacing::Idle,
634 has_canvas: false,
635 sub_document_nodes: HashSet::new(),
636 iframe_loads: HashMap::new(),
637
638 #[cfg(feature = "custom-widget")]
639 custom_widget_nodes: HashSet::new(),
640 #[cfg(feature = "custom-widget")]
641 pending_resource_deallocations: Vec::new(),
642
643 #[cfg(feature = "shadow-dom")]
644 custom_element_registry: crate::node::CustomElementRegistry::new(),
645 #[cfg(feature = "shadow-dom")]
646 shadow_host_nodes: HashSet::new(),
647 #[cfg(feature = "shadow-dom")]
648 custom_element_nodes: HashSet::new(),
649
650 deferred_construction_nodes: Vec::new(),
651 paint_damage: Default::default(),
652 image_cache: HashMap::new(),
653 pending_images: HashMap::new(),
654 pending_critical_resources: HashSet::new(),
655 controls_to_form: HashMap::new(),
656 net_provider,
657 navigation_provider,
658 shell_provider,
659 html_parser_provider,
660 abort_signal: config.abort_signal,
661 last_mousedown_time: None,
662 mousedown_position: taffy::Point::ZERO,
663 click_count: 0,
664 drag_mode: DragMode::None,
665 hovered_scrollbar: None,
666 scrollbar_activity: HashMap::new(),
667 scroll_animation: ScrollAnimationState::None,
668 text_selection: TextSelection::default(),
669 };
670
671 doc.root_node_id = doc.create_node(NodeData::Document(Box::default()));
673 doc.root_node_mut().flags.insert(NodeFlags::IS_IN_DOCUMENT);
674
675 match config.ua_stylesheets {
676 Some(stylesheets) => {
677 for ss in &stylesheets {
678 doc.add_user_agent_stylesheet(ss);
679 }
680 }
681 None => doc.add_user_agent_stylesheet(DEFAULT_CSS),
682 }
683
684 let stylo_element_data = StyloElementData {
686 styles: ElementStyles {
687 primary: Some(
688 ComputedValues::initial_values_with_font_override(Font::initial_values())
689 .to_arc(),
690 ),
691 ..Default::default()
692 },
693 ..Default::default()
694 };
695 let stylo_data = doc.root_node_mut().stylo_element_data_mut();
696 *stylo_data.ensure_init_mut() = stylo_element_data;
697
698 doc
699 }
700
701 pub fn set_net_provider(&mut self, net_provider: Arc<dyn NetProvider>) {
703 self.net_provider = net_provider;
704 }
705
706 pub fn set_navigation_provider(&mut self, navigation_provider: Arc<dyn NavigationProvider>) {
708 self.navigation_provider = navigation_provider;
709 }
710
711 pub fn set_shell_provider(&mut self, shell_provider: Arc<dyn ShellProvider>) {
713 self.shell_provider = shell_provider;
714 }
715
716 pub fn set_html_parser_provider(&mut self, html_parser_provider: Arc<dyn HtmlParserProvider>) {
718 self.html_parser_provider = html_parser_provider;
719 }
720
721 pub fn set_base_url(&mut self, url: &str) {
723 self.url = DocumentUrl::from(Url::parse(url).unwrap());
724 }
725
726 pub fn guard(&self) -> &SharedRwLock {
727 &self.guard
728 }
729
730 pub fn tree(&self) -> &NodeTree {
731 &self.nodes
732 }
733
734 pub fn id(&self) -> usize {
735 self.id
736 }
737
738 pub(crate) fn build_request(&self, url: url::Url) -> Request {
741 crate::net::stamped_request(url, self.abort_signal.as_ref())
742 }
743
744 pub fn favicon_url(&self) -> Option<String> {
745 self.tree().iter().find_map(|(_, node)| {
746 let data = &node.data;
747 if !data.is_element_with_tag_name(&local_name!("link")) {
748 return None;
749 }
750 let rel = data.attr(local_name!("rel"))?;
751 if !rel
752 .split_ascii_whitespace()
753 .any(|v| v.eq_ignore_ascii_case("icon"))
754 {
755 return None;
756 }
757 data.attr(local_name!("href")).map(|s| s.to_string())
758 })
759 }
760
761 pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
762 self.nodes.get(node_id)
763 }
764
765 pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
766 self.nodes.get_mut(node_id)
767 }
768
769 pub fn get_focussed_node_id(&self) -> Option<NodeId> {
770 self.focus_node_id
771 .or(self.try_root_element().map(|el| el.id))
772 }
773
774 pub fn mutate<'doc>(&'doc mut self) -> DocumentMutator<'doc> {
775 DocumentMutator::new(self)
776 }
777
778 pub fn handle_dom_event<F: FnMut(DomEvent)>(
779 &mut self,
780 event: &mut DomEvent,
781 dispatch_event: F,
782 ) {
783 handle_dom_event(self, event, dispatch_event)
784 }
785
786 pub fn as_any_mut(&mut self) -> &mut dyn Any {
787 self
788 }
789
790 pub fn label_bound_input_element(&self, label_node_id: NodeId) -> Option<&Node> {
797 let label_element = self.nodes[label_node_id].element_data()?;
798 if let Some(target_element_dom_id) = label_element.attr(local_name!("for")) {
799 TreeTraverser::new(self)
800 .filter_map(|id| {
801 let node = self.get_node(id)?;
802 let element_data = node.element_data()?;
803 if element_data.name.local != local_name!("input") {
804 return None;
805 }
806 let id = element_data.id.as_ref()?;
807 if *id == *target_element_dom_id {
808 Some(node)
809 } else {
810 None
811 }
812 })
813 .next()
814 } else {
815 TreeTraverser::new_with_root(self, label_node_id)
816 .filter_map(|child_id| {
817 let node = self.get_node(child_id)?;
818 let element_data = node.element_data()?;
819 if element_data.name.local == local_name!("input") {
820 Some(node)
821 } else {
822 None
823 }
824 })
825 .next()
826 }
827 }
828
829 pub fn run_pre_click_activation(&mut self, target: NodeId) -> Option<PreClickActivation> {
835 let node_id = crate::events::pointer::checkable_activation_target(self, target)?;
836 let el = self.get_node(node_id)?.data.downcast_element()?;
837 let is_radio = el.attr(local_name!("type")) == Some("radio");
838
839 if !is_radio {
840 let previous = el.checkbox_input_checked()?;
841 let el = self.get_node_mut(node_id)?.data.downcast_element_mut()?;
842 Self::toggle_checkbox(el);
843 return Some(PreClickActivation {
844 previous: vec![(node_id, previous)],
845 });
846 }
847
848 let radio_set = el.attr(local_name!("name")).map(str::to_string);
849 let Some(radio_set) = radio_set else {
850 let previous = el.checkbox_input_checked()?;
851 let el = self.get_node_mut(node_id)?.data.downcast_element_mut()?;
852 *el.checkbox_input_checked_mut()? = true;
853 return Some(PreClickActivation {
854 previous: vec![(node_id, previous)],
855 });
856 };
857
858 let mut previous: Vec<(NodeId, bool)> = Vec::new();
870 for (id, node) in self.nodes.iter_mut() {
871 let Some(el) = node.data.downcast_element_mut() else {
872 continue;
873 };
874 if el.attr(local_name!("name")) != Some(&*radio_set) {
875 continue;
876 }
877 let Some(is_checked) = el.checkbox_input_checked_mut() else {
878 continue;
879 };
880 previous.push((id, *is_checked));
881 *is_checked = id == node_id;
882 }
883 Some(PreClickActivation { previous })
884 }
885
886 pub fn undo_pre_click_activation(&mut self, activation: PreClickActivation) {
889 for (node_id, was_checked) in activation.previous {
890 let Some(node) = self.get_node_mut(node_id) else {
891 continue;
892 };
893 let Some(el) = node.data.downcast_element_mut() else {
894 continue;
895 };
896 if let Some(is_checked) = el.checkbox_input_checked_mut() {
897 *is_checked = was_checked;
898 }
899 }
900 }
901
902 pub fn toggle_checkbox(el: &mut ElementData) -> bool {
903 let Some(is_checked) = el.checkbox_input_checked_mut() else {
904 return false;
905 };
906 *is_checked = !*is_checked;
907
908 *is_checked
909 }
910
911 pub fn toggle_radio(&mut self, radio_set_name: String, target_radio_id: NodeId) {
912 for (i, node) in self.nodes.iter_mut() {
913 if let Some(node_data) = node.data.downcast_element_mut() {
914 if node_data.attr(local_name!("name")) == Some(&radio_set_name) {
915 let was_clicked = i == target_radio_id;
916 let Some(is_checked) = node_data.checkbox_input_checked_mut() else {
917 continue;
918 };
919 *is_checked = was_clicked;
920 }
921 }
922 }
923 }
924
925 pub fn toggle_details_open(&mut self, details_id: NodeId) {
929 use crate::qual_name;
930
931 let node = &self.nodes[details_id];
932 if !node.data.is_element_with_tag_name(&local_name!("details")) {
933 return;
934 }
935 let is_open = node.data.has_attr(local_name!("open"));
936
937 let mut mutator = self.mutate();
941 if is_open {
942 mutator.clear_attribute(details_id, qual_name!("open"));
943 } else {
944 mutator.set_attribute(details_id, qual_name!("open"), "");
945 }
946 drop(mutator);
947
948 self.shell_provider.request_redraw();
949 }
950
951 pub fn set_style_property(&mut self, node_id: NodeId, name: &str, value: &str) {
952 let node = &mut self.nodes[node_id];
953 let did_change = node.element_data_mut().unwrap().set_style_property(
954 name,
955 value,
956 &self.guard,
957 self.url.url_extra_data(),
958 );
959 if did_change {
960 node.mark_style_attr_updated();
961 }
962 }
963
964 pub fn remove_style_property(&mut self, node_id: NodeId, name: &str) {
965 let node = &mut self.nodes[node_id];
966 let did_change = node.element_data_mut().unwrap().remove_style_property(
967 name,
968 &self.guard,
969 self.url.url_extra_data(),
970 );
971 if did_change {
972 node.mark_style_attr_updated();
973 }
974 }
975
976 pub fn sub_document_node_ids(&self) -> Vec<NodeId> {
977 self.sub_document_nodes.iter().copied().collect()
978 }
979
980 pub fn set_sub_document(&mut self, node_id: NodeId, sub_document: Box<dyn Document>) {
981 self.nodes[node_id]
982 .element_data_mut()
983 .unwrap()
984 .set_sub_document(sub_document);
985 self.sub_document_nodes.insert(node_id);
986 }
987
988 pub fn remove_sub_document(&mut self, node_id: NodeId) {
989 self.nodes[node_id]
990 .element_data_mut()
991 .unwrap()
992 .remove_sub_document();
993 self.sub_document_nodes.remove(&node_id);
994 if let Some(load) = self.iframe_loads.remove(&node_id) {
995 load.abort_controller.abort();
996 }
997 }
998
999 pub fn poll_subdocuments(&mut self, waker: Option<&Waker>) -> bool {
1005 let mut has_changes = false;
1006 let node_ids: Vec<NodeId> = self.sub_document_nodes.iter().copied().collect();
1007 for node_id in node_ids {
1008 let Some(sub_doc) = self
1009 .nodes
1010 .get_mut(node_id)
1011 .and_then(|node| node.subdoc_mut())
1012 else {
1013 continue;
1014 };
1015 let task_context = waker.map(TaskContext::from_waker);
1016 has_changes |= sub_doc.poll(task_context);
1017 }
1018 has_changes
1019 }
1020
1021 #[cfg(feature = "custom-widget")]
1022 pub fn custom_widget_node_ids(&self) -> Vec<NodeId> {
1023 self.custom_widget_nodes.iter().copied().collect()
1024 }
1025
1026 #[cfg(feature = "custom-widget")]
1027 pub fn take_pending_resource_deallocations(&mut self) -> Vec<anyrender::ResourceId> {
1028 std::mem::take(&mut self.pending_resource_deallocations)
1029 }
1030
1031 #[cfg(feature = "custom-widget")]
1032 pub fn set_custom_widget(&mut self, node_id: NodeId, widget: Box<dyn crate::Widget>) {
1033 self.nodes[node_id]
1034 .element_data_mut()
1035 .unwrap()
1036 .set_custom_widget(widget);
1037 self.custom_widget_nodes.insert(node_id);
1038 }
1039
1040 #[cfg(feature = "custom-widget")]
1041 pub fn remove_custom_widget(&mut self, node_id: NodeId) {
1042 let resources_to_deallocate = self.nodes[node_id]
1043 .element_data_mut()
1044 .unwrap()
1045 .remove_custom_widget();
1046 self.pending_resource_deallocations
1047 .extend_from_slice(&resources_to_deallocate);
1048 self.custom_widget_nodes.remove(&node_id);
1049 }
1050
1051 #[cfg(feature = "shadow-dom")]
1055 pub fn custom_elements_mut(&mut self) -> &mut crate::node::CustomElementRegistry {
1056 &mut self.custom_element_registry
1057 }
1058
1059 #[cfg(feature = "shadow-dom")]
1062 pub fn define_custom_element(
1063 &mut self,
1064 name: markup5ever::LocalName,
1065 definition: crate::node::CustomElementDefinition,
1066 ) {
1067 self.custom_element_registry.define(name, definition);
1068 }
1069
1070 #[cfg(feature = "shadow-dom")]
1072 pub fn shadow_host_node_ids(&self) -> Vec<NodeId> {
1073 self.shadow_host_nodes.iter().copied().collect()
1074 }
1075
1076 #[cfg(feature = "shadow-dom")]
1078 pub fn shadow_root_id(&self, host_id: NodeId) -> Option<NodeId> {
1079 self.get_node(host_id)
1080 .and_then(|node| node.shadow_root_id())
1081 }
1082
1083 #[cfg(feature = "shadow-dom")]
1087 pub fn attach_shadow(&mut self, host_id: NodeId, mode: crate::node::ShadowRootMode) -> NodeId {
1088 if let Some(existing) = self.nodes[host_id].shadow_root_id() {
1089 return existing;
1090 }
1091
1092 let shadow_root_id = self.create_node(NodeData::ShadowRoot(
1093 crate::node::ShadowRootData::new(host_id, mode),
1094 ));
1095
1096 self.nodes[shadow_root_id].parent = Some(host_id);
1100 if self.nodes[host_id].flags.is_in_document() {
1101 self.nodes[shadow_root_id]
1102 .flags
1103 .insert(NodeFlags::IS_IN_DOCUMENT);
1104 }
1105
1106 self.nodes[host_id]
1107 .element_data_mut()
1108 .expect("Shadow host must be an element")
1109 .shadow_root = Some(shadow_root_id);
1110 self.shadow_host_nodes.insert(host_id);
1111
1112 self.nodes[host_id].insert_damage(ALL_DAMAGE);
1114 self.nodes[host_id].mark_ancestors_dirty();
1115
1116 shadow_root_id
1117 }
1118
1119 #[cfg(feature = "shadow-dom")]
1121 pub fn detach_shadow(&mut self, host_id: NodeId) {
1122 let shadow_root_id = self.nodes[host_id]
1123 .element_data_mut()
1124 .and_then(|el| el.shadow_root.take());
1125 if let Some(shadow_root_id) = shadow_root_id {
1126 self.drop_node_ignoring_parent(shadow_root_id);
1127 self.shadow_host_nodes.remove(&host_id);
1128 self.nodes[host_id].insert_damage(ALL_DAMAGE);
1129 self.nodes[host_id].mark_ancestors_dirty();
1130 }
1131 }
1132
1133 #[cfg(feature = "shadow-dom")]
1135 pub fn set_custom_element(
1136 &mut self,
1137 node_id: NodeId,
1138 controller: Box<dyn crate::node::CustomElement>,
1139 ) {
1140 use crate::node::{CustomElementData, SpecialElementData};
1141 self.nodes[node_id]
1142 .element_data_mut()
1143 .expect("Custom element host must be an element")
1144 .special_data = SpecialElementData::CustomElement(CustomElementData::new(controller));
1145 self.custom_element_nodes.insert(node_id);
1146 }
1147
1148 #[cfg(feature = "shadow-dom")]
1151 pub fn take_custom_element(
1152 &mut self,
1153 node_id: NodeId,
1154 ) -> Option<Box<dyn crate::node::CustomElement>> {
1155 use crate::node::SpecialElementData;
1156 self.custom_element_nodes.remove(&node_id);
1157 let element = self.nodes[node_id].element_data_mut()?;
1158 if matches!(element.special_data, SpecialElementData::CustomElement(_)) {
1159 if let SpecialElementData::CustomElement(mut data) = element.special_data.take() {
1160 return data.controller.take();
1161 }
1162 }
1163 None
1164 }
1165
1166 pub fn root_node(&self) -> &Node {
1167 &self.nodes[self.root_node_id]
1168 }
1169
1170 pub fn root_node_mut(&mut self) -> &mut Node {
1171 &mut self.nodes[self.root_node_id]
1172 }
1173
1174 pub fn set_paint_damage_tracking(&mut self, enabled: bool) {
1191 self.paint_damage.set_enabled(enabled);
1192 }
1193
1194 pub fn paint_damage_tracking(&self) -> bool {
1196 self.paint_damage.is_enabled()
1197 }
1198
1199 pub fn paint_damage(&self) -> &crate::paint_damage::PaintDamage {
1207 self.paint_damage.damage()
1208 }
1209
1210 pub fn try_root_element(&self) -> Option<&Node> {
1211 TDocument::as_node(&self.root_node()).first_element_child()
1212 }
1213
1214 pub fn root_element(&self) -> &Node {
1215 TDocument::as_node(&self.root_node())
1216 .first_element_child()
1217 .unwrap()
1218 .as_element()
1219 .unwrap()
1220 }
1221
1222 pub fn create_node(&mut self, node_data: NodeData) -> NodeId {
1223 let tree_ptr = self.nodes.as_mut() as *mut NodeTree;
1224 let guard = self.guard.clone();
1225
1226 self.nodes
1227 .insert_with_key(|id| Node::new(tree_ptr, id, guard, node_data))
1228 }
1229
1230 pub(crate) fn remove_node_from_tree(&mut self, node_id: NodeId) -> Option<Node> {
1234 self.clear_interaction_state_for_removed_node(node_id);
1235 self.nodes.remove(node_id)
1236 }
1237
1238 fn nearest_surviving_element_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
1243 let mut current = self.get_node(node_id)?.parent;
1244 while let Some(id) = current {
1245 let node = self.get_node(id)?;
1246 if node.is_element() && node.flags.is_in_document() {
1247 return Some(id);
1248 }
1249 current = node.parent;
1250 }
1251 None
1252 }
1253
1254 pub(crate) fn clear_interaction_state_for_removed_node(&mut self, node_id: NodeId) {
1275 if !self.nodes.contains_key(node_id) {
1276 return;
1277 }
1278
1279 if self.hover_node_id == Some(node_id) {
1280 self.hover_node_id = self.nearest_surviving_element_ancestor(node_id);
1281 self.hover_node_is_text = false;
1282 }
1283 if self.hover_hit_node_id == Some(node_id) {
1284 self.hover_hit_node_id = None;
1285 }
1286 if self.active_node_id == Some(node_id) {
1287 self.active_node_id = self.nearest_surviving_element_ancestor(node_id);
1288 }
1289 if self.focus_node_id == Some(node_id) {
1290 let shell_provider = self.shell_provider.clone();
1291 self.nodes[node_id].blur(shell_provider);
1292 self.focus_node_id = None;
1293 }
1294 if self.mousedown_node_id == Some(node_id) {
1295 self.mousedown_node_id = None;
1296 }
1297 if self.text_selection.anchor.node_or_parent == Some(node_id)
1298 || self.text_selection.focus.node_or_parent == Some(node_id)
1299 {
1300 self.text_selection.clear();
1301 }
1302 if self
1303 .hovered_scrollbar
1304 .is_some_and(|scrollbar| scrollbar.node_id == node_id)
1305 {
1306 self.hovered_scrollbar = None;
1307 }
1308 let drag_references_node = match &self.drag_mode {
1309 DragMode::Panning(state) => state.target == node_id,
1310 DragMode::ScrollbarDrag(state) => state.scrollbar.node_id == node_id,
1311 DragMode::Selecting | DragMode::None => false,
1312 };
1313 if drag_references_node {
1314 self.drag_mode = DragMode::None;
1315 }
1316 self.scrollbar_activity.remove(&node_id);
1317
1318 self.controls_to_form.remove(&node_id);
1323 }
1324
1325 pub(crate) fn drop_node_ignoring_parent(&mut self, node_id: NodeId) -> Option<Node> {
1326 self.drop_node_ignoring_parent_with(node_id, &mut |_| {})
1327 }
1328
1329 pub(crate) fn drop_node_ignoring_parent_with(
1332 &mut self,
1333 node_id: NodeId,
1334 on_drop: &mut dyn FnMut(NodeId),
1335 ) -> Option<Node> {
1336 let mut node = self.remove_node_from_tree(node_id);
1337 if let Some(node) = &mut node {
1338 on_drop(node_id);
1339 if let Some(before) = node.before() {
1340 self.drop_node_ignoring_parent_with(before, on_drop);
1341 }
1342 if let Some(after) = node.after() {
1343 self.drop_node_ignoring_parent_with(after, on_drop);
1344 }
1345
1346 for &child in &node.children {
1347 self.drop_node_ignoring_parent_with(child, on_drop);
1348 }
1349
1350 for &anon_id in &node.anonymous_blocks {
1353 self.deallocate_anonymous_block(anon_id);
1354 }
1355
1356 #[cfg(feature = "shadow-dom")]
1359 if let Some(shadow_root_id) = node.shadow_root_id() {
1360 self.shadow_host_nodes.remove(&node_id);
1361 self.custom_element_nodes.remove(&node_id);
1362 self.drop_node_ignoring_parent(shadow_root_id);
1363 }
1364 }
1365 node
1366 }
1367
1368 pub(crate) fn deallocate_anonymous_block(&mut self, anon_id: NodeId) {
1371 if !self.nodes.contains_key(anon_id) {
1374 return;
1375 }
1376
1377 let nested = std::mem::take(&mut self.nodes[anon_id].anonymous_blocks);
1379 for nested_id in nested {
1380 self.deallocate_anonymous_block(nested_id);
1381 }
1382
1383 self.remove_node_from_tree(anon_id);
1384 }
1385
1386 pub fn create_text_node(&mut self, text: &str) -> NodeId {
1387 let content = text.to_string();
1388 let data = NodeData::Text(TextNodeData::new(content));
1389 self.create_node(data)
1390 }
1391
1392 pub fn deep_clone_node(&mut self, node_id: NodeId) -> NodeId {
1393 let node = &self.nodes[node_id];
1395 let mut data = node.data.clone();
1396
1397 match &mut data {
1398 NodeData::Element(elem) | NodeData::AnonymousBlock(elem) => {
1399 if let Some(arc) = elem.style_attribute.as_mut() {
1400 let read_guard = self.guard().read();
1401 let block = arc.read_with(&read_guard);
1402 *arc = ServoArc::new(self.guard().wrap(block.clone()));
1403 }
1404 }
1405 _ => {}
1406 }
1407
1408 let children = node.children.clone();
1409
1410 let new_node_id = self.create_node(data);
1412
1413 let new_children: ThinVec<NodeId> = children
1415 .into_iter()
1416 .map(|child_id| self.deep_clone_node(child_id))
1417 .collect();
1418 for &child_id in &new_children {
1419 self.nodes[child_id].parent = Some(new_node_id);
1420 }
1421 self.nodes[new_node_id].children = new_children;
1422
1423 new_node_id
1424 }
1425
1426 pub(crate) fn remove_and_drop_pe(&mut self, node_id: NodeId) -> Option<Node> {
1427 fn remove_pe_ignoring_parent(doc: &mut BaseDocument, node_id: NodeId) -> Option<Node> {
1428 let mut node = doc.remove_node_from_tree(node_id);
1429 if let Some(node) = &mut node {
1430 for &child in &node.children {
1431 remove_pe_ignoring_parent(doc, child);
1432 }
1433 for &anon_id in &node.anonymous_blocks {
1434 doc.deallocate_anonymous_block(anon_id);
1435 }
1436 }
1437 node
1438 }
1439
1440 let node = remove_pe_ignoring_parent(self, node_id);
1441
1442 if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
1444 let parent = &mut self.nodes[parent_id];
1445 parent.children.retain(|id| *id != node_id);
1446 }
1447
1448 node
1449 }
1450
1451 pub(crate) fn resolve_url(&self, raw: &str) -> url::Url {
1452 self.url.resolve_relative(raw).unwrap_or_else(|| {
1453 panic!(
1454 "to be able to resolve {raw} with the base_url: {:?}",
1455 *self.url
1456 )
1457 })
1458 }
1459
1460 pub fn navigate_to_url(&self, raw: &str) -> bool {
1468 let Some(url) = self.url.resolve_relative(raw) else {
1469 return false;
1470 };
1471 self.navigation_provider
1472 .navigate_to(blitz_traits::navigation::NavigationOptions::new(
1473 url,
1474 None,
1475 self.id(),
1476 ));
1477 true
1478 }
1479
1480 pub fn current_url(&self) -> String {
1482 self.url.to_string()
1483 }
1484
1485 pub fn print_tree(&self) {
1486 crate::util::walk_tree(0, self.root_node());
1487 }
1488
1489 pub fn print_subtree(&self, node_id: NodeId) {
1490 crate::util::walk_tree(0, &self.nodes[node_id]);
1491 }
1492
1493 pub fn reload_resource_by_href(&mut self, href_to_reload: &str) {
1494 for &node_id in self.nodes_to_stylesheet.keys() {
1495 let node = &self.nodes[node_id];
1496 let Some(element) = node.element_data() else {
1497 continue;
1498 };
1499
1500 if element.name.local == local_name!("link") {
1501 if let Some(href) = element.attr(local_name!("href")) {
1502 if href == href_to_reload {
1504 let resolved_href = self.resolve_url(href);
1505 self.net_provider.fetch(
1506 self.id(),
1507 self.build_request(resolved_href.clone()),
1508 ResourceHandler::boxed(
1509 self.tx.clone(),
1510 self.id,
1511 Some(node_id),
1512 self.shell_provider.clone(),
1513 StylesheetHandler {
1514 source_url: resolved_href,
1515 guard: self.guard.clone(),
1516 net_provider: self.net_provider.clone(),
1517 abort_signal: self.abort_signal.clone(),
1518 },
1519 ),
1520 );
1521 }
1522 }
1523 }
1524 }
1525 }
1526
1527 pub fn process_style_element(&mut self, target_id: NodeId) {
1528 let css = self.nodes[target_id].text_content();
1529 let css = html_escape::decode_html_entities(&css);
1530 let sheet = self.make_stylesheet(&css, Origin::Author);
1531 self.add_stylesheet_for_node(sheet, target_id);
1532 }
1533
1534 pub fn remove_user_agent_stylesheet(&mut self, contents: &str) {
1535 if let Some(sheet) = self.ua_stylesheets.remove(contents) {
1536 self.stylist.remove_stylesheet(sheet, &self.guard.read());
1537 }
1538 }
1539
1540 pub fn url(&self) -> &url::Url {
1542 &self.url
1543 }
1544
1545 pub fn author_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
1548 self.nodes_to_stylesheet.values()
1549 }
1550
1551 pub fn useragent_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
1553 self.ua_stylesheets.values()
1554 }
1555
1556 pub fn add_user_agent_stylesheet(&mut self, css: &str) {
1557 let sheet = self.make_stylesheet(css, Origin::UserAgent);
1558 self.ua_stylesheets.insert(css.to_string(), sheet.clone());
1559 self.stylist.append_stylesheet(sheet, &self.guard.read());
1560 }
1561
1562 pub fn make_stylesheet(&self, css: impl AsRef<str>, origin: Origin) -> DocumentStyleSheet {
1563 let data = Stylesheet::from_str(
1564 css.as_ref(),
1565 self.url.url_extra_data(),
1566 origin,
1567 ServoArc::new(self.guard.wrap(MediaList::empty())),
1568 self.guard.clone(),
1569 Some(&StylesheetLoader {
1570 tx: self.tx.clone(),
1571 doc_id: self.id,
1572 net_provider: self.net_provider.clone(),
1573 shell_provider: self.shell_provider.clone(),
1574 abort_signal: self.abort_signal.clone(),
1575 }),
1576 None,
1577 QuirksMode::NoQuirks,
1578 AllowImportRules::Yes,
1579 );
1580
1581 DocumentStyleSheet(ServoArc::new(data))
1582 }
1583
1584 pub fn upsert_stylesheet_for_node(&mut self, node_id: NodeId) {
1585 let raw_styles = self.nodes[node_id].text_content();
1586 let sheet = self.make_stylesheet(raw_styles, Origin::Author);
1587 self.add_stylesheet_for_node(sheet, node_id);
1588 }
1589
1590 pub fn add_stylesheet_for_node(&mut self, stylesheet: DocumentStyleSheet, node_id: NodeId) {
1591 let old = self.nodes_to_stylesheet.insert(node_id, stylesheet.clone());
1592
1593 if let Some(old) = old {
1594 self.stylist.remove_stylesheet(old, &self.guard.read())
1595 }
1596
1597 crate::net::fetch_font_face(
1599 self.tx.clone(),
1600 self.id,
1601 Some(node_id),
1602 &stylesheet.0,
1603 &self.net_provider,
1604 &self.shell_provider,
1605 &self.guard.read(),
1606 self.abort_signal.as_ref(),
1607 );
1608
1609 let element = &mut self.nodes[node_id].element_data_mut().unwrap();
1611 element.special_data = SpecialElementData::Stylesheet(stylesheet.clone());
1612
1613 let insertion_point = self
1615 .nodes_to_stylesheet
1616 .range((Bound::Excluded(node_id), Bound::Unbounded))
1617 .next()
1618 .map(|(_, sheet)| sheet);
1619
1620 if let Some(insertion_point) = insertion_point {
1621 self.stylist.insert_stylesheet_before(
1622 stylesheet,
1623 insertion_point.clone(),
1624 &self.guard.read(),
1625 )
1626 } else {
1627 self.stylist
1628 .append_stylesheet(stylesheet, &self.guard.read())
1629 }
1630 }
1631
1632 pub fn handle_messages(&mut self) {
1633 let rx = self.rx.take().unwrap();
1636
1637 while let Ok(msg) = rx.try_recv() {
1638 self.handle_message(msg);
1639 }
1640
1641 self.rx = Some(rx);
1643 }
1644
1645 pub fn handle_message(&mut self, msg: DocumentEvent) {
1646 match msg {
1647 DocumentEvent::ResourceLoad(resource) => self.load_resource(resource),
1648 DocumentEvent::NavigateIframe { node_id, url } => self.navigate_iframe(node_id, url),
1649 }
1650 }
1651
1652 pub fn has_pending_critical_resources(&self) -> bool {
1654 !self.pending_critical_resources.is_empty()
1655 }
1656
1657 pub fn pending_image_count(&self) -> usize {
1664 self.pending_images.len()
1665 }
1666
1667 pub fn load_resource(&mut self, res: ResourceLoadResponse) {
1668 self.pending_critical_resources.remove(&res.request_id);
1669
1670 let resource = match res.result {
1671 Ok(resource) => resource,
1672 Err(err) => {
1673 if let Some(url) = res.resolved_url.as_ref() {
1674 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1675 #[cfg(feature = "tracing")]
1676 tracing::warn!(
1677 url = url.as_str(),
1678 waiting_nodes = waiting_nodes.len(),
1679 error = err.as_str(),
1680 "Resource load failed"
1681 );
1682 #[cfg(not(feature = "tracing"))]
1683 let _ = (waiting_nodes, err);
1684 } else {
1685 #[cfg(feature = "tracing")]
1686 tracing::warn!(error = err.as_str(), "Resource load failed (no url)");
1687 #[cfg(not(feature = "tracing"))]
1688 let _ = err;
1689 }
1690 return;
1691 }
1692 };
1693
1694 match resource {
1695 Resource::Css(css) => {
1696 let node_id = res.node_id.unwrap();
1697 self.add_stylesheet_for_node(css, node_id);
1698 }
1699 Resource::ImportSheet(import_rule, sheet) => {
1700 {
1709 let mut guard = self.guard.write();
1710 import_rule.write_with(&mut guard).stylesheet =
1711 style::stylesheets::import_rule::ImportSheet::Sheet(sheet.clone());
1712 }
1713
1714 crate::net::fetch_font_face(
1718 self.tx.clone(),
1719 self.id,
1720 res.node_id,
1721 &sheet,
1722 &self.net_provider,
1723 &self.shell_provider,
1724 &self.guard.read(),
1725 self.abort_signal.as_ref(),
1726 );
1727 }
1728 Resource::Image(_kind, width, height, image_data) => {
1729 let image = ImageData::Raster(RasterImageData::new(width, height, image_data));
1731
1732 let Some(url) = res.resolved_url.as_ref() else {
1733 return;
1734 };
1735
1736 self.apply_loaded_image(url, image);
1737 }
1738 #[cfg(feature = "svg")]
1739 Resource::Svg(_kind, svg) => {
1740 let image = ImageData::Svg(svg);
1742
1743 let Some(url) = res.resolved_url.as_ref() else {
1744 return;
1745 };
1746
1747 self.apply_loaded_image(url, image);
1748 }
1749 Resource::DocumentSrc(html) => {
1750 let Some(node_id) = res.node_id else {
1751 return;
1752 };
1753 self.apply_iframe_html(node_id, res.request_id, res.resolved_url, &html);
1754 }
1755 Resource::Font(bytes, overrides) => {
1756 let font = Blob::new(Arc::new(bytes));
1757
1758 let weight_override = overrides.weight.map(parley::fontique::FontWeight::new);
1764 let info_override = parley::fontique::FontInfoOverride {
1765 family_name: overrides.family_name.as_deref(),
1766 weight: weight_override,
1767 style: overrides.style,
1768 ..Default::default()
1769 };
1770
1771 let mut global_font_ctx = self.font_ctx.lock().unwrap();
1773 global_font_ctx
1774 .collection
1775 .register_fonts(font.clone(), Some(info_override));
1776
1777 #[cfg(feature = "parallel-construct")]
1778 {
1779 rayon::broadcast(|_ctx| {
1780 let mut font_ctx = self
1781 .thread_font_contexts
1782 .get_or(|| RefCell::new(Box::new(global_font_ctx.clone())))
1783 .borrow_mut();
1784 font_ctx
1785 .collection
1786 .register_fonts(font.clone(), Some(info_override));
1787 });
1788 }
1789 drop(global_font_ctx);
1790
1791 self.invalidate_inline_contexts();
1793 }
1794 Resource::None => {
1795 }
1797 }
1798 }
1799
1800 fn apply_loaded_image(&mut self, url: &str, image: ImageData) {
1803 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1805
1806 #[cfg(feature = "tracing")]
1807 tracing::info!(
1808 "Image {url} loaded, applying to {} nodes",
1809 waiting_nodes.len()
1810 );
1811
1812 self.image_cache.insert(url.to_string(), image.clone());
1814
1815 for (node_id, image_type) in waiting_nodes {
1817 let Some(node) = self.get_node_mut(node_id) else {
1818 continue;
1819 };
1820
1821 match image_type {
1822 ImageType::Image => {
1823 node.element_data_mut().unwrap().special_data =
1824 SpecialElementData::Image(Box::new(image.clone()));
1825
1826 node.cache_mut().clear();
1828 node.insert_damage(ALL_DAMAGE);
1829 }
1830 ImageType::Background(idx) | ImageType::Mask(idx) => {
1831 let layer_image = node.element_data_mut().and_then(|el| {
1832 let images = match image_type {
1833 ImageType::Background(_) => &mut el.background_images,
1834 ImageType::Mask(_) => &mut el.mask_images,
1835 ImageType::Image => unreachable!(),
1836 };
1837 images.get_mut(idx)
1838 });
1839 if let Some(Some(layer_image)) = layer_image {
1840 layer_image.status = Status::Ok;
1841 layer_image.image = image.clone();
1842 }
1843 }
1844 }
1845 }
1846 }
1847
1848 pub fn snapshot_node(&mut self, node_id: NodeId) {
1849 let node = &mut self.nodes[node_id];
1850
1851 let has_been_styled = node.primary_styles().is_some();
1856 if !has_been_styled {
1857 return;
1858 }
1859
1860 let opaque_node_id = TNode::opaque(&&*node);
1861 node.set_has_snapshot(true);
1862 node.snapshot_handled()
1863 .store(false, std::sync::atomic::Ordering::SeqCst);
1864
1865 if let Some(_existing_snapshot) = self.snapshots.get_mut(&opaque_node_id) {
1867 } else {
1870 let attrs: Option<Vec<_>> = node.attrs().map(|attrs| {
1871 attrs
1872 .iter()
1873 .map(|attr| {
1874 let ident = AttrIdentifier {
1875 local_name: GenericAtomIdent(attr.name.local.clone()),
1876 name: GenericAtomIdent(attr.name.local.clone()),
1877 namespace: GenericAtomIdent(attr.name.ns.clone()),
1878 prefix: None,
1879 };
1880
1881 let value = if attr.name.local == local_name!("id") {
1882 AttrValue::Atom(Atom::from(&*attr.value))
1883 } else if attr.name.local == local_name!("class") {
1884 let classes = attr
1885 .value
1886 .split_ascii_whitespace()
1887 .map(Atom::from)
1888 .collect();
1889 AttrValue::TokenList(OnceLock::from(attr.value.to_string()), classes)
1895 } else {
1896 AttrValue::String(attr.value.to_string())
1897 };
1898
1899 (ident, value)
1900 })
1901 .collect()
1902 });
1903
1904 let changed_attrs = attrs
1905 .as_ref()
1906 .map(|attrs| attrs.iter().map(|attr| attr.0.name.clone()).collect())
1907 .unwrap_or_default();
1908
1909 self.snapshots.insert(
1910 opaque_node_id,
1911 ServoElementSnapshot {
1912 state: Some(*node.element_state()),
1913 attrs,
1914 changed_attrs,
1915 class_changed: true,
1916 id_changed: true,
1917 other_attributes_changed: true,
1918 },
1919 );
1920 }
1921 }
1922
1923 pub fn snapshot_node_and(&mut self, node_id: NodeId, cb: impl FnOnce(&mut Node)) {
1930 if !self.nodes.contains_key(node_id) {
1931 return;
1932 }
1933 self.snapshot_node(node_id);
1934 cb(&mut self.nodes[node_id]);
1935 }
1936
1937 pub fn hit(&self, x: f32, y: f32) -> Option<HitResult> {
1939 self.hit_with_scrollbar(x, y).0
1940 }
1941
1942 pub fn nearest_non_anonymous_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
1955 let mut node = self.get_node(node_id)?;
1959 loop {
1960 let parent = match node.parent {
1961 Some(parent_id) => self.get_node(parent_id)?,
1962 None => return Some(node.id),
1963 };
1964 if !node.is_anonymous() && !parent.is_anonymous() {
1965 return Some(node.id);
1966 }
1967 node = parent;
1968 }
1969 }
1970
1971 pub fn focus_next_node(&mut self) -> Option<NodeId> {
1972 let focussed_node_id = self.get_focussed_node_id()?;
1973 let id = self.next_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
1974 self.set_focus_to(id);
1975 Some(id)
1976 }
1977
1978 pub fn focus_prev_node(&mut self) -> Option<NodeId> {
1980 let focussed_node_id = self.get_focussed_node_id()?;
1981 let id = self.prev_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
1982 self.set_focus_to(id);
1983 Some(id)
1984 }
1985
1986 pub fn clear_focus(&mut self) {
1988 if let Some(id) = self.focus_node_id {
1989 let shell_provider = self.shell_provider.clone();
1990 self.snapshot_node_and(id, |node| node.blur(shell_provider));
1991 self.focus_node_id = None;
1992 }
1993 }
1994
1995 pub fn set_mousedown_node_id(&mut self, node_id: Option<NodeId>) {
1996 self.mousedown_node_id = node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
1997 }
1998 pub fn set_focus_to(&mut self, focus_node_id: NodeId) -> bool {
1999 let Some(focus_node_id) = self.nearest_non_anonymous_ancestor(focus_node_id) else {
2000 return false;
2001 };
2002 if Some(focus_node_id) == self.focus_node_id {
2003 return false;
2004 }
2005
2006 #[cfg(feature = "tracing")]
2007 tracing::info!("Focussed node {focus_node_id}");
2008
2009 let shell_provider = self.shell_provider.clone();
2010
2011 if let Some(id) = self.focus_node_id {
2013 self.snapshot_node_and(id, |node| node.blur(shell_provider.clone()));
2014 }
2015
2016 self.snapshot_node_and(focus_node_id, |node| node.focus(shell_provider));
2018
2019 self.focus_node_id = Some(focus_node_id);
2020
2021 true
2022 }
2023
2024 pub fn active_node(&mut self) -> bool {
2025 let Some(hover_node_id) = self.get_hover_node_id() else {
2026 return false;
2027 };
2028
2029 if let Some(active_node_id) = self.active_node_id {
2030 if active_node_id == hover_node_id {
2031 return true;
2032 }
2033 self.unactive_node();
2034 }
2035
2036 debug_assert!(
2038 self.get_node(hover_node_id)
2039 .is_some_and(|node| !node.is_anonymous()),
2040 "interaction state must reference DOM nodes, not layout-generated nodes"
2041 );
2042 let active_node_id = Some(hover_node_id);
2043
2044 let node_path = self.maybe_node_layout_ancestors(active_node_id);
2045 for &id in node_path.iter() {
2046 self.snapshot_node_and(id, |node| node.active());
2047 }
2048
2049 self.active_node_id = active_node_id;
2050
2051 true
2052 }
2053
2054 pub fn unactive_node(&mut self) -> bool {
2055 let Some(active_node_id) = self.active_node_id.take() else {
2056 return false;
2057 };
2058
2059 let node_path = self.maybe_node_layout_ancestors(Some(active_node_id));
2060 for &id in node_path.iter() {
2061 self.snapshot_node_and(id, |node| node.unactive());
2062 }
2063
2064 true
2065 }
2066
2067 pub fn hovered_scrollbar(&self) -> Option<crate::node::ScrollbarRef> {
2069 self.hovered_scrollbar
2070 }
2071
2072 pub fn scrollbar_drag_target(&self) -> Option<crate::node::ScrollbarRef> {
2074 match &self.drag_mode {
2075 DragMode::ScrollbarDrag(state) => Some(state.scrollbar),
2076 _ => None,
2077 }
2078 }
2079
2080 pub fn scrollbar_opacity(&self, node_id: NodeId) -> f32 {
2085 let interacting = |scrollbar: &crate::node::ScrollbarRef| scrollbar.node_id == node_id;
2086 if self.hovered_scrollbar.as_ref().is_some_and(interacting)
2087 || self
2088 .scrollbar_drag_target()
2089 .as_ref()
2090 .is_some_and(interacting)
2091 {
2092 return 1.0;
2093 }
2094 self.scrollbar_activity.get(&node_id).map_or(1.0, |last| {
2095 crate::node::scrollbar::opacity_at(last.elapsed())
2096 })
2097 }
2098
2099 pub(crate) fn show_scrollbars(&mut self, node_id: NodeId) {
2102 if cfg!(feature = "scrollbars") {
2103 self.scrollbar_activity.insert(node_id, Instant::now());
2104 }
2105 }
2106
2107 fn scrollbars_animating(&self) -> bool {
2110 use crate::node::scrollbar::{FADE_DELAY, FADE_DURATION};
2111 self.scrollbar_activity
2112 .values()
2113 .any(|last| last.elapsed() < FADE_DELAY + FADE_DURATION)
2114 }
2115
2116 pub(crate) fn hit_with_scrollbar(
2120 &self,
2121 x: f32,
2122 y: f32,
2123 ) -> (Option<HitResult>, Option<crate::node::ScrollbarRef>) {
2124 if TDocument::as_node(&self.root_node())
2125 .first_element_child()
2126 .is_none()
2127 {
2128 #[cfg(feature = "tracing")]
2129 tracing::warn!("No DOM - not resolving hit test");
2130 return (None, None);
2131 }
2132 let mut scrollbar = None;
2133 let hit = self
2134 .root_element()
2135 .hit_inner(x, y, self.viewport().scale_f64(), &mut scrollbar);
2136 (hit, scrollbar)
2137 }
2138
2139 pub fn set_hover_to(&mut self, x: f32, y: f32) -> bool {
2140 self.semantic_hover_node_id = None;
2141 self.last_client_pointer_position = Some(taffy::Point {
2145 x: x - self.viewport_scroll.x as f32,
2146 y: y - self.viewport_scroll.y as f32,
2147 });
2148
2149 let (hit, hovered_scrollbar) = self.hit_with_scrollbar(x, y);
2150 let hovered_scrollbar =
2153 hovered_scrollbar.filter(|scrollbar| self.scrollbar_opacity(scrollbar.node_id) > 0.0);
2154 let scrollbar_changed = hovered_scrollbar != self.hovered_scrollbar;
2158 if scrollbar_changed {
2159 for scrollbar in [self.hovered_scrollbar, hovered_scrollbar]
2162 .into_iter()
2163 .flatten()
2164 {
2165 self.show_scrollbars(scrollbar.node_id);
2166 }
2167 }
2168 self.hovered_scrollbar = hovered_scrollbar;
2169
2170 let hit_node_id = hit.map(|hit| hit.node_id);
2175 let hover_node_id = hit_node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
2176 let new_is_text = hit.map(|hit| hit.is_text).unwrap_or(false);
2177
2178 self.apply_hover_target(hit_node_id, hover_node_id, new_is_text, scrollbar_changed)
2179 }
2180
2181 pub fn set_hover_to_node(&mut self, node_id: NodeId, x: f32, y: f32) -> bool {
2189 self.semantic_hover_node_id = Some(node_id);
2190 self.last_client_pointer_position = Some(taffy::Point {
2191 x: x - self.viewport_scroll.x as f32,
2192 y: y - self.viewport_scroll.y as f32,
2193 });
2194
2195 let hovered_scrollbar = self.hovered_scrollbar.take();
2196 let scrollbar_changed = hovered_scrollbar.is_some();
2197 if let Some(scrollbar) = hovered_scrollbar {
2198 self.show_scrollbars(scrollbar.node_id);
2199 }
2200 let hover_node_id = self.nearest_non_anonymous_ancestor(node_id);
2201 self.apply_hover_target(Some(node_id), hover_node_id, false, scrollbar_changed)
2202 }
2203
2204 fn apply_hover_target(
2205 &mut self,
2206 hit_node_id: Option<NodeId>,
2207 hover_node_id: Option<NodeId>,
2208 new_is_text: bool,
2209 scrollbar_changed: bool,
2210 ) -> bool {
2211 let hit_changed =
2212 hit_node_id != self.hover_hit_node_id || new_is_text != self.hover_node_is_text;
2213 self.hover_hit_node_id = hit_node_id;
2214 self.hover_node_is_text = new_is_text;
2215
2216 if hover_node_id == self.hover_node_id {
2218 if hit_changed {
2219 self.shell_provider.set_cursor(self.get_cursor());
2223 }
2224 return scrollbar_changed;
2225 }
2226
2227 let old_node_path = self.maybe_node_layout_ancestors(self.hover_node_id);
2228 let new_node_path = self.maybe_node_layout_ancestors(hover_node_id);
2229 let same_count = old_node_path
2230 .iter()
2231 .zip(&new_node_path)
2232 .take_while(|(o, n)| o == n)
2233 .count();
2234 for &id in old_node_path.iter().skip(same_count) {
2235 self.snapshot_node_and(id, |node| node.unhover());
2236 }
2237 for &id in new_node_path.iter().skip(same_count) {
2238 self.snapshot_node_and(id, |node| node.hover());
2239 }
2240
2241 self.hover_node_id = hover_node_id;
2242
2243 self.shell_provider.set_cursor(self.get_cursor());
2245
2246 self.shell_provider.request_redraw();
2248
2249 true
2250 }
2251
2252 pub fn clear_hover(&mut self) -> bool {
2253 self.last_client_pointer_position = None;
2256 self.semantic_hover_node_id = None;
2257 self.hover_hit_node_id = None;
2258
2259 let Some(hover_node_id) = self.hover_node_id else {
2260 return false;
2261 };
2262
2263 let old_node_path = self.maybe_node_layout_ancestors(Some(hover_node_id));
2264 for &id in old_node_path.iter() {
2265 self.snapshot_node_and(id, |node| node.unhover());
2266 }
2267
2268 self.hover_node_id = None;
2269 self.hover_node_is_text = false;
2270
2271 self.shell_provider.set_cursor(self.get_cursor());
2273
2274 self.shell_provider.request_redraw();
2276
2277 true
2278 }
2279
2280 pub fn refresh_hover(&mut self) -> bool {
2286 if let Some(node_id) = self.semantic_hover_node_id {
2287 if self.get_node(node_id).is_some() {
2288 let hover_node_id = self.nearest_non_anonymous_ancestor(node_id);
2289 return self.apply_hover_target(Some(node_id), hover_node_id, false, false);
2290 }
2291 self.semantic_hover_node_id = None;
2292 }
2293 let Some(pos) = self.last_client_pointer_position else {
2294 return false;
2295 };
2296 let x = pos.x + self.viewport_scroll.x as f32;
2297 let y = pos.y + self.viewport_scroll.y as f32;
2298 self.set_hover_to(x, y)
2299 }
2300
2301 pub fn get_hover_node_id(&self) -> Option<NodeId> {
2302 self.hover_node_id
2303 }
2304
2305 pub fn get_mousedown_node_id(&self) -> Option<NodeId> {
2306 self.mousedown_node_id
2307 }
2308
2309 pub fn set_viewport(&mut self, viewport: Viewport) {
2310 let scale_has_changed = viewport.scale_f64() != self.viewport.scale_f64();
2311 self.viewport = viewport;
2312 self.set_stylist_device(make_device(
2313 &self.viewport,
2314 self.media_type.clone(),
2315 self.font_ctx.clone(),
2316 ));
2317 self.scroll_viewport_by(0.0, 0.0); if scale_has_changed {
2320 self.invalidate_inline_contexts();
2321 self.shell_provider.request_redraw();
2322 }
2323 }
2324
2325 pub fn media_type(&self) -> &MediaType {
2327 &self.media_type
2328 }
2329
2330 pub fn set_media_type(&mut self, media_type: MediaType) {
2333 if self.media_type == media_type {
2334 return;
2335 }
2336 self.media_type = media_type;
2337 self.set_stylist_device(make_device(
2338 &self.viewport,
2339 self.media_type.clone(),
2340 self.font_ctx.clone(),
2341 ));
2342 }
2343
2344 pub fn viewport(&self) -> &Viewport {
2345 &self.viewport
2346 }
2347
2348 pub fn viewport_mut(&mut self) -> ViewportMut<'_> {
2349 ViewportMut::new(self)
2350 }
2351
2352 pub fn zoom_by(&mut self, increment: f32) {
2353 *self.viewport.zoom_mut() += increment;
2354 self.set_viewport(self.viewport.clone());
2355 }
2356
2357 pub fn zoom_to(&mut self, zoom: f32) {
2358 *self.viewport.zoom_mut() = zoom;
2359 self.set_viewport(self.viewport.clone());
2360 }
2361
2362 pub fn get_viewport(&self) -> Viewport {
2363 self.viewport.clone()
2364 }
2365
2366 pub fn incremental_layout(&self) -> bool {
2368 self.incremental_layout
2369 }
2370
2371 pub fn set_incremental_layout(&mut self, enabled: bool) {
2373 self.incremental_layout = enabled;
2374 }
2375
2376 pub fn devtools(&self) -> &DevtoolSettings {
2377 &self.devtool_settings
2378 }
2379
2380 pub fn devtools_mut(&mut self) -> &mut DevtoolSettings {
2381 &mut self.devtool_settings
2382 }
2383
2384 pub fn subdoc(&self, node_id: NodeId) -> Option<&dyn Document> {
2385 self.get_node(node_id)
2386 .and_then(|node| node.element_data())
2387 .and_then(|el| el.sub_doc_data())
2388 }
2389
2390 pub fn subdoc_mut(&mut self, node_id: NodeId) -> Option<&mut dyn Document> {
2391 self.get_node_mut(node_id)
2392 .and_then(|node| node.element_data_mut())
2393 .and_then(|el| el.sub_doc_data_mut())
2394 }
2395
2396 pub fn is_animating(&self) -> bool {
2397 #[cfg(feature = "custom-widget")]
2398 let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
2399 self.nodes[node_id]
2400 .element_data()
2401 .and_then(|el| el.custom_widget_data())
2402 .is_some_and(|data| data.widget.requires_redraw())
2403 });
2404 #[cfg(not(feature = "custom-widget"))]
2405 let custom_widget_is_animating = false;
2406
2407 let animating = self.has_canvas
2408 | self.has_active_animations
2409 | (self.subdoc_animation_pacing != AnimationPacing::Idle)
2410 | custom_widget_is_animating
2411 | (self.scroll_animation != ScrollAnimationState::None)
2412 | self.scrollbars_animating();
2413
2414 if animating && crate::debug::animation_reasons_enabled() {
2415 crate::debug::report_animation_reasons(
2416 self.id(),
2417 self.has_canvas,
2418 self.has_active_animations,
2419 self.subdoc_animation_pacing != AnimationPacing::Idle,
2420 custom_widget_is_animating,
2421 self.scroll_animation != ScrollAnimationState::None,
2422 self.scrollbars_animating(),
2423 self.animating_node_names().as_deref(),
2424 );
2425 }
2426
2427 animating
2428 }
2429
2430 pub fn animation_pacing(&self) -> AnimationPacing {
2435 let focused_text_input = self.focus_node_id.is_some_and(|node_id| {
2436 self.nodes
2437 .get(node_id)
2438 .and_then(|node| node.element_data())
2439 .is_some_and(|element| element.text_input_data().is_some())
2440 });
2441 #[cfg(feature = "custom-widget")]
2442 let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
2443 self.nodes[node_id]
2444 .element_data()
2445 .and_then(|el| el.custom_widget_data())
2446 .is_some_and(|data| data.widget.requires_redraw())
2447 });
2448 #[cfg(not(feature = "custom-widget"))]
2449 let custom_widget_is_animating = false;
2450
2451 if self.has_canvas
2452 || custom_widget_is_animating
2453 || self.scroll_animation != ScrollAnimationState::None
2454 || self.scrollbars_animating()
2455 {
2456 AnimationPacing::Interactive
2457 } else if self.has_active_animations {
2458 const SLOW_ANIMATION_SECONDS: f64 = 2.0;
2459 let sets = self.animations.sets.read();
2460 let has_fast_animation_or_transition = sets.values().any(|set| {
2461 set.transitions.iter().any(|transition| {
2462 matches!(
2463 transition.state,
2464 AnimationState::Pending | AnimationState::Running
2465 )
2466 }) || set.animations.iter().any(|animation| {
2467 matches!(
2468 animation.state,
2469 AnimationState::Pending | AnimationState::Running
2470 ) && animation.duration < SLOW_ANIMATION_SECONDS
2471 })
2472 });
2473 if has_fast_animation_or_transition {
2474 AnimationPacing::Interactive
2475 } else {
2476 AnimationPacing::SlowCss
2477 }
2478 } else if focused_text_input {
2479 AnimationPacing::Caret
2480 } else if self.subdoc_animation_pacing != AnimationPacing::Idle {
2481 self.subdoc_animation_pacing
2482 } else {
2483 AnimationPacing::Idle
2484 }
2485 }
2486
2487 fn animating_node_names(&self) -> Option<String> {
2494 if !self.has_active_animations {
2495 return None;
2496 }
2497 let sets = self.animations.sets.read();
2498 let mut described: Vec<String> = sets
2499 .iter()
2500 .filter(|(_, state)| state.needs_animation_ticks())
2501 .filter_map(|(key, state)| {
2502 let node_id = NodeId::from_u64(key.node.id() as u64);
2503 let node = self.nodes.get(node_id)?;
2504 let element = node.element_data()?;
2505 let name = element
2506 .attr(local_name!("id"))
2507 .map(|id| format!("#{id}"))
2508 .or_else(|| {
2509 element
2510 .attr(local_name!("class"))
2511 .and_then(|c| c.split_ascii_whitespace().next())
2512 .map(|c| format!(".{c}"))
2513 })
2514 .unwrap_or_else(|| element.name.local.to_string());
2515 Some(format!(
2516 "{name}(anim={},trans={},in_doc={})",
2517 state.animations.len(),
2518 state.transitions.len(),
2519 node.flags.is_in_document(),
2520 ))
2521 })
2522 .collect();
2523 described.sort();
2524 described.truncate(12);
2525 Some(described.join(" "))
2526 }
2527
2528 pub fn set_stylist_device(&mut self, device: Device) {
2530 let root_styles = self
2536 .try_root_element()
2537 .and_then(|root| root.primary_styles());
2538 if let Some(root_style) = root_styles.as_deref() {
2539 device.set_root_style(root_style);
2540
2541 let font = root_style.get_font();
2542 let font_size = font.clone_font_size().computed_size();
2543 device.set_root_font_size(root_style.effective_zoom.unzoom(font_size.px()));
2544
2545 let line_height = device
2546 .calc_line_height(font, root_style.writing_mode, None)
2547 .0;
2548 device.set_root_line_height(root_style.effective_zoom.unzoom(line_height.px()));
2549 }
2550 drop(root_styles);
2551
2552 let origins = {
2553 let guard = &self.guard;
2554 let guards = StylesheetGuards {
2555 author: &guard.read(),
2556 ua_or_user: &guard.read(),
2557 };
2558 self.stylist.set_device(device, &guards)
2559 };
2560 self.stylist.force_stylesheet_origins_dirty(origins);
2561 }
2562
2563 pub fn stylist_device(&mut self) -> &Device {
2564 self.stylist.device()
2565 }
2566
2567 pub fn get_cursor(&self) -> Option<CursorIcon> {
2575 let node_id = self
2580 .hover_hit_node_id
2581 .filter(|&id| self.nodes.contains_key(id))
2582 .or(self.get_hover_node_id());
2583 let Some(node_id) = node_id else {
2584 return Some(CursorIcon::Default);
2585 };
2586 let node = &self.nodes[node_id];
2587
2588 if let Some(subdoc) = node.subdoc().map(|doc| doc.inner()) {
2589 if subdoc.hover_hit_node_id.is_some() || subdoc.get_hover_node_id().is_some() {
2595 return subdoc.get_cursor();
2596 }
2597 return Some(CursorIcon::Default);
2598 }
2599
2600 let Some(style) = node.primary_styles() else {
2601 return Some(CursorIcon::Default);
2602 };
2603 let user_select = style.clone_user_select();
2604 let keyword = style.clone_cursor().keyword;
2605
2606 if keyword != CursorKind::Auto {
2608 return stylo_to_cursor_icon(keyword);
2609 }
2610
2611 if node
2613 .element_data()
2614 .is_some_and(|e| e.text_input_data().is_some())
2615 {
2616 return Some(CursorIcon::Text);
2617 }
2618
2619 let mut maybe_node = Some(node);
2621 while let Some(node) = maybe_node {
2622 if node.is_link() {
2623 return Some(CursorIcon::Pointer);
2624 }
2625
2626 maybe_node = node.layout_parent.get().map(|node_id| node.with(node_id));
2627 }
2628
2629 if self.hover_node_is_text {
2631 return Some(match user_select {
2632 UserSelect::Text | UserSelect::All | UserSelect::Auto => CursorIcon::Text,
2633 UserSelect::None => CursorIcon::Default,
2634 });
2635 }
2636
2637 Some(CursorIcon::Default)
2639 }
2640
2641 pub fn scroll_node_by<F: FnMut(DomEvent)>(
2642 &mut self,
2643 node_id: NodeId,
2644 x: f64,
2645 y: f64,
2646 dispatch_event: F,
2647 ) {
2648 self.scroll_node_by_has_changed(node_id, x, y, dispatch_event);
2649 }
2650
2651 pub fn scroll_node_by_has_changed<F: FnMut(DomEvent)>(
2661 &mut self,
2662 node_id: NodeId,
2663 x: f64,
2664 y: f64,
2665 dispatch_event: F,
2666 ) -> bool {
2667 let has_changed = self.scroll_node_by_inner(node_id, x, y, dispatch_event);
2668 if has_changed {
2669 self.resolve_sticky_positions();
2670 }
2671 has_changed
2672 }
2673
2674 fn scroll_node_by_inner<F: FnMut(DomEvent)>(
2675 &mut self,
2676 node_id: NodeId,
2677 x: f64,
2678 y: f64,
2679 mut dispatch_event: F,
2680 ) -> bool {
2681 if self.try_root_element().is_some_and(|el| el.id == node_id) {
2686 let has_changed = self.scroll_viewport_by_has_changed(x, y);
2687 if has_changed {
2688 let layout = *self.root_element().final_layout();
2689 let scale = self.viewport.scale() as f64;
2690 let event = BlitzScrollEvent {
2691 scroll_top: self.viewport_scroll.y,
2692 scroll_left: self.viewport_scroll.x,
2693 scroll_width: layout.size.width.max(layout.content_size.width) as i32,
2694 scroll_height: layout.size.height.max(layout.content_size.height) as i32,
2695 client_width: (self.viewport.window_size.0 as f64 / scale) as i32,
2696 client_height: (self.viewport.window_size.1 as f64 / scale) as i32,
2697 };
2698 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
2699 }
2700 return has_changed;
2701 }
2702
2703 let Some(node) = self.nodes.get_mut(node_id) else {
2704 return false;
2705 };
2706
2707 if node
2711 .element_data()
2712 .is_some_and(|el| el.text_input_data().is_some())
2713 {
2714 let parent = node.parent;
2715 let content_box_width = node.final_layout().content_box_width();
2716 let content_box_height = node.final_layout().content_box_height();
2717 let input = node
2718 .element_data_mut()
2719 .and_then(|el| el.text_input_data_mut())
2720 .unwrap();
2721
2722 let (bubble_x, bubble_y) = if input.is_multiline {
2723 (
2724 x,
2725 input.scroll_by(y as f32, content_box_width, content_box_height) as f64,
2726 )
2727 } else {
2728 (
2729 input.scroll_by(x as f32, content_box_width, content_box_height) as f64,
2730 y,
2731 )
2732 };
2733
2734 let has_changed = bubble_x != x || bubble_y != y;
2735
2736 if bubble_x != 0.0 || bubble_y != 0.0 {
2737 let bubbled = if let Some(parent) = parent {
2738 self.scroll_node_by_inner(parent, bubble_x, bubble_y, dispatch_event)
2739 } else {
2740 self.scroll_viewport_by_has_changed(bubble_x, bubble_y)
2741 };
2742 return bubbled | has_changed;
2743 }
2744
2745 return has_changed;
2746 }
2747
2748 let (can_x_scroll, can_y_scroll) = node
2749 .primary_styles()
2750 .map(|styles| {
2751 (
2752 matches!(styles.clone_overflow_x(), Overflow::Scroll | Overflow::Auto),
2753 matches!(styles.clone_overflow_y(), Overflow::Scroll | Overflow::Auto),
2754 )
2755 })
2756 .unwrap_or((false, false));
2757
2758 let initial = *node.scroll_offset();
2759 let new_x = node.scroll_offset().x - x;
2760 let new_y = node.scroll_offset().y - y;
2761
2762 let mut bubble_x = 0.0;
2763 let mut bubble_y = 0.0;
2764
2765 let scroll_width = node.final_layout().scroll_width() as f64;
2766 let scroll_height = node.final_layout().scroll_height() as f64;
2767
2768 if let Some(mut sub_doc) = node.subdoc_mut().map(|doc| doc.inner_mut()) {
2770 let has_changed = if let Some(hover_node_id) = sub_doc.get_hover_node_id() {
2771 sub_doc.scroll_node_by_has_changed(hover_node_id, x, y, dispatch_event)
2772 } else {
2773 sub_doc.scroll_viewport_by_has_changed(x, y)
2774 };
2775
2776 return has_changed;
2778 }
2779
2780 if !can_x_scroll {
2782 bubble_x = x
2783 } else if new_x < 0.0 {
2784 bubble_x = -new_x;
2785 node.scroll_offset_mut().x = 0.0;
2786 } else if new_x > scroll_width {
2787 bubble_x = scroll_width - new_x;
2788 node.scroll_offset_mut().x = scroll_width;
2789 } else {
2790 node.scroll_offset_mut().x = new_x;
2791 }
2792
2793 if !can_y_scroll {
2794 bubble_y = y
2795 } else if new_y < 0.0 {
2796 bubble_y = -new_y;
2797 node.scroll_offset_mut().y = 0.0;
2798 } else if new_y > scroll_height {
2799 bubble_y = scroll_height - new_y;
2800 node.scroll_offset_mut().y = scroll_height;
2801 } else {
2802 node.scroll_offset_mut().y = new_y;
2803 }
2804
2805 let has_changed = *node.scroll_offset() != initial;
2806
2807 if has_changed {
2808 let layout = *node.final_layout();
2809 let event = BlitzScrollEvent {
2810 scroll_top: node.scroll_offset().y,
2811 scroll_left: node.scroll_offset().x,
2812 scroll_width: layout.scroll_width() as i32,
2813 scroll_height: layout.scroll_height() as i32,
2814 client_width: layout.size.width as i32,
2815 client_height: layout.size.height as i32,
2816 };
2817
2818 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
2819 }
2820
2821 let parent = node.parent;
2822 if has_changed {
2823 self.show_scrollbars(node_id);
2824 }
2825
2826 if bubble_x != 0.0 || bubble_y != 0.0 {
2827 if let Some(parent) = parent {
2828 return self.scroll_node_by_inner(parent, bubble_x, bubble_y, dispatch_event)
2829 | has_changed;
2830 } else {
2831 return self.scroll_viewport_by_has_changed(bubble_x, bubble_y) | has_changed;
2832 }
2833 }
2834
2835 has_changed
2836 }
2837
2838 pub fn scroll_viewport_by(&mut self, x: f64, y: f64) {
2839 self.scroll_viewport_by_has_changed(x, y);
2840 }
2841
2842 pub fn scroll_viewport_by_has_changed(&mut self, x: f64, y: f64) -> bool {
2844 let (content_width, content_height) = match self.try_root_element() {
2849 Some(root) => {
2850 let root_layout = root.final_layout();
2851 (
2852 root_layout.size.width.max(root_layout.content_size.width) as f64,
2853 root_layout.size.height.max(root_layout.content_size.height) as f64,
2854 )
2855 }
2856 None => (0.0, 0.0),
2857 };
2858 let new_scroll = (self.viewport_scroll.x - x, self.viewport_scroll.y - y);
2859 let window_width = self.viewport.window_size.0 as f64 / self.viewport.scale() as f64;
2860 let window_height = self.viewport.window_size.1 as f64 / self.viewport.scale() as f64;
2861
2862 let initial = self.viewport_scroll;
2863 self.viewport_scroll.x =
2864 f64::max(0.0, f64::min(new_scroll.0, content_width - window_width));
2865 self.viewport_scroll.y =
2866 f64::max(0.0, f64::min(new_scroll.1, content_height - window_height));
2867
2868 let has_changed = self.viewport_scroll != initial;
2869 if has_changed {
2870 self.resolve_sticky_positions();
2875 self.resolve_fixed_positions();
2876 }
2877 has_changed
2878 }
2879
2880 pub fn scroll_by(
2881 &mut self,
2882 anchor_node_id: Option<NodeId>,
2883 scroll_x: f64,
2884 scroll_y: f64,
2885 dispatch_event: &mut dyn FnMut(DomEvent),
2886 ) -> bool {
2887 if let Some(anchor_node_id) = anchor_node_id {
2888 self.scroll_node_by_has_changed(anchor_node_id, scroll_x, scroll_y, dispatch_event)
2889 } else {
2890 self.scroll_viewport_by_has_changed(scroll_x, scroll_y)
2891 }
2892 }
2893
2894 pub fn viewport_scroll(&self) -> crate::Point<f64> {
2895 self.viewport_scroll
2896 }
2897
2898 pub fn set_viewport_scroll(&mut self, scroll: crate::Point<f64>) {
2899 self.viewport_scroll = scroll;
2900 }
2901
2902 pub fn get_fragment_target(&self, fragment: &str) -> Option<NodeId> {
2907 if let Some(node_id) = self.get_element_by_id(fragment) {
2908 return Some(node_id);
2909 }
2910
2911 self.nodes.iter().find_map(|(id, node)| {
2913 let el = node.element_data()?;
2914 (el.name.local == local_name!("a") && el.attr(local_name!("name")) == Some(fragment))
2915 .then_some(id)
2916 })
2917 }
2918
2919 pub fn nearest_scroll_container(&self, node_id: NodeId) -> Option<NodeId> {
2930 let mut current = Some(node_id);
2931 for _ in 0..64 {
2932 let id = current?;
2933 let node = self.nodes.get(id)?;
2934 if node.style().overflow.x.is_scroll_container()
2935 || node.style().overflow.y.is_scroll_container()
2936 {
2937 return Some(id);
2938 }
2939 current = node.parent;
2940 }
2941 None
2942 }
2943
2944 pub fn scroll_nearest_container_by(&mut self, node_id: NodeId, x: f64, y: f64) -> bool {
2945 self.scroll_nearest_container_by_with_events(node_id, x, y, |_| {})
2946 }
2947
2948 pub fn scroll_nearest_container_by_with_events<F: FnMut(DomEvent)>(
2949 &mut self,
2950 node_id: NodeId,
2951 x: f64,
2952 y: f64,
2953 mut dispatch_event: F,
2954 ) -> bool {
2955 let mut current = Some(node_id);
2956 for _ in 0..64 {
2957 let Some(id) = current else { break };
2958 let Some(node) = self.nodes.get(id) else {
2959 break;
2960 };
2961 let scrolls = node.style().overflow.x.is_scroll_container()
2962 || node.style().overflow.y.is_scroll_container();
2963 if scrolls {
2964 self.scroll_node_by(id, x, y, &mut dispatch_event);
2965 return true;
2966 }
2967 current = node.parent;
2968 }
2969 self.scroll_viewport_by(x, y);
2970 false
2971 }
2972
2973 pub fn scroll_to_node(&mut self, node_id: NodeId) {
2974 self.scroll_to_node_with_events(node_id, |_| {});
2975 }
2976
2977 pub fn scroll_to_node_with_events<F: FnMut(DomEvent)>(
2978 &mut self,
2979 node_id: NodeId,
2980 dispatch_event: F,
2981 ) {
2982 self.scroll_to_node_aligned_with_events(node_id, false, dispatch_event);
2983 }
2984
2985 pub fn scroll_to_node_centered_with_events<F: FnMut(DomEvent)>(
2991 &mut self,
2992 node_id: NodeId,
2993 dispatch_event: F,
2994 ) {
2995 self.scroll_to_node_aligned_with_events(node_id, true, dispatch_event);
2996 }
2997
2998 fn scroll_to_node_aligned_with_events<F: FnMut(DomEvent)>(
2999 &mut self,
3000 node_id: NodeId,
3001 centered: bool,
3002 mut dispatch_event: F,
3003 ) {
3004 let mut scroll_target = Some(node_id);
3011 let node_id = loop {
3012 let Some(candidate) = scroll_target else {
3013 return;
3014 };
3015 let Some(node) = self.nodes.get(candidate) else {
3016 return;
3017 };
3018 if matches!(
3019 node.data,
3020 NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
3021 ) {
3022 break candidate;
3023 }
3024 scroll_target = node.layout_parent.get().or(node.parent);
3025 };
3026
3027 let mut chain = Vec::new();
3039 let mut current = self.nodes.get(node_id).and_then(|node| node.parent);
3040 while let Some(id) = current {
3041 let Some(node) = self.nodes.get(id) else {
3042 break;
3043 };
3044 let scrolls = node.style().overflow.x.is_scroll_container()
3045 || node.style().overflow.y.is_scroll_container();
3046 if scrolls {
3047 chain.push(id);
3048 }
3049 current = node.parent;
3050 }
3051
3052 for container in chain {
3056 let Some(node) = self.nodes.get(node_id) else {
3057 return;
3058 };
3059 let target = node.absolute_position(0.0, 0.0);
3060 let Some(scroller) = self.nodes.get(container) else {
3061 continue;
3062 };
3063 let box_ = scroller.absolute_position(0.0, 0.0);
3064 let layout = scroller.final_layout();
3065 let target_layout = node.final_layout();
3066 let (dx, dy) = if centered {
3070 (
3071 f64::from(
3072 box_.x + layout.size.width / 2.0
3073 - (target.x + target_layout.size.width / 2.0),
3074 ),
3075 f64::from(
3076 box_.y + layout.size.height / 2.0
3077 - (target.y + target_layout.size.height / 2.0),
3078 ),
3079 )
3080 } else {
3081 (f64::from(box_.x - target.x), f64::from(box_.y - target.y))
3082 };
3083 self.scroll_node_by(container, dx, dy, &mut dispatch_event);
3084 }
3085
3086 let Some(node) = self.nodes.get(node_id) else {
3089 return;
3090 };
3091 let target = node.absolute_position(0.0, 0.0);
3092 let current = self.viewport_scroll;
3093 let (desired_x, desired_y) = if centered {
3094 let target_layout = node.final_layout();
3095 let scale = self.viewport.scale();
3096 let viewport_width = self.viewport.window_size.0 as f32 / scale;
3097 let viewport_height = self.viewport.window_size.1 as f32 / scale;
3098 (
3099 f64::from(target.x + target_layout.size.width / 2.0 - viewport_width / 2.0)
3100 .max(0.0),
3101 f64::from(target.y + target_layout.size.height / 2.0 - viewport_height / 2.0)
3102 .max(0.0),
3103 )
3104 } else {
3105 (f64::from(target.x), f64::from(target.y))
3106 };
3107
3108 let dx = current.x - desired_x;
3111 let dy = current.y - desired_y;
3112 if let Some(root) = self.try_root_element().map(|element| element.id) {
3113 self.scroll_node_by(root, dx, dy, dispatch_event);
3114 } else {
3115 self.scroll_viewport_by(dx, dy);
3116 }
3117 }
3118
3119 pub fn scroll_to_fragment(&mut self, fragment: &str) -> bool {
3125 let decoded = percent_encoding::percent_decode_str(fragment)
3127 .decode_utf8_lossy()
3128 .into_owned();
3129
3130 if !decoded.is_empty() {
3131 if let Some(node_id) = self.get_fragment_target(&decoded) {
3132 self.scroll_to_node(node_id);
3133 return true;
3134 }
3135 }
3136
3137 if decoded.is_empty() || decoded.eq_ignore_ascii_case("top") {
3140 let current = self.viewport_scroll;
3141 self.scroll_viewport_by(current.x, current.y);
3142 return true;
3143 }
3144
3145 false
3146 }
3147
3148 pub fn get_client_bounding_rect(&self, node_id: NodeId) -> Option<BoundingRect> {
3150 if let Some(rects) = self.inline_fragment_rects(node_id) {
3153 let rects: Vec<_> = rects
3154 .into_iter()
3155 .map(|rect| self.transformed_client_rect(node_id, rect))
3156 .collect();
3157 let x0 = rects.iter().map(|r| r.x).fold(f64::INFINITY, f64::min);
3158 let y0 = rects.iter().map(|r| r.y).fold(f64::INFINITY, f64::min);
3159 let x1 = rects
3160 .iter()
3161 .map(|r| r.x + r.width)
3162 .fold(f64::NEG_INFINITY, f64::max);
3163 let y1 = rects
3164 .iter()
3165 .map(|r| r.y + r.height)
3166 .fold(f64::NEG_INFINITY, f64::max);
3167 return match rects.is_empty() {
3168 true => None,
3169 false => Some(BoundingRect {
3170 x: x0,
3171 y: y0,
3172 width: x1 - x0,
3173 height: y1 - y0,
3174 }),
3175 };
3176 }
3177
3178 let node = self.get_node(node_id)?;
3179 if !matches!(
3180 node.data,
3181 NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
3182 ) {
3183 return None;
3184 }
3185 let pos = node.absolute_position(0.0, 0.0);
3186
3187 Some(self.transformed_client_rect(
3188 node_id,
3189 BoundingRect {
3190 x: pos.x as f64 - self.viewport_scroll.x,
3191 y: pos.y as f64 - self.viewport_scroll.y,
3192 width: node.unrounded_layout().size.width as f64,
3193 height: node.unrounded_layout().size.height as f64,
3194 },
3195 ))
3196 }
3197
3198 fn transformed_client_rect(&self, node_id: NodeId, rect: BoundingRect) -> BoundingRect {
3201 let mut matrix = kurbo::Affine::IDENTITY;
3202 let mut current = self.get_node(node_id);
3203 let scale = self.viewport.scale_f64();
3204 while let Some(node) = current {
3205 if matches!(
3206 node.data,
3207 NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
3208 ) && let Some(transform) = *node.transform()
3209 {
3210 let [a, b, c, d, e, f] = transform.as_coeffs();
3211 let css_transform = kurbo::Affine::new([a, b, c, d, e / scale, f / scale]);
3212 let origin = node.absolute_position(0.0, 0.0);
3213 let translate = kurbo::Affine::translate((
3214 origin.x as f64 - self.viewport_scroll.x,
3215 origin.y as f64 - self.viewport_scroll.y,
3216 ));
3217 matrix = translate * css_transform * translate.inverse() * matrix;
3218 }
3219 current = node
3220 .layout_parent
3221 .get()
3222 .or(node.parent)
3223 .and_then(|id| self.get_node(id));
3224 }
3225 let transformed = matrix.transform_rect_bbox(kurbo::Rect::new(
3226 rect.x,
3227 rect.y,
3228 rect.x + rect.width,
3229 rect.y + rect.height,
3230 ));
3231 BoundingRect {
3232 x: transformed.x0,
3233 y: transformed.y0,
3234 width: transformed.width(),
3235 height: transformed.height(),
3236 }
3237 }
3238
3239 pub fn node_client_rects(&self, node_id: NodeId) -> Vec<BoundingRect> {
3244 match self.inline_fragment_rects(node_id) {
3245 Some(rects) => rects
3246 .into_iter()
3247 .map(|rect| self.transformed_client_rect(node_id, rect))
3248 .collect(),
3249 None => self.get_client_bounding_rect(node_id).into_iter().collect(),
3250 }
3251 }
3252
3253 pub(crate) fn trace_escaped_inline_fragments(&self) {
3267 static TRACE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3268 if !*TRACE.get_or_init(|| std::env::var_os("BLITZ_TRACE_INLINE").is_some()) {
3269 return;
3270 }
3271 let mut reported = 0;
3272 for (id, node) in self.nodes.iter() {
3273 if !node.is_element() {
3274 continue;
3275 }
3276 let Some(rects) = self.inline_fragment_rects(id) else {
3277 continue;
3278 };
3279 let Some(root) = node.inline_root_ancestor() else {
3280 continue;
3281 };
3282 let root_layout = root.final_layout();
3283 let root_pos = root.absolute_position(0.0, 0.0);
3284 let root_right =
3285 root_pos.x as f64 + root_layout.size.width as f64 - self.viewport_scroll.x;
3286 for rect in &rects {
3287 if rect.x + rect.width > root_right + 1.0 {
3288 reported += 1;
3289 if reported <= 12 {
3290 eprintln!(
3291 "escaped-fragment node={id:?} rect=[{:.1},{:.1} {:.1}x{:.1}] \
3292root={:?} root_right={root_right:.1} root_w={:.1} lines={} layout_scale={:.2} vp_scale={:.2} layout_w={:.1}",
3293 rect.x,
3294 rect.y,
3295 rect.width,
3296 rect.height,
3297 root.id,
3298 root_layout.size.width,
3299 root.element_data()
3300 .and_then(|e| e.inline_layout_data.as_ref())
3301 .map(|i| i.layout.len())
3302 .unwrap_or(0),
3303 root.element_data()
3304 .and_then(|e| e.inline_layout_data.as_ref())
3305 .map(|i| i.layout.scale())
3306 .unwrap_or(0.0),
3307 self.viewport.scale(),
3308 root.element_data()
3309 .and_then(|e| e.inline_layout_data.as_ref())
3310 .map(|i| i.layout.width())
3311 .unwrap_or(0.0),
3312 );
3313 }
3314 break;
3315 }
3316 }
3317 }
3318 if reported > 0 {
3319 eprintln!("escaped-fragment total={reported}");
3320 }
3321
3322 let mut narrow = 0;
3327 for (id, node) in self.nodes.iter() {
3328 let Some(inline) = node
3329 .data
3330 .downcast_element()
3331 .and_then(|element| element.inline_layout_data.as_ref())
3332 else {
3333 continue;
3334 };
3335 let box_width = node.final_layout().size.width as f64 * self.viewport.scale() as f64;
3336 let broken_at = inline.layout.width() as f64;
3337 let full = inline.layout.calculate_content_widths().max as f64;
3340 if box_width > 40.0 && broken_at < box_width * 0.6 && full > box_width * 0.9 {
3341 narrow += 1;
3342 if narrow <= 12 {
3343 eprintln!(
3344 "narrow-break node={id:?} broken_at={broken_at:.1} box={box_width:.1} \
3345 max_content={full:.1} lines={} text={:?}",
3346 inline.layout.len(),
3347 inline.text.chars().take(40).collect::<String>(),
3348 );
3349 }
3350 }
3351 }
3352 if narrow > 0 {
3353 eprintln!("narrow-break total={narrow}");
3354 }
3355 }
3356
3357 pub fn inline_fragment_rects(&self, node_id: NodeId) -> Option<Vec<BoundingRect>> {
3358 use parley::PositionedLayoutItem;
3359
3360 let node = self.get_node(node_id)?;
3361
3362 let is_text = node.is_text_node();
3365 if !is_text {
3366 if !node.is_element() || node.flags.is_inline_root() {
3367 return None;
3368 }
3369 let display = node.primary_styles()?.clone_display();
3370 if !(display.outside() == DisplayOutside::Inline
3371 && display.inside() == DisplayInside::Flow)
3372 {
3373 return None;
3374 }
3375 }
3376
3377 let inline_root = node.inline_root_ancestor()?;
3378 let inline_layout = inline_root.element_data()?.inline_layout_data.as_ref()?;
3379 let layout = &inline_layout.layout;
3380 let scale = layout.scale() as f64;
3381
3382 let is_in_target = |mut id: NodeId| -> bool {
3385 loop {
3386 if id == node_id {
3387 return true;
3388 }
3389 if id == inline_root.id {
3390 return false;
3391 }
3392 match self.get_node(id).and_then(|n| n.parent) {
3393 Some(parent) => id = parent,
3394 None => return false,
3395 }
3396 }
3397 };
3398
3399 let root_layout = inline_root.final_layout();
3401 let root_pos = inline_root.absolute_position(0.0, 0.0);
3402 let origin_x = root_pos.x as f64
3403 + (root_layout.padding.left + root_layout.border.left) as f64
3404 - self.viewport_scroll.x;
3405 let origin_y = root_pos.y as f64
3406 + (root_layout.padding.top + root_layout.border.top) as f64
3407 - self.viewport_scroll.y;
3408
3409 let mut rects: Vec<BoundingRect> = Vec::new();
3410 for line in layout.lines() {
3411 let line_metrics = line.metrics();
3412 let mut line_rect: Option<(f64, f64, f64, f64)> = None;
3414 let mut add = |x0: f64, y0: f64, x1: f64, y1: f64| {
3415 line_rect = Some(match line_rect {
3416 Some((lx0, ly0, lx1, ly1)) => {
3417 (lx0.min(x0), ly0.min(y0), lx1.max(x1), ly1.max(y1))
3418 }
3419 None => (x0, y0, x1, y1),
3420 });
3421 };
3422
3423 for item in line.items() {
3424 match item {
3425 PositionedLayoutItem::GlyphRun(glyph_run) => {
3426 let brush = glyph_run.style().brush;
3427 let matches = if is_text {
3428 brush.text_node == Some(node_id)
3429 } else {
3430 is_in_target(brush.id)
3431 };
3432 if !matches {
3433 continue;
3434 }
3435 let x0 = glyph_run.offset() as f64;
3436 let x1 = x0 + glyph_run.advance() as f64;
3437 let y0 = line_metrics.block_min_coord as f64;
3443 let y1 = line_metrics.block_max_coord as f64;
3444 add(x0, y0, x1, y1);
3445 }
3446 PositionedLayoutItem::InlineBox(inline_box) => {
3447 if is_text || !is_in_target(NodeId::from_u64(inline_box.id)) {
3448 continue;
3449 }
3450 let x0 = inline_box.x as f64;
3451 let y0 = inline_box.y as f64;
3452 add(
3453 x0,
3454 y0,
3455 x0 + inline_box.width as f64,
3456 y0 + inline_box.height as f64,
3457 );
3458 }
3459 }
3460 }
3461
3462 if let Some((x0, y0, x1, y1)) = line_rect {
3463 rects.push(BoundingRect {
3464 x: origin_x + x0 / scale,
3465 y: origin_y + y0 / scale,
3466 width: (x1 - x0) / scale,
3467 height: (y1 - y0) / scale,
3468 });
3469 }
3470 }
3471
3472 Some(rects)
3473 }
3474
3475 pub fn find_title_node(&self) -> Option<&Node> {
3476 TreeTraverser::new(self)
3477 .find(|node_id| {
3478 let node = &self.nodes[*node_id];
3479 let Some(element) = node.element_data() else {
3480 return false;
3481 };
3482 if element.name.ns != ns!(html) || element.name.local != local_name!("title") {
3483 return false;
3484 }
3485 node.parent
3486 .and_then(|parent_id| self.nodes.get(parent_id))
3487 .and_then(Node::element_data)
3488 .is_some_and(|parent| {
3489 parent.name.ns == ns!(html) && parent.name.local == local_name!("head")
3490 })
3491 })
3492 .map(|node_id| &self.nodes[node_id])
3493 }
3494
3495 pub fn with_text_input(
3496 &mut self,
3497 node_id: NodeId,
3498 cb: impl FnOnce(PlainEditorDriver<TextBrush>),
3499 ) {
3500 let Some(node) = self.nodes.get_mut(node_id) else {
3501 return;
3502 };
3503
3504 if let Some(text_input) = node
3505 .element_data_mut()
3506 .and_then(|el| el.text_input_data_mut())
3507 {
3508 let mut font_ctx = self.font_ctx.lock().unwrap();
3509 let layout_ctx = &mut self.layout_ctx;
3510 let driver = text_input.editor.driver(&mut font_ctx, layout_ctx);
3511 cb(driver)
3512 }
3513 }
3514
3515 pub(crate) fn clamp_text_input_scroll(&mut self, node_id: NodeId) {
3518 let Some(node) = self.nodes.get_mut(node_id) else {
3519 return;
3520 };
3521
3522 let content_box_width = node.final_layout().content_box_width();
3523 let content_box_height = node.final_layout().content_box_height();
3524
3525 if let Some(text_input) = node
3526 .element_data_mut()
3527 .and_then(|el| el.text_input_data_mut())
3528 {
3529 text_input.clamp_scroll_offset(content_box_width, content_box_height);
3530 }
3531 }
3532
3533 pub(crate) fn compute_has_canvas(&self) -> bool {
3534 TreeTraverser::new(self).any(|node_id| {
3535 let node = &self.nodes[node_id];
3536 let Some(element) = node.element_data() else {
3537 return false;
3538 };
3539 if element.name.local == local_name!("canvas") && element.has_attr(local_name!("src")) {
3540 return true;
3541 }
3542
3543 false
3544 })
3545 }
3546
3547 pub fn find_text_position(&self, x: f32, y: f32) -> Option<(NodeId, usize)> {
3553 let hit = self.hit(x, y)?;
3554 let hit_node = self.get_node(hit.node_id)?;
3555 let inline_root = hit_node.inline_root_ancestor()?;
3556 let byte_offset = inline_root.text_offset_at_point(hit.x, hit.y)?;
3557 Some((inline_root.id, byte_offset))
3558 }
3559
3560 pub fn find_text_range(
3566 &self,
3567 x: f32,
3568 y: f32,
3569 granularity: TextGranularity,
3570 ) -> Option<(NodeId, usize, usize)> {
3571 let hit = self.hit(x, y)?;
3572 let hit_node = self.get_node(hit.node_id)?;
3573 let inline_root = hit_node.inline_root_ancestor()?;
3574 let range = inline_root.text_range_at_point(hit.x, hit.y, granularity)?;
3575 Some((inline_root.id, range.start, range.end))
3576 }
3577
3578 pub fn set_text_selection(
3580 &mut self,
3581 anchor_node: NodeId,
3582 anchor_offset: usize,
3583 focus_node: NodeId,
3584 focus_offset: usize,
3585 ) {
3586 self.text_selection =
3587 TextSelection::new(anchor_node, anchor_offset, focus_node, focus_offset);
3588
3589 if let (Some(parent), Some(idx)) = self.anonymous_block_location(anchor_node) {
3591 self.text_selection
3592 .anchor
3593 .set_anonymous(parent, idx, anchor_offset);
3594 }
3595 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
3596 self.text_selection
3597 .focus
3598 .set_anonymous(parent, idx, focus_offset);
3599 }
3600 }
3601
3602 fn anonymous_block_location(&self, node_id: NodeId) -> (Option<NodeId>, Option<usize>) {
3605 let Some(node) = self.get_node(node_id) else {
3606 return (None, None);
3607 };
3608
3609 if !node.is_anonymous() {
3610 return (None, None);
3611 }
3612
3613 let Some(parent_id) = node.parent else {
3614 return (None, None);
3615 };
3616
3617 let Some(parent) = self.get_node(parent_id) else {
3618 return (Some(parent_id), None);
3619 };
3620
3621 let layout_children = parent.layout_children.borrow();
3622 let Some(children) = layout_children.as_ref() else {
3623 return (Some(parent_id), None);
3624 };
3625
3626 let mut anon_index = 0;
3628 for &child_id in children.iter() {
3629 if child_id == node_id {
3630 return (Some(parent_id), Some(anon_index));
3631 }
3632 if self.get_node(child_id).is_some_and(|n| n.is_anonymous()) {
3633 anon_index += 1;
3634 }
3635 }
3636
3637 (Some(parent_id), None)
3638 }
3639
3640 pub fn clear_text_selection(&mut self) {
3642 self.text_selection.clear();
3643 }
3644
3645 pub fn update_selection_focus(&mut self, focus_node: NodeId, focus_offset: usize) {
3647 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
3649 self.text_selection
3650 .focus
3651 .set_anonymous(parent, idx, focus_offset);
3652 } else {
3653 self.text_selection.set_focus(focus_node, focus_offset);
3654 }
3655 }
3656
3657 pub fn extend_text_selection_to_point(&mut self, x: f32, y: f32) -> bool {
3660 if !self.text_selection.anchor.is_some() {
3661 return false;
3662 }
3663
3664 if let Some((node, offset)) = self.find_text_position(x, y) {
3665 self.update_selection_focus(node, offset);
3666 self.shell_provider.request_redraw();
3667 true
3668 } else {
3669 false
3670 }
3671 }
3672
3673 fn find_anonymous_block_by_index(
3675 &self,
3676 parent_id: NodeId,
3677 target_index: usize,
3678 ) -> Option<NodeId> {
3679 let parent = self.get_node(parent_id)?;
3680 let layout_children = parent.layout_children.borrow();
3681 let children = layout_children.as_ref()?;
3682
3683 children
3684 .iter()
3685 .filter(|&&child_id| self.get_node(child_id).is_some_and(|n| n.is_anonymous()))
3686 .nth(target_index)
3687 .copied()
3688 }
3689
3690 pub fn has_text_selection(&self) -> bool {
3692 self.text_selection.is_active()
3693 }
3694
3695 pub fn get_selected_text(&self) -> Option<String> {
3697 let ranges = self.get_text_selection_ranges();
3698 if ranges.is_empty() {
3699 return None;
3700 }
3701
3702 let mut result = String::new();
3703 for (node_id, start, end) in &ranges {
3704 let node = self.get_node(*node_id)?;
3705 let element_data = node.element_data()?;
3706 let inline_layout = element_data.inline_layout_data.as_ref()?;
3707
3708 if *end > inline_layout.text.len() {
3709 continue;
3710 }
3711
3712 if !result.is_empty() {
3713 result.push(' ');
3714 }
3715 result.push_str(&inline_layout.text[*start..*end]);
3716 }
3717
3718 if result.is_empty() {
3719 None
3720 } else {
3721 Some(result)
3722 }
3723 }
3724
3725 pub fn get_selected_html(&self) -> Option<String> {
3738 let ranges = self.get_text_selection_ranges();
3739 if ranges.is_empty() {
3740 return None;
3741 }
3742
3743 let mut result = String::new();
3744 for (node_id, start, end) in &ranges {
3745 let Some(node) = self.get_node(*node_id) else {
3746 continue;
3747 };
3748 let Some(element_data) = node.element_data() else {
3749 continue;
3750 };
3751 let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
3752 continue;
3753 };
3754 if *end > inline_layout.text.len() {
3755 continue;
3756 }
3757
3758 if !result.is_empty() {
3759 result.push(' ');
3760 }
3761
3762 let tag = element_data.name.local.as_ref();
3763 let fragment = escape_html(&inline_layout.text[*start..*end]);
3764 if matches!(
3769 tag,
3770 "b" | "strong" | "i" | "em" | "code" | "kbd" | "mark" | "s" | "u" | "sub" | "sup"
3771 ) {
3772 result.push_str(&format!("<{tag}>{fragment}</{tag}>"));
3773 } else {
3774 result.push_str(&fragment);
3775 }
3776 }
3777
3778 if result.is_empty() {
3779 None
3780 } else {
3781 Some(result)
3782 }
3783 }
3784
3785 pub fn get_text_selection_ranges(&self) -> Vec<(NodeId, usize, usize)> {
3788 let lookup = |parent_id, idx| self.find_anonymous_block_by_index(parent_id, idx);
3789
3790 let anchor_node = match self.text_selection.anchor.resolve_node_id(lookup) {
3791 Some(id) => id,
3792 None => return Vec::new(),
3793 };
3794 let focus_node = match self.text_selection.focus.resolve_node_id(lookup) {
3795 Some(id) => id,
3796 None => return Vec::new(),
3797 };
3798
3799 let node_is_in_doc = |node_id: NodeId| {
3802 self.nodes
3803 .get(node_id)
3804 .is_some_and(|node| node.flags.is_in_document())
3805 };
3806 if !node_is_in_doc(anchor_node) || !node_is_in_doc(focus_node) {
3807 return Vec::new();
3808 }
3809
3810 if anchor_node == focus_node {
3812 let start = self
3813 .text_selection
3814 .anchor
3815 .offset
3816 .min(self.text_selection.focus.offset);
3817 let end = self
3818 .text_selection
3819 .anchor
3820 .offset
3821 .max(self.text_selection.focus.offset);
3822
3823 if start == end {
3824 return Vec::new();
3825 }
3826 return vec![(anchor_node, start, end)];
3827 }
3828
3829 let inline_roots = self.collect_inline_roots_in_range(anchor_node, focus_node);
3831 if inline_roots.is_empty() {
3832 return Vec::new();
3833 }
3834
3835 let first_in_roots = inline_roots[0];
3838
3839 let (first_node, first_offset, last_node, last_offset) =
3840 if first_in_roots == anchor_node || (first_in_roots != focus_node) {
3841 (
3843 anchor_node,
3844 self.text_selection.anchor.offset,
3845 focus_node,
3846 self.text_selection.focus.offset,
3847 )
3848 } else {
3849 (
3851 focus_node,
3852 self.text_selection.focus.offset,
3853 anchor_node,
3854 self.text_selection.anchor.offset,
3855 )
3856 };
3857
3858 let mut ranges = Vec::with_capacity(inline_roots.len());
3859
3860 for &node_id in &inline_roots {
3861 let Some(node) = self.get_node(node_id) else {
3862 continue;
3863 };
3864 let Some(element_data) = node.element_data() else {
3865 continue;
3866 };
3867 let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
3868 continue;
3869 };
3870
3871 let text_len = inline_layout.text.len();
3872
3873 if node_id == first_node && node_id == last_node {
3874 let start = first_offset.min(last_offset);
3875 let end = first_offset.max(last_offset);
3876 if start < end && end <= text_len {
3877 ranges.push((node_id, start, end));
3878 }
3879 } else if node_id == first_node {
3880 if first_offset < text_len {
3881 ranges.push((node_id, first_offset, text_len));
3882 }
3883 } else if node_id == last_node {
3884 if last_offset > 0 && last_offset <= text_len {
3885 ranges.push((node_id, 0, last_offset));
3886 }
3887 } else if text_len > 0 {
3888 ranges.push((node_id, 0, text_len));
3889 }
3890 }
3891
3892 ranges
3893 }
3894}
3895
3896#[derive(Debug, Clone, Copy, PartialEq)]
3897pub struct BoundingRect {
3898 pub x: f64,
3899 pub y: f64,
3900 pub width: f64,
3901 pub height: f64,
3902}
3903
3904impl BaseDocument {
3907 pub fn set_recording_mutations(&mut self, recording: bool) {
3910 match (recording, self.mutation_log.is_some()) {
3911 (true, false) => self.mutation_log = Some(Vec::new()),
3912 (false, true) => self.mutation_log = None,
3913 _ => {}
3914 }
3915 }
3916
3917 pub fn is_recording_mutations(&self) -> bool {
3919 self.mutation_log.is_some()
3920 }
3921
3922 pub fn take_mutations(&mut self) -> Vec<crate::DomMutation> {
3924 self.mutation_log
3925 .as_mut()
3926 .map(std::mem::take)
3927 .unwrap_or_default()
3928 }
3929
3930 pub(crate) fn record_mutation(&mut self, mutation: crate::DomMutation) {
3931 if let Some(log) = self.mutation_log.as_mut() {
3932 if log.len() < crate::mutation_record::MUTATION_LOG_CAP {
3933 log.push(mutation);
3934 }
3935 }
3936 }
3937
3938 pub(crate) fn sibling_context(
3941 &self,
3942 node_id: NodeId,
3943 ) -> Option<(NodeId, Option<NodeId>, Option<NodeId>)> {
3944 let parent_id = self.nodes.get(node_id)?.parent?;
3945 let siblings = &self.nodes.get(parent_id)?.children;
3946 let index = siblings.iter().position(|id| *id == node_id)?;
3947 let previous = index.checked_sub(1).map(|i| siblings[i]);
3948 let next = siblings.get(index + 1).copied();
3949 Some((parent_id, previous, next))
3950 }
3951}
3952
3953impl AsRef<BaseDocument> for BaseDocument {
3954 fn as_ref(&self) -> &BaseDocument {
3955 self
3956 }
3957}
3958
3959impl AsMut<BaseDocument> for BaseDocument {
3960 fn as_mut(&mut self) -> &mut BaseDocument {
3961 self
3962 }
3963}
3964
3965#[cfg(test)]
3966mod hover_state_tests {
3967 use super::*;
3968 use crate::{Attribute, qual_name};
3969 use blitz_traits::shell::ColorScheme;
3970
3971 fn make_doc() -> (BaseDocument, NodeId) {
3978 let mut doc = BaseDocument::new(DocumentConfig {
3979 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3980 ..Default::default()
3981 });
3982 let root_id = doc.root_node().id;
3983 let style = |value: &str| Attribute {
3984 name: qual_name!("style"),
3985 value: value.into(),
3986 };
3987
3988 let mut mutator = doc.mutate();
3989 let html = mutator.create_element(qual_name!("html"), vec![]);
3990 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
3991 let container = mutator.create_element(qual_name!("div"), vec![style("width:300px")]);
3992 let text = mutator.create_text_node("some text");
3993 let block = mutator.create_element(qual_name!("div"), vec![style("height:50px")]);
3994 mutator.append_children(container, &[text, block]);
3995 mutator.append_children(body, &[container]);
3996 mutator.append_children(html, &[body]);
3997 mutator.append_children(root_id, &[html]);
3998 drop(mutator);
3999
4000 doc.resolve(0.0);
4001 (doc, container)
4002 }
4003
4004 fn text_has_size(doc: &BaseDocument, container: NodeId) -> bool {
4008 doc.nodes[container].final_layout().size.height > 50.0
4009 }
4010
4011 #[test]
4017 fn hovering_text_in_anonymous_block_reports_text_cursor() {
4018 let (mut doc, container) = make_doc();
4019 if !text_has_size(&doc, container) {
4020 eprintln!("skipping: no usable font (text measures 0x0)");
4021 return;
4022 }
4023
4024 doc.set_hover_to(5.0, 8.0);
4025 assert!(doc.hover_node_is_text, "expected a text hit");
4026 let hit_id = doc.hover_hit_node_id.expect("expected a hit node");
4027 assert!(
4028 doc.nodes[hit_id].is_anonymous(),
4029 "expected the hit node to be the anonymous inline root"
4030 );
4031 assert_eq!(
4032 doc.get_hover_node_id(),
4033 Some(container),
4034 "expected the stored hover target to be the containing element"
4035 );
4036 assert_eq!(doc.get_cursor(), Some(CursorIcon::Text));
4037 }
4038
4039 #[test]
4040 fn semantic_hover_keeps_the_resolved_node_instead_of_hit_testing_again() {
4041 let (mut doc, container) = make_doc();
4042
4043 doc.set_hover_to_node(container, 350.0, 250.0);
4047
4048 assert_eq!(doc.get_hover_node_id(), Some(container));
4049 assert_eq!(doc.hover_hit_node_id, Some(container));
4050
4051 doc.resolve(0.0);
4052 assert_eq!(
4053 doc.get_hover_node_id(),
4054 Some(container),
4055 "a resolve must not turn semantic identity back into a coordinate hit"
4056 );
4057 }
4058
4059 #[test]
4062 fn hovering_anonymous_block_whitespace_reports_default_cursor() {
4063 let (mut doc, container) = make_doc();
4064 if !text_has_size(&doc, container) {
4065 eprintln!("skipping: no usable font (text measures 0x0)");
4066 return;
4067 }
4068
4069 doc.set_hover_to(250.0, 8.0);
4070 assert!(!doc.hover_node_is_text);
4071 assert_eq!(doc.get_hover_node_id(), Some(container));
4072 assert_eq!(doc.get_cursor(), Some(CursorIcon::Default));
4073 }
4074}
4075
4076#[cfg(test)]
4077mod control_scroll_tests {
4078 use super::*;
4079 use crate::{Attribute, qual_name};
4080 use blitz_traits::shell::ColorScheme;
4081
4082 #[test]
4083 fn controlled_scroll_dispatches_the_dom_scroll_event() {
4084 let mut doc = BaseDocument::new(DocumentConfig {
4085 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
4086 ..Default::default()
4087 });
4088 let root_id = doc.root_node().id;
4089 let style = |value: &str| Attribute {
4090 name: qual_name!("style"),
4091 value: value.into(),
4092 };
4093
4094 let mut mutator = doc.mutate();
4095 let html = mutator.create_element(qual_name!("html"), vec![]);
4096 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
4097 let scroller = mutator.create_element(
4098 qual_name!("div"),
4099 vec![style("width:200px;height:100px;overflow-y:scroll")],
4100 );
4101 let spacer = mutator.create_element(qual_name!("div"), vec![style("height:400px")]);
4102 let target = mutator.create_element(qual_name!("button"), vec![style("height:40px")]);
4103 let target_text = mutator.create_text_node("Reveal me");
4104 mutator.append_children(target, &[target_text]);
4105 mutator.append_children(scroller, &[spacer, target]);
4106 mutator.append_children(body, &[scroller]);
4107 mutator.append_children(html, &[body]);
4108 mutator.append_children(root_id, &[html]);
4109 drop(mutator);
4110 doc.resolve(0.0);
4111
4112 doc.nodes[html].final_layout_mut().size.height = 300.0;
4116 doc.nodes[html].final_layout_mut().content_size.height = 600.0;
4117 doc.nodes[target].final_layout_mut().location.y = 400.0;
4118
4119 let mut events = Vec::new();
4120 doc.scroll_to_node_centered_with_events(target_text, |event| events.push(event));
4124
4125 assert!(
4126 (doc.viewport_scroll.y - 270.0).abs() < 0.1,
4127 "the 40px target should be centred in the 300px viewport"
4128 );
4129 assert!(
4130 events
4131 .iter()
4132 .any(|event| { event.target == html && event.name() == "scroll" })
4133 );
4134 }
4135}
4136
4137#[cfg(test)]
4138mod font_face_override_tests {
4139 use super::*;
4140 use crate::net::{FontFaceOverrides, Resource, ResourceLoadResponse};
4141
4142 #[test]
4158 fn font_face_overrides_alias_family_name() {
4159 const ALIAS: &str = "AliasedFamily";
4160
4161 let mut document = BaseDocument::new(DocumentConfig::default());
4162
4163 {
4165 let mut ctx = document.font_ctx.lock().unwrap();
4166 assert!(
4167 ctx.collection.family_id(ALIAS).is_none(),
4168 "alias must not exist before registration",
4169 );
4170 }
4171
4172 let response = ResourceLoadResponse {
4177 request_id: 0,
4178 node_id: None,
4179 resolved_url: Some(String::from("test://aliased-family")),
4180 result: Ok(Resource::Font(
4181 blitz_traits::net::Bytes::from_static(crate::BULLET_FONT),
4182 FontFaceOverrides {
4183 family_name: Some(String::from(ALIAS)),
4184 weight: Some(800.0),
4185 style: Some(parley::fontique::FontStyle::Italic),
4186 },
4187 )),
4188 };
4189 document.load_resource(response);
4190
4191 let mut ctx = document.font_ctx.lock().unwrap();
4194 let family_id = ctx
4195 .collection
4196 .family_id(ALIAS)
4197 .expect("CSS-declared family name should be registered as a family alias");
4198 let resolved_name = ctx
4199 .collection
4200 .family_name(family_id)
4201 .expect("family id should resolve back to a name");
4202 assert_eq!(
4203 resolved_name, ALIAS,
4204 "registered family should report the CSS-declared name, \
4205 not the font file's internal `name` table entry",
4206 );
4207 }
4208}
4209
4210#[cfg(test)]
4211mod clipboard_html_tests {
4212 use super::escape_html;
4213
4214 #[test]
4217 fn markup_characters_in_a_selection_are_escaped() {
4218 assert_eq!(
4219 escape_html("a < b && c > d"),
4220 "a < b && c > d"
4221 );
4222 }
4223
4224 #[test]
4227 fn an_ampersand_is_not_double_escaped() {
4228 assert_eq!(escape_html("<"), "&lt;");
4229 }
4230
4231 #[test]
4234 fn prose_is_untouched() {
4235 assert_eq!(escape_html("it's \"fine\""), "it's "fine"");
4236 }
4237}