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::resource_queue::DocumentSender;
11use crate::scrolling::ScrollAnimationState;
12use crate::selection::TextSelection;
13use crate::stylo_device::{DeviceChanges, make_device};
14use crate::stylo_to_cursor_icon::stylo_to_cursor_icon;
15use crate::traversal::TreeTraverser;
16use crate::url::DocumentUrl;
17use crate::util::ImageType;
18use crate::{
19 DEFAULT_CSS, DocumentConfig, DocumentMutator, DummyHtmlParserProvider, ElementData,
20 EventDriver, HtmlParserProvider, Node, NodeData, NoopEventHandler, StyleThreading,
21 TextNodeData,
22};
23use apace_traits::devtools::DevtoolSettings;
24use apace_traits::events::{DomEvent, HitResult, UiEvent};
25use apace_traits::navigation::{DummyNavigationProvider, NavigationProvider};
26use apace_traits::net::{AbortSignal, DummyNetProvider, NetProvider, Request};
27use apace_traits::node_id::NodeId;
28use apace_traits::shell::{DummyShellProvider, ShellProvider, Viewport};
29use cursor_icon::CursorIcon;
30use linebender_resource_handle::Blob;
31use markup5ever::{LocalName, local_name};
32use parley::{FontContext, PlainEditorDriver};
33use selectors::{Element, matching::QuirksMode};
34use smallvec::SmallVec;
35use std::any::Any;
36use std::cell::RefCell;
37use std::collections::{BTreeMap, Bound, HashMap, HashSet};
38use std::ops::{Deref, DerefMut};
39use std::rc::Rc;
40use std::str::FromStr;
41use std::sync::atomic::{AtomicUsize, Ordering};
42use std::sync::mpsc::{Receiver, channel};
43use std::sync::{Arc, LazyLock, Mutex, MutexGuard, OnceLock, RwLockReadGuard, RwLockWriteGuard};
44use std::task::{Context as TaskContext, Waker};
45use style::Atom;
46use style::animation::DocumentAnimationSet;
47use style::attr::{AttrIdentifier, AttrValue};
48use style::computed_value_flags::ComputedValueFlags;
49use style::data::{ElementData as StyloElementData, ElementStyles};
50use style::invalidation::element::restyle_hints::RestyleHint;
51use style::media_queries::MediaType;
52use style::properties::ComputedValues;
53use style::properties::style_structs::Font;
54use style::selector_parser::ServoElementSnapshot;
55use style::servo_arc::Arc as ServoArc;
56use style::values::GenericAtomIdent;
57use style::values::computed::UserSelect;
58use style::values::computed::ui::CursorKind;
59use style::{
60 device::Device,
61 dom::{TDocument, TNode},
62 media_queries::MediaList,
63 selector_parser::SnapshotMap,
64 shared_lock::{SharedRwLock, StylesheetGuards},
65 stylesheets::{AllowImportRules, DocumentStyleSheet, Origin, Stylesheet},
66 stylist::Stylist,
67};
68use style_dom::ElementState;
69use thin_vec::ThinVec;
70use url::Url;
71use web_time::Instant;
72
73#[cfg(feature = "parallel-construct")]
74use thread_local::ThreadLocal;
75
76#[derive(Clone, Debug, PartialEq)]
81pub struct UserAgentStylesheet(DocumentStyleSheet);
82
83impl UserAgentStylesheet {
84 pub fn parse(css: &str) -> Self {
85 let lock = SharedRwLock::read_only();
86 let data = Stylesheet::from_str(
87 css,
88 DocumentUrl::default().url_extra_data(),
89 Origin::UserAgent,
90 ServoArc::new(lock.wrap(MediaList::empty())),
91 lock,
92 None,
93 None,
94 QuirksMode::NoQuirks,
95 AllowImportRules::No,
96 );
97 Self(DocumentStyleSheet(ServoArc::new(data)))
98 }
99
100 pub fn default_sheet() -> Self {
102 static SHEET: LazyLock<UserAgentStylesheet> =
103 LazyLock::new(|| UserAgentStylesheet::parse(DEFAULT_CSS));
104 SHEET.clone()
105 }
106
107 pub fn sheet(&self) -> &DocumentStyleSheet {
108 &self.0
109 }
110}
111
112pub enum DocGuard<'a> {
113 Ref(&'a BaseDocument),
114 RefCell(std::cell::Ref<'a, BaseDocument>),
115 RwLock(RwLockReadGuard<'a, BaseDocument>),
116 Mutex(MutexGuard<'a, BaseDocument>),
117}
118
119impl Deref for DocGuard<'_> {
120 type Target = BaseDocument;
121 #[inline(always)]
122 fn deref(&self) -> &Self::Target {
123 match self {
124 Self::Ref(base_document) => base_document,
125 Self::RefCell(refcell_guard) => refcell_guard,
126 Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
127 Self::Mutex(mutex_guard) => mutex_guard,
128 }
129 }
130}
131
132pub enum DocGuardMut<'a> {
133 Ref(&'a mut BaseDocument),
134 RefCell(std::cell::RefMut<'a, BaseDocument>),
135 RwLock(RwLockWriteGuard<'a, BaseDocument>),
136 Mutex(MutexGuard<'a, BaseDocument>),
137}
138
139impl Deref for DocGuardMut<'_> {
140 type Target = BaseDocument;
141 #[inline(always)]
142 fn deref(&self) -> &Self::Target {
143 match self {
144 Self::Ref(base_document) => base_document,
145 Self::RefCell(refcell_guard) => refcell_guard,
146 Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
147 Self::Mutex(mutex_guard) => mutex_guard,
148 }
149 }
150}
151
152impl DerefMut for DocGuardMut<'_> {
153 #[inline(always)]
154 fn deref_mut(&mut self) -> &mut Self::Target {
155 match self {
156 Self::Ref(base_document) => base_document,
157 Self::RefCell(refcell_guard) => &mut *refcell_guard,
158 Self::RwLock(rw_lock_read_guard) => &mut *rw_lock_read_guard,
159 Self::Mutex(mutex_guard) => &mut *mutex_guard,
160 }
161 }
162}
163
164pub trait Document: Any + 'static {
167 fn inner(&self) -> DocGuard<'_>;
168 fn inner_mut(&mut self) -> DocGuardMut<'_>;
169
170 fn handle_ui_event(&mut self, event: UiEvent) {
172 let mut doc = self.inner_mut();
173 let mut driver = EventDriver::new(&mut *doc, NoopEventHandler);
174 driver.handle_ui_event(event);
175 }
176
177 fn poll(&mut self, task_context: Option<TaskContext>) -> bool {
179 let _ = task_context;
181 false
182 }
183
184 fn id(&self) -> usize {
186 self.inner().id
187 }
188}
189
190pub struct PlainDocument(pub BaseDocument);
191impl Document for PlainDocument {
192 fn inner(&self) -> DocGuard<'_> {
193 DocGuard::Ref(&self.0)
194 }
195 fn inner_mut(&mut self) -> DocGuardMut<'_> {
196 DocGuardMut::Ref(&mut self.0)
197 }
198}
199
200impl Document for BaseDocument {
201 fn inner(&self) -> DocGuard<'_> {
202 DocGuard::Ref(self)
203 }
204 fn inner_mut(&mut self) -> DocGuardMut<'_> {
205 DocGuardMut::Ref(self)
206 }
207}
208
209impl Document for Rc<RefCell<BaseDocument>> {
210 fn inner(&self) -> DocGuard<'_> {
211 DocGuard::RefCell(self.borrow())
212 }
213
214 fn inner_mut(&mut self) -> DocGuardMut<'_> {
215 DocGuardMut::RefCell(self.borrow_mut())
216 }
217}
218
219pub enum DocumentEvent {
220 ResourceLoad(ResourceLoadResponse),
221 NavigateIframe {
224 node_id: NodeId,
225 url: Url,
226 },
227}
228
229pub struct BaseDocument {
230 id: usize,
232 pub(crate) profile: Option<crate::profile::Profile>,
233
234 pub(crate) url: DocumentUrl,
237 pub(crate) devtool_settings: DevtoolSettings,
239 pub(crate) viewport: Viewport,
241 pub(crate) media_type: MediaType,
243 pub(crate) pending_device_changes: DeviceChanges,
247 pub(crate) style_threading: StyleThreading,
249 pub(crate) incremental_layout: bool,
253 pub(crate) subdocument_depth: usize,
256
257 pub(crate) tx: DocumentSender,
259 pub(crate) rx: Option<Receiver<DocumentEvent>>,
261
262 pub(crate) nodes: Box<NodeTree>,
267
268 pub(crate) root_node_id: NodeId,
270 pub(crate) top_layers: Vec<crate::TopLayer>,
271
272 pub(crate) stylist: Stylist,
275 pub(crate) animations: DocumentAnimationSet,
276 pub(crate) guard: SharedRwLock,
278 pub(crate) snapshots: SnapshotMap,
280
281 pub(crate) font_ctx: Arc<Mutex<parley::FontContext>>,
284 #[cfg(feature = "parallel-construct")]
285 pub(crate) thread_font_contexts: ThreadLocal<RefCell<Box<FontContext>>>,
287 pub(crate) layout_ctx: parley::LayoutContext<TextBrush>,
289
290 pub(crate) hover_node_id: Option<NodeId>,
294 pub(crate) hover_hit_node_id: Option<NodeId>,
298 pub(crate) hover_node_is_text: bool,
300 pub(crate) last_client_pointer_position: Option<takia::Point<f32>>,
302 pub(crate) focus_node_id: Option<NodeId>,
304 pub(crate) active_node_id: Option<NodeId>,
306 pub(crate) mousedown_node_id: Option<NodeId>,
308 pub(crate) last_mousedown_time: Option<Instant>,
310 pub(crate) mousedown_position: takia::Point<f32>,
312 pub(crate) click_count: u16,
314 pub(crate) drag_mode: DragMode,
316 pub(crate) hovered_scrollbar: Option<crate::node::ScrollbarRef>,
318 pub(crate) scrollbar_activity: HashMap<NodeId, Instant>,
321 pub(crate) scroll_animation: ScrollAnimationState,
323
324 pub(crate) text_selection: TextSelection,
326
327 pub(crate) has_active_animations: bool,
330 pub(crate) has_canvas: bool,
332 pub(crate) subdoc_is_animating: bool,
334
335 pub(crate) nodes_to_id: HashMap<String, SmallVec<[NodeId; 1]>>,
339 pub(crate) nodes_to_stylesheet: BTreeMap<NodeId, DocumentStyleSheet>,
341 pub(crate) stylesheet_generation: u64,
345 pub(crate) controls_to_form: HashMap<NodeId, NodeId>,
347 pub(crate) sub_document_nodes: HashSet<NodeId>,
349 pub(crate) iframe_loads: HashMap<NodeId, crate::iframe::IframeLoad>,
352 pub(crate) changed_nodes: HashSet<NodeId>,
354 pub(crate) deferred_construction_nodes: Vec<ConstructionTask>,
356
357 #[cfg(feature = "custom-widget")]
359 pub(crate) custom_widget_nodes: HashSet<NodeId>,
360 #[cfg(feature = "custom-widget")]
362 pub(crate) pending_resource_deallocations: Vec<anyrender::ResourceId>,
363
364 pub(crate) image_cache: crate::resources::ImageCache,
367 pub(crate) image_observations: crate::resources::ImageObservations,
368 pub(crate) resource_limits: crate::ResourceLimits,
369 pub(crate) resource_counters: crate::ResourceStats,
370
371 pub(crate) pending_images: HashMap<String, crate::resources::PendingImage>,
375
376 pub(crate) pending_style_image_nodes: Vec<NodeId>,
380
381 pub(crate) pending_critical_resources: HashSet<usize>,
384
385 pub net_provider: Arc<dyn NetProvider>,
388 pub navigation_provider: Arc<dyn NavigationProvider>,
391 pub shell_provider: Arc<dyn ShellProvider>,
393 pub html_parser_provider: Arc<dyn HtmlParserProvider>,
395 pub(crate) abort_signal: Option<AbortSignal>,
399}
400
401impl BaseDocument {
402 pub fn new(config: DocumentConfig) -> Self {
404 static ID_GENERATOR: AtomicUsize = AtomicUsize::new(1);
405
406 let id = ID_GENERATOR.fetch_add(1, Ordering::SeqCst);
407
408 let font_ctx = config
409 .font_ctx
410 .map(|mut font_ctx| {
411 font_ctx.source_cache.make_shared();
412 font_ctx
414 })
415 .unwrap_or_else(|| {
416 use parley::fontique::{Collection, CollectionOptions, SourceCache};
417 let mut font_ctx = FontContext {
418 source_cache: SourceCache::new_shared(),
419 collection: Collection::new(CollectionOptions {
420 shared: false,
421 system_fonts: cfg!(all(
422 feature = "system-fonts",
423 not(target_arch = "wasm32")
424 )),
425 }),
426 };
427 font_ctx
428 .collection
429 .register_fonts(Blob::new(Arc::new(crate::BULLET_FONT) as _), None);
430 font_ctx
431 });
432 let font_ctx = Arc::new(Mutex::new(font_ctx));
433
434 style_config::set_pref!("layout.grid.enabled", true);
436 style_config::set_pref!("layout.flexbox.balance", true);
437 style_config::set_pref!("layout.unimplemented", true);
438 style_config::set_pref!("layout.columns.enabled", true);
439 style_config::set_pref!("layout.css.basic-shape-shape.enabled", true);
440 style_config::set_pref!("layout.css.tree-counting-functions.enabled", true);
441 style_config::set_pref!("layout.css.progress-function.enabled", true);
442 style_config::set_pref!("layout.variable_fonts.enabled", true);
443 #[cfg(feature = "writing-mode")]
444 style_config::set_pref!("layout.writing-mode.enabled", true);
445 style_config::set_pref!("layout.threads", -1);
446
447 let viewport = config.viewport.unwrap_or_default();
448 let media_type = config.media_type.unwrap_or_else(MediaType::screen);
449 let device = make_device(&viewport, media_type.clone(), font_ctx.clone());
450 let stylist = Stylist::new(device, QuirksMode::NoQuirks);
451 let snapshots = SnapshotMap::new();
452 let nodes = Box::new(NodeTree::new());
453 let guard = SharedRwLock::new();
454 let nodes_to_id = HashMap::new();
455
456 let base_url = config
457 .base_url
458 .and_then(|url| DocumentUrl::from_str(&url).ok())
459 .unwrap_or_default();
460
461 let net_provider = config
462 .net_provider
463 .unwrap_or_else(|| Arc::new(DummyNetProvider));
464 let navigation_provider = config
465 .navigation_provider
466 .unwrap_or_else(|| Arc::new(DummyNavigationProvider));
467 let shell_provider = config
468 .shell_provider
469 .unwrap_or_else(|| Arc::new(DummyShellProvider));
470 let html_parser_provider = config
471 .html_parser_provider
472 .unwrap_or_else(|| Arc::new(DummyHtmlParserProvider));
473
474 let (tx, rx) = channel();
475 let tx = DocumentSender::new(tx);
476
477 let mut doc = Self {
478 id,
479 profile: None,
480 tx,
481 rx: Some(rx),
482
483 guard,
484 nodes,
485 root_node_id: NodeId::default(),
486 top_layers: Vec::new(),
487 stylist,
488 animations: DocumentAnimationSet::default(),
489 snapshots,
490 nodes_to_id,
491 viewport,
492 media_type,
493 pending_device_changes: DeviceChanges::empty(),
494 style_threading: config.style_threading,
495 incremental_layout: config.incremental.unwrap_or(true),
496 subdocument_depth: config.subdocument_depth,
497 devtool_settings: DevtoolSettings::default(),
498 url: base_url,
499 nodes_to_stylesheet: BTreeMap::new(),
500 stylesheet_generation: 0,
501 font_ctx,
502 #[cfg(feature = "parallel-construct")]
503 thread_font_contexts: ThreadLocal::new(),
504 layout_ctx: parley::LayoutContext::new(),
505
506 hover_node_id: None,
507 hover_hit_node_id: None,
508 hover_node_is_text: false,
509 last_client_pointer_position: None,
510 focus_node_id: None,
511 active_node_id: None,
512 mousedown_node_id: None,
513 has_active_animations: false,
514 subdoc_is_animating: false,
515 has_canvas: false,
516 sub_document_nodes: HashSet::new(),
517 iframe_loads: HashMap::new(),
518
519 #[cfg(feature = "custom-widget")]
520 custom_widget_nodes: HashSet::new(),
521 #[cfg(feature = "custom-widget")]
522 pending_resource_deallocations: Vec::new(),
523
524 changed_nodes: HashSet::new(),
525 deferred_construction_nodes: Vec::new(),
526 image_cache: crate::resources::ImageCache::default(),
527 resource_limits: crate::ResourceLimits::default(),
528 resource_counters: crate::ResourceStats::default(),
529 pending_images: HashMap::new(),
530 image_observations: Default::default(),
531 pending_style_image_nodes: Vec::new(),
532 pending_critical_resources: HashSet::new(),
533 controls_to_form: HashMap::new(),
534 net_provider,
535 navigation_provider,
536 shell_provider,
537 html_parser_provider,
538 abort_signal: config.abort_signal,
539 last_mousedown_time: None,
540 mousedown_position: takia::Point::ZERO,
541 click_count: 0,
542 drag_mode: DragMode::None,
543 hovered_scrollbar: None,
544 scrollbar_activity: HashMap::new(),
545 scroll_animation: ScrollAnimationState::None,
546 text_selection: TextSelection::default(),
547 };
548
549 doc.root_node_id = doc.create_node(NodeData::Document(Box::default()));
551 doc.root_node_mut().flags.insert(NodeFlags::IS_IN_DOCUMENT);
552
553 for sheet in config.ua_stylesheets {
554 doc.add_user_agent_stylesheet(sheet);
555 }
556
557 let stylo_element_data = StyloElementData {
559 styles: ElementStyles {
560 primary: Some(
561 ComputedValues::initial_values_with_font_override(Font::initial_values())
562 .to_arc(),
563 ),
564 ..Default::default()
565 },
566 ..Default::default()
567 };
568 let stylo_data = doc.root_node_mut().stylo_element_data_mut();
569 *stylo_data.ensure_init_mut() = stylo_element_data;
570
571 doc
572 }
573
574 pub fn set_net_provider(&mut self, net_provider: Arc<dyn NetProvider>) {
576 self.net_provider = net_provider;
577 }
578
579 pub fn set_navigation_provider(&mut self, navigation_provider: Arc<dyn NavigationProvider>) {
581 self.navigation_provider = navigation_provider;
582 }
583
584 pub fn set_shell_provider(&mut self, shell_provider: Arc<dyn ShellProvider>) {
586 self.shell_provider = shell_provider;
587 if let Some(node) = self.focus_node_id.and_then(|id| self.nodes.get(id)) {
588 if let Some(input) = node
589 .element_data()
590 .and_then(|element| element.text_input_data())
591 {
592 self.shell_provider.set_ime_purpose(if input.is_password() {
593 apace_traits::shell::ImePurpose::Password
594 } else {
595 apace_traits::shell::ImePurpose::Normal
596 });
597 self.shell_provider.set_ime_enabled(true);
598 input.sync_ime_context(self.shell_provider.as_ref());
599 if let Some((x, y, width, height)) = node.text_input_ime_cursor_area() {
600 self.shell_provider.set_ime_cursor_area(x, y, width, height);
601 }
602 }
603 }
604 }
605
606 pub fn set_html_parser_provider(&mut self, html_parser_provider: Arc<dyn HtmlParserProvider>) {
608 self.html_parser_provider = html_parser_provider;
609 }
610
611 pub fn set_base_url(&mut self, url: &str) {
613 self.url = DocumentUrl::from(Url::parse(url).unwrap());
614 }
615
616 pub fn base_url(&self) -> &Url {
618 &self.url
619 }
620
621 pub fn guard(&self) -> &SharedRwLock {
622 &self.guard
623 }
624
625 pub fn tree(&self) -> &NodeTree {
626 &self.nodes
627 }
628
629 pub fn id(&self) -> usize {
630 self.id
631 }
632
633 pub(crate) fn build_request(&self, url: url::Url) -> Request {
636 crate::net::stamped_request(url, self.abort_signal.as_ref())
637 }
638
639 pub fn favicon_url(&self) -> Option<String> {
640 self.tree().iter().find_map(|(_, node)| {
641 let data = &node.data;
642 if !data.is_element_with_tag_name(&local_name!("link")) {
643 return None;
644 }
645 let rel = data.attr(local_name!("rel"))?;
646 if !rel
647 .split_ascii_whitespace()
648 .any(|v| v.eq_ignore_ascii_case("icon"))
649 {
650 return None;
651 }
652 data.attr(local_name!("href")).map(|s| s.to_string())
653 })
654 }
655
656 pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
657 self.nodes.get(node_id)
658 }
659
660 pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
661 self.nodes.get_mut(node_id)
662 }
663
664 pub fn get_focussed_node_id(&self) -> Option<NodeId> {
665 self.focus_node_id
666 .or(self.try_root_element().map(|el| el.id))
667 }
668
669 pub fn mutate<'doc>(&'doc mut self) -> DocumentMutator<'doc> {
670 DocumentMutator::new(self)
671 }
672
673 pub fn handle_dom_event<F: FnMut(DomEvent)>(
674 &mut self,
675 event: &mut DomEvent,
676 dispatch_event: F,
677 ) {
678 handle_dom_event(self, event, dispatch_event)
679 }
680
681 pub fn as_any_mut(&mut self) -> &mut dyn Any {
682 self
683 }
684
685 pub fn label_bound_input_element(&self, label_node_id: NodeId) -> Option<&Node> {
692 let label_element = self.nodes[label_node_id].element_data()?;
693 if let Some(target_element_dom_id) = label_element.attr(local_name!("for")) {
694 TreeTraverser::new(self)
695 .filter_map(|id| {
696 let node = self.get_node(id)?;
697 let element_data = node.element_data()?;
698 if element_data.name.local != local_name!("input") {
699 return None;
700 }
701 let id = element_data.id.as_ref()?;
702 if *id == *target_element_dom_id {
703 Some(node)
704 } else {
705 None
706 }
707 })
708 .next()
709 } else {
710 TreeTraverser::new_with_root(self, label_node_id)
711 .filter_map(|child_id| {
712 let node = self.get_node(child_id)?;
713 let element_data = node.element_data()?;
714 if element_data.name.local == local_name!("input") {
715 Some(node)
716 } else {
717 None
718 }
719 })
720 .next()
721 }
722 }
723
724 pub fn toggle_checkbox(el: &mut ElementData) -> bool {
725 let Some(is_checked) = el.checkbox_input_checked() else {
726 return false;
727 };
728 let checked = !is_checked;
729 el.set_checkbox_input_indeterminate(false);
730 el.set_checkbox_input_checked(checked);
731
732 checked
733 }
734
735 pub fn toggle_details_open(&mut self, details_id: NodeId) {
739 use crate::qual_name;
740
741 let node = &self.nodes[details_id];
742 if !node.data.is_element_with_tag_name(&local_name!("details")) {
743 return;
744 }
745 let is_open = node.data.has_attr(local_name!("open"));
746
747 let mut mutator = self.mutate();
751 if is_open {
752 mutator.clear_attribute(details_id, qual_name!("open"));
753 } else {
754 mutator.set_attribute(details_id, qual_name!("open"), "");
755 }
756 drop(mutator);
757
758 self.shell_provider.request_redraw();
759 }
760
761 pub fn set_style_property(&mut self, node_id: NodeId, name: &str, value: &str) {
762 let node = &mut self.nodes[node_id];
763 let did_change = node.element_data_mut().unwrap().set_style_property(
764 name,
765 value,
766 &self.guard,
767 self.url.url_extra_data(),
768 );
769 if did_change {
770 node.set_restyle_hint(RestyleHint::RESTYLE_STYLE_ATTRIBUTE);
771 }
772 }
773
774 pub fn remove_style_property(&mut self, node_id: NodeId, name: &str) {
775 let node = &mut self.nodes[node_id];
776 let did_change = node.element_data_mut().unwrap().remove_style_property(
777 name,
778 &self.guard,
779 self.url.url_extra_data(),
780 );
781 if did_change {
782 node.set_restyle_hint(RestyleHint::RESTYLE_STYLE_ATTRIBUTE);
783 }
784 }
785
786 pub fn sub_document_node_ids(&self) -> Vec<NodeId> {
787 self.sub_document_nodes.iter().copied().collect()
788 }
789
790 pub fn set_sub_document(&mut self, node_id: NodeId, sub_document: Box<dyn Document>) {
791 self.nodes[node_id]
792 .element_data_mut()
793 .unwrap()
794 .set_sub_document(sub_document);
795 self.sub_document_nodes.insert(node_id);
796 }
797
798 pub fn remove_sub_document(&mut self, node_id: NodeId) {
799 self.nodes[node_id]
800 .element_data_mut()
801 .unwrap()
802 .remove_sub_document();
803 self.sub_document_nodes.remove(&node_id);
804 if let Some(load) = self.iframe_loads.remove(&node_id) {
805 load.abort_controller.abort();
806 }
807 }
808
809 pub fn poll_subdocuments(&mut self, waker: Option<&Waker>) -> bool {
815 let mut has_changes = false;
816 for node_id in self.sub_document_nodes.iter().copied() {
817 let Some(sub_doc) = self
818 .nodes
819 .get_mut(node_id)
820 .and_then(|node| node.subdoc_mut())
821 else {
822 continue;
823 };
824 let task_context = waker.map(TaskContext::from_waker);
825 has_changes |= sub_doc.poll(task_context);
826 }
827 has_changes
828 }
829
830 #[cfg(feature = "custom-widget")]
831 pub fn custom_widget_node_ids(&self) -> Vec<NodeId> {
832 self.custom_widget_nodes.iter().copied().collect()
833 }
834
835 #[cfg(feature = "custom-widget")]
836 pub fn take_pending_resource_deallocations(&mut self) -> Vec<anyrender::ResourceId> {
837 std::mem::take(&mut self.pending_resource_deallocations)
838 }
839
840 #[cfg(feature = "custom-widget")]
841 pub fn set_custom_widget(&mut self, node_id: NodeId, widget: Box<dyn crate::Widget>) {
842 self.nodes[node_id]
843 .element_data_mut()
844 .unwrap()
845 .set_custom_widget(widget);
846 self.custom_widget_nodes.insert(node_id);
847 }
848
849 #[cfg(feature = "custom-widget")]
850 pub fn remove_custom_widget(&mut self, node_id: NodeId) {
851 let resources_to_deallocate = self.nodes[node_id]
852 .element_data_mut()
853 .unwrap()
854 .remove_custom_widget();
855 self.pending_resource_deallocations
856 .extend_from_slice(&resources_to_deallocate);
857 self.custom_widget_nodes.remove(&node_id);
858 }
859
860 pub fn root_node(&self) -> &Node {
861 &self.nodes[self.root_node_id]
862 }
863
864 pub fn root_node_mut(&mut self) -> &mut Node {
865 &mut self.nodes[self.root_node_id]
866 }
867
868 pub fn try_root_element(&self) -> Option<&Node> {
869 TDocument::as_node(&self.root_node()).first_element_child()
870 }
871
872 pub fn root_element(&self) -> &Node {
873 TDocument::as_node(&self.root_node())
874 .first_element_child()
875 .unwrap()
876 .as_element()
877 .unwrap()
878 }
879
880 pub fn create_node(&mut self, node_data: NodeData) -> NodeId {
881 let tree_ptr = self.nodes.as_mut() as *mut NodeTree;
882 let guard = self.guard.clone();
883
884 let id = self
885 .nodes
886 .insert_with_key(|id| Node::new(tree_ptr, id, guard, node_data));
887
888 self.changed_nodes.insert(id);
890 id
891 }
892
893 pub(crate) fn remove_node_from_tree(&mut self, node_id: NodeId) -> Option<Node> {
897 self.retire_top_layer(node_id);
898 self.image_observations.retire(node_id);
899 self.clear_interaction_state_for_removed_node(node_id);
900 let node = self.nodes.remove(node_id)?;
901 for container in [node.layout_parent.get(), node.oof_containing_block.get()]
905 .into_iter()
906 .flatten()
907 {
908 self.retire_layout_reference(container, node_id);
909 }
910 Some(node)
911 }
912
913 fn retire_layout_reference(&mut self, container: NodeId, node_id: NodeId) {
917 if let Some(parent) = self.nodes.get_mut(container) {
918 if let Some(children) = parent.layout_children.get_mut() {
919 children.retain(|child| *child != node_id);
920 }
921 if parent.is_anonymous() {
925 parent.children.retain(|child| *child != node_id);
926 }
927 parent
928 .hoisted_children
929 .get_mut()
930 .retain(|child| *child != node_id);
931 parent.paint_children.get_mut().take();
932 parent.sc_contribution_cache.get_mut().clear();
933 parent.stacking_context = None;
934 parent.invalidate_layout_cache();
935 }
936 }
937
938 pub(crate) fn retire_moved_layout_references(&mut self, root: NodeId) {
943 fn visit(doc: &mut BaseDocument, root: NodeId, id: NodeId) {
944 let layout_parent = doc.nodes[id].layout_parent.get();
945 let oof_parent = doc.nodes[id].oof_containing_block.get();
946 for (owner, out_of_flow) in [(layout_parent, false), (oof_parent, true)] {
947 let Some(owner) = owner else { continue };
948 let mut ancestor = Some(owner);
949 let mut internal = false;
950 while let Some(current) = ancestor {
951 if current == root {
952 internal = true;
953 break;
954 }
955 ancestor = doc.nodes.get(current).and_then(|node| node.parent);
956 }
957 if !internal {
958 doc.retire_layout_reference(owner, id);
959 if out_of_flow {
960 doc.nodes[id].oof_containing_block.set(None);
961 } else {
962 doc.nodes[id].layout_parent.set(None);
963 }
964 }
965 }
966 doc.iter_children_and_pseudos_mut(id, |child, doc| visit(doc, root, child));
967 }
968 visit(self, root, root);
969 }
970
971 fn nearest_surviving_element_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
976 let mut current = self.get_node(node_id)?.parent;
977 while let Some(id) = current {
978 let node = self.get_node(id)?;
979 if node.is_element() && node.flags.is_in_document() {
980 return Some(id);
981 }
982 current = node.parent;
983 }
984 None
985 }
986
987 pub(crate) fn clear_interaction_state_for_removed_node(&mut self, node_id: NodeId) {
1008 if !self.nodes.contains_key(node_id) {
1009 return;
1010 }
1011
1012 if self.hover_node_id == Some(node_id) {
1013 self.hover_node_id = self.nearest_surviving_element_ancestor(node_id);
1014 self.hover_node_is_text = false;
1015 }
1016 if self.hover_hit_node_id == Some(node_id) {
1017 self.hover_hit_node_id = None;
1018 }
1019 if self.active_node_id == Some(node_id) {
1020 self.active_node_id = self.nearest_surviving_element_ancestor(node_id);
1021 }
1022 if self.focus_node_id == Some(node_id)
1023 || self.nodes[node_id]
1024 .element_data()
1025 .is_some_and(|el| el.element_state.contains(ElementState::FOCUS_WITHIN))
1026 {
1027 self.clear_focus();
1028 }
1029 if self.mousedown_node_id == Some(node_id) {
1030 self.mousedown_node_id = None;
1031 }
1032 if self.text_selection.anchor.node_or_parent == Some(node_id)
1033 || self.text_selection.focus.node_or_parent == Some(node_id)
1034 {
1035 self.text_selection.clear();
1036 }
1037 if self
1038 .hovered_scrollbar
1039 .is_some_and(|scrollbar| scrollbar.node_id == node_id)
1040 {
1041 self.hovered_scrollbar = None;
1042 }
1043 let drag_references_node = match &self.drag_mode {
1044 DragMode::Panning(state) => state.target == node_id,
1045 DragMode::ScrollbarDrag(state) => state.scrollbar.node_id == node_id,
1046 DragMode::Selecting | DragMode::None => false,
1047 };
1048 if drag_references_node {
1049 self.drag_mode = DragMode::None;
1050 }
1051 self.scrollbar_activity.remove(&node_id);
1052 }
1053
1054 pub(crate) fn drop_node_ignoring_parent(&mut self, node_id: NodeId) -> Option<Node> {
1055 self.drop_node_ignoring_parent_with(node_id, &mut |_| {})
1056 }
1057
1058 pub(crate) fn drop_node_ignoring_parent_with(
1061 &mut self,
1062 node_id: NodeId,
1063 on_drop: &mut dyn FnMut(NodeId),
1064 ) -> Option<Node> {
1065 let mut node = self.remove_node_from_tree(node_id);
1066 if let Some(node) = &mut node {
1067 on_drop(node_id);
1068 if let Some(before) = node.before() {
1069 self.drop_node_ignoring_parent_with(before, on_drop);
1070 }
1071 if let Some(after) = node.after() {
1072 self.drop_node_ignoring_parent_with(after, on_drop);
1073 }
1074
1075 for &child in &node.children {
1076 self.drop_node_ignoring_parent_with(child, on_drop);
1077 }
1078
1079 for &anon_id in &node.anonymous_blocks {
1082 self.deallocate_anonymous_block(anon_id);
1083 }
1084 }
1085 node
1086 }
1087
1088 pub(crate) fn deallocate_anonymous_block(&mut self, anon_id: NodeId) {
1091 if !self.nodes.contains_key(anon_id) {
1094 return;
1095 }
1096
1097 let nested = std::mem::take(&mut self.nodes[anon_id].anonymous_blocks);
1099 for nested_id in nested {
1100 self.deallocate_anonymous_block(nested_id);
1101 }
1102
1103 self.remove_node_from_tree(anon_id);
1104 }
1105
1106 pub fn has_changes(&self) -> bool {
1108 self.changed_nodes.is_empty()
1109 }
1110
1111 pub fn create_text_node(&mut self, text: &str) -> NodeId {
1112 let content = text.to_string();
1113 let data = NodeData::Text(TextNodeData::new(content));
1114 self.create_node(data)
1115 }
1116
1117 pub fn deep_clone_node(&mut self, node_id: NodeId) -> NodeId {
1118 let node = &self.nodes[node_id];
1120 let mut data = node.data.clone();
1121
1122 match &mut data {
1123 NodeData::Element(elem) | NodeData::AnonymousBlock(elem) => {
1124 if let Some(arc) = elem.style_attribute.as_mut() {
1125 let read_guard = self.guard().read();
1126 let block = arc.read_with(&read_guard);
1127 *arc = ServoArc::new(self.guard().wrap(block.clone()));
1128 }
1129 }
1130 _ => {}
1131 }
1132
1133 let children = node.children.clone();
1134
1135 let new_node_id = self.create_node(data);
1137
1138 let new_children: ThinVec<NodeId> = children
1140 .into_iter()
1141 .map(|child_id| self.deep_clone_node(child_id))
1142 .collect();
1143 for &child_id in &new_children {
1144 self.nodes[child_id].parent = Some(new_node_id);
1145 }
1146 self.nodes[new_node_id].children = new_children;
1147
1148 new_node_id
1149 }
1150
1151 pub(crate) fn remove_and_drop_pe(&mut self, node_id: NodeId) -> Option<Node> {
1152 fn remove_pe_ignoring_parent(doc: &mut BaseDocument, node_id: NodeId) -> Option<Node> {
1153 let mut node = doc.remove_node_from_tree(node_id);
1154 if let Some(node) = &mut node {
1155 for &child in &node.children {
1156 remove_pe_ignoring_parent(doc, child);
1157 }
1158 for &anon_id in &node.anonymous_blocks {
1159 doc.deallocate_anonymous_block(anon_id);
1160 }
1161 }
1162 node
1163 }
1164
1165 let node = remove_pe_ignoring_parent(self, node_id);
1166
1167 if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
1169 let parent = &mut self.nodes[parent_id];
1170 parent.children.retain(|id| *id != node_id);
1171 }
1172
1173 node
1174 }
1175
1176 pub(crate) fn resolve_url(&self, raw: &str) -> url::Url {
1177 self.url.resolve_relative(raw).unwrap_or_else(|| {
1178 panic!(
1179 "to be able to resolve {raw} with the base_url: {:?}",
1180 *self.url
1181 )
1182 })
1183 }
1184
1185 pub fn print_tree(&self) {
1186 crate::util::walk_tree(0, self.root_node());
1187 }
1188
1189 pub fn print_subtree(&self, node_id: NodeId) {
1190 crate::util::walk_tree(0, &self.nodes[node_id]);
1191 }
1192
1193 pub fn reload_resource_by_href(&mut self, href_to_reload: &str) {
1194 for &node_id in self.nodes_to_stylesheet.keys() {
1195 let node = &self.nodes[node_id];
1196 let Some(element) = node.element_data() else {
1197 continue;
1198 };
1199
1200 if element.name.local == local_name!("link") {
1201 if let Some(href) = element.attr(local_name!("href")) {
1202 if href == href_to_reload {
1204 let resolved_href = self.resolve_url(href);
1205 let Some(handler) = ResourceHandler::boxed(
1206 self.tx.clone(),
1207 self.id,
1208 Some(node_id),
1209 self.shell_provider.clone(),
1210 StylesheetHandler {
1211 guard: self.guard.clone(),
1212 net_provider: self.net_provider.clone(),
1213 abort_signal: self.abort_signal.clone(),
1214 },
1215 ) else {
1216 continue;
1217 };
1218 self.net_provider.fetch(
1219 self.id(),
1220 self.build_request(resolved_href.clone()),
1221 handler,
1222 );
1223 }
1224 }
1225 }
1226 }
1227 }
1228
1229 pub fn process_style_element(&mut self, target_id: NodeId) {
1230 let css = self.nodes[target_id].text_content();
1231 let css = html_escape::decode_html_entities(&css);
1232 let sheet = self.make_stylesheet(&css, Origin::Author);
1233 self.add_stylesheet_for_node(sheet, target_id);
1234 }
1235
1236 pub fn remove_user_agent_stylesheet(&mut self, sheet: &UserAgentStylesheet) {
1237 if self.useragent_stylesheets().any(|s| *s == sheet.0) {
1238 self.stylist
1239 .remove_stylesheet(sheet.0.clone(), &self.guard.read());
1240 }
1241 }
1242
1243 pub fn url(&self) -> &url::Url {
1245 &self.url
1246 }
1247
1248 pub fn author_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
1251 self.nodes_to_stylesheet.values()
1252 }
1253
1254 pub fn useragent_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
1256 (0..self.stylist.sheet_count(Origin::UserAgent))
1257 .filter_map(|i| self.stylist.sheet_at(Origin::UserAgent, i))
1258 }
1259
1260 pub fn add_user_agent_stylesheet(&mut self, sheet: UserAgentStylesheet) {
1262 if self.useragent_stylesheets().any(|s| *s == sheet.0) {
1263 return;
1264 }
1265 self.stylist.append_stylesheet(sheet.0, &self.guard.read());
1266 }
1267
1268 pub fn make_stylesheet(&self, css: impl AsRef<str>, origin: Origin) -> DocumentStyleSheet {
1269 let data = Stylesheet::from_str(
1270 css.as_ref(),
1271 self.url.url_extra_data(),
1272 origin,
1273 ServoArc::new(self.guard.wrap(MediaList::empty())),
1274 self.guard.clone(),
1275 Some(&StylesheetLoader {
1276 tx: self.tx.clone(),
1277 doc_id: self.id,
1278 net_provider: self.net_provider.clone(),
1279 shell_provider: self.shell_provider.clone(),
1280 abort_signal: self.abort_signal.clone(),
1281 import_depth: 0,
1282 }),
1283 None,
1284 QuirksMode::NoQuirks,
1285 AllowImportRules::Yes,
1286 );
1287
1288 DocumentStyleSheet(ServoArc::new(data))
1289 }
1290
1291 pub fn upsert_stylesheet_for_node(&mut self, node_id: NodeId) {
1292 let raw_styles = self.nodes[node_id].text_content();
1293 let sheet = self.make_stylesheet(raw_styles, Origin::Author);
1294 self.add_stylesheet_for_node(sheet, node_id);
1295 }
1296
1297 pub fn add_stylesheet_for_node(&mut self, stylesheet: DocumentStyleSheet, node_id: NodeId) {
1298 let old = self.nodes_to_stylesheet.insert(node_id, stylesheet.clone());
1299 self.stylesheet_generation += 1;
1300
1301 if let Some(old) = old {
1302 self.stylist.remove_stylesheet(old, &self.guard.read())
1303 }
1304
1305 crate::net::fetch_font_face(
1307 self.tx.clone(),
1308 self.id,
1309 Some(node_id),
1310 &stylesheet.0,
1311 &self.net_provider,
1312 &self.shell_provider,
1313 &self.guard.read(),
1314 self.abort_signal.as_ref(),
1315 );
1316
1317 let element = &mut self.nodes[node_id].element_data_mut().unwrap();
1319 element.special_data = SpecialElementData::Stylesheet(stylesheet.clone());
1320
1321 let insertion_point = self
1323 .nodes_to_stylesheet
1324 .range((Bound::Excluded(node_id), Bound::Unbounded))
1325 .next()
1326 .map(|(_, sheet)| sheet);
1327
1328 if let Some(insertion_point) = insertion_point {
1329 self.stylist.insert_stylesheet_before(
1330 stylesheet,
1331 insertion_point.clone(),
1332 &self.guard.read(),
1333 )
1334 } else {
1335 self.stylist
1336 .append_stylesheet(stylesheet, &self.guard.read())
1337 }
1338 }
1339
1340 pub fn handle_messages(&mut self) -> bool {
1341 self.tx.pool.observe();
1342 self.prune_resource_interests();
1343 let rx = self.rx.take().unwrap();
1346
1347 let mut processed = 0;
1348 while processed < 64
1349 && let Ok(msg) = rx.try_recv()
1350 {
1351 self.handle_message(msg);
1352 processed += 1;
1353 }
1354
1355 self.rx = Some(rx);
1357 if processed == 64 {
1358 self.shell_provider.request_redraw();
1359 }
1360 processed > 0
1361 }
1362
1363 pub fn handle_message(&mut self, msg: DocumentEvent) {
1364 match msg {
1365 DocumentEvent::ResourceLoad(resource) => self.load_resource(resource),
1366 DocumentEvent::NavigateIframe { node_id, url } => self.navigate_iframe(node_id, url),
1367 }
1368 }
1369
1370 pub fn has_pending_critical_resources(&self) -> bool {
1372 !self.pending_critical_resources.is_empty()
1373 }
1374
1375 pub fn load_resource(&mut self, res: ResourceLoadResponse) {
1376 self.pending_critical_resources.remove(&res.request_id);
1377 if res.ticket.as_ref().is_some_and(|ticket| !ticket.live()) {
1378 return;
1379 }
1380 if res.node_id.is_some_and(|id| !self.nodes.contains_key(id)) {
1381 return;
1382 }
1383
1384 let resource = match res.result {
1385 Ok(resource) => resource,
1386 Err(err) => {
1387 self.resource_counters.failures = self.resource_counters.failures.saturating_add(1);
1388 let requested_url = self.pending_images.iter().find_map(|(url, pending)| {
1391 (pending.request_id == res.request_id).then(|| url.clone())
1392 });
1393 if let Some(url) = requested_url.as_ref().or(res.resolved_url.as_ref()) {
1394 let waiting_nodes = if self
1395 .pending_images
1396 .get(url)
1397 .is_some_and(|pending| pending.request_id == res.request_id)
1398 {
1399 self.pending_images
1400 .remove(url)
1401 .map(|pending| pending.waiters)
1402 .unwrap_or_default()
1403 } else {
1404 Vec::new()
1405 };
1406 for (id, kind, request) in &waiting_nodes {
1407 if let Some(request) = request {
1408 self.image_observations.complete(
1409 *id,
1410 *request,
1411 crate::ImageLoadStatus::Failed,
1412 );
1413 }
1414 let Some(element) = self
1415 .nodes
1416 .get_mut(*id)
1417 .and_then(|node| node.element_data_mut())
1418 else {
1419 continue;
1420 };
1421 let layer = match kind {
1422 ImageType::Background(index) => {
1423 element.background_images.get_mut(*index)
1424 }
1425 ImageType::Mask(index) => element.mask_images.get_mut(*index),
1426 ImageType::Image => None,
1427 };
1428 if let Some(Some(layer)) = layer {
1429 layer.status = Status::Error;
1430 }
1431 }
1432 #[cfg(feature = "tracing")]
1433 tracing::warn!(
1434 url = url.as_str(),
1435 waiting_nodes = waiting_nodes.len(),
1436 error = err.as_str(),
1437 "Resource load failed"
1438 );
1439 #[cfg(not(feature = "tracing"))]
1440 let _ = (waiting_nodes, err);
1441 } else {
1442 #[cfg(feature = "tracing")]
1443 tracing::warn!(error = err.as_str(), "Resource load failed (no url)");
1444 #[cfg(not(feature = "tracing"))]
1445 let _ = err;
1446 }
1447 return;
1448 }
1449 };
1450
1451 match resource {
1452 Resource::Css(css) => {
1453 let node_id = res.node_id.unwrap();
1454 self.add_stylesheet_for_node(css, node_id);
1455 }
1456 Resource::ImportedCss(import_rule, sheet) => {
1457 let mut guard = self.guard.write();
1458 import_rule.write_with(&mut guard).stylesheet =
1459 style::stylesheets::import_rule::ImportSheet::Sheet(sheet);
1460 drop(guard);
1461 self.stylist
1462 .force_stylesheet_origins_dirty(style::stylesheets::OriginSet::all());
1463 }
1464 Resource::Image(_kind, width, height, image_data) => {
1465 let image = ImageData::Raster(RasterImageData::new(width, height, image_data));
1467
1468 self.apply_loaded_image(image, res.request_id);
1469 }
1470 #[cfg(feature = "svg")]
1471 Resource::Svg(_kind, svg) => {
1472 let image = ImageData::Svg(svg);
1474
1475 self.apply_loaded_image(image, res.request_id);
1476 }
1477 Resource::DocumentSrc(html) => {
1478 let Some(node_id) = res.node_id else {
1479 return;
1480 };
1481 self.apply_iframe_html(node_id, res.request_id, res.resolved_url, &html);
1482 }
1483 Resource::Font(bytes, overrides) => {
1484 if self.resource_counters.registered_fonts >= self.resource_limits.font_entries
1485 || bytes.len()
1486 > self
1487 .resource_limits
1488 .font_bytes
1489 .saturating_sub(self.resource_counters.registered_font_bytes)
1490 {
1491 self.resource_counters.rejected =
1492 self.resource_counters.rejected.saturating_add(1);
1493 return;
1494 }
1495 let byte_len = bytes.len();
1496 let font = Blob::new(Arc::new(bytes));
1497
1498 let weight_override = overrides.weight.map(parley::fontique::FontWeight::new);
1504 let info_override = parley::fontique::FontInfoOverride {
1505 family_name: overrides.family_name.as_deref(),
1506 weight: weight_override,
1507 style: overrides.style,
1508 ..Default::default()
1509 };
1510
1511 let mut global_font_ctx = self.font_ctx.lock().unwrap();
1513 let registered = global_font_ctx
1514 .collection
1515 .register_fonts(font.clone(), Some(info_override));
1516 if registered.is_empty() {
1517 self.resource_counters.failures =
1518 self.resource_counters.failures.saturating_add(1);
1519 return;
1520 }
1521 self.resource_counters.registered_fonts += 1;
1522 self.resource_counters.registered_font_bytes += byte_len;
1523
1524 #[cfg(feature = "parallel-construct")]
1525 {
1526 rayon::broadcast(|_ctx| {
1527 let mut font_ctx = self
1528 .thread_font_contexts
1529 .get_or(|| RefCell::new(Box::new(global_font_ctx.clone())))
1530 .borrow_mut();
1531 font_ctx
1532 .collection
1533 .register_fonts(font.clone(), Some(info_override));
1534 });
1535 }
1536 drop(global_font_ctx);
1537
1538 self.invalidate_inline_contexts();
1540 }
1541 Resource::None => {
1542 }
1544 }
1545 }
1546
1547 fn apply_loaded_image(&mut self, image: ImageData, request_id: usize) {
1550 let Some(url) = self
1552 .pending_images
1553 .iter()
1554 .find_map(|(url, pending)| (pending.request_id == request_id).then(|| url.clone()))
1555 else {
1556 return;
1557 };
1558 let waiting_nodes = self
1559 .pending_images
1560 .remove(&url)
1561 .map(|pending| pending.waiters)
1562 .unwrap_or_default();
1563
1564 #[cfg(feature = "tracing")]
1565 tracing::info!(
1566 "Image {url} loaded, applying to {} nodes",
1567 waiting_nodes.len()
1568 );
1569
1570 self.image_cache.insert(url.to_string(), image.clone());
1572
1573 for (node_id, image_type, request) in waiting_nodes {
1575 if let Some(request) = request {
1576 if self.image_request(node_id) != Some(request) {
1577 continue;
1578 }
1579 }
1580 let Some(node) = self.get_node_mut(node_id) else {
1581 continue;
1582 };
1583
1584 match image_type {
1585 ImageType::Image => {
1586 let element = node.element_data_mut().unwrap();
1587 #[cfg(feature = "custom-widget")]
1588 if element.custom_widget_data().is_some() {
1589 continue;
1590 }
1591 element.special_data = SpecialElementData::Image(Box::new(image.clone()));
1592
1593 node.clear_layout_cache();
1595 node.insert_damage(ALL_DAMAGE);
1596 }
1597 ImageType::Background(idx) | ImageType::Mask(idx) => {
1598 let layer_image = node.element_data_mut().and_then(|el| {
1599 let images = match image_type {
1600 ImageType::Background(_) => &mut el.background_images,
1601 ImageType::Mask(_) => &mut el.mask_images,
1602 ImageType::Image => unreachable!(),
1603 };
1604 images.get_mut(idx)
1605 });
1606 if let Some(Some(layer_image)) = layer_image {
1607 layer_image.status = Status::Ok;
1608 layer_image.image = image.clone();
1609 }
1610 }
1611 }
1612 if let Some(request) = request {
1613 self.image_observations
1614 .complete(node_id, request, crate::ImageLoadStatus::Loaded);
1615 }
1616 }
1617 }
1618
1619 pub fn snapshot_node(&mut self, node_id: NodeId) {
1623 self.snapshot_node_impl(node_id, true)
1624 }
1625
1626 pub fn snapshot_node_state_only(&mut self, node_id: NodeId) {
1630 self.snapshot_node_impl(node_id, false)
1631 }
1632
1633 fn snapshot_node_impl(&mut self, node_id: NodeId, capture_attrs: bool) {
1634 let node = &mut self.nodes[node_id];
1635
1636 let has_been_styled = node.primary_styles().is_some();
1641 if !has_been_styled {
1642 return;
1643 }
1644
1645 let opaque_node_id = TNode::opaque(&&*node);
1646 node.set_has_snapshot(true);
1647 node.snapshot_handled()
1648 .store(false, std::sync::atomic::Ordering::SeqCst);
1649
1650 let needs_attrs = capture_attrs
1656 && self
1657 .snapshots
1658 .get_mut(&opaque_node_id)
1659 .is_none_or(|snapshot| snapshot.attrs.is_none());
1660
1661 let (attrs, changed_attrs) = if needs_attrs {
1662 let node = &self.nodes[node_id];
1663 let attrs: Option<Vec<_>> = node.attrs().map(|attrs| {
1664 attrs
1665 .iter()
1666 .map(|attr| {
1667 let ident = AttrIdentifier {
1668 local_name: GenericAtomIdent(attr.name.local.clone()),
1669 name: GenericAtomIdent(attr.name.local.clone()),
1670 namespace: GenericAtomIdent(attr.name.ns.clone()),
1671 prefix: None,
1672 };
1673
1674 let value = if attr.name.local == local_name!("id") {
1675 AttrValue::Atom(Atom::from(&*attr.value))
1676 } else if attr.name.local == local_name!("class") {
1677 let classes = attr
1678 .value
1679 .split_ascii_whitespace()
1680 .map(Atom::from)
1681 .collect();
1682 AttrValue::TokenList(OnceLock::from(attr.value.clone()), classes)
1683 } else {
1684 AttrValue::String(attr.value.clone())
1685 };
1686
1687 (ident, value)
1688 })
1689 .collect()
1690 });
1691
1692 let changed_attrs: Vec<_> = attrs
1693 .as_ref()
1694 .map(|attrs| attrs.iter().map(|attr| attr.0.name.clone()).collect())
1695 .unwrap_or_default();
1696
1697 (attrs, changed_attrs)
1698 } else {
1699 (None, Vec::new())
1700 };
1701
1702 if let Some(snapshot) = self.snapshots.get_mut(&opaque_node_id) {
1703 if needs_attrs {
1706 snapshot.attrs = attrs;
1707 snapshot.changed_attrs = changed_attrs;
1708 snapshot.class_changed = true;
1709 snapshot.id_changed = true;
1710 snapshot.other_attributes_changed = true;
1711 }
1712 } else {
1713 self.snapshots.insert(
1714 opaque_node_id,
1715 ServoElementSnapshot {
1716 state: Some(*self.nodes[node_id].element_state()),
1717 attrs,
1718 changed_attrs,
1719 class_changed: needs_attrs,
1720 id_changed: needs_attrs,
1721 other_attributes_changed: needs_attrs,
1722 },
1723 );
1724 }
1725 }
1726
1727 pub fn style_depends_on_state(&self, state: ElementState) -> bool {
1730 self.stylist.iter_origins().any(|(data, _)| {
1731 data.has_state_dependency(state) || data.has_nth_of_state_dependency(state)
1732 })
1733 }
1734
1735 pub fn snapshot_node_and(
1739 &mut self,
1740 node_id: NodeId,
1741 state: ElementState,
1742 cb: impl FnOnce(&mut Node),
1743 ) {
1744 if self.style_depends_on_state(state) {
1745 self.snapshot_node_state_only(node_id);
1746 }
1747 cb(&mut self.nodes[node_id]);
1748 }
1749
1750 pub fn hit(&self, x: f32, y: f32) -> Option<HitResult> {
1752 self.hit_with_scrollbar(x, y).0
1753 }
1754
1755 pub fn element_from_point(&self, x: f32, y: f32) -> Option<NodeId> {
1763 let viewport = self.viewport();
1764 let scale = viewport.scale();
1765 let (viewport_width, viewport_height) = (
1766 viewport.window_size.0 as f32 / scale,
1767 viewport.window_size.1 as f32 / scale,
1768 );
1769 if x < 0.0 || y < 0.0 || x > viewport_width || y > viewport_height {
1770 return None;
1771 }
1772
1773 let scroll = self.viewport_scroll();
1774 let Some(hit) = self.hit(x + scroll.x as f32, y + scroll.y as f32) else {
1775 return self.try_root_element().map(|root| root.id);
1778 };
1779
1780 let mut node_id = self.nearest_non_anonymous_ancestor(hit.node_id)?;
1782 loop {
1783 let node = self.get_node(node_id)?;
1784 if node.is_element() {
1785 return Some(node_id);
1786 }
1787 node_id = node.parent?;
1788 }
1789 }
1790
1791 pub fn elements_from_point(&self, x: f32, y: f32) -> Vec<NodeId> {
1799 let mut element_ids = Vec::new();
1800 let mut current = self.element_from_point(x, y);
1801 while let Some(node_id) = current {
1802 element_ids.push(node_id);
1803 current = self
1804 .get_node(node_id)
1805 .and_then(|node| node.parent)
1806 .filter(|parent_id| {
1807 self.get_node(*parent_id)
1808 .is_some_and(|node| node.is_element())
1809 });
1810 }
1811 element_ids
1812 }
1813
1814 pub fn nearest_non_anonymous_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
1827 let mut node = self.get_node(node_id)?;
1831 loop {
1832 let parent = match node.parent {
1833 Some(parent_id) => self.get_node(parent_id)?,
1834 None => return Some(node.id),
1835 };
1836 if !node.is_anonymous() && !parent.is_anonymous() {
1837 return Some(node.id);
1838 }
1839 node = parent;
1840 }
1841 }
1842
1843 pub fn focus_next_node(&mut self) -> Option<NodeId> {
1844 let focussed_node_id = self.get_focussed_node_id()?;
1845 let id = self.next_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
1846 self.set_focus_to(id);
1847 Some(id)
1848 }
1849
1850 pub fn focus_prev_node(&mut self) -> Option<NodeId> {
1852 let focussed_node_id = self.get_focussed_node_id()?;
1853 let id = self.prev_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
1854 self.set_focus_to(id);
1855 Some(id)
1856 }
1857
1858 pub fn clear_focus(&mut self) {
1860 if let Some(id) = self.focus_node_id {
1861 let old_path = self.maybe_node_element_ancestors(Some(id));
1862 let shell_provider = self.shell_provider.clone();
1863 self.snapshot_node_and(id, ElementState::FOCUS | ElementState::FOCUSRING, |node| {
1864 node.blur(shell_provider)
1865 });
1866 self.focus_node_id = None;
1867 self.update_focus_within_paths(old_path, Vec::new());
1868 }
1869 }
1870
1871 pub fn set_mousedown_node_id(&mut self, node_id: Option<NodeId>) {
1872 self.mousedown_node_id = node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
1873 }
1874 pub fn set_focus_to(&mut self, focus_node_id: NodeId) -> bool {
1875 let Some(focus_node_id) = self.nearest_non_anonymous_ancestor(focus_node_id) else {
1876 return false;
1877 };
1878 if Some(focus_node_id) == self.focus_node_id {
1879 return false;
1880 }
1881 let old_path = self.maybe_node_element_ancestors(self.focus_node_id);
1882 let new_path = self.maybe_node_element_ancestors(Some(focus_node_id));
1883
1884 #[cfg(feature = "tracing")]
1885 tracing::info!("Focussed node {focus_node_id}");
1886
1887 let shell_provider = self.shell_provider.clone();
1888
1889 if let Some(id) = self.focus_node_id {
1891 self.snapshot_node_and(id, ElementState::FOCUS | ElementState::FOCUSRING, |node| {
1892 node.blur(shell_provider.clone())
1893 });
1894 }
1895
1896 self.snapshot_node_and(
1898 focus_node_id,
1899 ElementState::FOCUS | ElementState::FOCUSRING,
1900 |node| node.focus(shell_provider),
1901 );
1902
1903 self.focus_node_id = Some(focus_node_id);
1904 self.update_focus_within_paths(old_path, new_path);
1905
1906 true
1907 }
1908
1909 pub(crate) fn update_focus_within_paths(&mut self, old: Vec<NodeId>, new: Vec<NodeId>) {
1913 let common = old.iter().zip(&new).take_while(|(a, b)| a == b).count();
1914 for (path, enabled) in [(&old, false), (&new, true)] {
1915 for &id in path.iter().skip(common) {
1916 if self.nodes.get(id).is_none_or(|node| !node.is_element()) {
1917 continue;
1918 }
1919 if self.nodes[id]
1920 .element_state()
1921 .contains(ElementState::FOCUS_WITHIN)
1922 == enabled
1923 {
1924 continue;
1925 }
1926 self.snapshot_node_and(id, ElementState::FOCUS_WITHIN, |node| {
1927 node.element_state_mut()
1928 .set(ElementState::FOCUS_WITHIN, enabled);
1929 node.mark_ancestors_dirty();
1930 });
1931 }
1932 }
1933 }
1934
1935 pub fn active_node(&mut self) -> bool {
1936 let Some(hover_node_id) = self.get_hover_node_id() else {
1937 return false;
1938 };
1939
1940 if let Some(active_node_id) = self.active_node_id {
1941 if active_node_id == hover_node_id {
1942 return true;
1943 }
1944 self.unactive_node();
1945 }
1946
1947 debug_assert!(
1949 self.get_node(hover_node_id)
1950 .is_some_and(|node| !node.is_anonymous()),
1951 "interaction state must reference DOM nodes, not layout-generated nodes"
1952 );
1953 let active_node_id = Some(hover_node_id);
1954
1955 let node_path = self.maybe_node_element_ancestors(active_node_id);
1956 for &id in node_path.iter() {
1957 self.snapshot_node_and(id, ElementState::ACTIVE, |node| node.active());
1958 }
1959
1960 self.active_node_id = active_node_id;
1961
1962 true
1963 }
1964
1965 pub fn unactive_node(&mut self) -> bool {
1966 let Some(active_node_id) = self.active_node_id.take() else {
1967 return false;
1968 };
1969
1970 let node_path = self.maybe_node_element_ancestors(Some(active_node_id));
1971 for &id in node_path.iter() {
1972 self.snapshot_node_and(id, ElementState::ACTIVE, |node| node.unactive());
1973 }
1974
1975 true
1976 }
1977
1978 pub fn hovered_scrollbar(&self) -> Option<crate::node::ScrollbarRef> {
1980 self.hovered_scrollbar
1981 }
1982
1983 pub fn scrollbar_drag_target(&self) -> Option<crate::node::ScrollbarRef> {
1985 match &self.drag_mode {
1986 DragMode::ScrollbarDrag(state) => Some(state.scrollbar),
1987 _ => None,
1988 }
1989 }
1990
1991 pub fn scrollbar_opacity(&self, node_id: NodeId) -> f32 {
1996 let interacting = |scrollbar: &crate::node::ScrollbarRef| scrollbar.node_id == node_id;
1997 if self.hovered_scrollbar.as_ref().is_some_and(interacting)
1998 || self
1999 .scrollbar_drag_target()
2000 .as_ref()
2001 .is_some_and(interacting)
2002 {
2003 return 1.0;
2004 }
2005 self.scrollbar_activity.get(&node_id).map_or(0.0, |last| {
2006 crate::node::scrollbar::opacity_at(last.elapsed())
2007 })
2008 }
2009
2010 pub(crate) fn show_scrollbars(&mut self, node_id: NodeId) {
2013 if cfg!(feature = "scrollbars") {
2014 self.scrollbar_activity.insert(node_id, Instant::now());
2015 }
2016 }
2017
2018 fn scrollbars_animating(&self) -> bool {
2021 use crate::node::scrollbar::{FADE_DELAY, FADE_DURATION};
2022 self.scrollbar_activity
2023 .values()
2024 .any(|last| last.elapsed() < FADE_DELAY + FADE_DURATION)
2025 }
2026
2027 pub(crate) fn hit_with_scrollbar(
2031 &self,
2032 x: f32,
2033 y: f32,
2034 ) -> (Option<HitResult>, Option<crate::node::ScrollbarRef>) {
2035 if TDocument::as_node(&self.root_node())
2036 .first_element_child()
2037 .is_none()
2038 {
2039 #[cfg(feature = "tracing")]
2040 tracing::warn!("No DOM - not resolving hit test");
2041 return (None, None);
2042 }
2043 let mut scrollbar = None;
2044 let scroll = self.viewport_scroll();
2045 let client_x = x - scroll.x as f32;
2046 let client_y = y - scroll.y as f32;
2047 for layer in self.top_layers.iter().rev() {
2048 if !self.top_layer_is_rendered(layer.node_id) {
2049 continue;
2050 }
2051 let Some(node) = self.nodes.get(layer.node_id) else {
2052 continue;
2053 };
2054 if let Some(hit) = node.hit_inner(
2055 client_x,
2056 client_y,
2057 self.viewport().scale_f64(),
2058 &mut scrollbar,
2059 ) {
2060 return (Some(hit), scrollbar);
2061 }
2062 if layer.config.modal {
2063 return (
2064 Some(HitResult {
2065 node_id: layer.node_id,
2066 is_text: false,
2067 x: client_x - node.final_layout().location.x,
2068 y: client_y - node.final_layout().location.y,
2069 }),
2070 None,
2071 );
2072 }
2073 }
2074 let hit = self
2075 .root_element()
2076 .hit_inner(x, y, self.viewport().scale_f64(), &mut scrollbar);
2077 (hit, scrollbar)
2078 }
2079
2080 pub fn set_hover_to(&mut self, x: f32, y: f32) -> bool {
2081 self.last_client_pointer_position = Some(takia::Point {
2085 x: x - self.viewport_scroll().x as f32,
2086 y: y - self.viewport_scroll().y as f32,
2087 });
2088
2089 let (hit, hovered_scrollbar) = self.hit_with_scrollbar(x, y);
2090 let hovered_scrollbar =
2093 hovered_scrollbar.filter(|scrollbar| self.scrollbar_opacity(scrollbar.node_id) > 0.0);
2094 let scrollbar_changed = hovered_scrollbar != self.hovered_scrollbar;
2098 if scrollbar_changed {
2099 for scrollbar in [self.hovered_scrollbar, hovered_scrollbar]
2102 .into_iter()
2103 .flatten()
2104 {
2105 self.show_scrollbars(scrollbar.node_id);
2106 }
2107 }
2108 self.hovered_scrollbar = hovered_scrollbar;
2109
2110 let hit_node_id = hit.map(|hit| hit.node_id);
2115 let hover_node_id = hit_node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
2116 let new_is_text = hit.map(|hit| hit.is_text).unwrap_or(false);
2117
2118 self.hover_hit_node_id = hit_node_id;
2119 self.hover_node_is_text = new_is_text;
2120
2121 if hover_node_id == self.hover_node_id {
2123 return scrollbar_changed;
2124 }
2125
2126 let old_node_path = self.maybe_node_element_ancestors(self.hover_node_id);
2127 let new_node_path = self.maybe_node_element_ancestors(hover_node_id);
2128 let same_count = old_node_path
2129 .iter()
2130 .zip(&new_node_path)
2131 .take_while(|(o, n)| o == n)
2132 .count();
2133 for &id in old_node_path.iter().skip(same_count) {
2134 self.snapshot_node_and(id, ElementState::HOVER, |node| node.unhover());
2135 }
2136 for &id in new_node_path.iter().skip(same_count) {
2137 self.snapshot_node_and(id, ElementState::HOVER, |node| node.hover());
2138 }
2139
2140 self.hover_node_id = hover_node_id;
2141
2142 self.shell_provider.request_redraw();
2144
2145 true
2146 }
2147
2148 pub fn clear_hover(&mut self) -> bool {
2149 self.last_client_pointer_position = None;
2152 self.hover_hit_node_id = None;
2153
2154 let Some(hover_node_id) = self.hover_node_id else {
2155 return false;
2156 };
2157
2158 let old_node_path = self.maybe_node_element_ancestors(Some(hover_node_id));
2159 for &id in old_node_path.iter() {
2160 self.snapshot_node_and(id, ElementState::HOVER, |node| node.unhover());
2161 }
2162
2163 self.hover_node_id = None;
2164 self.hover_node_is_text = false;
2165
2166 self.shell_provider.request_redraw();
2168
2169 true
2170 }
2171
2172 pub fn refresh_hover(&mut self) -> bool {
2178 let Some(pos) = self.last_client_pointer_position else {
2179 return false;
2180 };
2181 let x = pos.x + self.viewport_scroll().x as f32;
2182 let y = pos.y + self.viewport_scroll().y as f32;
2183 let prev_hover = self.hover_state();
2184 let changed = self.set_hover_to(x, y);
2185 if self.hover_state() != prev_hover {
2186 self.update_cursor();
2187 }
2188 changed
2189 }
2190
2191 pub(crate) fn hover_state(&self) -> (Option<NodeId>, Option<NodeId>, bool) {
2194 (
2195 self.hover_node_id,
2196 self.hover_hit_node_id,
2197 self.hover_node_is_text,
2198 )
2199 }
2200
2201 pub(crate) fn update_cursor(&self) {
2206 self.shell_provider.set_cursor(self.get_cursor());
2207 }
2208
2209 pub fn get_hover_node_id(&self) -> Option<NodeId> {
2210 self.hover_node_id
2211 }
2212
2213 pub fn get_mousedown_node_id(&self) -> Option<NodeId> {
2214 self.mousedown_node_id
2215 }
2216
2217 pub fn set_viewport(&mut self, viewport: Viewport) {
2218 let changes = DeviceChanges::from_viewports(&self.viewport, &viewport);
2219 self.viewport = viewport;
2220 self.queue_device_changes(changes);
2221 }
2222
2223 pub fn media_type(&self) -> &MediaType {
2225 &self.media_type
2226 }
2227
2228 pub fn set_media_type(&mut self, media_type: MediaType) {
2231 if self.media_type == media_type {
2232 return;
2233 }
2234 self.media_type = media_type;
2235 self.queue_device_changes(DeviceChanges::MEDIA_TYPE);
2236 }
2237
2238 pub fn viewport(&self) -> &Viewport {
2239 &self.viewport
2240 }
2241
2242 pub fn viewport_mut(&mut self) -> ViewportMut<'_> {
2243 ViewportMut::new(self)
2244 }
2245
2246 pub fn zoom_by(&mut self, increment: f32) {
2247 *self.viewport_mut().zoom_mut() += increment;
2248 }
2249
2250 pub fn zoom_to(&mut self, zoom: f32) {
2251 *self.viewport_mut().zoom_mut() = zoom;
2252 }
2253
2254 pub fn get_viewport(&self) -> Viewport {
2255 self.viewport.clone()
2256 }
2257
2258 pub fn incremental_layout(&self) -> bool {
2266 self.incremental_layout
2267 }
2268
2269 pub fn set_incremental_layout(&mut self, enabled: bool) {
2274 self.incremental_layout = enabled;
2275 }
2276
2277 pub fn devtools(&self) -> &DevtoolSettings {
2278 &self.devtool_settings
2279 }
2280
2281 pub fn devtools_mut(&mut self) -> &mut DevtoolSettings {
2282 &mut self.devtool_settings
2283 }
2284
2285 pub fn subdoc(&self, node_id: NodeId) -> Option<&dyn Document> {
2286 self.get_node(node_id)
2287 .and_then(|node| node.element_data())
2288 .and_then(|el| el.sub_doc_data())
2289 }
2290
2291 pub fn subdoc_mut(&mut self, node_id: NodeId) -> Option<&mut dyn Document> {
2292 self.get_node_mut(node_id)
2293 .and_then(|node| node.element_data_mut())
2294 .and_then(|el| el.sub_doc_data_mut())
2295 }
2296
2297 pub fn is_animating(&self) -> bool {
2298 #[cfg(feature = "custom-widget")]
2299 let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
2300 self.nodes[node_id]
2301 .element_data()
2302 .and_then(|el| el.custom_widget_data())
2303 .is_some_and(|data| data.widget.requires_redraw())
2304 });
2305 #[cfg(not(feature = "custom-widget"))]
2306 let custom_widget_is_animating = false;
2307
2308 self.has_canvas
2309 | self.has_active_animations
2310 | self.subdoc_is_animating
2311 | custom_widget_is_animating
2312 | (self.scroll_animation != ScrollAnimationState::None)
2313 | self.scrollbars_animating()
2314 }
2315
2316 pub(crate) fn queue_device_changes(&mut self, changes: DeviceChanges) {
2319 if changes.is_empty() {
2320 return;
2321 }
2322 self.pending_device_changes |= changes;
2323 self.shell_provider.request_redraw();
2324 }
2325
2326 pub(crate) fn flush_pending_device_changes(&mut self) {
2329 let changes = std::mem::take(&mut self.pending_device_changes);
2330 if changes.is_empty() {
2331 return;
2332 }
2333
2334 self.set_stylist_device(make_device(
2335 &self.viewport,
2336 self.media_type.clone(),
2337 self.font_ctx.clone(),
2338 ));
2339 self.scroll_viewport_by(0.0, 0.0); if changes.contains(DeviceChanges::SCALE) {
2344 self.invalidate_inline_contexts();
2345 }
2346
2347 if changes.contains(DeviceChanges::COLOR_SCHEME) {
2351 if let Some(root_id) = self.try_root_element().map(|el| el.id) {
2352 self.nodes[root_id].set_restyle_hint(RestyleHint::recascade_subtree());
2353 }
2354 }
2355 }
2356
2357 pub fn set_stylist_device(&mut self, device: Device) {
2359 let root_styles = self
2365 .try_root_element()
2366 .and_then(|root| root.primary_styles());
2367 if let Some(root_style) = root_styles.as_deref() {
2368 device.set_root_style(root_style);
2369
2370 let font = root_style.get_font();
2371 let font_size = font.clone_font_size().computed_size();
2372 device.set_root_font_size(root_style.effective_zoom.unzoom(font_size.px()));
2373
2374 let line_height = device
2375 .calc_line_height(font, root_style.writing_mode, None)
2376 .0;
2377 device.set_root_line_height(root_style.effective_zoom.unzoom(line_height.px()));
2378 }
2379 drop(root_styles);
2380
2381 let old_device = self.stylist.device();
2382 let viewport_size_changed = old_device.au_viewport_size() != device.au_viewport_size();
2383 let styles_use_viewport_units = old_device.used_viewport_size();
2384
2385 let origins = {
2386 let guard = &self.guard;
2387 let guards = StylesheetGuards {
2388 author: &guard.read(),
2389 ua_or_user: &guard.read(),
2390 };
2391 self.stylist.set_device(device, &guards)
2392 };
2393
2394 if !origins.is_empty() {
2399 self.stylist.force_stylesheet_origins_dirty(origins);
2400 }
2401
2402 if viewport_size_changed && styles_use_viewport_units {
2407 if self
2408 .stylist
2409 .get_custom_property_initial_values_flags()
2410 .intersects(ComputedValueFlags::USES_VIEWPORT_UNITS)
2411 {
2412 self.stylist.rebuild_initial_values_for_custom_properties();
2413 }
2414 if let Some(root) = self.try_root_element() {
2415 style::invalidation::viewport_units::invalidate(root);
2416 }
2417 }
2418 }
2419
2420 pub fn stylist_device(&mut self) -> &Device {
2421 self.flush_pending_device_changes();
2422 self.stylist.device()
2423 }
2424
2425 pub fn get_cursor(&self) -> Option<CursorIcon> {
2426 let node_id = self
2431 .hover_hit_node_id
2432 .filter(|&id| self.nodes.contains_key(id))
2433 .or(self.get_hover_node_id())?;
2434 let node = &self.nodes[node_id];
2435
2436 if let Some(subdoc) = node.subdoc().map(|doc| doc.inner())
2437 && subdoc.get_hover_node_id().is_some()
2438 {
2439 return subdoc.get_cursor();
2440 }
2441
2442 let style = node.primary_styles()?;
2443 let user_select = style.clone_user_select();
2444 let keyword = style.clone_cursor().keyword;
2445
2446 if keyword != CursorKind::Auto {
2448 return stylo_to_cursor_icon(keyword);
2449 }
2450
2451 if node
2453 .element_data()
2454 .is_some_and(|e| e.text_input_data().is_some())
2455 {
2456 return Some(CursorIcon::Text);
2457 }
2458
2459 let mut maybe_node = Some(node);
2461 while let Some(node) = maybe_node {
2462 if node.is_link() {
2463 return Some(CursorIcon::Pointer);
2464 }
2465
2466 maybe_node = node.layout_parent.get().map(|node_id| node.with(node_id));
2467 }
2468
2469 if self.hover_node_is_text {
2471 return Some(match user_select {
2472 UserSelect::Text | UserSelect::All | UserSelect::Auto | UserSelect::Contain => {
2473 CursorIcon::Text
2474 }
2475 UserSelect::None => CursorIcon::Default,
2476 });
2477 }
2478
2479 Some(CursorIcon::Default)
2481 }
2482
2483 pub fn viewport_scroll(&self) -> crate::Point<f64> {
2484 self.nodes.viewport_scroll()
2485 }
2486
2487 pub fn set_viewport_scroll(&mut self, scroll: crate::Point<f64>) {
2488 self.nodes.set_viewport_scroll(scroll);
2489 }
2490
2491 pub fn get_fragment_target(&self, fragment: &str) -> Option<NodeId> {
2496 if let Some(node_id) = self.get_element_by_id(fragment) {
2497 return Some(node_id);
2498 }
2499
2500 self.nodes.iter().find_map(|(id, node)| {
2502 let el = node.element_data()?;
2503 (el.name.local == local_name!("a") && el.attr(local_name!("name")) == Some(fragment))
2504 .then_some(id)
2505 })
2506 }
2507
2508 #[cfg(not(feature = "writing-mode"))]
2511 pub(crate) fn physical_unrounded_geometry(
2512 &self,
2513 node_id: NodeId,
2514 ) -> (crate::util::Point<f32>, takia::Layout) {
2515 let mut pos = crate::util::Point { x: 0.0, y: 0.0 };
2516 let mut current = Some(node_id);
2517 while let Some(id) = current {
2518 let node = &self.nodes[id];
2519 pos.x += node.unrounded_layout().location.x - node.scroll_offset().x as f32;
2520 pos.y += node.unrounded_layout().location.y - node.scroll_offset().y as f32;
2521 if node.flags.is_top_layer() {
2522 pos.x += self.viewport_scroll().x as f32;
2523 pos.y += self.viewport_scroll().y as f32;
2524 }
2525 current = node.containing_block();
2526 }
2527 (pos, *self.nodes[node_id].unrounded_layout())
2528 }
2529
2530 pub fn get_client_bounding_rect(&self, node_id: NodeId) -> Option<BoundingRect> {
2532 let mut ancestor = Some(node_id);
2533 while let Some(id) = ancestor {
2534 let node = self.get_node(id)?;
2535 if node.flags.is_top_layer() {
2536 if !self.top_layer_is_rendered(id) {
2537 return Some(BoundingRect {
2538 x: 0.,
2539 y: 0.,
2540 width: 0.,
2541 height: 0.,
2542 });
2543 }
2544 break;
2545 }
2546 ancestor = node.parent;
2547 }
2548 if let Some(rects) = self.inline_fragment_rects(node_id) {
2551 return rects.reduce(BoundingRect::union);
2552 }
2553
2554 self.get_node(node_id)?;
2555 let (pos, layout) = self.physical_unrounded_geometry(node_id);
2556 let size = layout.size;
2557
2558 Some(BoundingRect {
2559 x: snap_to_layout_unit(pos.x as f64 - self.viewport_scroll().x),
2560 y: snap_to_layout_unit(pos.y as f64 - self.viewport_scroll().y),
2561 width: snap_to_layout_unit(size.width as f64),
2562 height: snap_to_layout_unit(size.height as f64),
2563 })
2564 }
2565
2566 pub fn offset_rect(&self, node_id: NodeId) -> Option<BoundingRect> {
2575 let node = self.get_node(node_id)?;
2576
2577 let Some(rects) = self.inline_fragment_rects(node_id) else {
2579 let pos = node.offset_top_left();
2580 let size = node.unrounded_layout().size;
2581 #[cfg(feature = "writing-mode")]
2582 let size = match node.containing_block() {
2583 Some(parent) if self.nodes[parent].layout_data().writing_mode.is_vertical() => {
2584 size.transpose()
2585 }
2586 _ => size,
2587 };
2588 return Some(BoundingRect {
2589 x: pos.x as f64,
2590 y: pos.y as f64,
2591 width: size.width as f64,
2592 height: size.height as f64,
2593 });
2594 };
2595
2596 let bounds = rects.reduce(BoundingRect::union)?;
2598
2599 let scroll = self.viewport_scroll();
2601 let mut x = bounds.x + scroll.x;
2602 let mut y = bounds.y + scroll.y;
2603
2604 let inline_root = node.inline_root_ancestor()?;
2606 let offset_parent = if inline_root.is_offset_parent() {
2607 Some(inline_root)
2608 } else {
2609 inline_root.offset_parent()
2610 };
2611 if let Some(parent) = offset_parent.filter(|parent| !parent.is_static_body()) {
2613 let (parent_pos, parent_layout) = self.physical_unrounded_geometry(parent.id);
2614 let border = parent_layout.border;
2615 x -= (parent_pos.x + border.left) as f64;
2616 y -= (parent_pos.y + border.top) as f64;
2617 }
2618
2619 Some(BoundingRect {
2620 x,
2621 y,
2622 width: bounds.width,
2623 height: bounds.height,
2624 })
2625 }
2626
2627 pub fn node_client_rects(&self, node_id: NodeId) -> Vec<BoundingRect> {
2632 match self.inline_fragment_rects(node_id) {
2633 Some(rects) => rects.collect(),
2634 None => self.get_client_bounding_rect(node_id).into_iter().collect(),
2635 }
2636 }
2637
2638 pub fn inline_fragment_rects(
2642 &self,
2643 node_id: NodeId,
2644 ) -> Option<impl Iterator<Item = BoundingRect> + '_> {
2645 let node = self.get_node(node_id)?;
2646
2647 let fragments = node.inline_fragment_boxes()?;
2650 let inline_root = node.inline_root_ancestor()?;
2651
2652 let (root_pos, _) = self.physical_unrounded_geometry(inline_root.id);
2654 let origin_x = root_pos.x as f64 - self.viewport_scroll().x;
2655 let origin_y = root_pos.y as f64 - self.viewport_scroll().y;
2656
2657 Some(fragments.map(move |rect| BoundingRect {
2658 x: snap_to_layout_unit(origin_x + rect.left as f64),
2659 y: snap_to_layout_unit(origin_y + rect.top as f64),
2660 width: snap_to_layout_unit((rect.right - rect.left) as f64),
2661 height: snap_to_layout_unit((rect.bottom - rect.top) as f64),
2662 }))
2663 }
2664
2665 pub fn find_element_by_tag_name(&self, tag: &LocalName) -> Option<&Node> {
2669 let root = self.try_root_element()?;
2670 if root.data.is_element_with_tag_name(tag) {
2671 return Some(root);
2672 }
2673 root.children
2674 .iter()
2675 .copied()
2676 .find(|child_id| {
2677 self.get_node(*child_id)
2678 .is_some_and(|child| child.data.is_element_with_tag_name(tag))
2679 })
2680 .or_else(|| {
2681 TreeTraverser::new(self)
2682 .find(|node_id| self.nodes[*node_id].data.is_element_with_tag_name(tag))
2683 })
2684 .map(|node_id| &self.nodes[node_id])
2685 }
2686
2687 pub fn find_body_node(&self) -> Option<&Node> {
2689 self.find_element_by_tag_name(&local_name!("body"))
2690 }
2691
2692 pub fn find_head_node(&self) -> Option<&Node> {
2694 self.find_element_by_tag_name(&local_name!("head"))
2695 }
2696
2697 pub fn find_title_node(&self) -> Option<&Node> {
2699 self.find_element_by_tag_name(&local_name!("title"))
2700 }
2701
2702 pub fn with_text_input(
2703 &mut self,
2704 node_id: NodeId,
2705 cb: impl FnOnce(PlainEditorDriver<TextBrush>),
2706 ) {
2707 let Some(node) = self.nodes.get_mut(node_id) else {
2708 return;
2709 };
2710
2711 if let Some(text_input) = node
2712 .element_data_mut()
2713 .and_then(|el| el.text_input_data_mut())
2714 {
2715 let mut font_ctx = self.font_ctx.lock().unwrap();
2716 let layout_ctx = &mut self.layout_ctx;
2717 let driver = text_input.editor.driver(&mut font_ctx, layout_ctx);
2718 cb(driver);
2719 text_input.refresh_display(&mut font_ctx, layout_ctx);
2720 }
2721 self.clamp_text_input_scroll(node_id);
2722 }
2723
2724 pub(crate) fn clamp_text_input_scroll(&mut self, node_id: NodeId) {
2727 self.sync_placeholder_state(node_id);
2728 let Some(node) = self.nodes.get_mut(node_id) else {
2729 return;
2730 };
2731
2732 let content_box_width = node.final_layout().content_box_width();
2733 let content_box_height = node.final_layout().content_box_height();
2734
2735 if let Some(text_input) = node
2736 .element_data_mut()
2737 .and_then(|el| el.text_input_data_mut())
2738 {
2739 text_input.clamp_scroll_offset(content_box_width, content_box_height);
2740 }
2741 if self.focus_node_id == Some(node_id) {
2742 if let Some((x, y, width, height)) = node.text_input_ime_cursor_area() {
2743 if let Some(input) = node
2744 .element_data()
2745 .and_then(|element| element.text_input_data())
2746 {
2747 input.sync_ime_context(self.shell_provider.as_ref());
2748 self.shell_provider.set_ime_purpose(if input.is_password() {
2749 apace_traits::shell::ImePurpose::Password
2750 } else {
2751 apace_traits::shell::ImePurpose::Normal
2752 });
2753 }
2754 self.shell_provider.set_ime_cursor_area(x, y, width, height);
2755 }
2756 }
2757 }
2758
2759 pub(crate) fn compute_has_canvas(&self) -> bool {
2760 TreeTraverser::new(self).any(|node_id| {
2761 let node = &self.nodes[node_id];
2762 let Some(element) = node.element_data() else {
2763 return false;
2764 };
2765 if element.name.local == local_name!("canvas") && element.has_attr(local_name!("src")) {
2766 return true;
2767 }
2768
2769 false
2770 })
2771 }
2772
2773 pub fn find_text_position(&self, x: f32, y: f32) -> Option<(NodeId, usize)> {
2779 let hit = self.hit(x, y)?;
2780 let hit_node = self.get_node(hit.node_id)?;
2781 let inline_root = hit_node.inline_root_ancestor()?;
2782 let byte_offset = inline_root.text_offset_at_point(hit.x, hit.y)?;
2783 Some((inline_root.id, byte_offset))
2784 }
2785
2786 pub fn set_text_selection(
2788 &mut self,
2789 anchor_node: NodeId,
2790 anchor_offset: usize,
2791 focus_node: NodeId,
2792 focus_offset: usize,
2793 ) {
2794 self.text_selection =
2795 TextSelection::new(anchor_node, anchor_offset, focus_node, focus_offset);
2796
2797 if let (Some(parent), Some(idx)) = self.anonymous_block_location(anchor_node) {
2799 self.text_selection
2800 .anchor
2801 .set_anonymous(parent, idx, anchor_offset);
2802 }
2803 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
2804 self.text_selection
2805 .focus
2806 .set_anonymous(parent, idx, focus_offset);
2807 }
2808 }
2809
2810 fn anonymous_block_location(&self, node_id: NodeId) -> (Option<NodeId>, Option<usize>) {
2813 let Some(node) = self.get_node(node_id) else {
2814 return (None, None);
2815 };
2816
2817 if !node.is_anonymous() {
2818 return (None, None);
2819 }
2820
2821 let Some(parent_id) = node.parent else {
2822 return (None, None);
2823 };
2824
2825 let Some(parent) = self.get_node(parent_id) else {
2826 return (Some(parent_id), None);
2827 };
2828
2829 let layout_children = parent.layout_children.borrow();
2830 let Some(children) = layout_children.as_ref() else {
2831 return (Some(parent_id), None);
2832 };
2833
2834 let mut anon_index = 0;
2836 for &child_id in children.iter() {
2837 if child_id == node_id {
2838 return (Some(parent_id), Some(anon_index));
2839 }
2840 if self.get_node(child_id).is_some_and(|n| n.is_anonymous()) {
2841 anon_index += 1;
2842 }
2843 }
2844
2845 (Some(parent_id), None)
2846 }
2847
2848 pub fn clear_text_selection(&mut self) {
2850 self.text_selection.clear();
2851 }
2852
2853 pub fn update_selection_focus(&mut self, focus_node: NodeId, focus_offset: usize) {
2855 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
2857 self.text_selection
2858 .focus
2859 .set_anonymous(parent, idx, focus_offset);
2860 } else {
2861 self.text_selection.set_focus(focus_node, focus_offset);
2862 }
2863 }
2864
2865 pub fn extend_text_selection_to_point(&mut self, x: f32, y: f32) -> bool {
2868 if !self.text_selection.anchor.is_some() {
2869 return false;
2870 }
2871
2872 if let Some((node, offset)) = self.find_text_position(x, y) {
2873 self.update_selection_focus(node, offset);
2874 self.shell_provider.request_redraw();
2875 true
2876 } else {
2877 false
2878 }
2879 }
2880
2881 fn find_anonymous_block_by_index(
2883 &self,
2884 parent_id: NodeId,
2885 target_index: usize,
2886 ) -> Option<NodeId> {
2887 let parent = self.get_node(parent_id)?;
2888 let layout_children = parent.layout_children.borrow();
2889 let children = layout_children.as_ref()?;
2890
2891 children
2892 .iter()
2893 .filter(|&&child_id| self.get_node(child_id).is_some_and(|n| n.is_anonymous()))
2894 .nth(target_index)
2895 .copied()
2896 }
2897
2898 pub fn has_text_selection(&self) -> bool {
2900 self.text_selection.is_active()
2901 }
2902
2903 pub fn get_selected_text(&self) -> Option<String> {
2905 let ranges = self.get_text_selection_ranges();
2906 if ranges.is_empty() {
2907 return None;
2908 }
2909
2910 let mut result = String::new();
2911 for (node_id, start, end) in &ranges {
2912 let node = self.get_node(*node_id)?;
2913 let element_data = node.element_data()?;
2914 let inline_layout = element_data.inline_layout_data.as_ref()?;
2915
2916 if *end > inline_layout.text.len() {
2917 continue;
2918 }
2919
2920 if !result.is_empty() {
2921 result.push(' ');
2922 }
2923 result.push_str(&inline_layout.text[*start..*end]);
2924 }
2925
2926 if result.is_empty() {
2927 None
2928 } else {
2929 Some(result)
2930 }
2931 }
2932
2933 pub fn get_text_selection_ranges(&self) -> Vec<(NodeId, usize, usize)> {
2936 let lookup = |parent_id, idx| self.find_anonymous_block_by_index(parent_id, idx);
2937
2938 let anchor_node = match self.text_selection.anchor.resolve_node_id(lookup) {
2939 Some(id) => id,
2940 None => return Vec::new(),
2941 };
2942 let focus_node = match self.text_selection.focus.resolve_node_id(lookup) {
2943 Some(id) => id,
2944 None => return Vec::new(),
2945 };
2946
2947 let node_is_in_doc = |node_id: NodeId| {
2950 self.nodes
2951 .get(node_id)
2952 .is_some_and(|node| node.flags.is_in_document())
2953 };
2954 if !node_is_in_doc(anchor_node) || !node_is_in_doc(focus_node) {
2955 return Vec::new();
2956 }
2957
2958 if anchor_node == focus_node {
2960 let start = self
2961 .text_selection
2962 .anchor
2963 .offset
2964 .min(self.text_selection.focus.offset);
2965 let end = self
2966 .text_selection
2967 .anchor
2968 .offset
2969 .max(self.text_selection.focus.offset);
2970
2971 if start == end {
2972 return Vec::new();
2973 }
2974 return vec![(anchor_node, start, end)];
2975 }
2976
2977 let inline_roots = self.collect_inline_roots_in_range(anchor_node, focus_node);
2979 if inline_roots.is_empty() {
2980 return Vec::new();
2981 }
2982
2983 let first_in_roots = inline_roots[0];
2986
2987 let (first_node, first_offset, last_node, last_offset) =
2988 if first_in_roots == anchor_node || (first_in_roots != focus_node) {
2989 (
2991 anchor_node,
2992 self.text_selection.anchor.offset,
2993 focus_node,
2994 self.text_selection.focus.offset,
2995 )
2996 } else {
2997 (
2999 focus_node,
3000 self.text_selection.focus.offset,
3001 anchor_node,
3002 self.text_selection.anchor.offset,
3003 )
3004 };
3005
3006 let mut ranges = Vec::with_capacity(inline_roots.len());
3007
3008 for &node_id in &inline_roots {
3009 let Some(node) = self.get_node(node_id) else {
3010 continue;
3011 };
3012 let Some(element_data) = node.element_data() else {
3013 continue;
3014 };
3015 let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
3016 continue;
3017 };
3018
3019 let text_len = inline_layout.text.len();
3020
3021 if node_id == first_node && node_id == last_node {
3022 let start = first_offset.min(last_offset);
3023 let end = first_offset.max(last_offset);
3024 if start < end && end <= text_len {
3025 ranges.push((node_id, start, end));
3026 }
3027 } else if node_id == first_node {
3028 if first_offset < text_len {
3029 ranges.push((node_id, first_offset, text_len));
3030 }
3031 } else if node_id == last_node {
3032 if last_offset > 0 && last_offset <= text_len {
3033 ranges.push((node_id, 0, last_offset));
3034 }
3035 } else if text_len > 0 {
3036 ranges.push((node_id, 0, text_len));
3037 }
3038 }
3039
3040 ranges
3041 }
3042}
3043
3044#[derive(Debug, Clone, Copy, PartialEq)]
3045pub struct BoundingRect {
3046 pub x: f64,
3047 pub y: f64,
3048 pub width: f64,
3049 pub height: f64,
3050}
3051
3052impl BoundingRect {
3053 pub fn union(self, other: BoundingRect) -> BoundingRect {
3055 let x = self.x.min(other.x);
3056 let y = self.y.min(other.y);
3057 BoundingRect {
3058 x,
3059 y,
3060 width: (self.x + self.width).max(other.x + other.width) - x,
3061 height: (self.y + self.height).max(other.y + other.height) - y,
3062 }
3063 }
3064}
3065
3066fn snap_to_layout_unit(value: f64) -> f64 {
3070 (value * 64.0).round() / 64.0
3071}
3072
3073impl AsRef<BaseDocument> for BaseDocument {
3074 fn as_ref(&self) -> &BaseDocument {
3075 self
3076 }
3077}
3078
3079impl AsMut<BaseDocument> for BaseDocument {
3080 fn as_mut(&mut self) -> &mut BaseDocument {
3081 self
3082 }
3083}
3084
3085#[cfg(test)]
3086mod zoom_tests {
3087 use super::*;
3088 use apace_traits::shell::ColorScheme;
3089 use std::sync::atomic::{AtomicUsize, Ordering};
3090
3091 #[derive(Default)]
3092 struct CountingShellProvider {
3093 redraw_count: AtomicUsize,
3094 }
3095 impl ShellProvider for CountingShellProvider {
3096 fn request_redraw(&self) {
3097 self.redraw_count.fetch_add(1, Ordering::SeqCst);
3098 }
3099 }
3100
3101 #[test]
3102 fn zoom_requests_redraw() {
3103 let shell_provider = Arc::new(CountingShellProvider::default());
3104 let mut doc = BaseDocument::new(DocumentConfig {
3105 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3106 shell_provider: Some(shell_provider.clone() as _),
3107 ..Default::default()
3108 });
3109 doc.resolve(0.0);
3110
3111 let base = shell_provider.redraw_count.load(Ordering::SeqCst);
3112 doc.zoom_by(0.5);
3113 assert!(shell_provider.redraw_count.load(Ordering::SeqCst) > base);
3114
3115 let base = shell_provider.redraw_count.load(Ordering::SeqCst);
3116 doc.zoom_to(1.0);
3117 assert!(shell_provider.redraw_count.load(Ordering::SeqCst) > base);
3118 }
3119}
3120
3121#[cfg(test)]
3122mod hover_state_tests {
3123 use super::*;
3124 use crate::{Attribute, qual_name};
3125 use apace_traits::shell::ColorScheme;
3126
3127 fn make_doc() -> (BaseDocument, NodeId) {
3134 let mut doc = BaseDocument::new(DocumentConfig {
3135 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3136 ..Default::default()
3137 });
3138 let root_id = doc.root_node().id;
3139 let style = |value: &str| Attribute {
3140 name: qual_name!("style"),
3141 value: value.to_string(),
3142 };
3143
3144 let mut mutator = doc.mutate();
3145 let html = mutator.create_element(qual_name!("html"), vec![]);
3146 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
3147 let container = mutator.create_element(qual_name!("div"), vec![style("width:300px")]);
3148 let text = mutator.create_text_node("some text");
3149 let block = mutator.create_element(qual_name!("div"), vec![style("height:50px")]);
3150 mutator.append_children(container, &[text, block]);
3151 mutator.append_children(body, &[container]);
3152 mutator.append_children(html, &[body]);
3153 mutator.append_children(root_id, &[html]);
3154 drop(mutator);
3155
3156 doc.resolve(0.0);
3157 (doc, container)
3158 }
3159
3160 fn text_has_size(doc: &BaseDocument, container: NodeId) -> bool {
3164 doc.nodes[container].final_layout().size.height > 50.0
3165 }
3166
3167 #[test]
3173 fn hovering_text_in_anonymous_block_reports_text_cursor() {
3174 let (mut doc, container) = make_doc();
3175 if !text_has_size(&doc, container) {
3176 eprintln!("skipping: no usable font (text measures 0x0)");
3177 return;
3178 }
3179
3180 doc.set_hover_to(5.0, 8.0);
3181 assert!(doc.hover_node_is_text, "expected a text hit");
3182 let hit_id = doc.hover_hit_node_id.expect("expected a hit node");
3183 assert!(
3184 doc.nodes[hit_id].is_anonymous(),
3185 "expected the hit node to be the anonymous inline root"
3186 );
3187 assert_eq!(
3188 doc.get_hover_node_id(),
3189 Some(container),
3190 "expected the stored hover target to be the containing element"
3191 );
3192 assert_eq!(doc.get_cursor(), Some(CursorIcon::Text));
3193 }
3194
3195 #[test]
3198 fn hovering_anonymous_block_whitespace_reports_default_cursor() {
3199 let (mut doc, container) = make_doc();
3200 if !text_has_size(&doc, container) {
3201 eprintln!("skipping: no usable font (text measures 0x0)");
3202 return;
3203 }
3204
3205 doc.set_hover_to(250.0, 8.0);
3206 assert!(!doc.hover_node_is_text);
3207 assert_eq!(doc.get_hover_node_id(), Some(container));
3208 assert_eq!(doc.get_cursor(), Some(CursorIcon::Default));
3209 }
3210}
3211
3212#[cfg(test)]
3213mod hover_invalidation_tests {
3214 use super::*;
3215 use crate::{Attribute, QualName, qual_name};
3216 use apace_traits::shell::ColorScheme;
3217
3218 fn make_doc() -> (BaseDocument, NodeId, NodeId) {
3221 let mut doc = BaseDocument::new(DocumentConfig {
3222 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3223 ..Default::default()
3224 });
3225 let sheet = UserAgentStylesheet::parse(
3226 "div:hover { background-color: rgb(255, 0, 0); } div:hover span { color: rgb(0, 255, 0); }",
3227 );
3228 doc.add_user_agent_stylesheet(sheet);
3229 let root_id = doc.root_node().id;
3230 let style = |value: &str| Attribute {
3231 name: qual_name!("style"),
3232 value: value.to_string(),
3233 };
3234
3235 let mut mutator = doc.mutate();
3236 let html = mutator.create_element(qual_name!("html"), vec![]);
3237 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
3238 let div =
3239 mutator.create_element(qual_name!("div"), vec![style("width:300px;height:100px")]);
3240 let span = mutator.create_element(qual_name!("span"), vec![]);
3241 let text = mutator.create_text_node("some text");
3242 mutator.append_children(span, &[text]);
3243 mutator.append_children(div, &[span]);
3244 mutator.append_children(body, &[div]);
3245 mutator.append_children(html, &[body]);
3246 mutator.append_children(root_id, &[html]);
3247 drop(mutator);
3248
3249 doc.resolve(0.0);
3250 (doc, div, span)
3251 }
3252
3253 fn bg_color(doc: &BaseDocument, id: NodeId) -> String {
3254 format!(
3255 "{:?}",
3256 doc.nodes[id]
3257 .primary_styles()
3258 .unwrap()
3259 .get_background()
3260 .background_color
3261 )
3262 }
3263
3264 fn text_color(doc: &BaseDocument, id: NodeId) -> String {
3265 format!(
3266 "{:?}",
3267 doc.nodes[id].primary_styles().unwrap().clone_color()
3268 )
3269 }
3270
3271 #[test]
3272 fn hover_styles_apply_and_clear() {
3273 let (mut doc, div, span) = make_doc();
3274 let initial_bg = bg_color(&doc, div);
3275 let initial_color = text_color(&doc, span);
3276
3277 doc.set_hover_to(10.0, 10.0);
3279 assert!(doc.nodes[div].is_hovered());
3280 doc.resolve(0.0);
3281 let hovered_bg = bg_color(&doc, div);
3282 let hovered_color = text_color(&doc, span);
3283 assert_ne!(initial_bg, hovered_bg, "hover should change div background");
3284 assert_ne!(
3285 initial_color, hovered_color,
3286 "hover should change span color"
3287 );
3288
3289 doc.set_hover_to(10.0, 200.0);
3291 assert!(!doc.nodes[div].is_hovered());
3292 doc.resolve(0.0);
3293 assert_eq!(
3294 bg_color(&doc, div),
3295 initial_bg,
3296 "unhover should restore div background"
3297 );
3298 assert_eq!(
3299 text_color(&doc, span),
3300 initial_color,
3301 "unhover should restore span color"
3302 );
3303 }
3304
3305 #[test]
3308 fn ancestor_hover_with_link_state_updates_descendant() {
3309 let mut doc = BaseDocument::new(DocumentConfig {
3310 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3311 ..Default::default()
3312 });
3313 let sheet = UserAgentStylesheet::parse(
3314 ".promo:link:hover .headline, .promo:visited:hover .headline { color: rgb(184, 0, 0); text-decoration-line: underline; }",
3315 );
3316 doc.add_user_agent_stylesheet(sheet);
3317 let root_id = doc.root_node().id;
3318 let attr = |name: QualName, value: &str| Attribute {
3319 name,
3320 value: value.to_string(),
3321 };
3322
3323 let mut mutator = doc.mutate();
3324 let html = mutator.create_element(qual_name!("html"), vec![]);
3325 let body = mutator.create_element(
3326 qual_name!("body"),
3327 vec![attr(qual_name!("style"), "margin:0")],
3328 );
3329 let a = mutator.create_element(
3330 qual_name!("a"),
3331 vec![
3332 attr(qual_name!("href"), "https://example.com"),
3333 attr(qual_name!("class"), "promo"),
3334 attr(
3335 qual_name!("style"),
3336 "display:block;width:300px;height:100px",
3337 ),
3338 ],
3339 );
3340 let p = mutator.create_element(qual_name!("p"), vec![]);
3341 let span = mutator.create_element(
3342 qual_name!("span"),
3343 vec![attr(qual_name!("class"), "headline")],
3344 );
3345 let text = mutator.create_text_node("Headline text");
3346 mutator.append_children(span, &[text]);
3347 mutator.append_children(p, &[span]);
3348 mutator.append_children(a, &[p]);
3349 mutator.append_children(body, &[a]);
3350 mutator.append_children(html, &[body]);
3351 mutator.append_children(root_id, &[html]);
3352 drop(mutator);
3353
3354 doc.resolve(0.0);
3355 let initial_color = text_color(&doc, span);
3356
3357 doc.set_hover_to(10.0, 10.0);
3358 assert!(doc.nodes[a].is_hovered());
3359 doc.resolve(0.0);
3360 let hovered_color = text_color(&doc, span);
3361 assert_ne!(
3362 initial_color, hovered_color,
3363 "hovering the anchor should change the headline color"
3364 );
3365
3366 doc.set_hover_to(10.0, 200.0);
3367 assert!(!doc.nodes[a].is_hovered());
3368 doc.resolve(0.0);
3369 assert_eq!(
3370 text_color(&doc, span),
3371 initial_color,
3372 "unhovering the anchor should restore the headline color"
3373 );
3374 }
3375
3376 #[test]
3377 fn stylesheet_change_upgrades_pending_state_only_snapshot() {
3378 let mut doc = BaseDocument::new(DocumentConfig {
3379 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3380 ..Default::default()
3381 });
3382 let root = doc.root_node().id;
3383 let mut m = doc.mutate();
3384 let html = m.create_element(qual_name!("html"), vec![]);
3385 let body = m.create_element(qual_name!("body"), vec![]);
3386 let input = m.create_element(
3387 qual_name!("input"),
3388 vec![
3389 Attribute {
3390 name: qual_name!("type"),
3391 value: "checkbox".into(),
3392 },
3393 Attribute {
3394 name: qual_name!("class"),
3395 value: "target".into(),
3396 },
3397 ],
3398 );
3399 m.append_children(body, &[input]);
3400 m.append_children(html, &[body]);
3401 m.append_children(root, &[html]);
3402 drop(m);
3403 doc.resolve(0.0);
3404 let initial = text_color(&doc, input);
3405 doc.snapshot_node_state_only(input);
3406 assert!(
3407 doc.snapshots
3408 .values()
3409 .any(|snapshot| snapshot.attrs.is_none())
3410 );
3411 BaseDocument::toggle_checkbox(doc.nodes[input].element_data_mut().unwrap());
3412 doc.nodes[input].mark_ancestors_dirty();
3413 doc.add_user_agent_stylesheet(UserAgentStylesheet::parse(
3414 ".target:checked { color: rgb(184, 0, 0); }",
3415 ));
3416 doc.resolve(0.0);
3417 assert_ne!(text_color(&doc, input), initial);
3418 assert!(doc.snapshots.is_empty());
3419 assert!(!doc.nodes[input].has_snapshot());
3420 }
3421
3422 #[test]
3423 fn block_to_inline_retires_layout_children_without_losing_embedded_controls() {
3424 let mut doc = BaseDocument::new(DocumentConfig {
3425 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3426 ..Default::default()
3427 });
3428 let style = |value: &str| Attribute {
3429 name: qual_name!("style"),
3430 value: value.into(),
3431 };
3432 let root = doc.root_node().id;
3433 let mut m = doc.mutate();
3434 let html = m.create_element(qual_name!("html"), vec![]);
3435 let body = m.create_element(qual_name!("body"), vec![style("margin:0")]);
3436 let panel = m.create_element(
3437 qual_name!("div"),
3438 vec![style("display:flex;flex-direction:column;width:300px")],
3439 );
3440 let block = m.create_element(qual_name!("div"), vec![style("width:80px;height:20px")]);
3441 let label = m.create_element(
3442 qual_name!("label"),
3443 vec![style("display:block;width:70px;height:20px")],
3444 );
3445 let input = m.create_element(
3446 qual_name!("input"),
3447 vec![
3448 Attribute {
3449 name: qual_name!("type"),
3450 value: "checkbox".into(),
3451 },
3452 style("width:14px;height:14px"),
3453 ],
3454 );
3455 let text = m.create_text_node("label");
3456 m.append_children(label, &[input, text]);
3457 m.append_children(panel, &[block, label]);
3458 m.append_children(body, &[panel]);
3459 m.append_children(html, &[body]);
3460 m.append_children(root, &[html]);
3461 drop(m);
3462 doc.resolve(0.0);
3463 let initial = doc.nodes.len();
3464 for turn in 0..200 {
3465 let inline = turn % 2 == 0;
3466 let mut m = doc.mutate();
3467 m.set_attribute(
3468 panel,
3469 qual_name!("style"),
3470 if inline {
3471 "width:300px"
3472 } else {
3473 "display:flex;flex-direction:column;width:300px"
3474 },
3475 );
3476 m.set_attribute(
3477 label,
3478 qual_name!("style"),
3479 if inline {
3480 "display:inline"
3481 } else {
3482 "display:block;width:70px;height:20px"
3483 },
3484 );
3485 drop(m);
3486 doc.resolve(0.0);
3487 assert!(
3488 doc.nodes.len() <= initial + 4,
3489 "anonymous nodes grew after turn {turn}"
3490 );
3491 assert!(doc.nodes.contains_key(input));
3492 assert!(doc.nodes[input].final_layout().size.width > 0.0);
3493 if inline {
3494 assert!(doc.nodes[label].layout_children.borrow().is_none());
3495 assert!(
3496 doc.nodes[label]
3497 .element_data()
3498 .unwrap()
3499 .inline_layout_data
3500 .is_none()
3501 );
3502 assert!(!doc.nodes[label].flags.is_inline_root());
3503 }
3504 }
3505 }
3506
3507 #[test]
3508 fn changing_only_wrap_mode_recollects_inherited_text_layout() {
3509 let mut doc = BaseDocument::new(DocumentConfig {
3510 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3511 ..Default::default()
3512 });
3513 doc.add_user_agent_stylesheet(UserAgentStylesheet::parse(
3514 "html,body,div,p{display:block}p{margin:0}",
3515 ));
3516 let root = doc.root_node().id;
3517 let mut m = doc.mutate();
3518 let html = m.create_element(qual_name!("html"), vec![]);
3519 let body = m.create_element(qual_name!("body"), vec![]);
3520 let panel = m.create_element(qual_name!("div"), vec![]);
3521 let paragraph = m.create_element(qual_name!("p"), vec![]);
3522 let text = m.create_text_node("alpha beta gamma delta epsilon zeta eta theta");
3523 m.append_children(paragraph, &[text]);
3524 m.append_children(panel, &[paragraph]);
3525 m.append_children(body, &[panel]);
3526 m.append_children(html, &[body]);
3527 m.append_children(root, &[html]);
3528 drop(m);
3529 for turn in 0..40 {
3530 let wrap = turn % 2 == 1;
3531 let mut m = doc.mutate();
3532 m.set_attribute(
3533 panel,
3534 qual_name!("style"),
3535 &format!(
3536 "width:120px;font:16px/20px monospace;white-space:{}",
3537 if wrap { "normal" } else { "nowrap" }
3538 ),
3539 );
3540 drop(m);
3541 doc.resolve(0.0);
3542 let height = doc.nodes[paragraph].final_layout().size.height;
3543 assert!(
3544 if wrap {
3545 height >= 60.0
3546 } else {
3547 (height - 20.0).abs() < 0.03
3548 },
3549 "turn={turn}, wrap={wrap}, height={height}"
3550 );
3551 }
3552 }
3553
3554 #[test]
3555 fn focus_within_tracks_only_live_ancestry_through_move_clear_and_removal() {
3556 let mut doc = BaseDocument::new(DocumentConfig {
3557 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3558 ..Default::default()
3559 });
3560 let root = doc.root_node().id;
3561 let mut m = doc.mutate();
3562 let html = m.create_element(qual_name!("html"), vec![]);
3563 let body = m.create_element(qual_name!("body"), vec![]);
3564 let left = m.create_element(qual_name!("label"), vec![]);
3565 let right = m.create_element(qual_name!("label"), vec![]);
3566 let input = m.create_element(
3567 qual_name!("input"),
3568 vec![Attribute {
3569 name: qual_name!("type"),
3570 value: "checkbox".into(),
3571 }],
3572 );
3573 m.append_children(left, &[input]);
3574 m.append_children(body, &[left, right]);
3575 m.append_children(html, &[body]);
3576 m.append_children(root, &[html]);
3577 drop(m);
3578 doc.resolve(0.0);
3579 let within = |doc: &BaseDocument, id| {
3580 doc.nodes[id]
3581 .element_state()
3582 .contains(ElementState::FOCUS_WITHIN)
3583 };
3584 assert!(doc.set_focus_to(input));
3585 assert!(within(&doc, input) && within(&doc, left) && within(&doc, body));
3586 assert!(!within(&doc, right));
3587 assert!(!doc.set_focus_to(input));
3588 doc.mutate().append_children(right, &[input]);
3589 assert_eq!(doc.get_focussed_node_id(), Some(input));
3590 assert!(!within(&doc, left));
3591 assert!(within(&doc, right) && within(&doc, body) && within(&doc, input));
3592 doc.resolve(0.0);
3593 doc.clear_focus();
3594 assert!(!within(&doc, right) && !within(&doc, body) && !within(&doc, input));
3595 doc.set_focus_to(input);
3596 doc.mutate().remove_and_drop_node(right);
3597 assert_eq!(doc.focus_node_id, None);
3599 assert!(!within(&doc, body));
3600 assert!(!doc.nodes.contains_key(input));
3601 doc.resolve(0.0);
3602 }
3603
3604 #[test]
3606 fn checkbox_toggle_updates_checked_styles() {
3607 let mut doc = BaseDocument::new(DocumentConfig {
3608 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3609 ..Default::default()
3610 });
3611 let sheet = UserAgentStylesheet::parse(
3612 "input:checked { color: rgb(184, 0, 0); } input:checked + label { color: rgb(0, 184, 0); }",
3613 );
3614 doc.add_user_agent_stylesheet(sheet);
3615 let root_id = doc.root_node().id;
3616 let attr = |name: QualName, value: &str| Attribute {
3617 name,
3618 value: value.to_string(),
3619 };
3620
3621 let mut mutator = doc.mutate();
3622 let html = mutator.create_element(qual_name!("html"), vec![]);
3623 let body = mutator.create_element(
3624 qual_name!("body"),
3625 vec![attr(qual_name!("style"), "margin:0")],
3626 );
3627 let input = mutator.create_element(
3628 qual_name!("input"),
3629 vec![attr(qual_name!("type"), "checkbox")],
3630 );
3631 let label = mutator.create_element(qual_name!("label"), vec![]);
3632 let text = mutator.create_text_node("label text");
3633 mutator.append_children(label, &[text]);
3634 mutator.append_children(body, &[input, label]);
3635 mutator.append_children(html, &[body]);
3636 mutator.append_children(root_id, &[html]);
3637 drop(mutator);
3638
3639 doc.resolve(0.0);
3640 let initial_input_color = text_color(&doc, input);
3641 let initial_label_color = text_color(&doc, label);
3642
3643 doc.snapshot_node_and(input, ElementState::CHECKED, |node| {
3645 if let Some(el) = node.element_data_mut() {
3646 BaseDocument::toggle_checkbox(el);
3647 }
3648 node.mark_ancestors_dirty();
3649 });
3650 doc.resolve(0.0);
3651 assert_ne!(
3652 text_color(&doc, input),
3653 initial_input_color,
3654 "checking should change the input color"
3655 );
3656 assert_ne!(
3657 text_color(&doc, label),
3658 initial_label_color,
3659 "checking should change the sibling label color"
3660 );
3661
3662 doc.snapshot_node_and(input, ElementState::CHECKED, |node| {
3664 if let Some(el) = node.element_data_mut() {
3665 BaseDocument::toggle_checkbox(el);
3666 }
3667 node.mark_ancestors_dirty();
3668 });
3669 doc.resolve(0.0);
3670 assert_eq!(
3671 text_color(&doc, input),
3672 initial_input_color,
3673 "unchecking should restore the input color"
3674 );
3675 assert_eq!(
3676 text_color(&doc, label),
3677 initial_label_color,
3678 "unchecking should restore the sibling label color"
3679 );
3680 }
3681
3682 #[test]
3686 fn hover_updates_existing_pseudo_element_style() {
3687 let mut doc = BaseDocument::new(DocumentConfig {
3688 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3689 ..Default::default()
3690 });
3691 let sheet = UserAgentStylesheet::parse(
3692 "div::before { content: \"x\"; color: rgb(1, 2, 3); } \
3693 div:hover::before { color: rgb(0, 0, 255); }",
3694 );
3695 doc.add_user_agent_stylesheet(sheet);
3696 let root_id = doc.root_node().id;
3697 let style = |value: &str| Attribute {
3698 name: qual_name!("style"),
3699 value: value.to_string(),
3700 };
3701
3702 let mut mutator = doc.mutate();
3703 let html = mutator.create_element(qual_name!("html"), vec![]);
3704 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
3705 let div = mutator.create_element(
3706 qual_name!("div"),
3707 vec![style("display:block;width:300px;height:100px")],
3708 );
3709 let text = mutator.create_text_node("some text");
3710 mutator.append_children(div, &[text]);
3711 mutator.append_children(body, &[div]);
3712 mutator.append_children(html, &[body]);
3713 mutator.append_children(root_id, &[html]);
3714 drop(mutator);
3715
3716 doc.resolve(0.0);
3717 let before = doc.nodes[div].before().expect("::before node should exist");
3718 let initial_color = text_color(&doc, before);
3719
3720 doc.set_hover_to(10.0, 10.0);
3721 assert!(doc.nodes[div].is_hovered());
3722 doc.resolve(0.0);
3723 assert_ne!(
3724 text_color(&doc, before),
3725 initial_color,
3726 "hover should change the ::before color"
3727 );
3728
3729 doc.set_hover_to(10.0, 200.0);
3730 assert!(!doc.nodes[div].is_hovered());
3731 doc.resolve(0.0);
3732 assert_eq!(
3733 text_color(&doc, before),
3734 initial_color,
3735 "unhover should restore the ::before color"
3736 );
3737 }
3738
3739 #[test]
3743 fn hover_updates_background_image_layers() {
3744 let mut doc = BaseDocument::new(DocumentConfig {
3745 viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
3746 ..Default::default()
3747 });
3748 let sheet = UserAgentStylesheet::parse(
3749 "div { background-image: url(\"https://example.com/a.png\"); } \
3750 div:hover { background-image: url(\"https://example.com/b.png\"); }",
3751 );
3752 doc.add_user_agent_stylesheet(sheet);
3753 let root_id = doc.root_node().id;
3754 let style = |value: &str| Attribute {
3755 name: qual_name!("style"),
3756 value: value.to_string(),
3757 };
3758
3759 let mut mutator = doc.mutate();
3760 let html = mutator.create_element(qual_name!("html"), vec![]);
3761 let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
3762 let div = mutator.create_element(
3763 qual_name!("div"),
3764 vec![style("display:block;width:300px;height:100px")],
3765 );
3766 mutator.append_children(body, &[div]);
3767 mutator.append_children(html, &[body]);
3768 mutator.append_children(root_id, &[html]);
3769 drop(mutator);
3770
3771 let background_image_url = |doc: &BaseDocument, id: NodeId| -> Option<String> {
3772 let elem = doc.nodes[id].data.downcast_element().unwrap();
3773 elem.background_images
3774 .first()
3775 .and_then(|img| img.as_ref())
3776 .map(|img| img.url.as_str().to_string())
3777 };
3778
3779 doc.resolve(0.0);
3780 assert_eq!(
3781 background_image_url(&doc, div).as_deref(),
3782 Some("https://example.com/a.png"),
3783 "initial resolve should flush the background image"
3784 );
3785
3786 doc.set_hover_to(10.0, 10.0);
3787 assert!(doc.nodes[div].is_hovered());
3788 doc.resolve(0.0);
3789 assert_eq!(
3790 background_image_url(&doc, div).as_deref(),
3791 Some("https://example.com/b.png"),
3792 "hover should flush the changed background image"
3793 );
3794
3795 doc.set_hover_to(10.0, 200.0);
3796 assert!(!doc.nodes[div].is_hovered());
3797 doc.resolve(0.0);
3798 assert_eq!(
3799 background_image_url(&doc, div).as_deref(),
3800 Some("https://example.com/a.png"),
3801 "unhover should flush the restored background image"
3802 );
3803 }
3804}
3805
3806#[cfg(test)]
3807mod font_face_override_tests {
3808 use super::*;
3809 use crate::net::{FontFaceOverrides, Resource, ResourceLoadResponse};
3810
3811 #[test]
3827 fn font_face_overrides_alias_family_name() {
3828 const ALIAS: &str = "AliasedFamily";
3829
3830 let mut document = BaseDocument::new(DocumentConfig::default());
3831
3832 {
3834 let mut ctx = document.font_ctx.lock().unwrap();
3835 assert!(
3836 ctx.collection.family_id(ALIAS).is_none(),
3837 "alias must not exist before registration",
3838 );
3839 }
3840
3841 let response = ResourceLoadResponse {
3846 ticket: None,
3847 request_id: 0,
3848 node_id: None,
3849 resolved_url: Some(String::from("test://aliased-family")),
3850 result: Ok(Resource::Font(
3851 apace_traits::net::Bytes::from_static(crate::BULLET_FONT),
3852 FontFaceOverrides {
3853 family_name: Some(String::from(ALIAS)),
3854 weight: Some(800.0),
3855 style: Some(parley::fontique::FontStyle::Italic),
3856 },
3857 )),
3858 };
3859 document.load_resource(response);
3860
3861 let mut ctx = document.font_ctx.lock().unwrap();
3864 let family_id = ctx
3865 .collection
3866 .family_id(ALIAS)
3867 .expect("CSS-declared family name should be registered as a family alias");
3868 let resolved_name = ctx
3869 .collection
3870 .family_name(family_id)
3871 .expect("family id should resolve back to a name");
3872 assert_eq!(
3873 resolved_name, ALIAS,
3874 "registered family should report the CSS-declared name, \
3875 not the font file's internal `name` table entry",
3876 );
3877 }
3878}