1use crate::events::{DragMode, ScrollAnimationState, handle_dom_event};
2use crate::font_metrics::BlitzFontMetricsProvider;
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::selection::TextSelection;
11use crate::stylo_to_cursor_icon::stylo_to_cursor_icon;
12use crate::traversal::TreeTraverser;
13use crate::url::DocumentUrl;
14use crate::util::ImageType;
15use crate::{
16 DEFAULT_CSS, DocumentConfig, DocumentMutator, DummyHtmlParserProvider, ElementData,
17 EventDriver, HtmlParserProvider, Node, NodeData, NoopEventHandler, StyleThreading,
18 TextNodeData,
19};
20use blitz_traits::devtools::DevtoolSettings;
21use blitz_traits::events::{BlitzScrollEvent, DomEvent, DomEventData, HitResult, UiEvent};
22use blitz_traits::navigation::{DummyNavigationProvider, NavigationProvider};
23use blitz_traits::net::{AbortSignal, DummyNetProvider, NetProvider, Request};
24use blitz_traits::shell::{ColorScheme, DummyShellProvider, ShellProvider, Viewport};
25use cursor_icon::CursorIcon;
26use linebender_resource_handle::Blob;
27use markup5ever::{local_name, ns};
28use parley::{FontContext, PlainEditorDriver};
29use selectors::{Element, matching::QuirksMode};
30use slab::Slab;
31use std::any::Any;
32use std::cell::RefCell;
33use std::collections::{BTreeMap, Bound, HashMap, HashSet};
34use std::ops::{Deref, DerefMut};
35use std::rc::Rc;
36use std::str::FromStr;
37use std::sync::atomic::{AtomicUsize, Ordering};
38use std::sync::mpsc::{Receiver, Sender, channel};
39use std::sync::{Arc, Mutex, MutexGuard, OnceLock, RwLockReadGuard, RwLockWriteGuard};
40use std::task::{Context as TaskContext, Waker};
41use style::Atom;
42use style::animation::DocumentAnimationSet;
43use style::attr::{AttrIdentifier, AttrValue};
44use style::data::{ElementData as StyloElementData, ElementStyles};
45use style::media_queries::MediaType;
46use style::properties::ComputedValues;
47use style::properties::style_structs::Font;
48use style::queries::values::PrefersColorScheme;
49use style::selector_parser::ServoElementSnapshot;
50use style::servo::media_features::PointerCapabilities;
51use style::servo_arc::Arc as ServoArc;
52use style::values::GenericAtomIdent;
53use style::values::computed::ui::CursorKind;
54use style::values::computed::{Overflow, UserSelect};
55use style::{
56 device::Device,
57 dom::{TDocument, TNode},
58 media_queries::MediaList,
59 selector_parser::SnapshotMap,
60 shared_lock::{SharedRwLock, StylesheetGuards},
61 stylesheets::{AllowImportRules, DocumentStyleSheet, Origin, Stylesheet},
62 stylist::Stylist,
63};
64use url::Url;
65use web_time::Instant;
66
67#[cfg(feature = "parallel-construct")]
68use thread_local::ThreadLocal;
69
70pub enum DocGuard<'a> {
71 Ref(&'a BaseDocument),
72 RefCell(std::cell::Ref<'a, BaseDocument>),
73 RwLock(RwLockReadGuard<'a, BaseDocument>),
74 Mutex(MutexGuard<'a, BaseDocument>),
75}
76
77impl Deref for DocGuard<'_> {
78 type Target = BaseDocument;
79 #[inline(always)]
80 fn deref(&self) -> &Self::Target {
81 match self {
82 Self::Ref(base_document) => base_document,
83 Self::RefCell(refcell_guard) => refcell_guard,
84 Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
85 Self::Mutex(mutex_guard) => mutex_guard,
86 }
87 }
88}
89
90pub enum DocGuardMut<'a> {
91 Ref(&'a mut BaseDocument),
92 RefCell(std::cell::RefMut<'a, BaseDocument>),
93 RwLock(RwLockWriteGuard<'a, BaseDocument>),
94 Mutex(MutexGuard<'a, BaseDocument>),
95}
96
97impl Deref for DocGuardMut<'_> {
98 type Target = BaseDocument;
99 #[inline(always)]
100 fn deref(&self) -> &Self::Target {
101 match self {
102 Self::Ref(base_document) => base_document,
103 Self::RefCell(refcell_guard) => refcell_guard,
104 Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
105 Self::Mutex(mutex_guard) => mutex_guard,
106 }
107 }
108}
109
110impl DerefMut for DocGuardMut<'_> {
111 #[inline(always)]
112 fn deref_mut(&mut self) -> &mut Self::Target {
113 match self {
114 Self::Ref(base_document) => base_document,
115 Self::RefCell(refcell_guard) => &mut *refcell_guard,
116 Self::RwLock(rw_lock_read_guard) => &mut *rw_lock_read_guard,
117 Self::Mutex(mutex_guard) => &mut *mutex_guard,
118 }
119 }
120}
121
122pub trait Document: Any + 'static {
125 fn inner(&self) -> DocGuard<'_>;
126 fn inner_mut(&mut self) -> DocGuardMut<'_>;
127
128 fn handle_ui_event(&mut self, event: UiEvent) {
130 let mut doc = self.inner_mut();
131 let mut driver = EventDriver::new(&mut *doc, NoopEventHandler);
132 driver.handle_ui_event(event);
133 }
134
135 fn poll(&mut self, task_context: Option<TaskContext>) -> bool {
137 let _ = task_context;
139 false
140 }
141
142 fn id(&self) -> usize {
144 self.inner().id
145 }
146}
147
148pub struct PlainDocument(pub BaseDocument);
149impl Document for PlainDocument {
150 fn inner(&self) -> DocGuard<'_> {
151 DocGuard::Ref(&self.0)
152 }
153 fn inner_mut(&mut self) -> DocGuardMut<'_> {
154 DocGuardMut::Ref(&mut self.0)
155 }
156}
157
158impl Document for BaseDocument {
159 fn inner(&self) -> DocGuard<'_> {
160 DocGuard::Ref(self)
161 }
162 fn inner_mut(&mut self) -> DocGuardMut<'_> {
163 DocGuardMut::Ref(self)
164 }
165}
166
167impl Document for Rc<RefCell<BaseDocument>> {
168 fn inner(&self) -> DocGuard<'_> {
169 DocGuard::RefCell(self.borrow())
170 }
171
172 fn inner_mut(&mut self) -> DocGuardMut<'_> {
173 DocGuardMut::RefCell(self.borrow_mut())
174 }
175}
176
177pub enum DocumentEvent {
178 ResourceLoad(ResourceLoadResponse),
179}
180
181pub struct BaseDocument {
182 id: usize,
184
185 pub(crate) url: DocumentUrl,
188 pub(crate) devtool_settings: DevtoolSettings,
190 pub(crate) viewport: Viewport,
192 pub(crate) viewport_scroll: crate::Point<f64>,
194 pub(crate) media_type: MediaType,
196 pub(crate) style_threading: StyleThreading,
198 pub(crate) incremental_layout: bool,
203
204 pub(crate) tx: Sender<DocumentEvent>,
206 pub(crate) rx: Option<Receiver<DocumentEvent>>,
208
209 pub(crate) nodes: Box<Slab<Node>>,
214
215 pub(crate) stylist: Stylist,
218 pub(crate) animations: DocumentAnimationSet,
219 pub(crate) guard: SharedRwLock,
221 pub(crate) snapshots: SnapshotMap,
223
224 pub(crate) font_ctx: Arc<Mutex<parley::FontContext>>,
227 #[cfg(feature = "parallel-construct")]
228 pub(crate) thread_font_contexts: ThreadLocal<RefCell<Box<FontContext>>>,
230 pub(crate) layout_ctx: parley::LayoutContext<TextBrush>,
232
233 pub(crate) hover_node_id: Option<usize>,
235 pub(crate) hover_node_is_text: bool,
237 pub(crate) focus_node_id: Option<usize>,
239 pub(crate) active_node_id: Option<usize>,
241 pub(crate) mousedown_node_id: Option<usize>,
243 pub(crate) last_mousedown_time: Option<Instant>,
245 pub(crate) mousedown_position: taffy::Point<f32>,
247 pub(crate) click_count: u16,
249 pub(crate) drag_mode: DragMode,
251 pub(crate) hovered_scrollbar: Option<crate::node::ScrollbarRef>,
253 pub(crate) scrollbar_activity: HashMap<usize, Instant>,
256 pub(crate) scroll_animation: ScrollAnimationState,
258
259 pub(crate) text_selection: TextSelection,
261
262 pub(crate) has_active_animations: bool,
265 pub(crate) has_canvas: bool,
267 pub(crate) subdoc_is_animating: bool,
269
270 pub(crate) nodes_to_id: HashMap<String, usize>,
272 pub(crate) nodes_to_stylesheet: BTreeMap<usize, DocumentStyleSheet>,
274 pub(crate) ua_stylesheets: HashMap<String, DocumentStyleSheet>,
277 pub(crate) controls_to_form: HashMap<usize, usize>,
279 pub(crate) sub_document_nodes: HashSet<usize>,
281 pub(crate) changed_nodes: HashSet<usize>,
283 pub(crate) deferred_construction_nodes: Vec<ConstructionTask>,
285
286 #[cfg(feature = "custom-widget")]
288 pub(crate) custom_widget_nodes: HashSet<usize>,
289 #[cfg(feature = "custom-widget")]
291 pub(crate) pending_resource_deallocations: Vec<anyrender::ResourceId>,
292
293 pub(crate) image_cache: HashMap<String, ImageData>,
296
297 pub(crate) pending_images: HashMap<String, Vec<(usize, ImageType)>>,
301
302 pub(crate) pending_critical_resources: HashSet<usize>,
304
305 pub net_provider: Arc<dyn NetProvider>,
308 pub navigation_provider: Arc<dyn NavigationProvider>,
311 pub shell_provider: Arc<dyn ShellProvider>,
313 pub html_parser_provider: Arc<dyn HtmlParserProvider>,
315 pub(crate) abort_signal: Option<AbortSignal>,
319}
320
321pub(crate) fn make_device(
322 viewport: &Viewport,
323 media_type: MediaType,
324 font_ctx: Arc<Mutex<FontContext>>,
325) -> Device {
326 let width = viewport.window_size.0 as f32 / viewport.scale();
327 let height = viewport.window_size.1 as f32 / viewport.scale();
328 let viewport_size = euclid::Size2D::new(width, height);
329 let device_size = euclid::Size2D::new(width, height) * viewport.scale();
330 let device_pixel_ratio = euclid::Scale::new(viewport.scale());
331
332 Device::new(
333 media_type,
334 selectors::matching::QuirksMode::NoQuirks,
335 viewport_size,
336 device_size,
337 device_pixel_ratio,
338 Box::new(BlitzFontMetricsProvider { font_ctx }),
339 ComputedValues::initial_values_with_font_override(Font::initial_values()),
340 match viewport.color_scheme {
341 ColorScheme::Light => PrefersColorScheme::Light,
342 ColorScheme::Dark => PrefersColorScheme::Dark,
343 },
344 PointerCapabilities::default(),
345 PointerCapabilities::default(),
346 )
347}
348
349impl BaseDocument {
350 pub fn new(config: DocumentConfig) -> Self {
352 static ID_GENERATOR: AtomicUsize = AtomicUsize::new(1);
353
354 let id = ID_GENERATOR.fetch_add(1, Ordering::SeqCst);
355
356 let font_ctx = config
357 .font_ctx
358 .map(|mut font_ctx| {
359 font_ctx.source_cache.make_shared();
360 font_ctx
362 })
363 .unwrap_or_else(|| {
364 use parley::fontique::{Collection, CollectionOptions, SourceCache};
365 let mut font_ctx = FontContext {
366 source_cache: SourceCache::new_shared(),
367 collection: Collection::new(CollectionOptions {
368 shared: false,
369 system_fonts: cfg!(all(
370 feature = "system-fonts",
371 not(target_arch = "wasm32")
372 )),
373 }),
374 };
375 font_ctx
376 .collection
377 .register_fonts(Blob::new(Arc::new(crate::BULLET_FONT) as _), None);
378 font_ctx
379 });
380 let font_ctx = Arc::new(Mutex::new(font_ctx));
381
382 style_config::set_pref!("layout.grid.enabled", true);
384 style_config::set_pref!("layout.unimplemented", true);
385 style_config::set_pref!("layout.columns.enabled", true);
386 style_config::set_pref!("layout.css.basic-shape-shape.enabled", true);
387 style_config::set_pref!("layout.threads", -1);
388
389 let viewport = config.viewport.unwrap_or_default();
390 let media_type = config.media_type.unwrap_or_else(MediaType::screen);
391 let device = make_device(&viewport, media_type.clone(), font_ctx.clone());
392 let stylist = Stylist::new(device, QuirksMode::NoQuirks);
393 let snapshots = SnapshotMap::new();
394 let nodes = Box::new(Slab::new());
395 let guard = SharedRwLock::new();
396 let nodes_to_id = HashMap::new();
397
398 let base_url = config
399 .base_url
400 .and_then(|url| DocumentUrl::from_str(&url).ok())
401 .unwrap_or_default();
402
403 let net_provider = config
404 .net_provider
405 .unwrap_or_else(|| Arc::new(DummyNetProvider));
406 let navigation_provider = config
407 .navigation_provider
408 .unwrap_or_else(|| Arc::new(DummyNavigationProvider));
409 let shell_provider = config
410 .shell_provider
411 .unwrap_or_else(|| Arc::new(DummyShellProvider));
412 let html_parser_provider = config
413 .html_parser_provider
414 .unwrap_or_else(|| Arc::new(DummyHtmlParserProvider));
415
416 let (tx, rx) = channel();
417
418 let mut doc = Self {
419 id,
420 tx,
421 rx: Some(rx),
422
423 guard,
424 nodes,
425 stylist,
426 animations: DocumentAnimationSet::default(),
427 snapshots,
428 nodes_to_id,
429 viewport,
430 media_type,
431 style_threading: config.style_threading,
432 incremental_layout: cfg!(feature = "incremental"),
433 devtool_settings: DevtoolSettings::default(),
434 viewport_scroll: crate::Point::ZERO,
435 url: base_url,
436 ua_stylesheets: HashMap::new(),
437 nodes_to_stylesheet: BTreeMap::new(),
438 font_ctx,
439 #[cfg(feature = "parallel-construct")]
440 thread_font_contexts: ThreadLocal::new(),
441 layout_ctx: parley::LayoutContext::new(),
442
443 hover_node_id: None,
444 hover_node_is_text: false,
445 focus_node_id: None,
446 active_node_id: None,
447 mousedown_node_id: None,
448 has_active_animations: false,
449 subdoc_is_animating: false,
450 has_canvas: false,
451 sub_document_nodes: HashSet::new(),
452
453 #[cfg(feature = "custom-widget")]
454 custom_widget_nodes: HashSet::new(),
455 #[cfg(feature = "custom-widget")]
456 pending_resource_deallocations: Vec::new(),
457
458 changed_nodes: HashSet::new(),
459 deferred_construction_nodes: Vec::new(),
460 image_cache: HashMap::new(),
461 pending_images: HashMap::new(),
462 pending_critical_resources: HashSet::new(),
463 controls_to_form: HashMap::new(),
464 net_provider,
465 navigation_provider,
466 shell_provider,
467 html_parser_provider,
468 abort_signal: config.abort_signal,
469 last_mousedown_time: None,
470 mousedown_position: taffy::Point::ZERO,
471 click_count: 0,
472 drag_mode: DragMode::None,
473 hovered_scrollbar: None,
474 scrollbar_activity: HashMap::new(),
475 scroll_animation: ScrollAnimationState::None,
476 text_selection: TextSelection::default(),
477 };
478
479 doc.create_node(NodeData::Document);
481 doc.root_node_mut().flags.insert(NodeFlags::IS_IN_DOCUMENT);
482
483 match config.ua_stylesheets {
484 Some(stylesheets) => {
485 for ss in &stylesheets {
486 doc.add_user_agent_stylesheet(ss);
487 }
488 }
489 None => doc.add_user_agent_stylesheet(DEFAULT_CSS),
490 }
491
492 let stylo_element_data = StyloElementData {
494 styles: ElementStyles {
495 primary: Some(
496 ComputedValues::initial_values_with_font_override(Font::initial_values())
497 .to_arc(),
498 ),
499 ..Default::default()
500 },
501 ..Default::default()
502 };
503 let stylo_data = &mut doc.root_node_mut().stylo_element_data;
504 *stylo_data.ensure_init_mut() = stylo_element_data;
505
506 doc
507 }
508
509 pub fn set_net_provider(&mut self, net_provider: Arc<dyn NetProvider>) {
511 self.net_provider = net_provider;
512 }
513
514 pub fn set_navigation_provider(&mut self, navigation_provider: Arc<dyn NavigationProvider>) {
516 self.navigation_provider = navigation_provider;
517 }
518
519 pub fn set_shell_provider(&mut self, shell_provider: Arc<dyn ShellProvider>) {
521 self.shell_provider = shell_provider;
522 }
523
524 pub fn set_html_parser_provider(&mut self, html_parser_provider: Arc<dyn HtmlParserProvider>) {
526 self.html_parser_provider = html_parser_provider;
527 }
528
529 pub fn set_base_url(&mut self, url: &str) {
531 self.url = DocumentUrl::from(Url::parse(url).unwrap());
532 }
533
534 pub fn guard(&self) -> &SharedRwLock {
535 &self.guard
536 }
537
538 pub fn tree(&self) -> &Slab<Node> {
539 &self.nodes
540 }
541
542 pub fn id(&self) -> usize {
543 self.id
544 }
545
546 pub(crate) fn build_request(&self, url: url::Url) -> Request {
549 crate::net::stamped_request(url, self.abort_signal.as_ref())
550 }
551
552 pub fn favicon_url(&self) -> Option<String> {
553 self.tree().iter().find_map(|(_, node)| {
554 let data = &node.data;
555 if !data.is_element_with_tag_name(&local_name!("link")) {
556 return None;
557 }
558 let rel = data.attr(local_name!("rel"))?;
559 if !rel
560 .split_ascii_whitespace()
561 .any(|v| v.eq_ignore_ascii_case("icon"))
562 {
563 return None;
564 }
565 data.attr(local_name!("href")).map(|s| s.to_string())
566 })
567 }
568
569 pub fn get_node(&self, node_id: usize) -> Option<&Node> {
570 self.nodes.get(node_id)
571 }
572
573 pub fn get_node_mut(&mut self, node_id: usize) -> Option<&mut Node> {
574 self.nodes.get_mut(node_id)
575 }
576
577 pub fn get_focussed_node_id(&self) -> Option<usize> {
578 self.focus_node_id
579 .or(self.try_root_element().map(|el| el.id))
580 }
581
582 pub fn mutate<'doc>(&'doc mut self) -> DocumentMutator<'doc> {
583 DocumentMutator::new(self)
584 }
585
586 pub fn handle_dom_event<F: FnMut(DomEvent)>(
587 &mut self,
588 event: &mut DomEvent,
589 dispatch_event: F,
590 ) {
591 handle_dom_event(self, event, dispatch_event)
592 }
593
594 pub fn as_any_mut(&mut self) -> &mut dyn Any {
595 self
596 }
597
598 pub fn label_bound_input_element(&self, label_node_id: usize) -> Option<&Node> {
605 let label_element = self.nodes[label_node_id].element_data()?;
606 if let Some(target_element_dom_id) = label_element.attr(local_name!("for")) {
607 TreeTraverser::new(self)
608 .filter_map(|id| {
609 let node = self.get_node(id)?;
610 let element_data = node.element_data()?;
611 if element_data.name.local != local_name!("input") {
612 return None;
613 }
614 let id = element_data.id.as_ref()?;
615 if *id == *target_element_dom_id {
616 Some(node)
617 } else {
618 None
619 }
620 })
621 .next()
622 } else {
623 TreeTraverser::new_with_root(self, label_node_id)
624 .filter_map(|child_id| {
625 let node = self.get_node(child_id)?;
626 let element_data = node.element_data()?;
627 if element_data.name.local == local_name!("input") {
628 Some(node)
629 } else {
630 None
631 }
632 })
633 .next()
634 }
635 }
636
637 pub fn toggle_checkbox(el: &mut ElementData) -> bool {
638 let Some(is_checked) = el.checkbox_input_checked_mut() else {
639 return false;
640 };
641 *is_checked = !*is_checked;
642
643 *is_checked
644 }
645
646 pub fn toggle_radio(&mut self, radio_set_name: String, target_radio_id: usize) {
647 for i in 0..self.nodes.len() {
648 let node = &mut self.nodes[i];
649 if let Some(node_data) = node.data.downcast_element_mut() {
650 if node_data.attr(local_name!("name")) == Some(&radio_set_name) {
651 let was_clicked = i == target_radio_id;
652 let Some(is_checked) = node_data.checkbox_input_checked_mut() else {
653 continue;
654 };
655 *is_checked = was_clicked;
656 }
657 }
658 }
659 }
660
661 pub fn toggle_details_open(&mut self, details_id: usize) {
665 use crate::qual_name;
666
667 let node = &self.nodes[details_id];
668 if !node.data.is_element_with_tag_name(&local_name!("details")) {
669 return;
670 }
671 let is_open = node.data.has_attr(local_name!("open"));
672
673 let mut mutator = self.mutate();
677 if is_open {
678 mutator.clear_attribute(details_id, qual_name!("open"));
679 } else {
680 mutator.set_attribute(details_id, qual_name!("open"), "");
681 }
682 drop(mutator);
683
684 self.shell_provider.request_redraw();
685 }
686
687 pub fn set_style_property(&mut self, node_id: usize, name: &str, value: &str) {
688 let node = &mut self.nodes[node_id];
689 let did_change = node.element_data_mut().unwrap().set_style_property(
690 name,
691 value,
692 &self.guard,
693 self.url.url_extra_data(),
694 );
695 if did_change {
696 node.mark_style_attr_updated();
697 }
698 }
699
700 pub fn remove_style_property(&mut self, node_id: usize, name: &str) {
701 let node = &mut self.nodes[node_id];
702 let did_change = node.element_data_mut().unwrap().remove_style_property(
703 name,
704 &self.guard,
705 self.url.url_extra_data(),
706 );
707 if did_change {
708 node.mark_style_attr_updated();
709 }
710 }
711
712 pub fn sub_document_node_ids(&self) -> Vec<usize> {
713 self.sub_document_nodes.iter().copied().collect()
714 }
715
716 pub fn set_sub_document(&mut self, node_id: usize, sub_document: Box<dyn Document>) {
717 self.nodes[node_id]
718 .element_data_mut()
719 .unwrap()
720 .set_sub_document(sub_document);
721 self.sub_document_nodes.insert(node_id);
722 }
723
724 pub fn remove_sub_document(&mut self, node_id: usize) {
725 self.nodes[node_id]
726 .element_data_mut()
727 .unwrap()
728 .remove_sub_document();
729 self.sub_document_nodes.remove(&node_id);
730 }
731
732 pub fn poll_subdocuments(&mut self, waker: Option<&Waker>) -> bool {
738 let mut has_changes = false;
739 let node_ids: Vec<usize> = self.sub_document_nodes.iter().copied().collect();
740 for node_id in node_ids {
741 let Some(sub_doc) = self
742 .nodes
743 .get_mut(node_id)
744 .and_then(|node| node.subdoc_mut())
745 else {
746 continue;
747 };
748 let task_context = waker.map(TaskContext::from_waker);
749 has_changes |= sub_doc.poll(task_context);
750 }
751 has_changes
752 }
753
754 #[cfg(feature = "custom-widget")]
755 pub fn custom_widget_node_ids(&self) -> Vec<usize> {
756 self.custom_widget_nodes.iter().copied().collect()
757 }
758
759 #[cfg(feature = "custom-widget")]
760 pub fn take_pending_resource_deallocations(&mut self) -> Vec<anyrender::ResourceId> {
761 std::mem::take(&mut self.pending_resource_deallocations)
762 }
763
764 #[cfg(feature = "custom-widget")]
765 pub fn set_custom_widget(&mut self, node_id: usize, widget: Box<dyn crate::Widget>) {
766 self.nodes[node_id]
767 .element_data_mut()
768 .unwrap()
769 .set_custom_widget(widget);
770 self.custom_widget_nodes.insert(node_id);
771 }
772
773 #[cfg(feature = "custom-widget")]
774 pub fn remove_custom_widget(&mut self, node_id: usize) {
775 let resources_to_deallocate = self.nodes[node_id]
776 .element_data_mut()
777 .unwrap()
778 .remove_custom_widget();
779 self.pending_resource_deallocations
780 .extend_from_slice(&resources_to_deallocate);
781 self.custom_widget_nodes.remove(&node_id);
782 }
783
784 pub fn root_node(&self) -> &Node {
785 &self.nodes[0]
786 }
787
788 pub fn root_node_mut(&mut self) -> &mut Node {
789 &mut self.nodes[0]
790 }
791
792 pub fn try_root_element(&self) -> Option<&Node> {
793 TDocument::as_node(&self.root_node()).first_element_child()
794 }
795
796 pub fn root_element(&self) -> &Node {
797 TDocument::as_node(&self.root_node())
798 .first_element_child()
799 .unwrap()
800 .as_element()
801 .unwrap()
802 }
803
804 pub fn create_node(&mut self, node_data: NodeData) -> usize {
805 let slab_ptr = self.nodes.as_mut() as *mut Slab<Node>;
806 let guard = self.guard.clone();
807
808 let entry = self.nodes.vacant_entry();
809 let id = entry.key();
810 entry.insert(Node::new(slab_ptr, id, guard, node_data));
811
812 self.changed_nodes.insert(id);
814 id
815 }
816
817 pub(crate) fn drop_node_ignoring_parent(&mut self, node_id: usize) -> Option<Node> {
818 self.drop_node_ignoring_parent_with(node_id, &mut |_| {})
819 }
820
821 pub(crate) fn drop_node_ignoring_parent_with(
824 &mut self,
825 node_id: usize,
826 on_drop: &mut dyn FnMut(usize),
827 ) -> Option<Node> {
828 let mut node = self.nodes.try_remove(node_id);
829 if let Some(node) = &mut node {
830 on_drop(node_id);
831 if let Some(before) = node.before {
832 self.drop_node_ignoring_parent_with(before, on_drop);
833 }
834 if let Some(after) = node.after {
835 self.drop_node_ignoring_parent_with(after, on_drop);
836 }
837
838 for &child in &node.children {
839 self.drop_node_ignoring_parent_with(child, on_drop);
840 }
841 }
842 node
843 }
844
845 pub fn has_changes(&self) -> bool {
847 self.changed_nodes.is_empty()
848 }
849
850 pub fn create_text_node(&mut self, text: &str) -> usize {
851 let content = text.to_string();
852 let data = NodeData::Text(TextNodeData::new(content));
853 self.create_node(data)
854 }
855
856 pub fn deep_clone_node(&mut self, node_id: usize) -> usize {
857 let node = &self.nodes[node_id];
859 let mut data = node.data.clone();
860
861 match &mut data {
862 NodeData::Element(elem) | NodeData::AnonymousBlock(elem) => {
863 if let Some(arc) = elem.style_attribute.as_mut() {
864 let read_guard = self.guard().read();
865 let block = arc.read_with(&read_guard);
866 *arc = ServoArc::new(self.guard().wrap(block.clone()));
867 }
868 }
869 _ => {}
870 }
871
872 let children = node.children.clone();
873
874 let new_node_id = self.create_node(data);
876
877 let new_children: Vec<usize> = children
879 .into_iter()
880 .map(|child_id| self.deep_clone_node(child_id))
881 .collect();
882 for &child_id in &new_children {
883 self.nodes[child_id].parent = Some(new_node_id);
884 }
885 self.nodes[new_node_id].children = new_children;
886
887 new_node_id
888 }
889
890 pub(crate) fn remove_and_drop_pe(&mut self, node_id: usize) -> Option<Node> {
891 fn remove_pe_ignoring_parent(doc: &mut BaseDocument, node_id: usize) -> Option<Node> {
892 let mut node = doc.nodes.try_remove(node_id);
893 if let Some(node) = &mut node {
894 for &child in &node.children {
895 remove_pe_ignoring_parent(doc, child);
896 }
897 }
898 node
899 }
900
901 let node = remove_pe_ignoring_parent(self, node_id);
902
903 if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
905 let parent = &mut self.nodes[parent_id];
906 parent.children.retain(|id| *id != node_id);
907 }
908
909 node
910 }
911
912 pub(crate) fn resolve_url(&self, raw: &str) -> url::Url {
913 self.url.resolve_relative(raw).unwrap_or_else(|| {
914 panic!(
915 "to be able to resolve {raw} with the base_url: {:?}",
916 *self.url
917 )
918 })
919 }
920
921 pub fn print_tree(&self) {
922 crate::util::walk_tree(0, self.root_node());
923 }
924
925 pub fn print_subtree(&self, node_id: usize) {
926 crate::util::walk_tree(0, &self.nodes[node_id]);
927 }
928
929 pub fn reload_resource_by_href(&mut self, href_to_reload: &str) {
930 for &node_id in self.nodes_to_stylesheet.keys() {
931 let node = &self.nodes[node_id];
932 let Some(element) = node.element_data() else {
933 continue;
934 };
935
936 if element.name.local == local_name!("link") {
937 if let Some(href) = element.attr(local_name!("href")) {
938 if href == href_to_reload {
940 let resolved_href = self.resolve_url(href);
941 self.net_provider.fetch(
942 self.id(),
943 self.build_request(resolved_href.clone()),
944 ResourceHandler::boxed(
945 self.tx.clone(),
946 self.id,
947 Some(node_id),
948 self.shell_provider.clone(),
949 StylesheetHandler {
950 source_url: resolved_href,
951 guard: self.guard.clone(),
952 net_provider: self.net_provider.clone(),
953 abort_signal: self.abort_signal.clone(),
954 },
955 ),
956 );
957 }
958 }
959 }
960 }
961 }
962
963 pub fn process_style_element(&mut self, target_id: usize) {
964 let css = self.nodes[target_id].text_content();
965 let css = html_escape::decode_html_entities(&css);
966 let sheet = self.make_stylesheet(&css, Origin::Author);
967 self.add_stylesheet_for_node(sheet, target_id);
968 }
969
970 pub fn remove_user_agent_stylesheet(&mut self, contents: &str) {
971 if let Some(sheet) = self.ua_stylesheets.remove(contents) {
972 self.stylist.remove_stylesheet(sheet, &self.guard.read());
973 }
974 }
975
976 pub fn add_user_agent_stylesheet(&mut self, css: &str) {
977 let sheet = self.make_stylesheet(css, Origin::UserAgent);
978 self.ua_stylesheets.insert(css.to_string(), sheet.clone());
979 self.stylist.append_stylesheet(sheet, &self.guard.read());
980 }
981
982 pub fn make_stylesheet(&self, css: impl AsRef<str>, origin: Origin) -> DocumentStyleSheet {
983 let data = Stylesheet::from_str(
984 css.as_ref(),
985 self.url.url_extra_data(),
986 origin,
987 ServoArc::new(self.guard.wrap(MediaList::empty())),
988 self.guard.clone(),
989 Some(&StylesheetLoader {
990 tx: self.tx.clone(),
991 doc_id: self.id,
992 net_provider: self.net_provider.clone(),
993 shell_provider: self.shell_provider.clone(),
994 abort_signal: self.abort_signal.clone(),
995 }),
996 None,
997 QuirksMode::NoQuirks,
998 AllowImportRules::Yes,
999 );
1000
1001 DocumentStyleSheet(ServoArc::new(data))
1002 }
1003
1004 pub fn upsert_stylesheet_for_node(&mut self, node_id: usize) {
1005 let raw_styles = self.nodes[node_id].text_content();
1006 let sheet = self.make_stylesheet(raw_styles, Origin::Author);
1007 self.add_stylesheet_for_node(sheet, node_id);
1008 }
1009
1010 pub fn add_stylesheet_for_node(&mut self, stylesheet: DocumentStyleSheet, node_id: usize) {
1011 let old = self.nodes_to_stylesheet.insert(node_id, stylesheet.clone());
1012
1013 if let Some(old) = old {
1014 self.stylist.remove_stylesheet(old, &self.guard.read())
1015 }
1016
1017 crate::net::fetch_font_face(
1019 self.tx.clone(),
1020 self.id,
1021 Some(node_id),
1022 &stylesheet.0,
1023 &self.net_provider,
1024 &self.shell_provider,
1025 &self.guard.read(),
1026 self.abort_signal.as_ref(),
1027 );
1028
1029 let element = &mut self.nodes[node_id].element_data_mut().unwrap();
1031 element.special_data = SpecialElementData::Stylesheet(stylesheet.clone());
1032
1033 let insertion_point = self
1035 .nodes_to_stylesheet
1036 .range((Bound::Excluded(node_id), Bound::Unbounded))
1037 .next()
1038 .map(|(_, sheet)| sheet);
1039
1040 if let Some(insertion_point) = insertion_point {
1041 self.stylist.insert_stylesheet_before(
1042 stylesheet,
1043 insertion_point.clone(),
1044 &self.guard.read(),
1045 )
1046 } else {
1047 self.stylist
1048 .append_stylesheet(stylesheet, &self.guard.read())
1049 }
1050 }
1051
1052 pub fn handle_messages(&mut self) {
1053 let rx = self.rx.take().unwrap();
1056
1057 while let Ok(msg) = rx.try_recv() {
1058 self.handle_message(msg);
1059 }
1060
1061 self.rx = Some(rx);
1063 }
1064
1065 pub fn handle_message(&mut self, msg: DocumentEvent) {
1066 match msg {
1067 DocumentEvent::ResourceLoad(resource) => self.load_resource(resource),
1068 }
1069 }
1070
1071 pub fn has_pending_critical_resources(&self) -> bool {
1073 !self.pending_critical_resources.is_empty()
1074 }
1075
1076 pub fn load_resource(&mut self, res: ResourceLoadResponse) {
1077 self.pending_critical_resources.remove(&res.request_id);
1078
1079 let resource = match res.result {
1080 Ok(resource) => resource,
1081 Err(err) => {
1082 if let Some(url) = res.resolved_url.as_ref() {
1083 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1084 #[cfg(feature = "tracing")]
1085 tracing::warn!(
1086 url = url.as_str(),
1087 waiting_nodes = waiting_nodes.len(),
1088 error = err.as_str(),
1089 "Resource load failed"
1090 );
1091 #[cfg(not(feature = "tracing"))]
1092 let _ = (waiting_nodes, err);
1093 } else {
1094 #[cfg(feature = "tracing")]
1095 tracing::warn!(error = err.as_str(), "Resource load failed (no url)");
1096 #[cfg(not(feature = "tracing"))]
1097 let _ = err;
1098 }
1099 return;
1100 }
1101 };
1102
1103 match resource {
1104 Resource::Css(css) => {
1105 let node_id = res.node_id.unwrap();
1106 self.add_stylesheet_for_node(css, node_id);
1107 }
1108 Resource::Image(_kind, width, height, image_data) => {
1109 let image = ImageData::Raster(RasterImageData::new(width, height, image_data));
1111
1112 let Some(url) = res.resolved_url.as_ref() else {
1113 return;
1114 };
1115
1116 self.apply_loaded_image(url, image);
1117 }
1118 #[cfg(feature = "svg")]
1119 Resource::Svg(_kind, svg) => {
1120 let image = ImageData::Svg(svg);
1122
1123 let Some(url) = res.resolved_url.as_ref() else {
1124 return;
1125 };
1126
1127 self.apply_loaded_image(url, image);
1128 }
1129 Resource::Font(bytes, overrides) => {
1130 let font = Blob::new(Arc::new(bytes));
1131
1132 let weight_override = overrides.weight.map(parley::fontique::FontWeight::new);
1138 let info_override = parley::fontique::FontInfoOverride {
1139 family_name: overrides.family_name.as_deref(),
1140 weight: weight_override,
1141 style: overrides.style,
1142 ..Default::default()
1143 };
1144
1145 let mut global_font_ctx = self.font_ctx.lock().unwrap();
1147 global_font_ctx
1148 .collection
1149 .register_fonts(font.clone(), Some(info_override));
1150
1151 #[cfg(feature = "parallel-construct")]
1152 {
1153 rayon::broadcast(|_ctx| {
1154 let mut font_ctx = self
1155 .thread_font_contexts
1156 .get_or(|| RefCell::new(Box::new(global_font_ctx.clone())))
1157 .borrow_mut();
1158 font_ctx
1159 .collection
1160 .register_fonts(font.clone(), Some(info_override));
1161 });
1162 }
1163 drop(global_font_ctx);
1164
1165 self.invalidate_inline_contexts();
1167 }
1168 Resource::None => {
1169 }
1171 }
1172 }
1173
1174 fn apply_loaded_image(&mut self, url: &str, image: ImageData) {
1177 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1179
1180 #[cfg(feature = "tracing")]
1181 tracing::info!(
1182 "Image {url} loaded, applying to {} nodes",
1183 waiting_nodes.len()
1184 );
1185
1186 self.image_cache.insert(url.to_string(), image.clone());
1188
1189 for (node_id, image_type) in waiting_nodes {
1191 let Some(node) = self.get_node_mut(node_id) else {
1192 continue;
1193 };
1194
1195 match image_type {
1196 ImageType::Image => {
1197 node.element_data_mut().unwrap().special_data =
1198 SpecialElementData::Image(Box::new(image.clone()));
1199
1200 node.cache.clear();
1202 node.insert_damage(ALL_DAMAGE);
1203 }
1204 ImageType::Background(idx) | ImageType::Mask(idx) => {
1205 let layer_image = node.element_data_mut().and_then(|el| {
1206 let images = match image_type {
1207 ImageType::Background(_) => &mut el.background_images,
1208 ImageType::Mask(_) => &mut el.mask_images,
1209 ImageType::Image => unreachable!(),
1210 };
1211 images.get_mut(idx)
1212 });
1213 if let Some(Some(layer_image)) = layer_image {
1214 layer_image.status = Status::Ok;
1215 layer_image.image = image.clone();
1216 }
1217 }
1218 }
1219 }
1220 }
1221
1222 pub fn snapshot_node(&mut self, node_id: usize) {
1223 let node = &mut self.nodes[node_id];
1224
1225 let has_been_styled = node.primary_styles().is_some();
1230 if !has_been_styled {
1231 return;
1232 }
1233
1234 let opaque_node_id = TNode::opaque(&&*node);
1235 node.has_snapshot = true;
1236 node.snapshot_handled
1237 .store(false, std::sync::atomic::Ordering::SeqCst);
1238
1239 if let Some(_existing_snapshot) = self.snapshots.get_mut(&opaque_node_id) {
1241 } else {
1244 let attrs: Option<Vec<_>> = node.attrs().map(|attrs| {
1245 attrs
1246 .iter()
1247 .map(|attr| {
1248 let ident = AttrIdentifier {
1249 local_name: GenericAtomIdent(attr.name.local.clone()),
1250 name: GenericAtomIdent(attr.name.local.clone()),
1251 namespace: GenericAtomIdent(attr.name.ns.clone()),
1252 prefix: None,
1253 };
1254
1255 let value = if attr.name.local == local_name!("id") {
1256 AttrValue::Atom(Atom::from(&*attr.value))
1257 } else if attr.name.local == local_name!("class") {
1258 let classes = attr
1259 .value
1260 .split_ascii_whitespace()
1261 .map(Atom::from)
1262 .collect();
1263 AttrValue::TokenList(OnceLock::from(attr.value.clone()), classes)
1264 } else {
1265 AttrValue::String(attr.value.clone())
1266 };
1267
1268 (ident, value)
1269 })
1270 .collect()
1271 });
1272
1273 let changed_attrs = attrs
1274 .as_ref()
1275 .map(|attrs| attrs.iter().map(|attr| attr.0.name.clone()).collect())
1276 .unwrap_or_default();
1277
1278 self.snapshots.insert(
1279 opaque_node_id,
1280 ServoElementSnapshot {
1281 state: Some(node.element_state),
1282 attrs,
1283 changed_attrs,
1284 class_changed: true,
1285 id_changed: true,
1286 other_attributes_changed: true,
1287 },
1288 );
1289 }
1290 }
1291
1292 pub fn snapshot_node_and(&mut self, node_id: usize, cb: impl FnOnce(&mut Node)) {
1293 self.snapshot_node(node_id);
1294 cb(&mut self.nodes[node_id]);
1295 }
1296
1297 pub fn hit(&self, x: f32, y: f32) -> Option<HitResult> {
1299 self.hit_with_scrollbar(x, y).0
1300 }
1301
1302 pub fn focus_next_node(&mut self) -> Option<usize> {
1303 let focussed_node_id = self.get_focussed_node_id()?;
1304 let id = self.next_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
1305 self.set_focus_to(id);
1306 Some(id)
1307 }
1308
1309 pub fn clear_focus(&mut self) {
1311 if let Some(id) = self.focus_node_id {
1312 let shell_provider = self.shell_provider.clone();
1313 self.snapshot_node_and(id, |node| node.blur(shell_provider));
1314 self.focus_node_id = None;
1315 }
1316 }
1317
1318 pub fn set_mousedown_node_id(&mut self, node_id: Option<usize>) {
1319 self.mousedown_node_id = node_id;
1320 }
1321 pub fn set_focus_to(&mut self, focus_node_id: usize) -> bool {
1322 if Some(focus_node_id) == self.focus_node_id {
1323 return false;
1324 }
1325
1326 #[cfg(feature = "tracing")]
1327 tracing::info!("Focussed node {focus_node_id}");
1328
1329 let shell_provider = self.shell_provider.clone();
1330
1331 if let Some(id) = self.focus_node_id {
1333 self.snapshot_node_and(id, |node| node.blur(shell_provider.clone()));
1334 }
1335
1336 self.snapshot_node_and(focus_node_id, |node| node.focus(shell_provider));
1338
1339 self.focus_node_id = Some(focus_node_id);
1340
1341 true
1342 }
1343
1344 pub fn active_node(&mut self) -> bool {
1345 let Some(hover_node_id) = self.get_hover_node_id() else {
1346 return false;
1347 };
1348
1349 if let Some(active_node_id) = self.active_node_id {
1350 if active_node_id == hover_node_id {
1351 return true;
1352 }
1353 self.unactive_node();
1354 }
1355
1356 let active_node_id = Some(hover_node_id);
1357
1358 let node_path = self.maybe_node_layout_ancestors(active_node_id);
1359 for &id in node_path.iter() {
1360 self.snapshot_node_and(id, |node| node.active());
1361 }
1362
1363 self.active_node_id = active_node_id;
1364
1365 true
1366 }
1367
1368 pub fn unactive_node(&mut self) -> bool {
1369 let Some(active_node_id) = self.active_node_id.take() else {
1370 return false;
1371 };
1372
1373 let node_path = self.maybe_node_layout_ancestors(Some(active_node_id));
1374 for &id in node_path.iter() {
1375 self.snapshot_node_and(id, |node| node.unactive());
1376 }
1377
1378 true
1379 }
1380
1381 pub fn hovered_scrollbar(&self) -> Option<crate::node::ScrollbarRef> {
1383 self.hovered_scrollbar
1384 }
1385
1386 pub fn scrollbar_drag_target(&self) -> Option<crate::node::ScrollbarRef> {
1388 match &self.drag_mode {
1389 DragMode::ScrollbarDrag(state) => Some(state.scrollbar),
1390 _ => None,
1391 }
1392 }
1393
1394 pub fn scrollbar_opacity(&self, node_id: usize) -> f32 {
1399 let interacting = |scrollbar: &crate::node::ScrollbarRef| scrollbar.node_id == node_id;
1400 if self.hovered_scrollbar.as_ref().is_some_and(interacting)
1401 || self
1402 .scrollbar_drag_target()
1403 .as_ref()
1404 .is_some_and(interacting)
1405 {
1406 return 1.0;
1407 }
1408 self.scrollbar_activity.get(&node_id).map_or(1.0, |last| {
1409 crate::node::scrollbar::opacity_at(last.elapsed())
1410 })
1411 }
1412
1413 pub(crate) fn show_scrollbars(&mut self, node_id: usize) {
1416 if cfg!(feature = "scrollbars") {
1417 self.scrollbar_activity.insert(node_id, Instant::now());
1418 }
1419 }
1420
1421 fn scrollbars_animating(&self) -> bool {
1424 use crate::node::scrollbar::{FADE_DELAY, FADE_DURATION};
1425 self.scrollbar_activity
1426 .values()
1427 .any(|last| last.elapsed() < FADE_DELAY + FADE_DURATION)
1428 }
1429
1430 pub(crate) fn hit_with_scrollbar(
1434 &self,
1435 x: f32,
1436 y: f32,
1437 ) -> (Option<HitResult>, Option<crate::node::ScrollbarRef>) {
1438 if TDocument::as_node(&&self.nodes[0])
1439 .first_element_child()
1440 .is_none()
1441 {
1442 #[cfg(feature = "tracing")]
1443 tracing::warn!("No DOM - not resolving hit test");
1444 return (None, None);
1445 }
1446 let mut scrollbar = None;
1447 let hit = self
1448 .root_element()
1449 .hit_inner(x, y, self.viewport().scale_f64(), &mut scrollbar);
1450 (hit, scrollbar)
1451 }
1452
1453 pub fn set_hover_to(&mut self, x: f32, y: f32) -> bool {
1454 let (hit, hovered_scrollbar) = self.hit_with_scrollbar(x, y);
1455 let hovered_scrollbar =
1458 hovered_scrollbar.filter(|scrollbar| self.scrollbar_opacity(scrollbar.node_id) > 0.0);
1459 let scrollbar_changed = hovered_scrollbar != self.hovered_scrollbar;
1463 if scrollbar_changed {
1464 for scrollbar in [self.hovered_scrollbar, hovered_scrollbar]
1467 .into_iter()
1468 .flatten()
1469 {
1470 self.show_scrollbars(scrollbar.node_id);
1471 }
1472 }
1473 self.hovered_scrollbar = hovered_scrollbar;
1474 let hover_node_id = hit.map(|hit| hit.node_id);
1475 let new_is_text = hit.map(|hit| hit.is_text).unwrap_or(false);
1476
1477 if hover_node_id == self.hover_node_id {
1479 return scrollbar_changed;
1480 }
1481
1482 let old_node_path = self.maybe_node_layout_ancestors(self.hover_node_id);
1483 let new_node_path = self.maybe_node_layout_ancestors(hover_node_id);
1484 let same_count = old_node_path
1485 .iter()
1486 .zip(&new_node_path)
1487 .take_while(|(o, n)| o == n)
1488 .count();
1489 for &id in old_node_path.iter().skip(same_count) {
1490 self.snapshot_node_and(id, |node| node.unhover());
1491 }
1492 for &id in new_node_path.iter().skip(same_count) {
1493 self.snapshot_node_and(id, |node| node.hover());
1494 }
1495
1496 self.hover_node_id = hover_node_id;
1497 self.hover_node_is_text = new_is_text;
1498
1499 self.shell_provider.set_cursor(self.get_cursor());
1501
1502 self.shell_provider.request_redraw();
1504
1505 true
1506 }
1507
1508 pub fn clear_hover(&mut self) -> bool {
1509 let Some(hover_node_id) = self.hover_node_id else {
1510 return false;
1511 };
1512
1513 let old_node_path = self.maybe_node_layout_ancestors(Some(hover_node_id));
1514 for &id in old_node_path.iter() {
1515 self.snapshot_node_and(id, |node| node.unhover());
1516 }
1517
1518 self.hover_node_id = None;
1519 self.hover_node_is_text = false;
1520
1521 self.shell_provider.set_cursor(self.get_cursor());
1523
1524 self.shell_provider.request_redraw();
1526
1527 true
1528 }
1529
1530 pub fn get_hover_node_id(&self) -> Option<usize> {
1531 self.hover_node_id
1532 }
1533
1534 pub fn set_viewport(&mut self, viewport: Viewport) {
1535 let scale_has_changed = viewport.scale_f64() != self.viewport.scale_f64();
1536 self.viewport = viewport;
1537 self.set_stylist_device(make_device(
1538 &self.viewport,
1539 self.media_type.clone(),
1540 self.font_ctx.clone(),
1541 ));
1542 self.scroll_viewport_by(0.0, 0.0); if scale_has_changed {
1545 self.invalidate_inline_contexts();
1546 self.shell_provider.request_redraw();
1547 }
1548 }
1549
1550 pub fn media_type(&self) -> &MediaType {
1552 &self.media_type
1553 }
1554
1555 pub fn set_media_type(&mut self, media_type: MediaType) {
1558 if self.media_type == media_type {
1559 return;
1560 }
1561 self.media_type = media_type;
1562 self.set_stylist_device(make_device(
1563 &self.viewport,
1564 self.media_type.clone(),
1565 self.font_ctx.clone(),
1566 ));
1567 }
1568
1569 pub fn viewport(&self) -> &Viewport {
1570 &self.viewport
1571 }
1572
1573 pub fn viewport_mut(&mut self) -> ViewportMut<'_> {
1574 ViewportMut::new(self)
1575 }
1576
1577 pub fn zoom_by(&mut self, increment: f32) {
1578 *self.viewport.zoom_mut() += increment;
1579 self.set_viewport(self.viewport.clone());
1580 }
1581
1582 pub fn zoom_to(&mut self, zoom: f32) {
1583 *self.viewport.zoom_mut() = zoom;
1584 self.set_viewport(self.viewport.clone());
1585 }
1586
1587 pub fn get_viewport(&self) -> Viewport {
1588 self.viewport.clone()
1589 }
1590
1591 pub fn incremental_layout(&self) -> bool {
1593 self.incremental_layout
1594 }
1595
1596 pub fn set_incremental_layout(&mut self, enabled: bool) {
1601 self.incremental_layout = enabled;
1602 }
1603
1604 pub fn devtools(&self) -> &DevtoolSettings {
1605 &self.devtool_settings
1606 }
1607
1608 pub fn devtools_mut(&mut self) -> &mut DevtoolSettings {
1609 &mut self.devtool_settings
1610 }
1611
1612 pub fn subdoc(&self, node_id: usize) -> Option<&dyn Document> {
1613 self.get_node(node_id)
1614 .and_then(|node| node.element_data())
1615 .and_then(|el| el.sub_doc_data())
1616 }
1617
1618 pub fn subdoc_mut(&mut self, node_id: usize) -> Option<&mut dyn Document> {
1619 self.get_node_mut(node_id)
1620 .and_then(|node| node.element_data_mut())
1621 .and_then(|el| el.sub_doc_data_mut())
1622 }
1623
1624 pub fn is_animating(&self) -> bool {
1625 #[cfg(feature = "custom-widget")]
1626 let has_custom_widgets = !self.custom_widget_nodes.is_empty();
1627 #[cfg(not(feature = "custom-widget"))]
1628 let has_custom_widgets = false;
1629
1630 self.has_canvas
1631 | self.has_active_animations
1632 | self.subdoc_is_animating
1633 | has_custom_widgets
1634 | (self.scroll_animation != ScrollAnimationState::None)
1635 | self.scrollbars_animating()
1636 }
1637
1638 pub fn set_stylist_device(&mut self, device: Device) {
1640 let root_styles = self
1646 .try_root_element()
1647 .and_then(|root| root.primary_styles());
1648 if let Some(root_style) = root_styles.as_deref() {
1649 device.set_root_style(root_style);
1650
1651 let font = root_style.get_font();
1652 let font_size = font.clone_font_size().computed_size();
1653 device.set_root_font_size(root_style.effective_zoom.unzoom(font_size.px()));
1654
1655 let line_height = device
1656 .calc_line_height(font, root_style.writing_mode, None)
1657 .0;
1658 device.set_root_line_height(root_style.effective_zoom.unzoom(line_height.px()));
1659 }
1660 drop(root_styles);
1661
1662 let origins = {
1663 let guard = &self.guard;
1664 let guards = StylesheetGuards {
1665 author: &guard.read(),
1666 ua_or_user: &guard.read(),
1667 };
1668 self.stylist.set_device(device, &guards)
1669 };
1670 self.stylist.force_stylesheet_origins_dirty(origins);
1671 }
1672
1673 pub fn stylist_device(&mut self) -> &Device {
1674 self.stylist.device()
1675 }
1676
1677 pub fn get_cursor(&self) -> Option<CursorIcon> {
1678 let node = &self.nodes[self.get_hover_node_id()?];
1679
1680 if let Some(subdoc) = node.subdoc().map(|doc| doc.inner()) {
1681 return subdoc.get_cursor();
1682 }
1683
1684 let style = node.primary_styles()?;
1685 let user_select = style.clone_user_select();
1686 let keyword = style.clone_cursor().keyword;
1687
1688 if keyword != CursorKind::Auto {
1690 return stylo_to_cursor_icon(keyword);
1691 }
1692
1693 if node
1695 .element_data()
1696 .is_some_and(|e| e.text_input_data().is_some())
1697 {
1698 return Some(CursorIcon::Text);
1699 }
1700
1701 let mut maybe_node = Some(node);
1703 while let Some(node) = maybe_node {
1704 if node.is_link() {
1705 return Some(CursorIcon::Pointer);
1706 }
1707
1708 maybe_node = node.layout_parent.get().map(|node_id| node.with(node_id));
1709 }
1710
1711 if self.hover_node_is_text {
1713 return Some(match user_select {
1714 UserSelect::Text | UserSelect::All | UserSelect::Auto => CursorIcon::Text,
1715 UserSelect::None => CursorIcon::Default,
1716 });
1717 }
1718
1719 Some(CursorIcon::Default)
1721 }
1722
1723 pub fn scroll_node_by<F: FnMut(DomEvent)>(
1724 &mut self,
1725 node_id: usize,
1726 x: f64,
1727 y: f64,
1728 dispatch_event: F,
1729 ) {
1730 self.scroll_node_by_has_changed(node_id, x, y, dispatch_event);
1731 }
1732
1733 pub fn scroll_node_by_has_changed<F: FnMut(DomEvent)>(
1737 &mut self,
1738 node_id: usize,
1739 x: f64,
1740 y: f64,
1741 mut dispatch_event: F,
1742 ) -> bool {
1743 if self.try_root_element().is_some_and(|el| el.id == node_id) {
1748 let has_changed = self.scroll_viewport_by_has_changed(x, y);
1749 if has_changed {
1750 let layout = self.root_element().final_layout;
1751 let scale = self.viewport.scale() as f64;
1752 let event = BlitzScrollEvent {
1753 scroll_top: self.viewport_scroll.y,
1754 scroll_left: self.viewport_scroll.x,
1755 scroll_width: layout.size.width.max(layout.content_size.width) as i32,
1756 scroll_height: layout.size.height.max(layout.content_size.height) as i32,
1757 client_width: (self.viewport.window_size.0 as f64 / scale) as i32,
1758 client_height: (self.viewport.window_size.1 as f64 / scale) as i32,
1759 };
1760 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
1761 }
1762 return has_changed;
1763 }
1764
1765 let Some(node) = self.nodes.get_mut(node_id) else {
1766 return false;
1767 };
1768
1769 if node
1773 .element_data()
1774 .is_some_and(|el| el.text_input_data().is_some())
1775 {
1776 let parent = node.parent;
1777 let content_box_width = node.final_layout.content_box_width();
1778 let content_box_height = node.final_layout.content_box_height();
1779 let input = node
1780 .element_data_mut()
1781 .and_then(|el| el.text_input_data_mut())
1782 .unwrap();
1783
1784 let (bubble_x, bubble_y) = if input.is_multiline {
1785 (
1786 x,
1787 input.scroll_by(y as f32, content_box_width, content_box_height) as f64,
1788 )
1789 } else {
1790 (
1791 input.scroll_by(x as f32, content_box_width, content_box_height) as f64,
1792 y,
1793 )
1794 };
1795
1796 let has_changed = bubble_x != x || bubble_y != y;
1797
1798 if bubble_x != 0.0 || bubble_y != 0.0 {
1799 let bubbled = if let Some(parent) = parent {
1800 self.scroll_node_by_has_changed(parent, bubble_x, bubble_y, dispatch_event)
1801 } else {
1802 self.scroll_viewport_by_has_changed(bubble_x, bubble_y)
1803 };
1804 return bubbled | has_changed;
1805 }
1806
1807 return has_changed;
1808 }
1809
1810 let (can_x_scroll, can_y_scroll) = node
1811 .primary_styles()
1812 .map(|styles| {
1813 (
1814 matches!(styles.clone_overflow_x(), Overflow::Scroll | Overflow::Auto),
1815 matches!(styles.clone_overflow_y(), Overflow::Scroll | Overflow::Auto),
1816 )
1817 })
1818 .unwrap_or((false, false));
1819
1820 let initial = node.scroll_offset;
1821 let new_x = node.scroll_offset.x - x;
1822 let new_y = node.scroll_offset.y - y;
1823
1824 let mut bubble_x = 0.0;
1825 let mut bubble_y = 0.0;
1826
1827 let scroll_width = node.final_layout.scroll_width() as f64;
1828 let scroll_height = node.final_layout.scroll_height() as f64;
1829
1830 if let Some(mut sub_doc) = node.subdoc_mut().map(|doc| doc.inner_mut()) {
1832 let has_changed = if let Some(hover_node_id) = sub_doc.get_hover_node_id() {
1833 sub_doc.scroll_node_by_has_changed(hover_node_id, x, y, dispatch_event)
1834 } else {
1835 sub_doc.scroll_viewport_by_has_changed(x, y)
1836 };
1837
1838 return has_changed;
1840 }
1841
1842 if !can_x_scroll {
1844 bubble_x = x
1845 } else if new_x < 0.0 {
1846 bubble_x = -new_x;
1847 node.scroll_offset.x = 0.0;
1848 } else if new_x > scroll_width {
1849 bubble_x = scroll_width - new_x;
1850 node.scroll_offset.x = scroll_width;
1851 } else {
1852 node.scroll_offset.x = new_x;
1853 }
1854
1855 if !can_y_scroll {
1856 bubble_y = y
1857 } else if new_y < 0.0 {
1858 bubble_y = -new_y;
1859 node.scroll_offset.y = 0.0;
1860 } else if new_y > scroll_height {
1861 bubble_y = scroll_height - new_y;
1862 node.scroll_offset.y = scroll_height;
1863 } else {
1864 node.scroll_offset.y = new_y;
1865 }
1866
1867 let has_changed = node.scroll_offset != initial;
1868
1869 if has_changed {
1870 let layout = node.final_layout;
1871 let event = BlitzScrollEvent {
1872 scroll_top: node.scroll_offset.y,
1873 scroll_left: node.scroll_offset.x,
1874 scroll_width: layout.scroll_width() as i32,
1875 scroll_height: layout.scroll_height() as i32,
1876 client_width: layout.size.width as i32,
1877 client_height: layout.size.height as i32,
1878 };
1879
1880 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
1881 }
1882
1883 let parent = node.parent;
1884 if has_changed {
1885 self.show_scrollbars(node_id);
1886 }
1887
1888 if bubble_x != 0.0 || bubble_y != 0.0 {
1889 if let Some(parent) = parent {
1890 return self.scroll_node_by_has_changed(parent, bubble_x, bubble_y, dispatch_event)
1891 | has_changed;
1892 } else {
1893 return self.scroll_viewport_by_has_changed(bubble_x, bubble_y) | has_changed;
1894 }
1895 }
1896
1897 has_changed
1898 }
1899
1900 pub fn scroll_viewport_by(&mut self, x: f64, y: f64) {
1901 self.scroll_viewport_by_has_changed(x, y);
1902 }
1903
1904 pub fn scroll_viewport_by_has_changed(&mut self, x: f64, y: f64) -> bool {
1906 let root_layout = &self.root_element().final_layout;
1910 let content_width = root_layout.size.width.max(root_layout.content_size.width) as f64;
1911 let content_height = root_layout.size.height.max(root_layout.content_size.height) as f64;
1912 let new_scroll = (self.viewport_scroll.x - x, self.viewport_scroll.y - y);
1913 let window_width = self.viewport.window_size.0 as f64 / self.viewport.scale() as f64;
1914 let window_height = self.viewport.window_size.1 as f64 / self.viewport.scale() as f64;
1915
1916 let initial = self.viewport_scroll;
1917 self.viewport_scroll.x =
1918 f64::max(0.0, f64::min(new_scroll.0, content_width - window_width));
1919 self.viewport_scroll.y =
1920 f64::max(0.0, f64::min(new_scroll.1, content_height - window_height));
1921
1922 self.viewport_scroll != initial
1923 }
1924
1925 pub fn scroll_by(
1926 &mut self,
1927 anchor_node_id: Option<usize>,
1928 scroll_x: f64,
1929 scroll_y: f64,
1930 dispatch_event: &mut dyn FnMut(DomEvent),
1931 ) -> bool {
1932 if let Some(anchor_node_id) = anchor_node_id {
1933 self.scroll_node_by_has_changed(anchor_node_id, scroll_x, scroll_y, dispatch_event)
1934 } else {
1935 self.scroll_viewport_by_has_changed(scroll_x, scroll_y)
1936 }
1937 }
1938
1939 pub fn viewport_scroll(&self) -> crate::Point<f64> {
1940 self.viewport_scroll
1941 }
1942
1943 pub fn set_viewport_scroll(&mut self, scroll: crate::Point<f64>) {
1944 self.viewport_scroll = scroll;
1945 }
1946
1947 pub fn get_fragment_target(&self, fragment: &str) -> Option<usize> {
1952 if let Some(node_id) = self.get_element_by_id(fragment) {
1953 return Some(node_id);
1954 }
1955
1956 self.nodes.iter().find_map(|(id, node)| {
1958 let el = node.element_data()?;
1959 (el.name.local == local_name!("a") && el.attr(local_name!("name")) == Some(fragment))
1960 .then_some(id)
1961 })
1962 }
1963
1964 pub fn scroll_to_node(&mut self, node_id: usize) {
1966 let Some(node) = self.nodes.get(node_id) else {
1967 return;
1968 };
1969
1970 let target = node.absolute_position(0.0, 0.0);
1973 let current = self.viewport_scroll;
1974
1975 self.scroll_viewport_by(current.x - target.x as f64, current.y - target.y as f64);
1978 }
1979
1980 pub fn scroll_to_fragment(&mut self, fragment: &str) -> bool {
1986 let decoded = percent_encoding::percent_decode_str(fragment)
1988 .decode_utf8_lossy()
1989 .into_owned();
1990
1991 if !decoded.is_empty() {
1992 if let Some(node_id) = self.get_fragment_target(&decoded) {
1993 self.scroll_to_node(node_id);
1994 return true;
1995 }
1996 }
1997
1998 if decoded.is_empty() || decoded.eq_ignore_ascii_case("top") {
2001 let current = self.viewport_scroll;
2002 self.scroll_viewport_by(current.x, current.y);
2003 return true;
2004 }
2005
2006 false
2007 }
2008
2009 pub fn get_client_bounding_rect(&self, node_id: usize) -> Option<BoundingRect> {
2011 let node = self.get_node(node_id)?;
2012 let pos = node.absolute_position(0.0, 0.0);
2013
2014 Some(BoundingRect {
2015 x: pos.x as f64 - self.viewport_scroll.x,
2016 y: pos.y as f64 - self.viewport_scroll.y,
2017 width: node.unrounded_layout.size.width as f64,
2018 height: node.unrounded_layout.size.height as f64,
2019 })
2020 }
2021
2022 pub fn find_title_node(&self) -> Option<&Node> {
2023 TreeTraverser::new(self)
2024 .find(|node_id| {
2025 let node = &self.nodes[*node_id];
2026 let Some(element) = node.element_data() else {
2027 return false;
2028 };
2029 if element.name.ns != ns!(html) || element.name.local != local_name!("title") {
2030 return false;
2031 }
2032 node.parent
2033 .and_then(|parent_id| self.nodes.get(parent_id))
2034 .and_then(Node::element_data)
2035 .is_some_and(|parent| {
2036 parent.name.ns == ns!(html) && parent.name.local == local_name!("head")
2037 })
2038 })
2039 .map(|node_id| &self.nodes[node_id])
2040 }
2041
2042 pub fn with_text_input(
2043 &mut self,
2044 node_id: usize,
2045 cb: impl FnOnce(PlainEditorDriver<TextBrush>),
2046 ) {
2047 let Some(node) = self.nodes.get_mut(node_id) else {
2048 return;
2049 };
2050
2051 if let Some(text_input) = node
2052 .element_data_mut()
2053 .and_then(|el| el.text_input_data_mut())
2054 {
2055 let mut font_ctx = self.font_ctx.lock().unwrap();
2056 let layout_ctx = &mut self.layout_ctx;
2057 let driver = text_input.editor.driver(&mut font_ctx, layout_ctx);
2058 cb(driver)
2059 }
2060 }
2061
2062 pub(crate) fn clamp_text_input_scroll(&mut self, node_id: usize) {
2065 let Some(node) = self.nodes.get_mut(node_id) else {
2066 return;
2067 };
2068
2069 let content_box_width = node.final_layout.content_box_width();
2070 let content_box_height = node.final_layout.content_box_height();
2071
2072 if let Some(text_input) = node
2073 .element_data_mut()
2074 .and_then(|el| el.text_input_data_mut())
2075 {
2076 text_input.clamp_scroll_offset(content_box_width, content_box_height);
2077 }
2078 }
2079
2080 pub(crate) fn compute_has_canvas(&self) -> bool {
2081 TreeTraverser::new(self).any(|node_id| {
2082 let node = &self.nodes[node_id];
2083 let Some(element) = node.element_data() else {
2084 return false;
2085 };
2086 if element.name.local == local_name!("canvas") && element.has_attr(local_name!("src")) {
2087 return true;
2088 }
2089
2090 false
2091 })
2092 }
2093
2094 pub fn find_text_position(&self, x: f32, y: f32) -> Option<(usize, usize)> {
2100 let hit = self.hit(x, y)?;
2101 let hit_node = self.get_node(hit.node_id)?;
2102 let inline_root = hit_node.inline_root_ancestor()?;
2103 let byte_offset = inline_root.text_offset_at_point(hit.x, hit.y)?;
2104 Some((inline_root.id, byte_offset))
2105 }
2106
2107 pub fn set_text_selection(
2109 &mut self,
2110 anchor_node: usize,
2111 anchor_offset: usize,
2112 focus_node: usize,
2113 focus_offset: usize,
2114 ) {
2115 self.text_selection =
2116 TextSelection::new(anchor_node, anchor_offset, focus_node, focus_offset);
2117
2118 if let (Some(parent), Some(idx)) = self.anonymous_block_location(anchor_node) {
2120 self.text_selection
2121 .anchor
2122 .set_anonymous(parent, idx, anchor_offset);
2123 }
2124 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
2125 self.text_selection
2126 .focus
2127 .set_anonymous(parent, idx, focus_offset);
2128 }
2129 }
2130
2131 fn anonymous_block_location(&self, node_id: usize) -> (Option<usize>, Option<usize>) {
2134 let Some(node) = self.get_node(node_id) else {
2135 return (None, None);
2136 };
2137
2138 if !node.is_anonymous() {
2139 return (None, None);
2140 }
2141
2142 let Some(parent_id) = node.parent else {
2143 return (None, None);
2144 };
2145
2146 let Some(parent) = self.get_node(parent_id) else {
2147 return (Some(parent_id), None);
2148 };
2149
2150 let layout_children = parent.layout_children.borrow();
2151 let Some(children) = layout_children.as_ref() else {
2152 return (Some(parent_id), None);
2153 };
2154
2155 let mut anon_index = 0;
2157 for &child_id in children.iter() {
2158 if child_id == node_id {
2159 return (Some(parent_id), Some(anon_index));
2160 }
2161 if self.get_node(child_id).is_some_and(|n| n.is_anonymous()) {
2162 anon_index += 1;
2163 }
2164 }
2165
2166 (Some(parent_id), None)
2167 }
2168
2169 pub fn clear_text_selection(&mut self) {
2171 self.text_selection.clear();
2172 }
2173
2174 pub fn update_selection_focus(&mut self, focus_node: usize, focus_offset: usize) {
2176 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
2178 self.text_selection
2179 .focus
2180 .set_anonymous(parent, idx, focus_offset);
2181 } else {
2182 self.text_selection.set_focus(focus_node, focus_offset);
2183 }
2184 }
2185
2186 pub fn extend_text_selection_to_point(&mut self, x: f32, y: f32) -> bool {
2189 if !self.text_selection.anchor.is_some() {
2190 return false;
2191 }
2192
2193 if let Some((node, offset)) = self.find_text_position(x, y) {
2194 self.update_selection_focus(node, offset);
2195 self.shell_provider.request_redraw();
2196 true
2197 } else {
2198 false
2199 }
2200 }
2201
2202 fn find_anonymous_block_by_index(
2204 &self,
2205 parent_id: usize,
2206 target_index: usize,
2207 ) -> Option<usize> {
2208 let parent = self.get_node(parent_id)?;
2209 let layout_children = parent.layout_children.borrow();
2210 let children = layout_children.as_ref()?;
2211
2212 children
2213 .iter()
2214 .filter(|&&child_id| self.get_node(child_id).is_some_and(|n| n.is_anonymous()))
2215 .nth(target_index)
2216 .copied()
2217 }
2218
2219 pub fn has_text_selection(&self) -> bool {
2221 self.text_selection.is_active()
2222 }
2223
2224 pub fn get_selected_text(&self) -> Option<String> {
2226 let ranges = self.get_text_selection_ranges();
2227 if ranges.is_empty() {
2228 return None;
2229 }
2230
2231 let mut result = String::new();
2232 for (node_id, start, end) in &ranges {
2233 let node = self.get_node(*node_id)?;
2234 let element_data = node.element_data()?;
2235 let inline_layout = element_data.inline_layout_data.as_ref()?;
2236
2237 if *end > inline_layout.text.len() {
2238 continue;
2239 }
2240
2241 if !result.is_empty() {
2242 result.push(' ');
2243 }
2244 result.push_str(&inline_layout.text[*start..*end]);
2245 }
2246
2247 if result.is_empty() {
2248 None
2249 } else {
2250 Some(result)
2251 }
2252 }
2253
2254 pub fn get_text_selection_ranges(&self) -> Vec<(usize, usize, usize)> {
2257 let lookup = |parent_id, idx| self.find_anonymous_block_by_index(parent_id, idx);
2258
2259 let anchor_node = match self.text_selection.anchor.resolve_node_id(lookup) {
2260 Some(id) => id,
2261 None => return Vec::new(),
2262 };
2263 let focus_node = match self.text_selection.focus.resolve_node_id(lookup) {
2264 Some(id) => id,
2265 None => return Vec::new(),
2266 };
2267
2268 let node_is_in_doc = |node_id: usize| {
2271 self.nodes
2272 .get(node_id)
2273 .is_some_and(|node| node.flags.is_in_document())
2274 };
2275 if !node_is_in_doc(anchor_node) || !node_is_in_doc(focus_node) {
2276 return Vec::new();
2277 }
2278
2279 if anchor_node == focus_node {
2281 let start = self
2282 .text_selection
2283 .anchor
2284 .offset
2285 .min(self.text_selection.focus.offset);
2286 let end = self
2287 .text_selection
2288 .anchor
2289 .offset
2290 .max(self.text_selection.focus.offset);
2291
2292 if start == end {
2293 return Vec::new();
2294 }
2295 return vec![(anchor_node, start, end)];
2296 }
2297
2298 let inline_roots = self.collect_inline_roots_in_range(anchor_node, focus_node);
2300 if inline_roots.is_empty() {
2301 return Vec::new();
2302 }
2303
2304 let first_in_roots = inline_roots[0];
2307
2308 let (first_node, first_offset, last_node, last_offset) =
2309 if first_in_roots == anchor_node || (first_in_roots != focus_node) {
2310 (
2312 anchor_node,
2313 self.text_selection.anchor.offset,
2314 focus_node,
2315 self.text_selection.focus.offset,
2316 )
2317 } else {
2318 (
2320 focus_node,
2321 self.text_selection.focus.offset,
2322 anchor_node,
2323 self.text_selection.anchor.offset,
2324 )
2325 };
2326
2327 let mut ranges = Vec::with_capacity(inline_roots.len());
2328
2329 for &node_id in &inline_roots {
2330 let Some(node) = self.get_node(node_id) else {
2331 continue;
2332 };
2333 let Some(element_data) = node.element_data() else {
2334 continue;
2335 };
2336 let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
2337 continue;
2338 };
2339
2340 let text_len = inline_layout.text.len();
2341
2342 if node_id == first_node && node_id == last_node {
2343 let start = first_offset.min(last_offset);
2344 let end = first_offset.max(last_offset);
2345 if start < end && end <= text_len {
2346 ranges.push((node_id, start, end));
2347 }
2348 } else if node_id == first_node {
2349 if first_offset < text_len {
2350 ranges.push((node_id, first_offset, text_len));
2351 }
2352 } else if node_id == last_node {
2353 if last_offset > 0 && last_offset <= text_len {
2354 ranges.push((node_id, 0, last_offset));
2355 }
2356 } else if text_len > 0 {
2357 ranges.push((node_id, 0, text_len));
2358 }
2359 }
2360
2361 ranges
2362 }
2363}
2364
2365pub struct BoundingRect {
2366 pub x: f64,
2367 pub y: f64,
2368 pub width: f64,
2369 pub height: f64,
2370}
2371
2372impl AsRef<BaseDocument> for BaseDocument {
2373 fn as_ref(&self) -> &BaseDocument {
2374 self
2375 }
2376}
2377
2378impl AsMut<BaseDocument> for BaseDocument {
2379 fn as_mut(&mut self) -> &mut BaseDocument {
2380 self
2381 }
2382}
2383
2384#[cfg(test)]
2385mod font_face_override_tests {
2386 use super::*;
2387 use crate::net::{FontFaceOverrides, Resource, ResourceLoadResponse};
2388
2389 #[test]
2405 fn font_face_overrides_alias_family_name() {
2406 const ALIAS: &str = "AliasedFamily";
2407
2408 let mut document = BaseDocument::new(DocumentConfig::default());
2409
2410 {
2412 let mut ctx = document.font_ctx.lock().unwrap();
2413 assert!(
2414 ctx.collection.family_id(ALIAS).is_none(),
2415 "alias must not exist before registration",
2416 );
2417 }
2418
2419 let response = ResourceLoadResponse {
2424 request_id: 0,
2425 node_id: None,
2426 resolved_url: Some(String::from("test://aliased-family")),
2427 result: Ok(Resource::Font(
2428 blitz_traits::net::Bytes::from_static(crate::BULLET_FONT),
2429 FontFaceOverrides {
2430 family_name: Some(String::from(ALIAS)),
2431 weight: Some(800.0),
2432 style: Some(parley::fontique::FontStyle::Italic),
2433 },
2434 )),
2435 };
2436 document.load_resource(response);
2437
2438 let mut ctx = document.font_ctx.lock().unwrap();
2441 let family_id = ctx
2442 .collection
2443 .family_id(ALIAS)
2444 .expect("CSS-declared family name should be registered as a family alias");
2445 let resolved_name = ctx
2446 .collection
2447 .family_name(family_id)
2448 .expect("family id should resolve back to a name");
2449 assert_eq!(
2450 resolved_name, ALIAS,
2451 "registered family should report the CSS-declared name, \
2452 not the font file's internal `name` table entry",
2453 );
2454 }
2455}