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