1use crate::NodeTree;
2use crate::events::{DragMode, handle_dom_event};
3use crate::layout::construct::ConstructionTask;
4use crate::layout::damage::ALL_DAMAGE;
5use crate::mutator::ViewportMut;
6use crate::net::{
7 Resource, ResourceHandler, ResourceLoadResponse, StylesheetHandler, StylesheetLoader,
8};
9use crate::node::{ImageData, NodeFlags, RasterImageData, SpecialElementData, Status, TextBrush};
10use crate::scrolling::ScrollAnimationState;
11use crate::selection::TextSelection;
12use crate::stylo_device::{DeviceChanges, make_device};
13use crate::stylo_to_cursor_icon::stylo_to_cursor_icon;
14use crate::traversal::TreeTraverser;
15use crate::url::DocumentUrl;
16use crate::util::ImageType;
17use crate::{
18 DEFAULT_CSS, DocumentConfig, DocumentMutator, DummyHtmlParserProvider, ElementData,
19 EventDriver, HtmlParserProvider, Node, NodeData, NoopEventHandler, StyleThreading,
20 TextNodeData,
21};
22use blitz_traits::devtools::DevtoolSettings;
23use blitz_traits::events::{DomEvent, HitResult, UiEvent};
24use blitz_traits::navigation::{DummyNavigationProvider, NavigationProvider};
25use blitz_traits::net::{AbortSignal, DummyNetProvider, NetProvider, Request};
26use blitz_traits::node_id::NodeId;
27use blitz_traits::shell::{DummyShellProvider, ShellProvider, Viewport};
28use cursor_icon::CursorIcon;
29use linebender_resource_handle::Blob;
30use markup5ever::{LocalName, local_name};
31use parley::{FontContext, PlainEditorDriver};
32use selectors::{Element, matching::QuirksMode};
33use smallvec::SmallVec;
34use std::any::Any;
35use std::cell::RefCell;
36use std::collections::{BTreeMap, Bound, HashMap, HashSet};
37use std::ops::{Deref, DerefMut};
38use std::rc::Rc;
39use std::str::FromStr;
40use std::sync::atomic::{AtomicUsize, Ordering};
41use std::sync::mpsc::{Receiver, Sender, channel};
42use std::sync::{Arc, LazyLock, Mutex, MutexGuard, OnceLock, RwLockReadGuard, RwLockWriteGuard};
43use std::task::{Context as TaskContext, Waker};
44use style::Atom;
45use style::animation::DocumentAnimationSet;
46use style::attr::{AttrIdentifier, AttrValue};
47use style::computed_value_flags::ComputedValueFlags;
48use style::data::{ElementData as StyloElementData, ElementStyles};
49use style::invalidation::element::restyle_hints::RestyleHint;
50use style::media_queries::MediaType;
51use style::properties::ComputedValues;
52use style::properties::style_structs::Font;
53use style::selector_parser::ServoElementSnapshot;
54use style::servo_arc::Arc as ServoArc;
55use style::values::GenericAtomIdent;
56use style::values::computed::UserSelect;
57use style::values::computed::ui::CursorKind;
58use style::{
59 device::Device,
60 dom::{TDocument, TNode},
61 media_queries::MediaList,
62 selector_parser::SnapshotMap,
63 shared_lock::{SharedRwLock, StylesheetGuards},
64 stylesheets::{AllowImportRules, DocumentStyleSheet, Origin, Stylesheet},
65 stylist::Stylist,
66};
67use style_dom::ElementState;
68use thin_vec::ThinVec;
69use url::Url;
70use web_time::Instant;
71
72#[cfg(feature = "parallel-construct")]
73use thread_local::ThreadLocal;
74
75#[derive(Clone, Debug, PartialEq)]
80pub struct UserAgentStylesheet(DocumentStyleSheet);
81
82impl UserAgentStylesheet {
83 pub fn parse(css: &str) -> Self {
84 let lock = SharedRwLock::read_only();
85 let data = Stylesheet::from_str(
86 css,
87 DocumentUrl::default().url_extra_data(),
88 Origin::UserAgent,
89 ServoArc::new(lock.wrap(MediaList::empty())),
90 lock,
91 None,
92 None,
93 QuirksMode::NoQuirks,
94 AllowImportRules::No,
95 );
96 Self(DocumentStyleSheet(ServoArc::new(data)))
97 }
98
99 pub fn default_sheet() -> Self {
101 static SHEET: LazyLock<UserAgentStylesheet> =
102 LazyLock::new(|| UserAgentStylesheet::parse(DEFAULT_CSS));
103 SHEET.clone()
104 }
105
106 pub fn sheet(&self) -> &DocumentStyleSheet {
107 &self.0
108 }
109}
110
111pub enum DocGuard<'a> {
112 Ref(&'a BaseDocument),
113 RefCell(std::cell::Ref<'a, BaseDocument>),
114 RwLock(RwLockReadGuard<'a, BaseDocument>),
115 Mutex(MutexGuard<'a, BaseDocument>),
116}
117
118impl Deref for DocGuard<'_> {
119 type Target = BaseDocument;
120 #[inline(always)]
121 fn deref(&self) -> &Self::Target {
122 match self {
123 Self::Ref(base_document) => base_document,
124 Self::RefCell(refcell_guard) => refcell_guard,
125 Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
126 Self::Mutex(mutex_guard) => mutex_guard,
127 }
128 }
129}
130
131pub enum DocGuardMut<'a> {
132 Ref(&'a mut BaseDocument),
133 RefCell(std::cell::RefMut<'a, BaseDocument>),
134 RwLock(RwLockWriteGuard<'a, BaseDocument>),
135 Mutex(MutexGuard<'a, BaseDocument>),
136}
137
138impl Deref for DocGuardMut<'_> {
139 type Target = BaseDocument;
140 #[inline(always)]
141 fn deref(&self) -> &Self::Target {
142 match self {
143 Self::Ref(base_document) => base_document,
144 Self::RefCell(refcell_guard) => refcell_guard,
145 Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
146 Self::Mutex(mutex_guard) => mutex_guard,
147 }
148 }
149}
150
151impl DerefMut for DocGuardMut<'_> {
152 #[inline(always)]
153 fn deref_mut(&mut self) -> &mut Self::Target {
154 match self {
155 Self::Ref(base_document) => base_document,
156 Self::RefCell(refcell_guard) => &mut *refcell_guard,
157 Self::RwLock(rw_lock_read_guard) => &mut *rw_lock_read_guard,
158 Self::Mutex(mutex_guard) => &mut *mutex_guard,
159 }
160 }
161}
162
163pub trait Document: Any + 'static {
166 fn inner(&self) -> DocGuard<'_>;
167 fn inner_mut(&mut self) -> DocGuardMut<'_>;
168
169 fn handle_ui_event(&mut self, event: UiEvent) {
171 let mut doc = self.inner_mut();
172 let mut driver = EventDriver::new(&mut *doc, NoopEventHandler);
173 driver.handle_ui_event(event);
174 }
175
176 fn poll(&mut self, task_context: Option<TaskContext>) -> bool {
178 let _ = task_context;
180 false
181 }
182
183 fn id(&self) -> usize {
185 self.inner().id
186 }
187}
188
189pub struct PlainDocument(pub BaseDocument);
190impl Document for PlainDocument {
191 fn inner(&self) -> DocGuard<'_> {
192 DocGuard::Ref(&self.0)
193 }
194 fn inner_mut(&mut self) -> DocGuardMut<'_> {
195 DocGuardMut::Ref(&mut self.0)
196 }
197}
198
199impl Document for BaseDocument {
200 fn inner(&self) -> DocGuard<'_> {
201 DocGuard::Ref(self)
202 }
203 fn inner_mut(&mut self) -> DocGuardMut<'_> {
204 DocGuardMut::Ref(self)
205 }
206}
207
208impl Document for Rc<RefCell<BaseDocument>> {
209 fn inner(&self) -> DocGuard<'_> {
210 DocGuard::RefCell(self.borrow())
211 }
212
213 fn inner_mut(&mut self) -> DocGuardMut<'_> {
214 DocGuardMut::RefCell(self.borrow_mut())
215 }
216}
217
218pub enum DocumentEvent {
219 ResourceLoad(ResourceLoadResponse),
220 NavigateIframe {
223 node_id: NodeId,
224 url: Url,
225 },
226}
227
228pub struct BaseDocument {
229 id: usize,
231
232 pub(crate) url: DocumentUrl,
235 pub(crate) devtool_settings: DevtoolSettings,
237 pub(crate) viewport: Viewport,
239 pub(crate) media_type: MediaType,
241 pub(crate) pending_device_changes: DeviceChanges,
245 pub(crate) style_threading: StyleThreading,
247 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) stylist: Stylist,
272 pub(crate) animations: DocumentAnimationSet,
273 pub(crate) guard: SharedRwLock,
275 pub(crate) snapshots: SnapshotMap,
277
278 pub(crate) font_ctx: Arc<Mutex<parley::FontContext>>,
281 #[cfg(feature = "parallel-construct")]
282 pub(crate) thread_font_contexts: ThreadLocal<RefCell<Box<FontContext>>>,
284 pub(crate) layout_ctx: parley::LayoutContext<TextBrush>,
286
287 pub(crate) hover_node_id: Option<NodeId>,
291 pub(crate) hover_hit_node_id: Option<NodeId>,
295 pub(crate) hover_node_is_text: bool,
297 pub(crate) last_client_pointer_position: Option<taffy::Point<f32>>,
299 pub(crate) focus_node_id: Option<NodeId>,
301 pub(crate) active_node_id: Option<NodeId>,
303 pub(crate) mousedown_node_id: Option<NodeId>,
305 pub(crate) last_mousedown_time: Option<Instant>,
307 pub(crate) mousedown_position: taffy::Point<f32>,
309 pub(crate) click_count: u16,
311 pub(crate) drag_mode: DragMode,
313 pub(crate) hovered_scrollbar: Option<crate::node::ScrollbarRef>,
315 pub(crate) scrollbar_activity: HashMap<NodeId, Instant>,
318 pub(crate) scroll_animation: ScrollAnimationState,
320
321 pub(crate) text_selection: TextSelection,
323
324 pub(crate) has_active_animations: bool,
327 pub(crate) has_canvas: bool,
329 pub(crate) subdoc_is_animating: bool,
331
332 pub(crate) nodes_to_id: HashMap<String, SmallVec<[NodeId; 1]>>,
336 pub(crate) nodes_to_stylesheet: BTreeMap<NodeId, DocumentStyleSheet>,
338 pub(crate) stylesheet_generation: u64,
342 pub(crate) controls_to_form: HashMap<NodeId, NodeId>,
344 pub(crate) sub_document_nodes: HashSet<NodeId>,
346 pub(crate) iframe_loads: HashMap<NodeId, crate::iframe::IframeLoad>,
349 pub(crate) changed_nodes: HashSet<NodeId>,
351 pub(crate) deferred_construction_nodes: Vec<ConstructionTask>,
353
354 #[cfg(feature = "custom-widget")]
356 pub(crate) custom_widget_nodes: HashSet<NodeId>,
357 #[cfg(feature = "custom-widget")]
359 pub(crate) pending_resource_deallocations: Vec<anyrender::ResourceId>,
360
361 pub(crate) image_cache: HashMap<String, ImageData>,
364
365 pub(crate) pending_images: HashMap<String, Vec<(NodeId, ImageType)>>,
369
370 pub(crate) pending_style_image_nodes: Vec<NodeId>,
374
375 pub(crate) pending_critical_resources: HashSet<usize>,
378
379 pub net_provider: Arc<dyn NetProvider>,
382 pub navigation_provider: Arc<dyn NavigationProvider>,
385 pub shell_provider: Arc<dyn ShellProvider>,
387 pub html_parser_provider: Arc<dyn HtmlParserProvider>,
389 pub(crate) abort_signal: Option<AbortSignal>,
393}
394
395impl BaseDocument {
396 pub fn new(config: DocumentConfig) -> Self {
398 static ID_GENERATOR: AtomicUsize = AtomicUsize::new(1);
399
400 let id = ID_GENERATOR.fetch_add(1, Ordering::SeqCst);
401
402 let font_ctx = config
403 .font_ctx
404 .map(|mut font_ctx| {
405 font_ctx.source_cache.make_shared();
406 font_ctx
408 })
409 .unwrap_or_else(|| {
410 use parley::fontique::{Collection, CollectionOptions, SourceCache};
411 let mut font_ctx = FontContext {
412 source_cache: SourceCache::new_shared(),
413 collection: Collection::new(CollectionOptions {
414 shared: false,
415 system_fonts: cfg!(all(
416 feature = "system-fonts",
417 not(target_arch = "wasm32")
418 )),
419 }),
420 };
421 font_ctx
422 .collection
423 .register_fonts(Blob::new(Arc::new(crate::BULLET_FONT) as _), None);
424 font_ctx
425 });
426 let font_ctx = Arc::new(Mutex::new(font_ctx));
427
428 style_config::set_pref!("layout.grid.enabled", true);
430 style_config::set_pref!("layout.flexbox.balance", true);
431 style_config::set_pref!("layout.unimplemented", true);
432 style_config::set_pref!("layout.columns.enabled", true);
433 style_config::set_pref!("layout.css.basic-shape-shape.enabled", true);
434 style_config::set_pref!("layout.css.tree-counting-functions.enabled", true);
435 style_config::set_pref!("layout.css.progress-function.enabled", true);
436 style_config::set_pref!("layout.variable_fonts.enabled", true);
437 #[cfg(feature = "writing-mode")]
438 style_config::set_pref!("layout.writing-mode.enabled", true);
439 style_config::set_pref!("layout.threads", -1);
440
441 let viewport = config.viewport.unwrap_or_default();
442 let media_type = config.media_type.unwrap_or_else(MediaType::screen);
443 let device = make_device(&viewport, media_type.clone(), font_ctx.clone());
444 let stylist = Stylist::new(device, QuirksMode::NoQuirks);
445 let snapshots = SnapshotMap::new();
446 let nodes = Box::new(NodeTree::new());
447 let guard = SharedRwLock::new();
448 let nodes_to_id = HashMap::new();
449
450 let base_url = config
451 .base_url
452 .and_then(|url| DocumentUrl::from_str(&url).ok())
453 .unwrap_or_default();
454
455 let net_provider = config
456 .net_provider
457 .unwrap_or_else(|| Arc::new(DummyNetProvider));
458 let navigation_provider = config
459 .navigation_provider
460 .unwrap_or_else(|| Arc::new(DummyNavigationProvider));
461 let shell_provider = config
462 .shell_provider
463 .unwrap_or_else(|| Arc::new(DummyShellProvider));
464 let html_parser_provider = config
465 .html_parser_provider
466 .unwrap_or_else(|| Arc::new(DummyHtmlParserProvider));
467
468 let (tx, rx) = channel();
469
470 let mut doc = Self {
471 id,
472 tx,
473 rx: Some(rx),
474
475 guard,
476 nodes,
477 root_node_id: NodeId::default(),
478 stylist,
479 animations: DocumentAnimationSet::default(),
480 snapshots,
481 nodes_to_id,
482 viewport,
483 media_type,
484 pending_device_changes: DeviceChanges::empty(),
485 style_threading: config.style_threading,
486 incremental_layout: config.incremental.unwrap_or(true),
487 subdocument_depth: config.subdocument_depth,
488 devtool_settings: DevtoolSettings::default(),
489 url: base_url,
490 nodes_to_stylesheet: BTreeMap::new(),
491 stylesheet_generation: 0,
492 font_ctx,
493 #[cfg(feature = "parallel-construct")]
494 thread_font_contexts: ThreadLocal::new(),
495 layout_ctx: parley::LayoutContext::new(),
496
497 hover_node_id: None,
498 hover_hit_node_id: None,
499 hover_node_is_text: false,
500 last_client_pointer_position: None,
501 focus_node_id: None,
502 active_node_id: None,
503 mousedown_node_id: None,
504 has_active_animations: false,
505 subdoc_is_animating: false,
506 has_canvas: false,
507 sub_document_nodes: HashSet::new(),
508 iframe_loads: HashMap::new(),
509
510 #[cfg(feature = "custom-widget")]
511 custom_widget_nodes: HashSet::new(),
512 #[cfg(feature = "custom-widget")]
513 pending_resource_deallocations: Vec::new(),
514
515 changed_nodes: HashSet::new(),
516 deferred_construction_nodes: Vec::new(),
517 image_cache: HashMap::new(),
518 pending_images: HashMap::new(),
519 pending_style_image_nodes: Vec::new(),
520 pending_critical_resources: HashSet::new(),
521 controls_to_form: HashMap::new(),
522 net_provider,
523 navigation_provider,
524 shell_provider,
525 html_parser_provider,
526 abort_signal: config.abort_signal,
527 last_mousedown_time: None,
528 mousedown_position: taffy::Point::ZERO,
529 click_count: 0,
530 drag_mode: DragMode::None,
531 hovered_scrollbar: None,
532 scrollbar_activity: HashMap::new(),
533 scroll_animation: ScrollAnimationState::None,
534 text_selection: TextSelection::default(),
535 };
536
537 doc.root_node_id = doc.create_node(NodeData::Document(Box::default()));
539 doc.root_node_mut().flags.insert(NodeFlags::IS_IN_DOCUMENT);
540
541 for sheet in config.ua_stylesheets {
542 doc.add_user_agent_stylesheet(sheet);
543 }
544
545 let stylo_element_data = StyloElementData {
547 styles: ElementStyles {
548 primary: Some(
549 ComputedValues::initial_values_with_font_override(Font::initial_values())
550 .to_arc(),
551 ),
552 ..Default::default()
553 },
554 ..Default::default()
555 };
556 let stylo_data = doc.root_node_mut().stylo_element_data_mut();
557 *stylo_data.ensure_init_mut() = stylo_element_data;
558
559 doc
560 }
561
562 pub fn set_net_provider(&mut self, net_provider: Arc<dyn NetProvider>) {
564 self.net_provider = net_provider;
565 }
566
567 pub fn set_navigation_provider(&mut self, navigation_provider: Arc<dyn NavigationProvider>) {
569 self.navigation_provider = navigation_provider;
570 }
571
572 pub fn set_shell_provider(&mut self, shell_provider: Arc<dyn ShellProvider>) {
574 self.shell_provider = shell_provider;
575 }
576
577 pub fn set_html_parser_provider(&mut self, html_parser_provider: Arc<dyn HtmlParserProvider>) {
579 self.html_parser_provider = html_parser_provider;
580 }
581
582 pub fn set_base_url(&mut self, url: &str) {
584 self.url = DocumentUrl::from(Url::parse(url).unwrap());
585 }
586
587 pub fn base_url(&self) -> &Url {
589 &self.url
590 }
591
592 pub fn guard(&self) -> &SharedRwLock {
593 &self.guard
594 }
595
596 pub fn tree(&self) -> &NodeTree {
597 &self.nodes
598 }
599
600 pub fn id(&self) -> usize {
601 self.id
602 }
603
604 pub(crate) fn build_request(&self, url: url::Url) -> Request {
607 crate::net::stamped_request(url, self.abort_signal.as_ref())
608 }
609
610 pub fn favicon_url(&self) -> Option<String> {
611 self.tree().iter().find_map(|(_, node)| {
612 let data = &node.data;
613 if !data.is_element_with_tag_name(&local_name!("link")) {
614 return None;
615 }
616 let rel = data.attr(local_name!("rel"))?;
617 if !rel
618 .split_ascii_whitespace()
619 .any(|v| v.eq_ignore_ascii_case("icon"))
620 {
621 return None;
622 }
623 data.attr(local_name!("href")).map(|s| s.to_string())
624 })
625 }
626
627 pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
628 self.nodes.get(node_id)
629 }
630
631 pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
632 self.nodes.get_mut(node_id)
633 }
634
635 pub fn get_focussed_node_id(&self) -> Option<NodeId> {
636 self.focus_node_id
637 .or(self.try_root_element().map(|el| el.id))
638 }
639
640 pub fn mutate<'doc>(&'doc mut self) -> DocumentMutator<'doc> {
641 DocumentMutator::new(self)
642 }
643
644 pub fn handle_dom_event<F: FnMut(DomEvent)>(
645 &mut self,
646 event: &mut DomEvent,
647 dispatch_event: F,
648 ) {
649 handle_dom_event(self, event, dispatch_event)
650 }
651
652 pub fn as_any_mut(&mut self) -> &mut dyn Any {
653 self
654 }
655
656 pub fn label_bound_input_element(&self, label_node_id: NodeId) -> Option<&Node> {
663 let label_element = self.nodes[label_node_id].element_data()?;
664 if let Some(target_element_dom_id) = label_element.attr(local_name!("for")) {
665 TreeTraverser::new(self)
666 .filter_map(|id| {
667 let node = self.get_node(id)?;
668 let element_data = node.element_data()?;
669 if element_data.name.local != local_name!("input") {
670 return None;
671 }
672 let id = element_data.id.as_ref()?;
673 if *id == *target_element_dom_id {
674 Some(node)
675 } else {
676 None
677 }
678 })
679 .next()
680 } else {
681 TreeTraverser::new_with_root(self, label_node_id)
682 .filter_map(|child_id| {
683 let node = self.get_node(child_id)?;
684 let element_data = node.element_data()?;
685 if element_data.name.local == local_name!("input") {
686 Some(node)
687 } else {
688 None
689 }
690 })
691 .next()
692 }
693 }
694
695 pub fn toggle_checkbox(el: &mut ElementData) -> bool {
696 let Some(is_checked) = el.checkbox_input_checked() else {
697 return false;
698 };
699 let checked = !is_checked;
700 el.set_checkbox_input_checked(checked);
701
702 checked
703 }
704
705 pub fn toggle_radio(&mut self, radio_set_name: String, target_radio_id: NodeId) {
706 let radio_ids: Vec<NodeId> = self
707 .nodes
708 .iter()
709 .filter_map(|(i, node)| {
710 let el = node.data.downcast_element()?;
711 (el.attr(local_name!("name")) == Some(&radio_set_name)
712 && el.checkbox_input_checked().is_some())
713 .then_some(i)
714 })
715 .collect();
716
717 for i in radio_ids {
718 let checked = i == target_radio_id;
719 self.snapshot_node_and(i, ElementState::CHECKED, |node| {
720 if let Some(el) = node.element_data_mut() {
721 el.set_checkbox_input_checked(checked);
722 }
723 node.mark_ancestors_dirty();
724 });
725 }
726 }
727
728 pub fn toggle_details_open(&mut self, details_id: NodeId) {
732 use crate::qual_name;
733
734 let node = &self.nodes[details_id];
735 if !node.data.is_element_with_tag_name(&local_name!("details")) {
736 return;
737 }
738 let is_open = node.data.has_attr(local_name!("open"));
739
740 let mut mutator = self.mutate();
744 if is_open {
745 mutator.clear_attribute(details_id, qual_name!("open"));
746 } else {
747 mutator.set_attribute(details_id, qual_name!("open"), "");
748 }
749 drop(mutator);
750
751 self.shell_provider.request_redraw();
752 }
753
754 pub fn set_style_property(&mut self, node_id: NodeId, name: &str, value: &str) {
755 let node = &mut self.nodes[node_id];
756 let did_change = node.element_data_mut().unwrap().set_style_property(
757 name,
758 value,
759 &self.guard,
760 self.url.url_extra_data(),
761 );
762 if did_change {
763 node.set_restyle_hint(RestyleHint::RESTYLE_STYLE_ATTRIBUTE);
764 }
765 }
766
767 pub fn remove_style_property(&mut self, node_id: NodeId, name: &str) {
768 let node = &mut self.nodes[node_id];
769 let did_change = node.element_data_mut().unwrap().remove_style_property(
770 name,
771 &self.guard,
772 self.url.url_extra_data(),
773 );
774 if did_change {
775 node.set_restyle_hint(RestyleHint::RESTYLE_STYLE_ATTRIBUTE);
776 }
777 }
778
779 pub fn sub_document_node_ids(&self) -> Vec<NodeId> {
780 self.sub_document_nodes.iter().copied().collect()
781 }
782
783 pub fn set_sub_document(&mut self, node_id: NodeId, sub_document: Box<dyn Document>) {
784 self.nodes[node_id]
785 .element_data_mut()
786 .unwrap()
787 .set_sub_document(sub_document);
788 self.sub_document_nodes.insert(node_id);
789 }
790
791 pub fn remove_sub_document(&mut self, node_id: NodeId) {
792 self.nodes[node_id]
793 .element_data_mut()
794 .unwrap()
795 .remove_sub_document();
796 self.sub_document_nodes.remove(&node_id);
797 if let Some(load) = self.iframe_loads.remove(&node_id) {
798 load.abort_controller.abort();
799 }
800 }
801
802 pub fn poll_subdocuments(&mut self, waker: Option<&Waker>) -> bool {
808 let mut has_changes = false;
809 for node_id in self.sub_document_nodes.iter().copied() {
810 let Some(sub_doc) = self
811 .nodes
812 .get_mut(node_id)
813 .and_then(|node| node.subdoc_mut())
814 else {
815 continue;
816 };
817 let task_context = waker.map(TaskContext::from_waker);
818 has_changes |= sub_doc.poll(task_context);
819 }
820 has_changes
821 }
822
823 #[cfg(feature = "custom-widget")]
824 pub fn custom_widget_node_ids(&self) -> Vec<NodeId> {
825 self.custom_widget_nodes.iter().copied().collect()
826 }
827
828 #[cfg(feature = "custom-widget")]
829 pub fn take_pending_resource_deallocations(&mut self) -> Vec<anyrender::ResourceId> {
830 std::mem::take(&mut self.pending_resource_deallocations)
831 }
832
833 #[cfg(feature = "custom-widget")]
834 pub fn set_custom_widget(&mut self, node_id: NodeId, widget: Box<dyn crate::Widget>) {
835 self.nodes[node_id]
836 .element_data_mut()
837 .unwrap()
838 .set_custom_widget(widget);
839 self.custom_widget_nodes.insert(node_id);
840 }
841
842 #[cfg(feature = "custom-widget")]
843 pub fn remove_custom_widget(&mut self, node_id: NodeId) {
844 let resources_to_deallocate = self.nodes[node_id]
845 .element_data_mut()
846 .unwrap()
847 .remove_custom_widget();
848 self.pending_resource_deallocations
849 .extend_from_slice(&resources_to_deallocate);
850 self.custom_widget_nodes.remove(&node_id);
851 }
852
853 pub fn root_node(&self) -> &Node {
854 &self.nodes[self.root_node_id]
855 }
856
857 pub fn root_node_mut(&mut self) -> &mut Node {
858 &mut self.nodes[self.root_node_id]
859 }
860
861 pub fn try_root_element(&self) -> Option<&Node> {
862 TDocument::as_node(&self.root_node()).first_element_child()
863 }
864
865 pub fn root_element(&self) -> &Node {
866 TDocument::as_node(&self.root_node())
867 .first_element_child()
868 .unwrap()
869 .as_element()
870 .unwrap()
871 }
872
873 pub fn create_node(&mut self, node_data: NodeData) -> NodeId {
874 let tree_ptr = self.nodes.as_mut() as *mut NodeTree;
875 let guard = self.guard.clone();
876
877 let id = self
878 .nodes
879 .insert_with_key(|id| Node::new(tree_ptr, id, guard, node_data));
880
881 self.changed_nodes.insert(id);
883 id
884 }
885
886 pub(crate) fn remove_node_from_tree(&mut self, node_id: NodeId) -> Option<Node> {
890 self.clear_interaction_state_for_removed_node(node_id);
891 self.nodes.remove(node_id)
892 }
893
894 fn nearest_surviving_element_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
899 let mut current = self.get_node(node_id)?.parent;
900 while let Some(id) = current {
901 let node = self.get_node(id)?;
902 if node.is_element() && node.flags.is_in_document() {
903 return Some(id);
904 }
905 current = node.parent;
906 }
907 None
908 }
909
910 pub(crate) fn clear_interaction_state_for_removed_node(&mut self, node_id: NodeId) {
931 if !self.nodes.contains_key(node_id) {
932 return;
933 }
934
935 if self.hover_node_id == Some(node_id) {
936 self.hover_node_id = self.nearest_surviving_element_ancestor(node_id);
937 self.hover_node_is_text = false;
938 }
939 if self.hover_hit_node_id == Some(node_id) {
940 self.hover_hit_node_id = None;
941 }
942 if self.active_node_id == Some(node_id) {
943 self.active_node_id = self.nearest_surviving_element_ancestor(node_id);
944 }
945 if self.focus_node_id == Some(node_id) {
946 let shell_provider = self.shell_provider.clone();
947 self.nodes[node_id].blur(shell_provider);
948 self.focus_node_id = None;
949 }
950 if self.mousedown_node_id == Some(node_id) {
951 self.mousedown_node_id = None;
952 }
953 if self.text_selection.anchor.node_or_parent == Some(node_id)
954 || self.text_selection.focus.node_or_parent == Some(node_id)
955 {
956 self.text_selection.clear();
957 }
958 if self
959 .hovered_scrollbar
960 .is_some_and(|scrollbar| scrollbar.node_id == node_id)
961 {
962 self.hovered_scrollbar = None;
963 }
964 let drag_references_node = match &self.drag_mode {
965 DragMode::Panning(state) => state.target == node_id,
966 DragMode::ScrollbarDrag(state) => state.scrollbar.node_id == node_id,
967 DragMode::Selecting | DragMode::None => false,
968 };
969 if drag_references_node {
970 self.drag_mode = DragMode::None;
971 }
972 self.scrollbar_activity.remove(&node_id);
973 }
974
975 pub(crate) fn drop_node_ignoring_parent(&mut self, node_id: NodeId) -> Option<Node> {
976 self.drop_node_ignoring_parent_with(node_id, &mut |_| {})
977 }
978
979 pub(crate) fn drop_node_ignoring_parent_with(
982 &mut self,
983 node_id: NodeId,
984 on_drop: &mut dyn FnMut(NodeId),
985 ) -> Option<Node> {
986 let mut node = self.remove_node_from_tree(node_id);
987 if let Some(node) = &mut node {
988 on_drop(node_id);
989 if let Some(before) = node.before() {
990 self.drop_node_ignoring_parent_with(before, on_drop);
991 }
992 if let Some(after) = node.after() {
993 self.drop_node_ignoring_parent_with(after, on_drop);
994 }
995
996 for &child in &node.children {
997 self.drop_node_ignoring_parent_with(child, on_drop);
998 }
999
1000 for &anon_id in &node.anonymous_blocks {
1003 self.deallocate_anonymous_block(anon_id);
1004 }
1005 }
1006 node
1007 }
1008
1009 pub(crate) fn deallocate_anonymous_block(&mut self, anon_id: NodeId) {
1012 if !self.nodes.contains_key(anon_id) {
1015 return;
1016 }
1017
1018 let nested = std::mem::take(&mut self.nodes[anon_id].anonymous_blocks);
1020 for nested_id in nested {
1021 self.deallocate_anonymous_block(nested_id);
1022 }
1023
1024 self.remove_node_from_tree(anon_id);
1025 }
1026
1027 pub fn has_changes(&self) -> bool {
1029 self.changed_nodes.is_empty()
1030 }
1031
1032 pub fn create_text_node(&mut self, text: &str) -> NodeId {
1033 let content = text.to_string();
1034 let data = NodeData::Text(TextNodeData::new(content));
1035 self.create_node(data)
1036 }
1037
1038 pub fn deep_clone_node(&mut self, node_id: NodeId) -> NodeId {
1039 let node = &self.nodes[node_id];
1041 let mut data = node.data.clone();
1042
1043 match &mut data {
1044 NodeData::Element(elem) | NodeData::AnonymousBlock(elem) => {
1045 if let Some(arc) = elem.style_attribute.as_mut() {
1046 let read_guard = self.guard().read();
1047 let block = arc.read_with(&read_guard);
1048 *arc = ServoArc::new(self.guard().wrap(block.clone()));
1049 }
1050 }
1051 _ => {}
1052 }
1053
1054 let children = node.children.clone();
1055
1056 let new_node_id = self.create_node(data);
1058
1059 let new_children: ThinVec<NodeId> = children
1061 .into_iter()
1062 .map(|child_id| self.deep_clone_node(child_id))
1063 .collect();
1064 for &child_id in &new_children {
1065 self.nodes[child_id].parent = Some(new_node_id);
1066 }
1067 self.nodes[new_node_id].children = new_children;
1068
1069 new_node_id
1070 }
1071
1072 pub(crate) fn remove_and_drop_pe(&mut self, node_id: NodeId) -> Option<Node> {
1073 fn remove_pe_ignoring_parent(doc: &mut BaseDocument, node_id: NodeId) -> Option<Node> {
1074 let mut node = doc.remove_node_from_tree(node_id);
1075 if let Some(node) = &mut node {
1076 for &child in &node.children {
1077 remove_pe_ignoring_parent(doc, child);
1078 }
1079 for &anon_id in &node.anonymous_blocks {
1080 doc.deallocate_anonymous_block(anon_id);
1081 }
1082 }
1083 node
1084 }
1085
1086 let node = remove_pe_ignoring_parent(self, node_id);
1087
1088 if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
1090 let parent = &mut self.nodes[parent_id];
1091 parent.children.retain(|id| *id != node_id);
1092 }
1093
1094 node
1095 }
1096
1097 pub(crate) fn resolve_url(&self, raw: &str) -> url::Url {
1098 self.url.resolve_relative(raw).unwrap_or_else(|| {
1099 panic!(
1100 "to be able to resolve {raw} with the base_url: {:?}",
1101 *self.url
1102 )
1103 })
1104 }
1105
1106 pub fn print_tree(&self) {
1107 crate::util::walk_tree(0, self.root_node());
1108 }
1109
1110 pub fn print_subtree(&self, node_id: NodeId) {
1111 crate::util::walk_tree(0, &self.nodes[node_id]);
1112 }
1113
1114 pub fn reload_resource_by_href(&mut self, href_to_reload: &str) {
1115 for &node_id in self.nodes_to_stylesheet.keys() {
1116 let node = &self.nodes[node_id];
1117 let Some(element) = node.element_data() else {
1118 continue;
1119 };
1120
1121 if element.name.local == local_name!("link") {
1122 if let Some(href) = element.attr(local_name!("href")) {
1123 if href == href_to_reload {
1125 let resolved_href = self.resolve_url(href);
1126 self.net_provider.fetch(
1127 self.id(),
1128 self.build_request(resolved_href.clone()),
1129 ResourceHandler::boxed(
1130 self.tx.clone(),
1131 self.id,
1132 Some(node_id),
1133 self.shell_provider.clone(),
1134 StylesheetHandler {
1135 source_url: resolved_href,
1136 guard: self.guard.clone(),
1137 net_provider: self.net_provider.clone(),
1138 abort_signal: self.abort_signal.clone(),
1139 },
1140 ),
1141 );
1142 }
1143 }
1144 }
1145 }
1146 }
1147
1148 pub fn process_style_element(&mut self, target_id: NodeId) {
1149 let css = self.nodes[target_id].text_content();
1150 let css = html_escape::decode_html_entities(&css);
1151 let sheet = self.make_stylesheet(&css, Origin::Author);
1152 self.add_stylesheet_for_node(sheet, target_id);
1153 }
1154
1155 pub fn remove_user_agent_stylesheet(&mut self, sheet: &UserAgentStylesheet) {
1156 if self.useragent_stylesheets().any(|s| *s == sheet.0) {
1157 self.stylist
1158 .remove_stylesheet(sheet.0.clone(), &self.guard.read());
1159 }
1160 }
1161
1162 pub fn url(&self) -> &url::Url {
1164 &self.url
1165 }
1166
1167 pub fn author_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
1170 self.nodes_to_stylesheet.values()
1171 }
1172
1173 pub fn useragent_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
1175 (0..self.stylist.sheet_count(Origin::UserAgent))
1176 .filter_map(|i| self.stylist.sheet_at(Origin::UserAgent, i))
1177 }
1178
1179 pub fn add_user_agent_stylesheet(&mut self, sheet: UserAgentStylesheet) {
1181 if self.useragent_stylesheets().any(|s| *s == sheet.0) {
1182 return;
1183 }
1184 self.stylist.append_stylesheet(sheet.0, &self.guard.read());
1185 }
1186
1187 pub fn make_stylesheet(&self, css: impl AsRef<str>, origin: Origin) -> DocumentStyleSheet {
1188 let data = Stylesheet::from_str(
1189 css.as_ref(),
1190 self.url.url_extra_data(),
1191 origin,
1192 ServoArc::new(self.guard.wrap(MediaList::empty())),
1193 self.guard.clone(),
1194 Some(&StylesheetLoader {
1195 tx: self.tx.clone(),
1196 doc_id: self.id,
1197 net_provider: self.net_provider.clone(),
1198 shell_provider: self.shell_provider.clone(),
1199 abort_signal: self.abort_signal.clone(),
1200 import_depth: 0,
1201 }),
1202 None,
1203 QuirksMode::NoQuirks,
1204 AllowImportRules::Yes,
1205 );
1206
1207 DocumentStyleSheet(ServoArc::new(data))
1208 }
1209
1210 pub fn upsert_stylesheet_for_node(&mut self, node_id: NodeId) {
1211 let raw_styles = self.nodes[node_id].text_content();
1212 let sheet = self.make_stylesheet(raw_styles, Origin::Author);
1213 self.add_stylesheet_for_node(sheet, node_id);
1214 }
1215
1216 pub fn add_stylesheet_for_node(&mut self, stylesheet: DocumentStyleSheet, node_id: NodeId) {
1217 let old = self.nodes_to_stylesheet.insert(node_id, stylesheet.clone());
1218 self.stylesheet_generation += 1;
1219
1220 if let Some(old) = old {
1221 self.stylist.remove_stylesheet(old, &self.guard.read())
1222 }
1223
1224 crate::net::fetch_font_face(
1226 self.tx.clone(),
1227 self.id,
1228 Some(node_id),
1229 &stylesheet.0,
1230 &self.net_provider,
1231 &self.shell_provider,
1232 &self.guard.read(),
1233 self.abort_signal.as_ref(),
1234 );
1235
1236 let element = &mut self.nodes[node_id].element_data_mut().unwrap();
1238 element.special_data = SpecialElementData::Stylesheet(stylesheet.clone());
1239
1240 let insertion_point = self
1242 .nodes_to_stylesheet
1243 .range((Bound::Excluded(node_id), Bound::Unbounded))
1244 .next()
1245 .map(|(_, sheet)| sheet);
1246
1247 if let Some(insertion_point) = insertion_point {
1248 self.stylist.insert_stylesheet_before(
1249 stylesheet,
1250 insertion_point.clone(),
1251 &self.guard.read(),
1252 )
1253 } else {
1254 self.stylist
1255 .append_stylesheet(stylesheet, &self.guard.read())
1256 }
1257 }
1258
1259 pub fn handle_messages(&mut self) {
1260 let rx = self.rx.take().unwrap();
1263
1264 while let Ok(msg) = rx.try_recv() {
1265 self.handle_message(msg);
1266 }
1267
1268 self.rx = Some(rx);
1270 }
1271
1272 pub fn handle_message(&mut self, msg: DocumentEvent) {
1273 match msg {
1274 DocumentEvent::ResourceLoad(resource) => self.load_resource(resource),
1275 DocumentEvent::NavigateIframe { node_id, url } => self.navigate_iframe(node_id, url),
1276 }
1277 }
1278
1279 pub fn has_pending_critical_resources(&self) -> bool {
1281 !self.pending_critical_resources.is_empty()
1282 }
1283
1284 pub fn load_resource(&mut self, res: ResourceLoadResponse) {
1285 self.pending_critical_resources.remove(&res.request_id);
1286
1287 let resource = match res.result {
1288 Ok(resource) => resource,
1289 Err(err) => {
1290 if let Some(url) = res.resolved_url.as_ref() {
1291 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1292 #[cfg(feature = "tracing")]
1293 tracing::warn!(
1294 url = url.as_str(),
1295 waiting_nodes = waiting_nodes.len(),
1296 error = err.as_str(),
1297 "Resource load failed"
1298 );
1299 #[cfg(not(feature = "tracing"))]
1300 let _ = (waiting_nodes, err);
1301 } else {
1302 #[cfg(feature = "tracing")]
1303 tracing::warn!(error = err.as_str(), "Resource load failed (no url)");
1304 #[cfg(not(feature = "tracing"))]
1305 let _ = err;
1306 }
1307 return;
1308 }
1309 };
1310
1311 match resource {
1312 Resource::Css(css) => {
1313 let node_id = res.node_id.unwrap();
1314 self.add_stylesheet_for_node(css, node_id);
1315 }
1316 Resource::ImportedCss(import_rule, sheet) => {
1317 let mut guard = self.guard.write();
1318 import_rule.write_with(&mut guard).stylesheet =
1319 style::stylesheets::import_rule::ImportSheet::Sheet(sheet);
1320 drop(guard);
1321 self.stylist
1322 .force_stylesheet_origins_dirty(style::stylesheets::OriginSet::all());
1323 }
1324 Resource::Image(_kind, width, height, image_data) => {
1325 let image = ImageData::Raster(RasterImageData::new(width, height, image_data));
1327
1328 let Some(url) = res.resolved_url.as_ref() else {
1329 return;
1330 };
1331
1332 self.apply_loaded_image(url, image);
1333 }
1334 #[cfg(feature = "svg")]
1335 Resource::Svg(_kind, svg) => {
1336 let image = ImageData::Svg(svg);
1338
1339 let Some(url) = res.resolved_url.as_ref() else {
1340 return;
1341 };
1342
1343 self.apply_loaded_image(url, image);
1344 }
1345 Resource::DocumentSrc(html) => {
1346 let Some(node_id) = res.node_id else {
1347 return;
1348 };
1349 self.apply_iframe_html(node_id, res.request_id, res.resolved_url, &html);
1350 }
1351 Resource::Font(bytes, overrides) => {
1352 let font = Blob::new(Arc::new(bytes));
1353
1354 let weight_override = overrides.weight.map(parley::fontique::FontWeight::new);
1360 let info_override = parley::fontique::FontInfoOverride {
1361 family_name: overrides.family_name.as_deref(),
1362 weight: weight_override,
1363 style: overrides.style,
1364 ..Default::default()
1365 };
1366
1367 let mut global_font_ctx = self.font_ctx.lock().unwrap();
1369 global_font_ctx
1370 .collection
1371 .register_fonts(font.clone(), Some(info_override));
1372
1373 #[cfg(feature = "parallel-construct")]
1374 {
1375 rayon::broadcast(|_ctx| {
1376 let mut font_ctx = self
1377 .thread_font_contexts
1378 .get_or(|| RefCell::new(Box::new(global_font_ctx.clone())))
1379 .borrow_mut();
1380 font_ctx
1381 .collection
1382 .register_fonts(font.clone(), Some(info_override));
1383 });
1384 }
1385 drop(global_font_ctx);
1386
1387 self.invalidate_inline_contexts();
1389 }
1390 Resource::None => {
1391 }
1393 }
1394 }
1395
1396 fn apply_loaded_image(&mut self, url: &str, image: ImageData) {
1399 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1401
1402 #[cfg(feature = "tracing")]
1403 tracing::info!(
1404 "Image {url} loaded, applying to {} nodes",
1405 waiting_nodes.len()
1406 );
1407
1408 self.image_cache.insert(url.to_string(), image.clone());
1410
1411 for (node_id, image_type) in waiting_nodes {
1413 let Some(node) = self.get_node_mut(node_id) else {
1414 continue;
1415 };
1416
1417 match image_type {
1418 ImageType::Image => {
1419 let element = node.element_data_mut().unwrap();
1420 #[cfg(feature = "custom-widget")]
1421 if element.custom_widget_data().is_some() {
1422 continue;
1423 }
1424 element.special_data = SpecialElementData::Image(Box::new(image.clone()));
1425
1426 node.clear_layout_cache();
1428 node.insert_damage(ALL_DAMAGE);
1429 }
1430 ImageType::Background(idx) | ImageType::Mask(idx) => {
1431 let layer_image = node.element_data_mut().and_then(|el| {
1432 let images = match image_type {
1433 ImageType::Background(_) => &mut el.background_images,
1434 ImageType::Mask(_) => &mut el.mask_images,
1435 ImageType::Image => unreachable!(),
1436 };
1437 images.get_mut(idx)
1438 });
1439 if let Some(Some(layer_image)) = layer_image {
1440 layer_image.status = Status::Ok;
1441 layer_image.image = image.clone();
1442 }
1443 }
1444 }
1445 }
1446 }
1447
1448 pub fn snapshot_node(&mut self, node_id: NodeId) {
1452 self.snapshot_node_impl(node_id, true)
1453 }
1454
1455 pub fn snapshot_node_state_only(&mut self, node_id: NodeId) {
1459 self.snapshot_node_impl(node_id, false)
1460 }
1461
1462 fn snapshot_node_impl(&mut self, node_id: NodeId, capture_attrs: bool) {
1463 let node = &mut self.nodes[node_id];
1464
1465 let has_been_styled = node.primary_styles().is_some();
1470 if !has_been_styled {
1471 return;
1472 }
1473
1474 let opaque_node_id = TNode::opaque(&&*node);
1475 node.set_has_snapshot(true);
1476 node.snapshot_handled()
1477 .store(false, std::sync::atomic::Ordering::SeqCst);
1478
1479 let needs_attrs = capture_attrs
1485 && self
1486 .snapshots
1487 .get_mut(&opaque_node_id)
1488 .is_none_or(|snapshot| snapshot.attrs.is_none());
1489
1490 let (attrs, changed_attrs) = if needs_attrs {
1491 let node = &self.nodes[node_id];
1492 let attrs: Option<Vec<_>> = node.attrs().map(|attrs| {
1493 attrs
1494 .iter()
1495 .map(|attr| {
1496 let ident = AttrIdentifier {
1497 local_name: GenericAtomIdent(attr.name.local.clone()),
1498 name: GenericAtomIdent(attr.name.local.clone()),
1499 namespace: GenericAtomIdent(attr.name.ns.clone()),
1500 prefix: None,
1501 };
1502
1503 let value = if attr.name.local == local_name!("id") {
1504 AttrValue::Atom(Atom::from(&*attr.value))
1505 } else if attr.name.local == local_name!("class") {
1506 let classes = attr
1507 .value
1508 .split_ascii_whitespace()
1509 .map(Atom::from)
1510 .collect();
1511 AttrValue::TokenList(OnceLock::from(attr.value.clone()), classes)
1512 } else {
1513 AttrValue::String(attr.value.clone())
1514 };
1515
1516 (ident, value)
1517 })
1518 .collect()
1519 });
1520
1521 let changed_attrs: Vec<_> = attrs
1522 .as_ref()
1523 .map(|attrs| attrs.iter().map(|attr| attr.0.name.clone()).collect())
1524 .unwrap_or_default();
1525
1526 (attrs, changed_attrs)
1527 } else {
1528 (None, Vec::new())
1529 };
1530
1531 if let Some(snapshot) = self.snapshots.get_mut(&opaque_node_id) {
1532 if needs_attrs {
1535 snapshot.attrs = attrs;
1536 snapshot.changed_attrs = changed_attrs;
1537 snapshot.class_changed = true;
1538 snapshot.id_changed = true;
1539 snapshot.other_attributes_changed = true;
1540 }
1541 } else {
1542 self.snapshots.insert(
1543 opaque_node_id,
1544 ServoElementSnapshot {
1545 state: Some(*self.nodes[node_id].element_state()),
1546 attrs,
1547 changed_attrs,
1548 class_changed: needs_attrs,
1549 id_changed: needs_attrs,
1550 other_attributes_changed: needs_attrs,
1551 },
1552 );
1553 }
1554 }
1555
1556 pub fn style_depends_on_state(&self, state: ElementState) -> bool {
1559 self.stylist.iter_origins().any(|(data, _)| {
1560 data.has_state_dependency(state) || data.has_nth_of_state_dependency(state)
1561 })
1562 }
1563
1564 pub fn snapshot_node_and(
1568 &mut self,
1569 node_id: NodeId,
1570 state: ElementState,
1571 cb: impl FnOnce(&mut Node),
1572 ) {
1573 if self.style_depends_on_state(state) {
1574 self.snapshot_node_state_only(node_id);
1575 }
1576 cb(&mut self.nodes[node_id]);
1577 }
1578
1579 pub fn hit(&self, x: f32, y: f32) -> Option<HitResult> {
1581 self.hit_with_scrollbar(x, y).0
1582 }
1583
1584 pub fn element_from_point(&self, x: f32, y: f32) -> Option<NodeId> {
1592 let viewport = self.viewport();
1593 let scale = viewport.scale();
1594 let (viewport_width, viewport_height) = (
1595 viewport.window_size.0 as f32 / scale,
1596 viewport.window_size.1 as f32 / scale,
1597 );
1598 if x < 0.0 || y < 0.0 || x > viewport_width || y > viewport_height {
1599 return None;
1600 }
1601
1602 let Some(hit) = self.hit(x, y) else {
1603 return self.try_root_element().map(|root| root.id);
1606 };
1607
1608 let mut node_id = self.nearest_non_anonymous_ancestor(hit.node_id)?;
1610 loop {
1611 let node = self.get_node(node_id)?;
1612 if node.is_element() {
1613 return Some(node_id);
1614 }
1615 node_id = node.parent?;
1616 }
1617 }
1618
1619 pub fn elements_from_point(&self, x: f32, y: f32) -> Vec<NodeId> {
1627 let mut element_ids = Vec::new();
1628 let mut current = self.element_from_point(x, y);
1629 while let Some(node_id) = current {
1630 element_ids.push(node_id);
1631 current = self
1632 .get_node(node_id)
1633 .and_then(|node| node.parent)
1634 .filter(|parent_id| {
1635 self.get_node(*parent_id)
1636 .is_some_and(|node| node.is_element())
1637 });
1638 }
1639 element_ids
1640 }
1641
1642 pub fn nearest_non_anonymous_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
1655 let mut node = self.get_node(node_id)?;
1659 loop {
1660 let parent = match node.parent {
1661 Some(parent_id) => self.get_node(parent_id)?,
1662 None => return Some(node.id),
1663 };
1664 if !node.is_anonymous() && !parent.is_anonymous() {
1665 return Some(node.id);
1666 }
1667 node = parent;
1668 }
1669 }
1670
1671 pub fn focus_next_node(&mut self) -> Option<NodeId> {
1672 let focussed_node_id = self.get_focussed_node_id()?;
1673 let id = self.next_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
1674 self.set_focus_to(id);
1675 Some(id)
1676 }
1677
1678 pub fn focus_prev_node(&mut self) -> Option<NodeId> {
1680 let focussed_node_id = self.get_focussed_node_id()?;
1681 let id = self.prev_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
1682 self.set_focus_to(id);
1683 Some(id)
1684 }
1685
1686 pub fn clear_focus(&mut self) {
1688 if let Some(id) = self.focus_node_id {
1689 let shell_provider = self.shell_provider.clone();
1690 self.snapshot_node_and(id, ElementState::FOCUS | ElementState::FOCUSRING, |node| {
1691 node.blur(shell_provider)
1692 });
1693 self.focus_node_id = None;
1694 }
1695 }
1696
1697 pub fn set_mousedown_node_id(&mut self, node_id: Option<NodeId>) {
1698 self.mousedown_node_id = node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
1699 }
1700 pub fn set_focus_to(&mut self, focus_node_id: NodeId) -> bool {
1701 let Some(focus_node_id) = self.nearest_non_anonymous_ancestor(focus_node_id) else {
1702 return false;
1703 };
1704 if Some(focus_node_id) == self.focus_node_id {
1705 return false;
1706 }
1707
1708 #[cfg(feature = "tracing")]
1709 tracing::info!("Focussed node {focus_node_id}");
1710
1711 let shell_provider = self.shell_provider.clone();
1712
1713 if let Some(id) = self.focus_node_id {
1715 self.snapshot_node_and(id, ElementState::FOCUS | ElementState::FOCUSRING, |node| {
1716 node.blur(shell_provider.clone())
1717 });
1718 }
1719
1720 self.snapshot_node_and(
1722 focus_node_id,
1723 ElementState::FOCUS | ElementState::FOCUSRING,
1724 |node| node.focus(shell_provider),
1725 );
1726
1727 self.focus_node_id = Some(focus_node_id);
1728
1729 true
1730 }
1731
1732 pub fn active_node(&mut self) -> bool {
1733 let Some(hover_node_id) = self.get_hover_node_id() else {
1734 return false;
1735 };
1736
1737 if let Some(active_node_id) = self.active_node_id {
1738 if active_node_id == hover_node_id {
1739 return true;
1740 }
1741 self.unactive_node();
1742 }
1743
1744 debug_assert!(
1746 self.get_node(hover_node_id)
1747 .is_some_and(|node| !node.is_anonymous()),
1748 "interaction state must reference DOM nodes, not layout-generated nodes"
1749 );
1750 let active_node_id = Some(hover_node_id);
1751
1752 let node_path = self.maybe_node_element_ancestors(active_node_id);
1753 for &id in node_path.iter() {
1754 self.snapshot_node_and(id, ElementState::ACTIVE, |node| node.active());
1755 }
1756
1757 self.active_node_id = active_node_id;
1758
1759 true
1760 }
1761
1762 pub fn unactive_node(&mut self) -> bool {
1763 let Some(active_node_id) = self.active_node_id.take() else {
1764 return false;
1765 };
1766
1767 let node_path = self.maybe_node_element_ancestors(Some(active_node_id));
1768 for &id in node_path.iter() {
1769 self.snapshot_node_and(id, ElementState::ACTIVE, |node| node.unactive());
1770 }
1771
1772 true
1773 }
1774
1775 pub fn hovered_scrollbar(&self) -> Option<crate::node::ScrollbarRef> {
1777 self.hovered_scrollbar
1778 }
1779
1780 pub fn scrollbar_drag_target(&self) -> Option<crate::node::ScrollbarRef> {
1782 match &self.drag_mode {
1783 DragMode::ScrollbarDrag(state) => Some(state.scrollbar),
1784 _ => None,
1785 }
1786 }
1787
1788 pub fn scrollbar_opacity(&self, node_id: NodeId) -> f32 {
1793 let interacting = |scrollbar: &crate::node::ScrollbarRef| scrollbar.node_id == node_id;
1794 if self.hovered_scrollbar.as_ref().is_some_and(interacting)
1795 || self
1796 .scrollbar_drag_target()
1797 .as_ref()
1798 .is_some_and(interacting)
1799 {
1800 return 1.0;
1801 }
1802 self.scrollbar_activity.get(&node_id).map_or(0.0, |last| {
1803 crate::node::scrollbar::opacity_at(last.elapsed())
1804 })
1805 }
1806
1807 pub(crate) fn show_scrollbars(&mut self, node_id: NodeId) {
1810 if cfg!(feature = "scrollbars") {
1811 self.scrollbar_activity.insert(node_id, Instant::now());
1812 }
1813 }
1814
1815 fn scrollbars_animating(&self) -> bool {
1818 use crate::node::scrollbar::{FADE_DELAY, FADE_DURATION};
1819 self.scrollbar_activity
1820 .values()
1821 .any(|last| last.elapsed() < FADE_DELAY + FADE_DURATION)
1822 }
1823
1824 pub(crate) fn hit_with_scrollbar(
1828 &self,
1829 x: f32,
1830 y: f32,
1831 ) -> (Option<HitResult>, Option<crate::node::ScrollbarRef>) {
1832 if TDocument::as_node(&self.root_node())
1833 .first_element_child()
1834 .is_none()
1835 {
1836 #[cfg(feature = "tracing")]
1837 tracing::warn!("No DOM - not resolving hit test");
1838 return (None, None);
1839 }
1840 let mut scrollbar = None;
1841 let hit = self
1842 .root_element()
1843 .hit_inner(x, y, self.viewport().scale_f64(), &mut scrollbar);
1844 (hit, scrollbar)
1845 }
1846
1847 pub fn set_hover_to(&mut self, x: f32, y: f32) -> bool {
1848 self.last_client_pointer_position = Some(taffy::Point {
1852 x: x - self.viewport_scroll().x as f32,
1853 y: y - self.viewport_scroll().y as f32,
1854 });
1855
1856 let (hit, hovered_scrollbar) = self.hit_with_scrollbar(x, y);
1857 let hovered_scrollbar =
1860 hovered_scrollbar.filter(|scrollbar| self.scrollbar_opacity(scrollbar.node_id) > 0.0);
1861 let scrollbar_changed = hovered_scrollbar != self.hovered_scrollbar;
1865 if scrollbar_changed {
1866 for scrollbar in [self.hovered_scrollbar, hovered_scrollbar]
1869 .into_iter()
1870 .flatten()
1871 {
1872 self.show_scrollbars(scrollbar.node_id);
1873 }
1874 }
1875 self.hovered_scrollbar = hovered_scrollbar;
1876
1877 let hit_node_id = hit.map(|hit| hit.node_id);
1882 let hover_node_id = hit_node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
1883 let new_is_text = hit.map(|hit| hit.is_text).unwrap_or(false);
1884
1885 self.hover_hit_node_id = hit_node_id;
1886 self.hover_node_is_text = new_is_text;
1887
1888 if hover_node_id == self.hover_node_id {
1890 return scrollbar_changed;
1891 }
1892
1893 let old_node_path = self.maybe_node_element_ancestors(self.hover_node_id);
1894 let new_node_path = self.maybe_node_element_ancestors(hover_node_id);
1895 let same_count = old_node_path
1896 .iter()
1897 .zip(&new_node_path)
1898 .take_while(|(o, n)| o == n)
1899 .count();
1900 for &id in old_node_path.iter().skip(same_count) {
1901 self.snapshot_node_and(id, ElementState::HOVER, |node| node.unhover());
1902 }
1903 for &id in new_node_path.iter().skip(same_count) {
1904 self.snapshot_node_and(id, ElementState::HOVER, |node| node.hover());
1905 }
1906
1907 self.hover_node_id = hover_node_id;
1908
1909 self.shell_provider.request_redraw();
1911
1912 true
1913 }
1914
1915 pub fn clear_hover(&mut self) -> bool {
1916 self.last_client_pointer_position = None;
1919 self.hover_hit_node_id = None;
1920
1921 let Some(hover_node_id) = self.hover_node_id else {
1922 return false;
1923 };
1924
1925 let old_node_path = self.maybe_node_element_ancestors(Some(hover_node_id));
1926 for &id in old_node_path.iter() {
1927 self.snapshot_node_and(id, ElementState::HOVER, |node| node.unhover());
1928 }
1929
1930 self.hover_node_id = None;
1931 self.hover_node_is_text = false;
1932
1933 self.shell_provider.request_redraw();
1935
1936 true
1937 }
1938
1939 pub fn refresh_hover(&mut self) -> bool {
1945 let Some(pos) = self.last_client_pointer_position else {
1946 return false;
1947 };
1948 let x = pos.x + self.viewport_scroll().x as f32;
1949 let y = pos.y + self.viewport_scroll().y as f32;
1950 let prev_hover = self.hover_state();
1951 let changed = self.set_hover_to(x, y);
1952 if self.hover_state() != prev_hover {
1953 self.update_cursor();
1954 }
1955 changed
1956 }
1957
1958 pub(crate) fn hover_state(&self) -> (Option<NodeId>, Option<NodeId>, bool) {
1961 (
1962 self.hover_node_id,
1963 self.hover_hit_node_id,
1964 self.hover_node_is_text,
1965 )
1966 }
1967
1968 pub(crate) fn update_cursor(&self) {
1973 self.shell_provider.set_cursor(self.get_cursor());
1974 }
1975
1976 pub fn get_hover_node_id(&self) -> Option<NodeId> {
1977 self.hover_node_id
1978 }
1979
1980 pub fn get_mousedown_node_id(&self) -> Option<NodeId> {
1981 self.mousedown_node_id
1982 }
1983
1984 pub fn set_viewport(&mut self, viewport: Viewport) {
1985 let changes = DeviceChanges::from_viewports(&self.viewport, &viewport);
1986 self.viewport = viewport;
1987 self.queue_device_changes(changes);
1988 }
1989
1990 pub fn media_type(&self) -> &MediaType {
1992 &self.media_type
1993 }
1994
1995 pub fn set_media_type(&mut self, media_type: MediaType) {
1998 if self.media_type == media_type {
1999 return;
2000 }
2001 self.media_type = media_type;
2002 self.queue_device_changes(DeviceChanges::MEDIA_TYPE);
2003 }
2004
2005 pub fn viewport(&self) -> &Viewport {
2006 &self.viewport
2007 }
2008
2009 pub fn viewport_mut(&mut self) -> ViewportMut<'_> {
2010 ViewportMut::new(self)
2011 }
2012
2013 pub fn zoom_by(&mut self, increment: f32) {
2014 *self.viewport_mut().zoom_mut() += increment;
2015 }
2016
2017 pub fn zoom_to(&mut self, zoom: f32) {
2018 *self.viewport_mut().zoom_mut() = zoom;
2019 }
2020
2021 pub fn get_viewport(&self) -> Viewport {
2022 self.viewport.clone()
2023 }
2024
2025 pub fn incremental_layout(&self) -> bool {
2033 self.incremental_layout
2034 }
2035
2036 pub fn set_incremental_layout(&mut self, enabled: bool) {
2041 self.incremental_layout = enabled;
2042 }
2043
2044 pub fn devtools(&self) -> &DevtoolSettings {
2045 &self.devtool_settings
2046 }
2047
2048 pub fn devtools_mut(&mut self) -> &mut DevtoolSettings {
2049 &mut self.devtool_settings
2050 }
2051
2052 pub fn subdoc(&self, node_id: NodeId) -> Option<&dyn Document> {
2053 self.get_node(node_id)
2054 .and_then(|node| node.element_data())
2055 .and_then(|el| el.sub_doc_data())
2056 }
2057
2058 pub fn subdoc_mut(&mut self, node_id: NodeId) -> Option<&mut dyn Document> {
2059 self.get_node_mut(node_id)
2060 .and_then(|node| node.element_data_mut())
2061 .and_then(|el| el.sub_doc_data_mut())
2062 }
2063
2064 pub fn is_animating(&self) -> bool {
2065 #[cfg(feature = "custom-widget")]
2066 let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
2067 self.nodes[node_id]
2068 .element_data()
2069 .and_then(|el| el.custom_widget_data())
2070 .is_some_and(|data| data.widget.requires_redraw())
2071 });
2072 #[cfg(not(feature = "custom-widget"))]
2073 let custom_widget_is_animating = false;
2074
2075 self.has_canvas
2076 | self.has_active_animations
2077 | self.subdoc_is_animating
2078 | custom_widget_is_animating
2079 | (self.scroll_animation != ScrollAnimationState::None)
2080 | self.scrollbars_animating()
2081 }
2082
2083 pub(crate) fn queue_device_changes(&mut self, changes: DeviceChanges) {
2086 if changes.is_empty() {
2087 return;
2088 }
2089 self.pending_device_changes |= changes;
2090 self.shell_provider.request_redraw();
2091 }
2092
2093 pub(crate) fn flush_pending_device_changes(&mut self) {
2096 let changes = std::mem::take(&mut self.pending_device_changes);
2097 if changes.is_empty() {
2098 return;
2099 }
2100
2101 self.set_stylist_device(make_device(
2102 &self.viewport,
2103 self.media_type.clone(),
2104 self.font_ctx.clone(),
2105 ));
2106 self.scroll_viewport_by(0.0, 0.0); if changes.contains(DeviceChanges::SCALE) {
2111 self.invalidate_inline_contexts();
2112 }
2113
2114 if changes.contains(DeviceChanges::COLOR_SCHEME) {
2118 if let Some(root_id) = self.try_root_element().map(|el| el.id) {
2119 self.nodes[root_id].set_restyle_hint(RestyleHint::recascade_subtree());
2120 }
2121 }
2122 }
2123
2124 pub fn set_stylist_device(&mut self, device: Device) {
2126 let root_styles = self
2132 .try_root_element()
2133 .and_then(|root| root.primary_styles());
2134 if let Some(root_style) = root_styles.as_deref() {
2135 device.set_root_style(root_style);
2136
2137 let font = root_style.get_font();
2138 let font_size = font.clone_font_size().computed_size();
2139 device.set_root_font_size(root_style.effective_zoom.unzoom(font_size.px()));
2140
2141 let line_height = device
2142 .calc_line_height(font, root_style.writing_mode, None)
2143 .0;
2144 device.set_root_line_height(root_style.effective_zoom.unzoom(line_height.px()));
2145 }
2146 drop(root_styles);
2147
2148 let old_device = self.stylist.device();
2149 let viewport_size_changed = old_device.au_viewport_size() != device.au_viewport_size();
2150 let styles_use_viewport_units = old_device.used_viewport_size();
2151
2152 let origins = {
2153 let guard = &self.guard;
2154 let guards = StylesheetGuards {
2155 author: &guard.read(),
2156 ua_or_user: &guard.read(),
2157 };
2158 self.stylist.set_device(device, &guards)
2159 };
2160
2161 if !origins.is_empty() {
2166 self.stylist.force_stylesheet_origins_dirty(origins);
2167 }
2168
2169 if viewport_size_changed && styles_use_viewport_units {
2174 if self
2175 .stylist
2176 .get_custom_property_initial_values_flags()
2177 .intersects(ComputedValueFlags::USES_VIEWPORT_UNITS)
2178 {
2179 self.stylist.rebuild_initial_values_for_custom_properties();
2180 }
2181 if let Some(root) = self.try_root_element() {
2182 style::invalidation::viewport_units::invalidate(root);
2183 }
2184 }
2185 }
2186
2187 pub fn stylist_device(&mut self) -> &Device {
2188 self.flush_pending_device_changes();
2189 self.stylist.device()
2190 }
2191
2192 pub fn get_cursor(&self) -> Option<CursorIcon> {
2193 let node_id = self
2198 .hover_hit_node_id
2199 .filter(|&id| self.nodes.contains_key(id))
2200 .or(self.get_hover_node_id())?;
2201 let node = &self.nodes[node_id];
2202
2203 if let Some(subdoc) = node.subdoc().map(|doc| doc.inner())
2204 && subdoc.get_hover_node_id().is_some()
2205 {
2206 return subdoc.get_cursor();
2207 }
2208
2209 let style = node.primary_styles()?;
2210 let user_select = style.clone_user_select();
2211 let keyword = style.clone_cursor().keyword;
2212
2213 if keyword != CursorKind::Auto {
2215 return stylo_to_cursor_icon(keyword);
2216 }
2217
2218 if node
2220 .element_data()
2221 .is_some_and(|e| e.text_input_data().is_some())
2222 {
2223 return Some(CursorIcon::Text);
2224 }
2225
2226 let mut maybe_node = Some(node);
2228 while let Some(node) = maybe_node {
2229 if node.is_link() {
2230 return Some(CursorIcon::Pointer);
2231 }
2232
2233 maybe_node = node.layout_parent.get().map(|node_id| node.with(node_id));
2234 }
2235
2236 if self.hover_node_is_text {
2238 return Some(match user_select {
2239 UserSelect::Text | UserSelect::All | UserSelect::Auto | UserSelect::Contain => {
2240 CursorIcon::Text
2241 }
2242 UserSelect::None => CursorIcon::Default,
2243 });
2244 }
2245
2246 Some(CursorIcon::Default)
2248 }
2249
2250 pub fn viewport_scroll(&self) -> crate::Point<f64> {
2251 self.nodes.viewport_scroll()
2252 }
2253
2254 pub fn set_viewport_scroll(&mut self, scroll: crate::Point<f64>) {
2255 self.nodes.set_viewport_scroll(scroll);
2256 }
2257
2258 pub fn get_fragment_target(&self, fragment: &str) -> Option<NodeId> {
2263 if let Some(node_id) = self.get_element_by_id(fragment) {
2264 return Some(node_id);
2265 }
2266
2267 self.nodes.iter().find_map(|(id, node)| {
2269 let el = node.element_data()?;
2270 (el.name.local == local_name!("a") && el.attr(local_name!("name")) == Some(fragment))
2271 .then_some(id)
2272 })
2273 }
2274
2275 #[cfg(not(feature = "writing-mode"))]
2278 pub(crate) fn physical_unrounded_geometry(
2279 &self,
2280 node_id: NodeId,
2281 ) -> (crate::util::Point<f32>, taffy::Layout) {
2282 let mut pos = crate::util::Point { x: 0.0, y: 0.0 };
2283 let mut current = Some(node_id);
2284 while let Some(id) = current {
2285 let node = &self.nodes[id];
2286 pos.x += node.unrounded_layout().location.x - node.scroll_offset().x as f32;
2287 pos.y += node.unrounded_layout().location.y - node.scroll_offset().y as f32;
2288 current = node.containing_block();
2289 }
2290 (pos, *self.nodes[node_id].unrounded_layout())
2291 }
2292
2293 pub fn get_client_bounding_rect(&self, node_id: NodeId) -> Option<BoundingRect> {
2295 if let Some(rects) = self.inline_fragment_rects(node_id) {
2298 return rects.reduce(BoundingRect::union);
2299 }
2300
2301 self.get_node(node_id)?;
2302 let (pos, layout) = self.physical_unrounded_geometry(node_id);
2303 let size = layout.size;
2304
2305 Some(BoundingRect {
2306 x: snap_to_layout_unit(pos.x as f64 - self.viewport_scroll().x),
2307 y: snap_to_layout_unit(pos.y as f64 - self.viewport_scroll().y),
2308 width: snap_to_layout_unit(size.width as f64),
2309 height: snap_to_layout_unit(size.height as f64),
2310 })
2311 }
2312
2313 pub fn offset_rect(&self, node_id: NodeId) -> Option<BoundingRect> {
2322 let node = self.get_node(node_id)?;
2323
2324 let Some(rects) = self.inline_fragment_rects(node_id) else {
2326 let pos = node.offset_top_left();
2327 let size = node.unrounded_layout().size;
2328 #[cfg(feature = "writing-mode")]
2329 let size = match node.containing_block() {
2330 Some(parent) if self.nodes[parent].layout_data().writing_mode.is_vertical() => {
2331 size.transpose()
2332 }
2333 _ => size,
2334 };
2335 return Some(BoundingRect {
2336 x: pos.x as f64,
2337 y: pos.y as f64,
2338 width: size.width as f64,
2339 height: size.height as f64,
2340 });
2341 };
2342
2343 let bounds = rects.reduce(BoundingRect::union)?;
2345
2346 let scroll = self.viewport_scroll();
2348 let mut x = bounds.x + scroll.x;
2349 let mut y = bounds.y + scroll.y;
2350
2351 let inline_root = node.inline_root_ancestor()?;
2353 let offset_parent = if inline_root.is_offset_parent() {
2354 Some(inline_root)
2355 } else {
2356 inline_root.offset_parent()
2357 };
2358 if let Some(parent) = offset_parent.filter(|parent| !parent.is_static_body()) {
2360 let (parent_pos, parent_layout) = self.physical_unrounded_geometry(parent.id);
2361 let border = parent_layout.border;
2362 x -= (parent_pos.x + border.left) as f64;
2363 y -= (parent_pos.y + border.top) as f64;
2364 }
2365
2366 Some(BoundingRect {
2367 x,
2368 y,
2369 width: bounds.width,
2370 height: bounds.height,
2371 })
2372 }
2373
2374 pub fn node_client_rects(&self, node_id: NodeId) -> Vec<BoundingRect> {
2379 match self.inline_fragment_rects(node_id) {
2380 Some(rects) => rects.collect(),
2381 None => self.get_client_bounding_rect(node_id).into_iter().collect(),
2382 }
2383 }
2384
2385 pub fn inline_fragment_rects(
2389 &self,
2390 node_id: NodeId,
2391 ) -> Option<impl Iterator<Item = BoundingRect> + '_> {
2392 let node = self.get_node(node_id)?;
2393
2394 let fragments = node.inline_fragment_boxes()?;
2397 let inline_root = node.inline_root_ancestor()?;
2398
2399 let (root_pos, _) = self.physical_unrounded_geometry(inline_root.id);
2401 let origin_x = root_pos.x as f64 - self.viewport_scroll().x;
2402 let origin_y = root_pos.y as f64 - self.viewport_scroll().y;
2403
2404 Some(fragments.map(move |rect| BoundingRect {
2405 x: snap_to_layout_unit(origin_x + rect.left as f64),
2406 y: snap_to_layout_unit(origin_y + rect.top as f64),
2407 width: snap_to_layout_unit((rect.right - rect.left) as f64),
2408 height: snap_to_layout_unit((rect.bottom - rect.top) as f64),
2409 }))
2410 }
2411
2412 pub fn find_element_by_tag_name(&self, tag: &LocalName) -> Option<&Node> {
2416 let root = self.try_root_element()?;
2417 if root.data.is_element_with_tag_name(tag) {
2418 return Some(root);
2419 }
2420 root.children
2421 .iter()
2422 .copied()
2423 .find(|child_id| {
2424 self.get_node(*child_id)
2425 .is_some_and(|child| child.data.is_element_with_tag_name(tag))
2426 })
2427 .or_else(|| {
2428 TreeTraverser::new(self)
2429 .find(|node_id| self.nodes[*node_id].data.is_element_with_tag_name(tag))
2430 })
2431 .map(|node_id| &self.nodes[node_id])
2432 }
2433
2434 pub fn find_body_node(&self) -> Option<&Node> {
2436 self.find_element_by_tag_name(&local_name!("body"))
2437 }
2438
2439 pub fn find_head_node(&self) -> Option<&Node> {
2441 self.find_element_by_tag_name(&local_name!("head"))
2442 }
2443
2444 pub fn find_title_node(&self) -> Option<&Node> {
2446 self.find_element_by_tag_name(&local_name!("title"))
2447 }
2448
2449 pub fn with_text_input(
2450 &mut self,
2451 node_id: NodeId,
2452 cb: impl FnOnce(PlainEditorDriver<TextBrush>),
2453 ) {
2454 let Some(node) = self.nodes.get_mut(node_id) else {
2455 return;
2456 };
2457
2458 if let Some(text_input) = node
2459 .element_data_mut()
2460 .and_then(|el| el.text_input_data_mut())
2461 {
2462 let mut font_ctx = self.font_ctx.lock().unwrap();
2463 let layout_ctx = &mut self.layout_ctx;
2464 let driver = text_input.editor.driver(&mut font_ctx, layout_ctx);
2465 cb(driver)
2466 }
2467 }
2468
2469 pub(crate) fn clamp_text_input_scroll(&mut self, node_id: NodeId) {
2472 let Some(node) = self.nodes.get_mut(node_id) else {
2473 return;
2474 };
2475
2476 let content_box_width = node.final_layout().content_box_width();
2477 let content_box_height = node.final_layout().content_box_height();
2478
2479 if let Some(text_input) = node
2480 .element_data_mut()
2481 .and_then(|el| el.text_input_data_mut())
2482 {
2483 text_input.clamp_scroll_offset(content_box_width, content_box_height);
2484 }
2485 }
2486
2487 pub(crate) fn compute_has_canvas(&self) -> bool {
2488 TreeTraverser::new(self).any(|node_id| {
2489 let node = &self.nodes[node_id];
2490 let Some(element) = node.element_data() else {
2491 return false;
2492 };
2493 if element.name.local == local_name!("canvas") && element.has_attr(local_name!("src")) {
2494 return true;
2495 }
2496
2497 false
2498 })
2499 }
2500
2501 pub fn find_text_position(&self, x: f32, y: f32) -> Option<(NodeId, usize)> {
2507 let hit = self.hit(x, y)?;
2508 let hit_node = self.get_node(hit.node_id)?;
2509 let inline_root = hit_node.inline_root_ancestor()?;
2510 let byte_offset = inline_root.text_offset_at_point(hit.x, hit.y)?;
2511 Some((inline_root.id, byte_offset))
2512 }
2513
2514 pub fn set_text_selection(
2516 &mut self,
2517 anchor_node: NodeId,
2518 anchor_offset: usize,
2519 focus_node: NodeId,
2520 focus_offset: usize,
2521 ) {
2522 self.text_selection =
2523 TextSelection::new(anchor_node, anchor_offset, focus_node, focus_offset);
2524
2525 if let (Some(parent), Some(idx)) = self.anonymous_block_location(anchor_node) {
2527 self.text_selection
2528 .anchor
2529 .set_anonymous(parent, idx, anchor_offset);
2530 }
2531 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
2532 self.text_selection
2533 .focus
2534 .set_anonymous(parent, idx, focus_offset);
2535 }
2536 }
2537
2538 fn anonymous_block_location(&self, node_id: NodeId) -> (Option<NodeId>, Option<usize>) {
2541 let Some(node) = self.get_node(node_id) else {
2542 return (None, None);
2543 };
2544
2545 if !node.is_anonymous() {
2546 return (None, None);
2547 }
2548
2549 let Some(parent_id) = node.parent else {
2550 return (None, None);
2551 };
2552
2553 let Some(parent) = self.get_node(parent_id) else {
2554 return (Some(parent_id), None);
2555 };
2556
2557 let layout_children = parent.layout_children.borrow();
2558 let Some(children) = layout_children.as_ref() else {
2559 return (Some(parent_id), None);
2560 };
2561
2562 let mut anon_index = 0;
2564 for &child_id in children.iter() {
2565 if child_id == node_id {
2566 return (Some(parent_id), Some(anon_index));
2567 }
2568 if self.get_node(child_id).is_some_and(|n| n.is_anonymous()) {
2569 anon_index += 1;
2570 }
2571 }
2572
2573 (Some(parent_id), None)
2574 }
2575
2576 pub fn clear_text_selection(&mut self) {
2578 self.text_selection.clear();
2579 }
2580
2581 pub fn update_selection_focus(&mut self, focus_node: NodeId, focus_offset: usize) {
2583 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
2585 self.text_selection
2586 .focus
2587 .set_anonymous(parent, idx, focus_offset);
2588 } else {
2589 self.text_selection.set_focus(focus_node, focus_offset);
2590 }
2591 }
2592
2593 pub fn extend_text_selection_to_point(&mut self, x: f32, y: f32) -> bool {
2596 if !self.text_selection.anchor.is_some() {
2597 return false;
2598 }
2599
2600 if let Some((node, offset)) = self.find_text_position(x, y) {
2601 self.update_selection_focus(node, offset);
2602 self.shell_provider.request_redraw();
2603 true
2604 } else {
2605 false
2606 }
2607 }
2608
2609 fn find_anonymous_block_by_index(
2611 &self,
2612 parent_id: NodeId,
2613 target_index: usize,
2614 ) -> Option<NodeId> {
2615 let parent = self.get_node(parent_id)?;
2616 let layout_children = parent.layout_children.borrow();
2617 let children = layout_children.as_ref()?;
2618
2619 children
2620 .iter()
2621 .filter(|&&child_id| self.get_node(child_id).is_some_and(|n| n.is_anonymous()))
2622 .nth(target_index)
2623 .copied()
2624 }
2625
2626 pub fn has_text_selection(&self) -> bool {
2628 self.text_selection.is_active()
2629 }
2630
2631 pub fn get_selected_text(&self) -> Option<String> {
2633 let ranges = self.get_text_selection_ranges();
2634 if ranges.is_empty() {
2635 return None;
2636 }
2637
2638 let mut result = String::new();
2639 for (node_id, start, end) in &ranges {
2640 let node = self.get_node(*node_id)?;
2641 let element_data = node.element_data()?;
2642 let inline_layout = element_data.inline_layout_data.as_ref()?;
2643
2644 if *end > inline_layout.text.len() {
2645 continue;
2646 }
2647
2648 if !result.is_empty() {
2649 result.push(' ');
2650 }
2651 result.push_str(&inline_layout.text[*start..*end]);
2652 }
2653
2654 if result.is_empty() {
2655 None
2656 } else {
2657 Some(result)
2658 }
2659 }
2660
2661 pub fn get_text_selection_ranges(&self) -> Vec<(NodeId, usize, usize)> {
2664 let lookup = |parent_id, idx| self.find_anonymous_block_by_index(parent_id, idx);
2665
2666 let anchor_node = match self.text_selection.anchor.resolve_node_id(lookup) {
2667 Some(id) => id,
2668 None => return Vec::new(),
2669 };
2670 let focus_node = match self.text_selection.focus.resolve_node_id(lookup) {
2671 Some(id) => id,
2672 None => return Vec::new(),
2673 };
2674
2675 let node_is_in_doc = |node_id: NodeId| {
2678 self.nodes
2679 .get(node_id)
2680 .is_some_and(|node| node.flags.is_in_document())
2681 };
2682 if !node_is_in_doc(anchor_node) || !node_is_in_doc(focus_node) {
2683 return Vec::new();
2684 }
2685
2686 if anchor_node == focus_node {
2688 let start = self
2689 .text_selection
2690 .anchor
2691 .offset
2692 .min(self.text_selection.focus.offset);
2693 let end = self
2694 .text_selection
2695 .anchor
2696 .offset
2697 .max(self.text_selection.focus.offset);
2698
2699 if start == end {
2700 return Vec::new();
2701 }
2702 return vec![(anchor_node, start, end)];
2703 }
2704
2705 let inline_roots = self.collect_inline_roots_in_range(anchor_node, focus_node);
2707 if inline_roots.is_empty() {
2708 return Vec::new();
2709 }
2710
2711 let first_in_roots = inline_roots[0];
2714
2715 let (first_node, first_offset, last_node, last_offset) =
2716 if first_in_roots == anchor_node || (first_in_roots != focus_node) {
2717 (
2719 anchor_node,
2720 self.text_selection.anchor.offset,
2721 focus_node,
2722 self.text_selection.focus.offset,
2723 )
2724 } else {
2725 (
2727 focus_node,
2728 self.text_selection.focus.offset,
2729 anchor_node,
2730 self.text_selection.anchor.offset,
2731 )
2732 };
2733
2734 let mut ranges = Vec::with_capacity(inline_roots.len());
2735
2736 for &node_id in &inline_roots {
2737 let Some(node) = self.get_node(node_id) else {
2738 continue;
2739 };
2740 let Some(element_data) = node.element_data() else {
2741 continue;
2742 };
2743 let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
2744 continue;
2745 };
2746
2747 let text_len = inline_layout.text.len();
2748
2749 if node_id == first_node && node_id == last_node {
2750 let start = first_offset.min(last_offset);
2751 let end = first_offset.max(last_offset);
2752 if start < end && end <= text_len {
2753 ranges.push((node_id, start, end));
2754 }
2755 } else if node_id == first_node {
2756 if first_offset < text_len {
2757 ranges.push((node_id, first_offset, text_len));
2758 }
2759 } else if node_id == last_node {
2760 if last_offset > 0 && last_offset <= text_len {
2761 ranges.push((node_id, 0, last_offset));
2762 }
2763 } else if text_len > 0 {
2764 ranges.push((node_id, 0, text_len));
2765 }
2766 }
2767
2768 ranges
2769 }
2770}
2771
2772#[derive(Debug, Clone, Copy, PartialEq)]
2773pub struct BoundingRect {
2774 pub x: f64,
2775 pub y: f64,
2776 pub width: f64,
2777 pub height: f64,
2778}
2779
2780impl BoundingRect {
2781 pub fn union(self, other: BoundingRect) -> BoundingRect {
2783 let x = self.x.min(other.x);
2784 let y = self.y.min(other.y);
2785 BoundingRect {
2786 x,
2787 y,
2788 width: (self.x + self.width).max(other.x + other.width) - x,
2789 height: (self.y + self.height).max(other.y + other.height) - y,
2790 }
2791 }
2792}
2793
2794fn snap_to_layout_unit(value: f64) -> f64 {
2798 (value * 64.0).round() / 64.0
2799}
2800
2801impl AsRef<BaseDocument> for BaseDocument {
2802 fn as_ref(&self) -> &BaseDocument {
2803 self
2804 }
2805}
2806
2807impl AsMut<BaseDocument> for BaseDocument {
2808 fn as_mut(&mut self) -> &mut BaseDocument {
2809 self
2810 }
2811}
2812
2813#[cfg(test)]
2814mod zoom_tests {
2815 use super::*;
2816 use blitz_traits::shell::ColorScheme;
2817 use std::sync::atomic::{AtomicUsize, Ordering};
2818
2819 #[derive(Default)]
2820 struct CountingShellProvider {
2821 redraw_count: AtomicUsize,
2822 }
2823 impl ShellProvider for CountingShellProvider {
2824 fn request_redraw(&self) {
2825 self.redraw_count.fetch_add(1, Ordering::SeqCst);
2826 }
2827 }
2828
2829 #[test]
2830 fn zoom_requests_redraw() {
2831 let shell_provider = Arc::new(CountingShellProvider::default());
2832 let mut doc = BaseDocument::new(DocumentConfig {
2833 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
2834 shell_provider: Some(shell_provider.clone() as _),
2835 ..Default::default()
2836 });
2837 doc.resolve(0.0);
2838
2839 let base = shell_provider.redraw_count.load(Ordering::SeqCst);
2840 doc.zoom_by(0.5);
2841 assert!(shell_provider.redraw_count.load(Ordering::SeqCst) > base);
2842
2843 let base = shell_provider.redraw_count.load(Ordering::SeqCst);
2844 doc.zoom_to(1.0);
2845 assert!(shell_provider.redraw_count.load(Ordering::SeqCst) > base);
2846 }
2847}
2848
2849#[cfg(test)]
2850mod hover_state_tests {
2851 use super::*;
2852 use crate::{Attribute, qual_name};
2853 use blitz_traits::shell::ColorScheme;
2854
2855 fn make_doc() -> (BaseDocument, NodeId) {
2862 let mut doc = BaseDocument::new(DocumentConfig {
2863 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
2864 ..Default::default()
2865 });
2866 let root_id = doc.root_node().id;
2867 let style = |value: &str| Attribute {
2868 name: qual_name!("style"),
2869 value: value.to_string(),
2870 };
2871
2872 let mut mutator = doc.mutate();
2873 let html = mutator.create_element(qual_name!("html"), vec![]);
2874 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
2875 let container = mutator.create_element(qual_name!("div"), vec![style("width:300px")]);
2876 let text = mutator.create_text_node("some text");
2877 let block = mutator.create_element(qual_name!("div"), vec![style("height:50px")]);
2878 mutator.append_children(container, &[text, block]);
2879 mutator.append_children(body, &[container]);
2880 mutator.append_children(html, &[body]);
2881 mutator.append_children(root_id, &[html]);
2882 drop(mutator);
2883
2884 doc.resolve(0.0);
2885 (doc, container)
2886 }
2887
2888 fn text_has_size(doc: &BaseDocument, container: NodeId) -> bool {
2892 doc.nodes[container].final_layout().size.height > 50.0
2893 }
2894
2895 #[test]
2901 fn hovering_text_in_anonymous_block_reports_text_cursor() {
2902 let (mut doc, container) = make_doc();
2903 if !text_has_size(&doc, container) {
2904 eprintln!("skipping: no usable font (text measures 0x0)");
2905 return;
2906 }
2907
2908 doc.set_hover_to(5.0, 8.0);
2909 assert!(doc.hover_node_is_text, "expected a text hit");
2910 let hit_id = doc.hover_hit_node_id.expect("expected a hit node");
2911 assert!(
2912 doc.nodes[hit_id].is_anonymous(),
2913 "expected the hit node to be the anonymous inline root"
2914 );
2915 assert_eq!(
2916 doc.get_hover_node_id(),
2917 Some(container),
2918 "expected the stored hover target to be the containing element"
2919 );
2920 assert_eq!(doc.get_cursor(), Some(CursorIcon::Text));
2921 }
2922
2923 #[test]
2926 fn hovering_anonymous_block_whitespace_reports_default_cursor() {
2927 let (mut doc, container) = make_doc();
2928 if !text_has_size(&doc, container) {
2929 eprintln!("skipping: no usable font (text measures 0x0)");
2930 return;
2931 }
2932
2933 doc.set_hover_to(250.0, 8.0);
2934 assert!(!doc.hover_node_is_text);
2935 assert_eq!(doc.get_hover_node_id(), Some(container));
2936 assert_eq!(doc.get_cursor(), Some(CursorIcon::Default));
2937 }
2938}
2939
2940#[cfg(test)]
2941mod hover_invalidation_tests {
2942 use super::*;
2943 use crate::{Attribute, QualName, qual_name};
2944 use blitz_traits::shell::ColorScheme;
2945
2946 fn make_doc() -> (BaseDocument, NodeId, NodeId) {
2949 let mut doc = BaseDocument::new(DocumentConfig {
2950 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
2951 ..Default::default()
2952 });
2953 let sheet = UserAgentStylesheet::parse(
2954 "div:hover { background-color: rgb(255, 0, 0); } div:hover span { color: rgb(0, 255, 0); }",
2955 );
2956 doc.add_user_agent_stylesheet(sheet);
2957 let root_id = doc.root_node().id;
2958 let style = |value: &str| Attribute {
2959 name: qual_name!("style"),
2960 value: value.to_string(),
2961 };
2962
2963 let mut mutator = doc.mutate();
2964 let html = mutator.create_element(qual_name!("html"), vec![]);
2965 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
2966 let div =
2967 mutator.create_element(qual_name!("div"), vec![style("width:300px;height:100px")]);
2968 let span = mutator.create_element(qual_name!("span"), vec![]);
2969 let text = mutator.create_text_node("some text");
2970 mutator.append_children(span, &[text]);
2971 mutator.append_children(div, &[span]);
2972 mutator.append_children(body, &[div]);
2973 mutator.append_children(html, &[body]);
2974 mutator.append_children(root_id, &[html]);
2975 drop(mutator);
2976
2977 doc.resolve(0.0);
2978 (doc, div, span)
2979 }
2980
2981 fn bg_color(doc: &BaseDocument, id: NodeId) -> String {
2982 format!(
2983 "{:?}",
2984 doc.nodes[id]
2985 .primary_styles()
2986 .unwrap()
2987 .get_background()
2988 .background_color
2989 )
2990 }
2991
2992 fn text_color(doc: &BaseDocument, id: NodeId) -> String {
2993 format!(
2994 "{:?}",
2995 doc.nodes[id].primary_styles().unwrap().clone_color()
2996 )
2997 }
2998
2999 #[test]
3000 fn hover_styles_apply_and_clear() {
3001 let (mut doc, div, span) = make_doc();
3002 let initial_bg = bg_color(&doc, div);
3003 let initial_color = text_color(&doc, span);
3004
3005 doc.set_hover_to(10.0, 10.0);
3007 assert!(doc.nodes[div].is_hovered());
3008 doc.resolve(0.0);
3009 let hovered_bg = bg_color(&doc, div);
3010 let hovered_color = text_color(&doc, span);
3011 assert_ne!(initial_bg, hovered_bg, "hover should change div background");
3012 assert_ne!(
3013 initial_color, hovered_color,
3014 "hover should change span color"
3015 );
3016
3017 doc.set_hover_to(10.0, 200.0);
3019 assert!(!doc.nodes[div].is_hovered());
3020 doc.resolve(0.0);
3021 assert_eq!(
3022 bg_color(&doc, div),
3023 initial_bg,
3024 "unhover should restore div background"
3025 );
3026 assert_eq!(
3027 text_color(&doc, span),
3028 initial_color,
3029 "unhover should restore span color"
3030 );
3031 }
3032
3033 #[test]
3036 fn ancestor_hover_with_link_state_updates_descendant() {
3037 let mut doc = BaseDocument::new(DocumentConfig {
3038 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3039 ..Default::default()
3040 });
3041 let sheet = UserAgentStylesheet::parse(
3042 ".promo:link:hover .headline, .promo:visited:hover .headline { color: rgb(184, 0, 0); text-decoration-line: underline; }",
3043 );
3044 doc.add_user_agent_stylesheet(sheet);
3045 let root_id = doc.root_node().id;
3046 let attr = |name: QualName, value: &str| Attribute {
3047 name,
3048 value: value.to_string(),
3049 };
3050
3051 let mut mutator = doc.mutate();
3052 let html = mutator.create_element(qual_name!("html"), vec![]);
3053 let body = mutator.create_element(
3054 qual_name!("body"),
3055 vec![attr(qual_name!("style"), "margin:0")],
3056 );
3057 let a = mutator.create_element(
3058 qual_name!("a"),
3059 vec![
3060 attr(qual_name!("href"), "https://example.com"),
3061 attr(qual_name!("class"), "promo"),
3062 attr(
3063 qual_name!("style"),
3064 "display:block;width:300px;height:100px",
3065 ),
3066 ],
3067 );
3068 let p = mutator.create_element(qual_name!("p"), vec![]);
3069 let span = mutator.create_element(
3070 qual_name!("span"),
3071 vec![attr(qual_name!("class"), "headline")],
3072 );
3073 let text = mutator.create_text_node("Headline text");
3074 mutator.append_children(span, &[text]);
3075 mutator.append_children(p, &[span]);
3076 mutator.append_children(a, &[p]);
3077 mutator.append_children(body, &[a]);
3078 mutator.append_children(html, &[body]);
3079 mutator.append_children(root_id, &[html]);
3080 drop(mutator);
3081
3082 doc.resolve(0.0);
3083 let initial_color = text_color(&doc, span);
3084
3085 doc.set_hover_to(10.0, 10.0);
3086 assert!(doc.nodes[a].is_hovered());
3087 doc.resolve(0.0);
3088 let hovered_color = text_color(&doc, span);
3089 assert_ne!(
3090 initial_color, hovered_color,
3091 "hovering the anchor should change the headline color"
3092 );
3093
3094 doc.set_hover_to(10.0, 200.0);
3095 assert!(!doc.nodes[a].is_hovered());
3096 doc.resolve(0.0);
3097 assert_eq!(
3098 text_color(&doc, span),
3099 initial_color,
3100 "unhovering the anchor should restore the headline color"
3101 );
3102 }
3103
3104 #[test]
3106 fn checkbox_toggle_updates_checked_styles() {
3107 let mut doc = BaseDocument::new(DocumentConfig {
3108 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3109 ..Default::default()
3110 });
3111 let sheet = UserAgentStylesheet::parse(
3112 "input:checked { color: rgb(184, 0, 0); } input:checked + label { color: rgb(0, 184, 0); }",
3113 );
3114 doc.add_user_agent_stylesheet(sheet);
3115 let root_id = doc.root_node().id;
3116 let attr = |name: QualName, value: &str| Attribute {
3117 name,
3118 value: value.to_string(),
3119 };
3120
3121 let mut mutator = doc.mutate();
3122 let html = mutator.create_element(qual_name!("html"), vec![]);
3123 let body = mutator.create_element(
3124 qual_name!("body"),
3125 vec![attr(qual_name!("style"), "margin:0")],
3126 );
3127 let input = mutator.create_element(
3128 qual_name!("input"),
3129 vec![attr(qual_name!("type"), "checkbox")],
3130 );
3131 let label = mutator.create_element(qual_name!("label"), vec![]);
3132 let text = mutator.create_text_node("label text");
3133 mutator.append_children(label, &[text]);
3134 mutator.append_children(body, &[input, label]);
3135 mutator.append_children(html, &[body]);
3136 mutator.append_children(root_id, &[html]);
3137 drop(mutator);
3138
3139 doc.resolve(0.0);
3140 let initial_input_color = text_color(&doc, input);
3141 let initial_label_color = text_color(&doc, label);
3142
3143 doc.snapshot_node_and(input, ElementState::CHECKED, |node| {
3145 if let Some(el) = node.element_data_mut() {
3146 BaseDocument::toggle_checkbox(el);
3147 }
3148 node.mark_ancestors_dirty();
3149 });
3150 doc.resolve(0.0);
3151 assert_ne!(
3152 text_color(&doc, input),
3153 initial_input_color,
3154 "checking should change the input color"
3155 );
3156 assert_ne!(
3157 text_color(&doc, label),
3158 initial_label_color,
3159 "checking should change the sibling label color"
3160 );
3161
3162 doc.snapshot_node_and(input, ElementState::CHECKED, |node| {
3164 if let Some(el) = node.element_data_mut() {
3165 BaseDocument::toggle_checkbox(el);
3166 }
3167 node.mark_ancestors_dirty();
3168 });
3169 doc.resolve(0.0);
3170 assert_eq!(
3171 text_color(&doc, input),
3172 initial_input_color,
3173 "unchecking should restore the input color"
3174 );
3175 assert_eq!(
3176 text_color(&doc, label),
3177 initial_label_color,
3178 "unchecking should restore the sibling label color"
3179 );
3180 }
3181
3182 #[test]
3186 fn hover_updates_existing_pseudo_element_style() {
3187 let mut doc = BaseDocument::new(DocumentConfig {
3188 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3189 ..Default::default()
3190 });
3191 let sheet = UserAgentStylesheet::parse(
3192 "div::before { content: \"x\"; color: rgb(1, 2, 3); } \
3193 div:hover::before { color: rgb(0, 0, 255); }",
3194 );
3195 doc.add_user_agent_stylesheet(sheet);
3196 let root_id = doc.root_node().id;
3197 let style = |value: &str| Attribute {
3198 name: qual_name!("style"),
3199 value: value.to_string(),
3200 };
3201
3202 let mut mutator = doc.mutate();
3203 let html = mutator.create_element(qual_name!("html"), vec![]);
3204 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
3205 let div = mutator.create_element(
3206 qual_name!("div"),
3207 vec![style("display:block;width:300px;height:100px")],
3208 );
3209 let text = mutator.create_text_node("some text");
3210 mutator.append_children(div, &[text]);
3211 mutator.append_children(body, &[div]);
3212 mutator.append_children(html, &[body]);
3213 mutator.append_children(root_id, &[html]);
3214 drop(mutator);
3215
3216 doc.resolve(0.0);
3217 let before = doc.nodes[div].before().expect("::before node should exist");
3218 let initial_color = text_color(&doc, before);
3219
3220 doc.set_hover_to(10.0, 10.0);
3221 assert!(doc.nodes[div].is_hovered());
3222 doc.resolve(0.0);
3223 assert_ne!(
3224 text_color(&doc, before),
3225 initial_color,
3226 "hover should change the ::before color"
3227 );
3228
3229 doc.set_hover_to(10.0, 200.0);
3230 assert!(!doc.nodes[div].is_hovered());
3231 doc.resolve(0.0);
3232 assert_eq!(
3233 text_color(&doc, before),
3234 initial_color,
3235 "unhover should restore the ::before color"
3236 );
3237 }
3238
3239 #[test]
3243 fn hover_updates_background_image_layers() {
3244 let mut doc = BaseDocument::new(DocumentConfig {
3245 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3246 ..Default::default()
3247 });
3248 let sheet = UserAgentStylesheet::parse(
3249 "div { background-image: url(\"https://example.com/a.png\"); } \
3250 div:hover { background-image: url(\"https://example.com/b.png\"); }",
3251 );
3252 doc.add_user_agent_stylesheet(sheet);
3253 let root_id = doc.root_node().id;
3254 let style = |value: &str| Attribute {
3255 name: qual_name!("style"),
3256 value: value.to_string(),
3257 };
3258
3259 let mut mutator = doc.mutate();
3260 let html = mutator.create_element(qual_name!("html"), vec![]);
3261 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
3262 let div = mutator.create_element(
3263 qual_name!("div"),
3264 vec![style("display:block;width:300px;height:100px")],
3265 );
3266 mutator.append_children(body, &[div]);
3267 mutator.append_children(html, &[body]);
3268 mutator.append_children(root_id, &[html]);
3269 drop(mutator);
3270
3271 let background_image_url = |doc: &BaseDocument, id: NodeId| -> Option<String> {
3272 let elem = doc.nodes[id].data.downcast_element().unwrap();
3273 elem.background_images
3274 .first()
3275 .and_then(|img| img.as_ref())
3276 .map(|img| img.url.as_str().to_string())
3277 };
3278
3279 doc.resolve(0.0);
3280 assert_eq!(
3281 background_image_url(&doc, div).as_deref(),
3282 Some("https://example.com/a.png"),
3283 "initial resolve should flush the background image"
3284 );
3285
3286 doc.set_hover_to(10.0, 10.0);
3287 assert!(doc.nodes[div].is_hovered());
3288 doc.resolve(0.0);
3289 assert_eq!(
3290 background_image_url(&doc, div).as_deref(),
3291 Some("https://example.com/b.png"),
3292 "hover should flush the changed background image"
3293 );
3294
3295 doc.set_hover_to(10.0, 200.0);
3296 assert!(!doc.nodes[div].is_hovered());
3297 doc.resolve(0.0);
3298 assert_eq!(
3299 background_image_url(&doc, div).as_deref(),
3300 Some("https://example.com/a.png"),
3301 "unhover should flush the restored background image"
3302 );
3303 }
3304}
3305
3306#[cfg(test)]
3307mod font_face_override_tests {
3308 use super::*;
3309 use crate::net::{FontFaceOverrides, Resource, ResourceLoadResponse};
3310
3311 #[test]
3327 fn font_face_overrides_alias_family_name() {
3328 const ALIAS: &str = "AliasedFamily";
3329
3330 let mut document = BaseDocument::new(DocumentConfig::default());
3331
3332 {
3334 let mut ctx = document.font_ctx.lock().unwrap();
3335 assert!(
3336 ctx.collection.family_id(ALIAS).is_none(),
3337 "alias must not exist before registration",
3338 );
3339 }
3340
3341 let response = ResourceLoadResponse {
3346 request_id: 0,
3347 node_id: None,
3348 resolved_url: Some(String::from("test://aliased-family")),
3349 result: Ok(Resource::Font(
3350 blitz_traits::net::Bytes::from_static(crate::BULLET_FONT),
3351 FontFaceOverrides {
3352 family_name: Some(String::from(ALIAS)),
3353 weight: Some(800.0),
3354 style: Some(parley::fontique::FontStyle::Italic),
3355 },
3356 )),
3357 };
3358 document.load_resource(response);
3359
3360 let mut ctx = document.font_ctx.lock().unwrap();
3363 let family_id = ctx
3364 .collection
3365 .family_id(ALIAS)
3366 .expect("CSS-declared family name should be registered as a family alias");
3367 let resolved_name = ctx
3368 .collection
3369 .family_name(family_id)
3370 .expect("family id should resolve back to a name");
3371 assert_eq!(
3372 resolved_name, ALIAS,
3373 "registered family should report the CSS-declared name, \
3374 not the font file's internal `name` table entry",
3375 );
3376 }
3377}