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
349fn incremental_layout_default() -> bool {
358 match std::env::var("BLITZ_INCREMENTAL").ok().as_deref() {
359 Some("0" | "false" | "off") => false,
360 Some(_) => true,
361 None => cfg!(feature = "incremental"),
362 }
363}
364
365impl BaseDocument {
366 pub fn new(config: DocumentConfig) -> Self {
368 static ID_GENERATOR: AtomicUsize = AtomicUsize::new(1);
369
370 let id = ID_GENERATOR.fetch_add(1, Ordering::SeqCst);
371
372 let font_ctx = config
373 .font_ctx
374 .map(|mut font_ctx| {
375 font_ctx.source_cache.make_shared();
376 font_ctx
378 })
379 .unwrap_or_else(|| {
380 use parley::fontique::{Collection, CollectionOptions, SourceCache};
381 let mut font_ctx = FontContext {
382 source_cache: SourceCache::new_shared(),
383 collection: Collection::new(CollectionOptions {
384 shared: false,
385 system_fonts: cfg!(all(
386 feature = "system-fonts",
387 not(target_arch = "wasm32")
388 )),
389 }),
390 };
391 font_ctx
392 .collection
393 .register_fonts(Blob::new(Arc::new(crate::BULLET_FONT) as _), None);
394 font_ctx
395 });
396 let font_ctx = Arc::new(Mutex::new(font_ctx));
397
398 style_config::set_pref!("layout.grid.enabled", true);
400 style_config::set_pref!("layout.unimplemented", true);
401 style_config::set_pref!("layout.columns.enabled", true);
402 style_config::set_pref!("layout.css.basic-shape-shape.enabled", true);
403 style_config::set_pref!("layout.threads", -1);
404
405 let viewport = config.viewport.unwrap_or_default();
406 let media_type = config.media_type.unwrap_or_else(MediaType::screen);
407 let device = make_device(&viewport, media_type.clone(), font_ctx.clone());
408 let stylist = Stylist::new(device, QuirksMode::NoQuirks);
409 let snapshots = SnapshotMap::new();
410 let nodes = Box::new(Slab::new());
411 let guard = SharedRwLock::new();
412 let nodes_to_id = HashMap::new();
413
414 let base_url = config
415 .base_url
416 .and_then(|url| DocumentUrl::from_str(&url).ok())
417 .unwrap_or_default();
418
419 let net_provider = config
420 .net_provider
421 .unwrap_or_else(|| Arc::new(DummyNetProvider));
422 let navigation_provider = config
423 .navigation_provider
424 .unwrap_or_else(|| Arc::new(DummyNavigationProvider));
425 let shell_provider = config
426 .shell_provider
427 .unwrap_or_else(|| Arc::new(DummyShellProvider));
428 let html_parser_provider = config
429 .html_parser_provider
430 .unwrap_or_else(|| Arc::new(DummyHtmlParserProvider));
431
432 let (tx, rx) = channel();
433
434 let mut doc = Self {
435 id,
436 tx,
437 rx: Some(rx),
438
439 guard,
440 nodes,
441 stylist,
442 animations: DocumentAnimationSet::default(),
443 snapshots,
444 nodes_to_id,
445 viewport,
446 media_type,
447 style_threading: config.style_threading,
448 incremental_layout: incremental_layout_default(),
449 devtool_settings: DevtoolSettings::default(),
450 viewport_scroll: crate::Point::ZERO,
451 url: base_url,
452 ua_stylesheets: HashMap::new(),
453 nodes_to_stylesheet: BTreeMap::new(),
454 font_ctx,
455 #[cfg(feature = "parallel-construct")]
456 thread_font_contexts: ThreadLocal::new(),
457 layout_ctx: parley::LayoutContext::new(),
458
459 hover_node_id: None,
460 hover_node_is_text: false,
461 focus_node_id: None,
462 active_node_id: None,
463 mousedown_node_id: None,
464 has_active_animations: false,
465 subdoc_is_animating: false,
466 has_canvas: false,
467 sub_document_nodes: HashSet::new(),
468
469 #[cfg(feature = "custom-widget")]
470 custom_widget_nodes: HashSet::new(),
471 #[cfg(feature = "custom-widget")]
472 pending_resource_deallocations: Vec::new(),
473
474 changed_nodes: HashSet::new(),
475 deferred_construction_nodes: Vec::new(),
476 image_cache: HashMap::new(),
477 pending_images: HashMap::new(),
478 pending_critical_resources: HashSet::new(),
479 controls_to_form: HashMap::new(),
480 net_provider,
481 navigation_provider,
482 shell_provider,
483 html_parser_provider,
484 abort_signal: config.abort_signal,
485 last_mousedown_time: None,
486 mousedown_position: taffy::Point::ZERO,
487 click_count: 0,
488 drag_mode: DragMode::None,
489 hovered_scrollbar: None,
490 scrollbar_activity: HashMap::new(),
491 scroll_animation: ScrollAnimationState::None,
492 text_selection: TextSelection::default(),
493 };
494
495 doc.create_node(NodeData::Document);
497 doc.root_node_mut().flags.insert(NodeFlags::IS_IN_DOCUMENT);
498
499 match config.ua_stylesheets {
500 Some(stylesheets) => {
501 for ss in &stylesheets {
502 doc.add_user_agent_stylesheet(ss);
503 }
504 }
505 None => doc.add_user_agent_stylesheet(DEFAULT_CSS),
506 }
507
508 let stylo_element_data = StyloElementData {
510 styles: ElementStyles {
511 primary: Some(
512 ComputedValues::initial_values_with_font_override(Font::initial_values())
513 .to_arc(),
514 ),
515 ..Default::default()
516 },
517 ..Default::default()
518 };
519 let stylo_data = &mut doc.root_node_mut().stylo_element_data;
520 *stylo_data.ensure_init_mut() = stylo_element_data;
521
522 doc
523 }
524
525 pub fn set_net_provider(&mut self, net_provider: Arc<dyn NetProvider>) {
527 self.net_provider = net_provider;
528 }
529
530 pub fn set_navigation_provider(&mut self, navigation_provider: Arc<dyn NavigationProvider>) {
532 self.navigation_provider = navigation_provider;
533 }
534
535 pub fn set_shell_provider(&mut self, shell_provider: Arc<dyn ShellProvider>) {
537 self.shell_provider = shell_provider;
538 }
539
540 pub fn set_html_parser_provider(&mut self, html_parser_provider: Arc<dyn HtmlParserProvider>) {
542 self.html_parser_provider = html_parser_provider;
543 }
544
545 pub fn set_base_url(&mut self, url: &str) {
547 self.url = DocumentUrl::from(Url::parse(url).unwrap());
548 }
549
550 pub fn guard(&self) -> &SharedRwLock {
551 &self.guard
552 }
553
554 pub fn tree(&self) -> &Slab<Node> {
555 &self.nodes
556 }
557
558 pub fn id(&self) -> usize {
559 self.id
560 }
561
562 pub(crate) fn build_request(&self, url: url::Url) -> Request {
565 crate::net::stamped_request(url, self.abort_signal.as_ref())
566 }
567
568 pub fn favicon_url(&self) -> Option<String> {
569 self.tree().iter().find_map(|(_, node)| {
570 let data = &node.data;
571 if !data.is_element_with_tag_name(&local_name!("link")) {
572 return None;
573 }
574 let rel = data.attr(local_name!("rel"))?;
575 if !rel
576 .split_ascii_whitespace()
577 .any(|v| v.eq_ignore_ascii_case("icon"))
578 {
579 return None;
580 }
581 data.attr(local_name!("href")).map(|s| s.to_string())
582 })
583 }
584
585 pub fn get_node(&self, node_id: usize) -> Option<&Node> {
586 self.nodes.get(node_id)
587 }
588
589 pub fn get_node_mut(&mut self, node_id: usize) -> Option<&mut Node> {
590 self.nodes.get_mut(node_id)
591 }
592
593 pub fn get_focussed_node_id(&self) -> Option<usize> {
594 self.focus_node_id
595 .or(self.try_root_element().map(|el| el.id))
596 }
597
598 pub fn mutate<'doc>(&'doc mut self) -> DocumentMutator<'doc> {
599 DocumentMutator::new(self)
600 }
601
602 pub fn handle_dom_event<F: FnMut(DomEvent)>(
603 &mut self,
604 event: &mut DomEvent,
605 dispatch_event: F,
606 ) {
607 handle_dom_event(self, event, dispatch_event)
608 }
609
610 pub fn as_any_mut(&mut self) -> &mut dyn Any {
611 self
612 }
613
614 pub fn label_bound_input_element(&self, label_node_id: usize) -> Option<&Node> {
621 let label_element = self.nodes[label_node_id].element_data()?;
622 if let Some(target_element_dom_id) = label_element.attr(local_name!("for")) {
623 TreeTraverser::new(self)
624 .filter_map(|id| {
625 let node = self.get_node(id)?;
626 let element_data = node.element_data()?;
627 if element_data.name.local != local_name!("input") {
628 return None;
629 }
630 let id = element_data.id.as_ref()?;
631 if *id == *target_element_dom_id {
632 Some(node)
633 } else {
634 None
635 }
636 })
637 .next()
638 } else {
639 TreeTraverser::new_with_root(self, label_node_id)
640 .filter_map(|child_id| {
641 let node = self.get_node(child_id)?;
642 let element_data = node.element_data()?;
643 if element_data.name.local == local_name!("input") {
644 Some(node)
645 } else {
646 None
647 }
648 })
649 .next()
650 }
651 }
652
653 pub fn toggle_checkbox(el: &mut ElementData) -> bool {
654 let Some(is_checked) = el.checkbox_input_checked_mut() else {
655 return false;
656 };
657 *is_checked = !*is_checked;
658
659 *is_checked
660 }
661
662 pub fn toggle_radio(&mut self, radio_set_name: String, target_radio_id: usize) {
663 for i in 0..self.nodes.len() {
664 let node = &mut self.nodes[i];
665 if let Some(node_data) = node.data.downcast_element_mut() {
666 if node_data.attr(local_name!("name")) == Some(&radio_set_name) {
667 let was_clicked = i == target_radio_id;
668 let Some(is_checked) = node_data.checkbox_input_checked_mut() else {
669 continue;
670 };
671 *is_checked = was_clicked;
672 }
673 }
674 }
675 }
676
677 pub fn toggle_details_open(&mut self, details_id: usize) {
681 use crate::qual_name;
682
683 let node = &self.nodes[details_id];
684 if !node.data.is_element_with_tag_name(&local_name!("details")) {
685 return;
686 }
687 let is_open = node.data.has_attr(local_name!("open"));
688
689 let mut mutator = self.mutate();
693 if is_open {
694 mutator.clear_attribute(details_id, qual_name!("open"));
695 } else {
696 mutator.set_attribute(details_id, qual_name!("open"), "");
697 }
698 drop(mutator);
699
700 self.shell_provider.request_redraw();
701 }
702
703 pub fn set_style_property(&mut self, node_id: usize, name: &str, value: &str) {
704 let node = &mut self.nodes[node_id];
705 let did_change = node.element_data_mut().unwrap().set_style_property(
706 name,
707 value,
708 &self.guard,
709 self.url.url_extra_data(),
710 );
711 if did_change {
712 node.mark_style_attr_updated();
713 }
714 }
715
716 pub fn remove_style_property(&mut self, node_id: usize, name: &str) {
717 let node = &mut self.nodes[node_id];
718 let did_change = node.element_data_mut().unwrap().remove_style_property(
719 name,
720 &self.guard,
721 self.url.url_extra_data(),
722 );
723 if did_change {
724 node.mark_style_attr_updated();
725 }
726 }
727
728 pub fn sub_document_node_ids(&self) -> Vec<usize> {
729 self.sub_document_nodes.iter().copied().collect()
730 }
731
732 pub fn set_sub_document(&mut self, node_id: usize, sub_document: Box<dyn Document>) {
733 self.nodes[node_id]
734 .element_data_mut()
735 .unwrap()
736 .set_sub_document(sub_document);
737 self.sub_document_nodes.insert(node_id);
738 }
739
740 pub fn remove_sub_document(&mut self, node_id: usize) {
741 self.nodes[node_id]
742 .element_data_mut()
743 .unwrap()
744 .remove_sub_document();
745 self.sub_document_nodes.remove(&node_id);
746 }
747
748 pub fn poll_subdocuments(&mut self, waker: Option<&Waker>) -> bool {
754 let mut has_changes = false;
755 let node_ids: Vec<usize> = self.sub_document_nodes.iter().copied().collect();
756 for node_id in node_ids {
757 let Some(sub_doc) = self
758 .nodes
759 .get_mut(node_id)
760 .and_then(|node| node.subdoc_mut())
761 else {
762 continue;
763 };
764 let task_context = waker.map(TaskContext::from_waker);
765 has_changes |= sub_doc.poll(task_context);
766 }
767 has_changes
768 }
769
770 #[cfg(feature = "custom-widget")]
771 pub fn custom_widget_node_ids(&self) -> Vec<usize> {
772 self.custom_widget_nodes.iter().copied().collect()
773 }
774
775 #[cfg(feature = "custom-widget")]
776 pub fn take_pending_resource_deallocations(&mut self) -> Vec<anyrender::ResourceId> {
777 std::mem::take(&mut self.pending_resource_deallocations)
778 }
779
780 #[cfg(feature = "custom-widget")]
781 pub fn set_custom_widget(&mut self, node_id: usize, widget: Box<dyn crate::Widget>) {
782 self.nodes[node_id]
783 .element_data_mut()
784 .unwrap()
785 .set_custom_widget(widget);
786 self.custom_widget_nodes.insert(node_id);
787 }
788
789 #[cfg(feature = "custom-widget")]
790 pub fn remove_custom_widget(&mut self, node_id: usize) {
791 let resources_to_deallocate = self.nodes[node_id]
792 .element_data_mut()
793 .unwrap()
794 .remove_custom_widget();
795 self.pending_resource_deallocations
796 .extend_from_slice(&resources_to_deallocate);
797 self.custom_widget_nodes.remove(&node_id);
798 }
799
800 pub fn root_node(&self) -> &Node {
801 &self.nodes[0]
802 }
803
804 pub fn root_node_mut(&mut self) -> &mut Node {
805 &mut self.nodes[0]
806 }
807
808 pub fn try_root_element(&self) -> Option<&Node> {
809 TDocument::as_node(&self.root_node()).first_element_child()
810 }
811
812 pub fn root_element(&self) -> &Node {
813 TDocument::as_node(&self.root_node())
814 .first_element_child()
815 .unwrap()
816 .as_element()
817 .unwrap()
818 }
819
820 pub fn create_node(&mut self, node_data: NodeData) -> usize {
821 let slab_ptr = self.nodes.as_mut() as *mut Slab<Node>;
822 let guard = self.guard.clone();
823
824 let entry = self.nodes.vacant_entry();
825 let id = entry.key();
826 entry.insert(Node::new(slab_ptr, id, guard, node_data));
827
828 self.changed_nodes.insert(id);
830 id
831 }
832
833 pub(crate) fn drop_node_ignoring_parent(&mut self, node_id: usize) -> Option<Node> {
834 self.drop_node_ignoring_parent_with(node_id, &mut |_| {})
835 }
836
837 pub(crate) fn drop_node_ignoring_parent_with(
840 &mut self,
841 node_id: usize,
842 on_drop: &mut dyn FnMut(usize),
843 ) -> Option<Node> {
844 let mut node = self.nodes.try_remove(node_id);
845 if let Some(node) = &mut node {
846 on_drop(node_id);
847 if let Some(before) = node.before {
848 self.drop_node_ignoring_parent_with(before, on_drop);
849 }
850 if let Some(after) = node.after {
851 self.drop_node_ignoring_parent_with(after, on_drop);
852 }
853
854 for &child in &node.children {
855 self.drop_node_ignoring_parent_with(child, on_drop);
856 }
857 }
858 node
859 }
860
861 pub fn has_changes(&self) -> bool {
863 self.changed_nodes.is_empty()
864 }
865
866 pub fn create_text_node(&mut self, text: &str) -> usize {
867 let content = text.to_string();
868 let data = NodeData::Text(TextNodeData::new(content));
869 self.create_node(data)
870 }
871
872 pub fn deep_clone_node(&mut self, node_id: usize) -> usize {
873 let node = &self.nodes[node_id];
875 let mut data = node.data.clone();
876
877 match &mut data {
878 NodeData::Element(elem) | NodeData::AnonymousBlock(elem) => {
879 if let Some(arc) = elem.style_attribute.as_mut() {
880 let read_guard = self.guard().read();
881 let block = arc.read_with(&read_guard);
882 *arc = ServoArc::new(self.guard().wrap(block.clone()));
883 }
884 }
885 _ => {}
886 }
887
888 let children = node.children.clone();
889
890 let new_node_id = self.create_node(data);
892
893 let new_children: Vec<usize> = children
895 .into_iter()
896 .map(|child_id| self.deep_clone_node(child_id))
897 .collect();
898 for &child_id in &new_children {
899 self.nodes[child_id].parent = Some(new_node_id);
900 }
901 self.nodes[new_node_id].children = new_children;
902
903 new_node_id
904 }
905
906 pub(crate) fn remove_and_drop_pe(&mut self, node_id: usize) -> Option<Node> {
907 fn remove_pe_ignoring_parent(doc: &mut BaseDocument, node_id: usize) -> Option<Node> {
908 let mut node = doc.nodes.try_remove(node_id);
909 if let Some(node) = &mut node {
910 for &child in &node.children {
911 remove_pe_ignoring_parent(doc, child);
912 }
913 }
914 node
915 }
916
917 let node = remove_pe_ignoring_parent(self, node_id);
918
919 if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
921 let parent = &mut self.nodes[parent_id];
922 parent.children.retain(|id| *id != node_id);
923 }
924
925 node
926 }
927
928 pub(crate) fn resolve_url(&self, raw: &str) -> url::Url {
929 self.url.resolve_relative(raw).unwrap_or_else(|| {
930 panic!(
931 "to be able to resolve {raw} with the base_url: {:?}",
932 *self.url
933 )
934 })
935 }
936
937 pub fn print_tree(&self) {
938 crate::util::walk_tree(0, self.root_node());
939 }
940
941 pub fn print_subtree(&self, node_id: usize) {
942 crate::util::walk_tree(0, &self.nodes[node_id]);
943 }
944
945 pub fn reload_resource_by_href(&mut self, href_to_reload: &str) {
946 for &node_id in self.nodes_to_stylesheet.keys() {
947 let node = &self.nodes[node_id];
948 let Some(element) = node.element_data() else {
949 continue;
950 };
951
952 if element.name.local == local_name!("link") {
953 if let Some(href) = element.attr(local_name!("href")) {
954 if href == href_to_reload {
956 let resolved_href = self.resolve_url(href);
957 self.net_provider.fetch(
958 self.id(),
959 self.build_request(resolved_href.clone()),
960 ResourceHandler::boxed(
961 self.tx.clone(),
962 self.id,
963 Some(node_id),
964 self.shell_provider.clone(),
965 StylesheetHandler {
966 source_url: resolved_href,
967 guard: self.guard.clone(),
968 net_provider: self.net_provider.clone(),
969 abort_signal: self.abort_signal.clone(),
970 },
971 ),
972 );
973 }
974 }
975 }
976 }
977 }
978
979 pub fn process_style_element(&mut self, target_id: usize) {
980 let css = self.nodes[target_id].text_content();
981 let css = html_escape::decode_html_entities(&css);
982 let sheet = self.make_stylesheet(&css, Origin::Author);
983 self.add_stylesheet_for_node(sheet, target_id);
984 }
985
986 pub fn remove_user_agent_stylesheet(&mut self, contents: &str) {
987 if let Some(sheet) = self.ua_stylesheets.remove(contents) {
988 self.stylist.remove_stylesheet(sheet, &self.guard.read());
989 }
990 }
991
992 pub fn add_user_agent_stylesheet(&mut self, css: &str) {
993 let sheet = self.make_stylesheet(css, Origin::UserAgent);
994 self.ua_stylesheets.insert(css.to_string(), sheet.clone());
995 self.stylist.append_stylesheet(sheet, &self.guard.read());
996 }
997
998 pub fn make_stylesheet(&self, css: impl AsRef<str>, origin: Origin) -> DocumentStyleSheet {
999 let data = Stylesheet::from_str(
1000 css.as_ref(),
1001 self.url.url_extra_data(),
1002 origin,
1003 ServoArc::new(self.guard.wrap(MediaList::empty())),
1004 self.guard.clone(),
1005 Some(&StylesheetLoader {
1006 tx: self.tx.clone(),
1007 doc_id: self.id,
1008 net_provider: self.net_provider.clone(),
1009 shell_provider: self.shell_provider.clone(),
1010 abort_signal: self.abort_signal.clone(),
1011 }),
1012 None,
1013 QuirksMode::NoQuirks,
1014 AllowImportRules::Yes,
1015 );
1016
1017 DocumentStyleSheet(ServoArc::new(data))
1018 }
1019
1020 pub fn upsert_stylesheet_for_node(&mut self, node_id: usize) {
1021 let raw_styles = self.nodes[node_id].text_content();
1022 let sheet = self.make_stylesheet(raw_styles, Origin::Author);
1023 self.add_stylesheet_for_node(sheet, node_id);
1024 }
1025
1026 pub fn add_stylesheet_for_node(&mut self, stylesheet: DocumentStyleSheet, node_id: usize) {
1027 let old = self.nodes_to_stylesheet.insert(node_id, stylesheet.clone());
1028
1029 if let Some(old) = old {
1030 self.stylist.remove_stylesheet(old, &self.guard.read())
1031 }
1032
1033 crate::net::fetch_font_face(
1035 self.tx.clone(),
1036 self.id,
1037 Some(node_id),
1038 &stylesheet.0,
1039 &self.net_provider,
1040 &self.shell_provider,
1041 &self.guard.read(),
1042 self.abort_signal.as_ref(),
1043 );
1044
1045 let element = &mut self.nodes[node_id].element_data_mut().unwrap();
1047 element.special_data = SpecialElementData::Stylesheet(stylesheet.clone());
1048
1049 let insertion_point = self
1051 .nodes_to_stylesheet
1052 .range((Bound::Excluded(node_id), Bound::Unbounded))
1053 .next()
1054 .map(|(_, sheet)| sheet);
1055
1056 if let Some(insertion_point) = insertion_point {
1057 self.stylist.insert_stylesheet_before(
1058 stylesheet,
1059 insertion_point.clone(),
1060 &self.guard.read(),
1061 )
1062 } else {
1063 self.stylist
1064 .append_stylesheet(stylesheet, &self.guard.read())
1065 }
1066 }
1067
1068 pub fn handle_messages(&mut self) {
1069 let rx = self.rx.take().unwrap();
1072
1073 while let Ok(msg) = rx.try_recv() {
1074 self.handle_message(msg);
1075 }
1076
1077 self.rx = Some(rx);
1079 }
1080
1081 pub fn handle_message(&mut self, msg: DocumentEvent) {
1082 match msg {
1083 DocumentEvent::ResourceLoad(resource) => self.load_resource(resource),
1084 }
1085 }
1086
1087 pub fn has_pending_critical_resources(&self) -> bool {
1089 !self.pending_critical_resources.is_empty()
1090 }
1091
1092 pub fn load_resource(&mut self, res: ResourceLoadResponse) {
1093 self.pending_critical_resources.remove(&res.request_id);
1094
1095 let resource = match res.result {
1096 Ok(resource) => resource,
1097 Err(err) => {
1098 if let Some(url) = res.resolved_url.as_ref() {
1099 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1100 #[cfg(feature = "tracing")]
1101 tracing::warn!(
1102 url = url.as_str(),
1103 waiting_nodes = waiting_nodes.len(),
1104 error = err.as_str(),
1105 "Resource load failed"
1106 );
1107 #[cfg(not(feature = "tracing"))]
1108 let _ = (waiting_nodes, err);
1109 } else {
1110 #[cfg(feature = "tracing")]
1111 tracing::warn!(error = err.as_str(), "Resource load failed (no url)");
1112 #[cfg(not(feature = "tracing"))]
1113 let _ = err;
1114 }
1115 return;
1116 }
1117 };
1118
1119 match resource {
1120 Resource::Css(css) => {
1121 let node_id = res.node_id.unwrap();
1122 self.add_stylesheet_for_node(css, node_id);
1123 }
1124 Resource::Image(_kind, width, height, image_data) => {
1125 let image = ImageData::Raster(RasterImageData::new(width, height, image_data));
1127
1128 let Some(url) = res.resolved_url.as_ref() else {
1129 return;
1130 };
1131
1132 self.apply_loaded_image(url, image);
1133 }
1134 #[cfg(feature = "svg")]
1135 Resource::Svg(_kind, svg) => {
1136 let image = ImageData::Svg(svg);
1138
1139 let Some(url) = res.resolved_url.as_ref() else {
1140 return;
1141 };
1142
1143 self.apply_loaded_image(url, image);
1144 }
1145 Resource::Font(bytes, overrides) => {
1146 let font = Blob::new(Arc::new(bytes));
1147
1148 let weight_override = overrides.weight.map(parley::fontique::FontWeight::new);
1154 let info_override = parley::fontique::FontInfoOverride {
1155 family_name: overrides.family_name.as_deref(),
1156 weight: weight_override,
1157 style: overrides.style,
1158 ..Default::default()
1159 };
1160
1161 let mut global_font_ctx = self.font_ctx.lock().unwrap();
1163 global_font_ctx
1164 .collection
1165 .register_fonts(font.clone(), Some(info_override));
1166
1167 #[cfg(feature = "parallel-construct")]
1168 {
1169 rayon::broadcast(|_ctx| {
1170 let mut font_ctx = self
1171 .thread_font_contexts
1172 .get_or(|| RefCell::new(Box::new(global_font_ctx.clone())))
1173 .borrow_mut();
1174 font_ctx
1175 .collection
1176 .register_fonts(font.clone(), Some(info_override));
1177 });
1178 }
1179 drop(global_font_ctx);
1180
1181 self.invalidate_inline_contexts();
1183 }
1184 Resource::None => {
1185 }
1187 }
1188 }
1189
1190 fn apply_loaded_image(&mut self, url: &str, image: ImageData) {
1193 let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1195
1196 #[cfg(feature = "tracing")]
1197 tracing::info!(
1198 "Image {url} loaded, applying to {} nodes",
1199 waiting_nodes.len()
1200 );
1201
1202 self.image_cache.insert(url.to_string(), image.clone());
1204
1205 for (node_id, image_type) in waiting_nodes {
1207 let Some(node) = self.get_node_mut(node_id) else {
1208 continue;
1209 };
1210
1211 match image_type {
1212 ImageType::Image => {
1213 node.element_data_mut().unwrap().special_data =
1214 SpecialElementData::Image(Box::new(image.clone()));
1215
1216 node.cache.clear();
1218 node.insert_damage(ALL_DAMAGE);
1219 }
1220 ImageType::Background(idx) | ImageType::Mask(idx) => {
1221 let layer_image = node.element_data_mut().and_then(|el| {
1222 let images = match image_type {
1223 ImageType::Background(_) => &mut el.background_images,
1224 ImageType::Mask(_) => &mut el.mask_images,
1225 ImageType::Image => unreachable!(),
1226 };
1227 images.get_mut(idx)
1228 });
1229 if let Some(Some(layer_image)) = layer_image {
1230 layer_image.status = Status::Ok;
1231 layer_image.image = image.clone();
1232 }
1233 }
1234 }
1235 }
1236 }
1237
1238 pub fn snapshot_node(&mut self, node_id: usize) {
1239 let node = &mut self.nodes[node_id];
1240
1241 let has_been_styled = node.primary_styles().is_some();
1246 if !has_been_styled {
1247 return;
1248 }
1249
1250 let opaque_node_id = TNode::opaque(&&*node);
1251 node.has_snapshot = true;
1252 node.snapshot_handled
1253 .store(false, std::sync::atomic::Ordering::SeqCst);
1254
1255 if let Some(_existing_snapshot) = self.snapshots.get_mut(&opaque_node_id) {
1257 } else {
1260 let attrs: Option<Vec<_>> = node.attrs().map(|attrs| {
1261 attrs
1262 .iter()
1263 .map(|attr| {
1264 let ident = AttrIdentifier {
1265 local_name: GenericAtomIdent(attr.name.local.clone()),
1266 name: GenericAtomIdent(attr.name.local.clone()),
1267 namespace: GenericAtomIdent(attr.name.ns.clone()),
1268 prefix: None,
1269 };
1270
1271 let value = if attr.name.local == local_name!("id") {
1272 AttrValue::Atom(Atom::from(&*attr.value))
1273 } else if attr.name.local == local_name!("class") {
1274 let classes = attr
1275 .value
1276 .split_ascii_whitespace()
1277 .map(Atom::from)
1278 .collect();
1279 AttrValue::TokenList(OnceLock::from(attr.value.clone()), classes)
1280 } else {
1281 AttrValue::String(attr.value.clone())
1282 };
1283
1284 (ident, value)
1285 })
1286 .collect()
1287 });
1288
1289 let changed_attrs = attrs
1290 .as_ref()
1291 .map(|attrs| attrs.iter().map(|attr| attr.0.name.clone()).collect())
1292 .unwrap_or_default();
1293
1294 self.snapshots.insert(
1295 opaque_node_id,
1296 ServoElementSnapshot {
1297 state: Some(node.element_state),
1298 attrs,
1299 changed_attrs,
1300 class_changed: true,
1301 id_changed: true,
1302 other_attributes_changed: true,
1303 },
1304 );
1305 }
1306 }
1307
1308 pub fn snapshot_node_and(&mut self, node_id: usize, cb: impl FnOnce(&mut Node)) {
1309 self.snapshot_node(node_id);
1310 cb(&mut self.nodes[node_id]);
1311 }
1312
1313 pub fn hit(&self, x: f32, y: f32) -> Option<HitResult> {
1315 self.hit_with_scrollbar(x, y).0
1316 }
1317
1318 pub fn focus_next_node(&mut self) -> Option<usize> {
1319 let focussed_node_id = self.get_focussed_node_id()?;
1320 let id = self.next_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
1321 self.set_focus_to(id);
1322 Some(id)
1323 }
1324
1325 pub fn clear_focus(&mut self) {
1327 if let Some(id) = self.focus_node_id {
1328 let shell_provider = self.shell_provider.clone();
1329 self.snapshot_node_and(id, |node| node.blur(shell_provider));
1330 self.focus_node_id = None;
1331 }
1332 }
1333
1334 pub fn set_mousedown_node_id(&mut self, node_id: Option<usize>) {
1335 self.mousedown_node_id = node_id;
1336 }
1337 pub fn set_focus_to(&mut self, focus_node_id: usize) -> bool {
1338 if Some(focus_node_id) == self.focus_node_id {
1339 return false;
1340 }
1341
1342 #[cfg(feature = "tracing")]
1343 tracing::info!("Focussed node {focus_node_id}");
1344
1345 let shell_provider = self.shell_provider.clone();
1346
1347 if let Some(id) = self.focus_node_id {
1349 self.snapshot_node_and(id, |node| node.blur(shell_provider.clone()));
1350 }
1351
1352 self.snapshot_node_and(focus_node_id, |node| node.focus(shell_provider));
1354
1355 self.focus_node_id = Some(focus_node_id);
1356
1357 true
1358 }
1359
1360 pub fn active_node(&mut self) -> bool {
1361 let Some(hover_node_id) = self.get_hover_node_id() else {
1362 return false;
1363 };
1364
1365 if let Some(active_node_id) = self.active_node_id {
1366 if active_node_id == hover_node_id {
1367 return true;
1368 }
1369 self.unactive_node();
1370 }
1371
1372 let active_node_id = Some(hover_node_id);
1373
1374 let node_path = self.maybe_node_layout_ancestors(active_node_id);
1375 for &id in node_path.iter() {
1376 self.snapshot_node_and(id, |node| node.active());
1377 }
1378
1379 self.active_node_id = active_node_id;
1380
1381 true
1382 }
1383
1384 pub fn unactive_node(&mut self) -> bool {
1385 let Some(active_node_id) = self.active_node_id.take() else {
1386 return false;
1387 };
1388
1389 let node_path = self.maybe_node_layout_ancestors(Some(active_node_id));
1390 for &id in node_path.iter() {
1391 self.snapshot_node_and(id, |node| node.unactive());
1392 }
1393
1394 true
1395 }
1396
1397 pub fn hovered_scrollbar(&self) -> Option<crate::node::ScrollbarRef> {
1399 self.hovered_scrollbar
1400 }
1401
1402 pub fn scrollbar_drag_target(&self) -> Option<crate::node::ScrollbarRef> {
1404 match &self.drag_mode {
1405 DragMode::ScrollbarDrag(state) => Some(state.scrollbar),
1406 _ => None,
1407 }
1408 }
1409
1410 pub fn scrollbar_opacity(&self, node_id: usize) -> f32 {
1415 let interacting = |scrollbar: &crate::node::ScrollbarRef| scrollbar.node_id == node_id;
1416 if self.hovered_scrollbar.as_ref().is_some_and(interacting)
1417 || self
1418 .scrollbar_drag_target()
1419 .as_ref()
1420 .is_some_and(interacting)
1421 {
1422 return 1.0;
1423 }
1424 self.scrollbar_activity.get(&node_id).map_or(1.0, |last| {
1425 crate::node::scrollbar::opacity_at(last.elapsed())
1426 })
1427 }
1428
1429 pub(crate) fn show_scrollbars(&mut self, node_id: usize) {
1432 if cfg!(feature = "scrollbars") {
1433 self.scrollbar_activity.insert(node_id, Instant::now());
1434 }
1435 }
1436
1437 fn scrollbars_animating(&self) -> bool {
1440 use crate::node::scrollbar::{FADE_DELAY, FADE_DURATION};
1441 self.scrollbar_activity
1442 .values()
1443 .any(|last| last.elapsed() < FADE_DELAY + FADE_DURATION)
1444 }
1445
1446 pub(crate) fn hit_with_scrollbar(
1450 &self,
1451 x: f32,
1452 y: f32,
1453 ) -> (Option<HitResult>, Option<crate::node::ScrollbarRef>) {
1454 if TDocument::as_node(&&self.nodes[0])
1455 .first_element_child()
1456 .is_none()
1457 {
1458 #[cfg(feature = "tracing")]
1459 tracing::warn!("No DOM - not resolving hit test");
1460 return (None, None);
1461 }
1462 let mut scrollbar = None;
1463 let hit = self
1464 .root_element()
1465 .hit_inner(x, y, self.viewport().scale_f64(), &mut scrollbar);
1466 (hit, scrollbar)
1467 }
1468
1469 pub fn set_hover_to(&mut self, x: f32, y: f32) -> bool {
1470 let (hit, hovered_scrollbar) = self.hit_with_scrollbar(x, y);
1471 let hovered_scrollbar =
1474 hovered_scrollbar.filter(|scrollbar| self.scrollbar_opacity(scrollbar.node_id) > 0.0);
1475 let scrollbar_changed = hovered_scrollbar != self.hovered_scrollbar;
1479 if scrollbar_changed {
1480 for scrollbar in [self.hovered_scrollbar, hovered_scrollbar]
1483 .into_iter()
1484 .flatten()
1485 {
1486 self.show_scrollbars(scrollbar.node_id);
1487 }
1488 }
1489 self.hovered_scrollbar = hovered_scrollbar;
1490 let hover_node_id = hit.map(|hit| hit.node_id);
1491 let new_is_text = hit.map(|hit| hit.is_text).unwrap_or(false);
1492
1493 if hover_node_id == self.hover_node_id {
1495 return scrollbar_changed;
1496 }
1497
1498 let old_node_path = self.maybe_node_layout_ancestors(self.hover_node_id);
1499 let new_node_path = self.maybe_node_layout_ancestors(hover_node_id);
1500 let same_count = old_node_path
1501 .iter()
1502 .zip(&new_node_path)
1503 .take_while(|(o, n)| o == n)
1504 .count();
1505 for &id in old_node_path.iter().skip(same_count) {
1506 self.snapshot_node_and(id, |node| node.unhover());
1507 }
1508 for &id in new_node_path.iter().skip(same_count) {
1509 self.snapshot_node_and(id, |node| node.hover());
1510 }
1511
1512 self.hover_node_id = hover_node_id;
1513 self.hover_node_is_text = new_is_text;
1514
1515 self.shell_provider.set_cursor(self.get_cursor());
1517
1518 self.shell_provider.request_redraw();
1520
1521 true
1522 }
1523
1524 pub fn clear_hover(&mut self) -> bool {
1525 let Some(hover_node_id) = self.hover_node_id else {
1526 return false;
1527 };
1528
1529 let old_node_path = self.maybe_node_layout_ancestors(Some(hover_node_id));
1530 for &id in old_node_path.iter() {
1531 self.snapshot_node_and(id, |node| node.unhover());
1532 }
1533
1534 self.hover_node_id = None;
1535 self.hover_node_is_text = false;
1536
1537 self.shell_provider.set_cursor(self.get_cursor());
1539
1540 self.shell_provider.request_redraw();
1542
1543 true
1544 }
1545
1546 pub fn get_hover_node_id(&self) -> Option<usize> {
1547 self.hover_node_id
1548 }
1549
1550 pub fn set_viewport(&mut self, viewport: Viewport) {
1551 let scale_has_changed = viewport.scale_f64() != self.viewport.scale_f64();
1552 self.viewport = viewport;
1553 self.set_stylist_device(make_device(
1554 &self.viewport,
1555 self.media_type.clone(),
1556 self.font_ctx.clone(),
1557 ));
1558 self.scroll_viewport_by(0.0, 0.0); if scale_has_changed {
1561 self.invalidate_inline_contexts();
1562 self.shell_provider.request_redraw();
1563 }
1564 }
1565
1566 pub fn media_type(&self) -> &MediaType {
1568 &self.media_type
1569 }
1570
1571 pub fn set_media_type(&mut self, media_type: MediaType) {
1574 if self.media_type == media_type {
1575 return;
1576 }
1577 self.media_type = media_type;
1578 self.set_stylist_device(make_device(
1579 &self.viewport,
1580 self.media_type.clone(),
1581 self.font_ctx.clone(),
1582 ));
1583 }
1584
1585 pub fn viewport(&self) -> &Viewport {
1586 &self.viewport
1587 }
1588
1589 pub fn viewport_mut(&mut self) -> ViewportMut<'_> {
1590 ViewportMut::new(self)
1591 }
1592
1593 pub fn zoom_by(&mut self, increment: f32) {
1594 *self.viewport.zoom_mut() += increment;
1595 self.set_viewport(self.viewport.clone());
1596 }
1597
1598 pub fn zoom_to(&mut self, zoom: f32) {
1599 *self.viewport.zoom_mut() = zoom;
1600 self.set_viewport(self.viewport.clone());
1601 }
1602
1603 pub fn get_viewport(&self) -> Viewport {
1604 self.viewport.clone()
1605 }
1606
1607 pub fn incremental_layout(&self) -> bool {
1609 self.incremental_layout
1610 }
1611
1612 pub fn set_incremental_layout(&mut self, enabled: bool) {
1617 self.incremental_layout = enabled;
1618 }
1619
1620 pub fn devtools(&self) -> &DevtoolSettings {
1621 &self.devtool_settings
1622 }
1623
1624 pub fn devtools_mut(&mut self) -> &mut DevtoolSettings {
1625 &mut self.devtool_settings
1626 }
1627
1628 pub fn subdoc(&self, node_id: usize) -> Option<&dyn Document> {
1629 self.get_node(node_id)
1630 .and_then(|node| node.element_data())
1631 .and_then(|el| el.sub_doc_data())
1632 }
1633
1634 pub fn subdoc_mut(&mut self, node_id: usize) -> Option<&mut dyn Document> {
1635 self.get_node_mut(node_id)
1636 .and_then(|node| node.element_data_mut())
1637 .and_then(|el| el.sub_doc_data_mut())
1638 }
1639
1640 pub fn is_animating(&self) -> bool {
1641 #[cfg(feature = "custom-widget")]
1642 let has_custom_widgets = !self.custom_widget_nodes.is_empty();
1643 #[cfg(not(feature = "custom-widget"))]
1644 let has_custom_widgets = false;
1645
1646 self.has_canvas
1647 | self.has_active_animations
1648 | self.subdoc_is_animating
1649 | has_custom_widgets
1650 | (self.scroll_animation != ScrollAnimationState::None)
1651 | self.scrollbars_animating()
1652 }
1653
1654 pub fn set_stylist_device(&mut self, device: Device) {
1656 let root_styles = self
1662 .try_root_element()
1663 .and_then(|root| root.primary_styles());
1664 if let Some(root_style) = root_styles.as_deref() {
1665 device.set_root_style(root_style);
1666
1667 let font = root_style.get_font();
1668 let font_size = font.clone_font_size().computed_size();
1669 device.set_root_font_size(root_style.effective_zoom.unzoom(font_size.px()));
1670
1671 let line_height = device
1672 .calc_line_height(font, root_style.writing_mode, None)
1673 .0;
1674 device.set_root_line_height(root_style.effective_zoom.unzoom(line_height.px()));
1675 }
1676 drop(root_styles);
1677
1678 let origins = {
1679 let guard = &self.guard;
1680 let guards = StylesheetGuards {
1681 author: &guard.read(),
1682 ua_or_user: &guard.read(),
1683 };
1684 self.stylist.set_device(device, &guards)
1685 };
1686 self.stylist.force_stylesheet_origins_dirty(origins);
1687 }
1688
1689 pub fn stylist_device(&mut self) -> &Device {
1690 self.stylist.device()
1691 }
1692
1693 pub fn get_cursor(&self) -> Option<CursorIcon> {
1694 let node = &self.nodes[self.get_hover_node_id()?];
1695
1696 if let Some(subdoc) = node.subdoc().map(|doc| doc.inner()) {
1697 return subdoc.get_cursor();
1698 }
1699
1700 let style = node.primary_styles()?;
1701 let user_select = style.clone_user_select();
1702 let keyword = style.clone_cursor().keyword;
1703
1704 if keyword != CursorKind::Auto {
1706 return stylo_to_cursor_icon(keyword);
1707 }
1708
1709 if node
1711 .element_data()
1712 .is_some_and(|e| e.text_input_data().is_some())
1713 {
1714 return Some(CursorIcon::Text);
1715 }
1716
1717 let mut maybe_node = Some(node);
1719 while let Some(node) = maybe_node {
1720 if node.is_link() {
1721 return Some(CursorIcon::Pointer);
1722 }
1723
1724 maybe_node = node.layout_parent.get().map(|node_id| node.with(node_id));
1725 }
1726
1727 if self.hover_node_is_text {
1729 return Some(match user_select {
1730 UserSelect::Text | UserSelect::All | UserSelect::Auto => CursorIcon::Text,
1731 UserSelect::None => CursorIcon::Default,
1732 });
1733 }
1734
1735 Some(CursorIcon::Default)
1737 }
1738
1739 pub fn scroll_node_by<F: FnMut(DomEvent)>(
1740 &mut self,
1741 node_id: usize,
1742 x: f64,
1743 y: f64,
1744 dispatch_event: F,
1745 ) {
1746 self.scroll_node_by_has_changed(node_id, x, y, dispatch_event);
1747 }
1748
1749 pub fn scroll_node_by_has_changed<F: FnMut(DomEvent)>(
1753 &mut self,
1754 node_id: usize,
1755 x: f64,
1756 y: f64,
1757 mut dispatch_event: F,
1758 ) -> bool {
1759 if self.try_root_element().is_some_and(|el| el.id == node_id) {
1764 let has_changed = self.scroll_viewport_by_has_changed(x, y);
1765 if has_changed {
1766 let layout = self.root_element().final_layout;
1767 let scale = self.viewport.scale() as f64;
1768 let event = BlitzScrollEvent {
1769 scroll_top: self.viewport_scroll.y,
1770 scroll_left: self.viewport_scroll.x,
1771 scroll_width: layout.size.width.max(layout.content_size.width) as i32,
1772 scroll_height: layout.size.height.max(layout.content_size.height) as i32,
1773 client_width: (self.viewport.window_size.0 as f64 / scale) as i32,
1774 client_height: (self.viewport.window_size.1 as f64 / scale) as i32,
1775 };
1776 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
1777 }
1778 return has_changed;
1779 }
1780
1781 let Some(node) = self.nodes.get_mut(node_id) else {
1782 return false;
1783 };
1784
1785 if node
1789 .element_data()
1790 .is_some_and(|el| el.text_input_data().is_some())
1791 {
1792 let parent = node.parent;
1793 let content_box_width = node.final_layout.content_box_width();
1794 let content_box_height = node.final_layout.content_box_height();
1795 let input = node
1796 .element_data_mut()
1797 .and_then(|el| el.text_input_data_mut())
1798 .unwrap();
1799
1800 let (bubble_x, bubble_y) = if input.is_multiline {
1801 (
1802 x,
1803 input.scroll_by(y as f32, content_box_width, content_box_height) as f64,
1804 )
1805 } else {
1806 (
1807 input.scroll_by(x as f32, content_box_width, content_box_height) as f64,
1808 y,
1809 )
1810 };
1811
1812 let has_changed = bubble_x != x || bubble_y != y;
1813
1814 if bubble_x != 0.0 || bubble_y != 0.0 {
1815 let bubbled = if let Some(parent) = parent {
1816 self.scroll_node_by_has_changed(parent, bubble_x, bubble_y, dispatch_event)
1817 } else {
1818 self.scroll_viewport_by_has_changed(bubble_x, bubble_y)
1819 };
1820 return bubbled | has_changed;
1821 }
1822
1823 return has_changed;
1824 }
1825
1826 let (can_x_scroll, can_y_scroll) = node
1827 .primary_styles()
1828 .map(|styles| {
1829 (
1830 matches!(styles.clone_overflow_x(), Overflow::Scroll | Overflow::Auto),
1831 matches!(styles.clone_overflow_y(), Overflow::Scroll | Overflow::Auto),
1832 )
1833 })
1834 .unwrap_or((false, false));
1835
1836 let initial = node.scroll_offset;
1837 let new_x = node.scroll_offset.x - x;
1838 let new_y = node.scroll_offset.y - y;
1839
1840 let mut bubble_x = 0.0;
1841 let mut bubble_y = 0.0;
1842
1843 let scroll_width = node.final_layout.scroll_width() as f64;
1844 let scroll_height = node.final_layout.scroll_height() as f64;
1845
1846 if let Some(mut sub_doc) = node.subdoc_mut().map(|doc| doc.inner_mut()) {
1848 let has_changed = if let Some(hover_node_id) = sub_doc.get_hover_node_id() {
1849 sub_doc.scroll_node_by_has_changed(hover_node_id, x, y, dispatch_event)
1850 } else {
1851 sub_doc.scroll_viewport_by_has_changed(x, y)
1852 };
1853
1854 return has_changed;
1856 }
1857
1858 if !can_x_scroll {
1860 bubble_x = x
1861 } else if new_x < 0.0 {
1862 bubble_x = -new_x;
1863 node.scroll_offset.x = 0.0;
1864 } else if new_x > scroll_width {
1865 bubble_x = scroll_width - new_x;
1866 node.scroll_offset.x = scroll_width;
1867 } else {
1868 node.scroll_offset.x = new_x;
1869 }
1870
1871 if !can_y_scroll {
1872 bubble_y = y
1873 } else if new_y < 0.0 {
1874 bubble_y = -new_y;
1875 node.scroll_offset.y = 0.0;
1876 } else if new_y > scroll_height {
1877 bubble_y = scroll_height - new_y;
1878 node.scroll_offset.y = scroll_height;
1879 } else {
1880 node.scroll_offset.y = new_y;
1881 }
1882
1883 let has_changed = node.scroll_offset != initial;
1884
1885 if has_changed {
1886 let layout = node.final_layout;
1887 let event = BlitzScrollEvent {
1888 scroll_top: node.scroll_offset.y,
1889 scroll_left: node.scroll_offset.x,
1890 scroll_width: layout.scroll_width() as i32,
1891 scroll_height: layout.scroll_height() as i32,
1892 client_width: layout.size.width as i32,
1893 client_height: layout.size.height as i32,
1894 };
1895
1896 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
1897 }
1898
1899 let parent = node.parent;
1900 if has_changed {
1901 self.show_scrollbars(node_id);
1902 }
1903
1904 if bubble_x != 0.0 || bubble_y != 0.0 {
1905 if let Some(parent) = parent {
1906 return self.scroll_node_by_has_changed(parent, bubble_x, bubble_y, dispatch_event)
1907 | has_changed;
1908 } else {
1909 return self.scroll_viewport_by_has_changed(bubble_x, bubble_y) | has_changed;
1910 }
1911 }
1912
1913 has_changed
1914 }
1915
1916 pub fn scroll_viewport_by(&mut self, x: f64, y: f64) {
1917 self.scroll_viewport_by_has_changed(x, y);
1918 }
1919
1920 pub fn scroll_viewport_by_has_changed(&mut self, x: f64, y: f64) -> bool {
1922 let root_layout = &self.root_element().final_layout;
1926 let content_width = root_layout.size.width.max(root_layout.content_size.width) as f64;
1927 let content_height = root_layout.size.height.max(root_layout.content_size.height) as f64;
1928 let new_scroll = (self.viewport_scroll.x - x, self.viewport_scroll.y - y);
1929 let window_width = self.viewport.window_size.0 as f64 / self.viewport.scale() as f64;
1930 let window_height = self.viewport.window_size.1 as f64 / self.viewport.scale() as f64;
1931
1932 let initial = self.viewport_scroll;
1933 self.viewport_scroll.x =
1934 f64::max(0.0, f64::min(new_scroll.0, content_width - window_width));
1935 self.viewport_scroll.y =
1936 f64::max(0.0, f64::min(new_scroll.1, content_height - window_height));
1937
1938 self.viewport_scroll != initial
1939 }
1940
1941 pub fn scroll_by(
1942 &mut self,
1943 anchor_node_id: Option<usize>,
1944 scroll_x: f64,
1945 scroll_y: f64,
1946 dispatch_event: &mut dyn FnMut(DomEvent),
1947 ) -> bool {
1948 if let Some(anchor_node_id) = anchor_node_id {
1949 self.scroll_node_by_has_changed(anchor_node_id, scroll_x, scroll_y, dispatch_event)
1950 } else {
1951 self.scroll_viewport_by_has_changed(scroll_x, scroll_y)
1952 }
1953 }
1954
1955 pub fn viewport_scroll(&self) -> crate::Point<f64> {
1956 self.viewport_scroll
1957 }
1958
1959 pub fn set_viewport_scroll(&mut self, scroll: crate::Point<f64>) {
1960 self.viewport_scroll = scroll;
1961 }
1962
1963 pub fn get_fragment_target(&self, fragment: &str) -> Option<usize> {
1968 if let Some(node_id) = self.get_element_by_id(fragment) {
1969 return Some(node_id);
1970 }
1971
1972 self.nodes.iter().find_map(|(id, node)| {
1974 let el = node.element_data()?;
1975 (el.name.local == local_name!("a") && el.attr(local_name!("name")) == Some(fragment))
1976 .then_some(id)
1977 })
1978 }
1979
1980 pub fn scroll_to_node(&mut self, node_id: usize) {
1982 let Some(node) = self.nodes.get(node_id) else {
1983 return;
1984 };
1985
1986 let target = node.absolute_position(0.0, 0.0);
1989 let current = self.viewport_scroll;
1990
1991 self.scroll_viewport_by(current.x - target.x as f64, current.y - target.y as f64);
1994 }
1995
1996 pub fn scroll_to_fragment(&mut self, fragment: &str) -> bool {
2002 let decoded = percent_encoding::percent_decode_str(fragment)
2004 .decode_utf8_lossy()
2005 .into_owned();
2006
2007 if !decoded.is_empty() {
2008 if let Some(node_id) = self.get_fragment_target(&decoded) {
2009 self.scroll_to_node(node_id);
2010 return true;
2011 }
2012 }
2013
2014 if decoded.is_empty() || decoded.eq_ignore_ascii_case("top") {
2017 let current = self.viewport_scroll;
2018 self.scroll_viewport_by(current.x, current.y);
2019 return true;
2020 }
2021
2022 false
2023 }
2024
2025 pub fn get_client_bounding_rect(&self, node_id: usize) -> Option<BoundingRect> {
2027 let node = self.get_node(node_id)?;
2028 let pos = node.absolute_position(0.0, 0.0);
2029
2030 Some(BoundingRect {
2031 x: pos.x as f64 - self.viewport_scroll.x,
2032 y: pos.y as f64 - self.viewport_scroll.y,
2033 width: node.unrounded_layout.size.width as f64,
2034 height: node.unrounded_layout.size.height as f64,
2035 })
2036 }
2037
2038 pub fn find_title_node(&self) -> Option<&Node> {
2039 TreeTraverser::new(self)
2040 .find(|node_id| {
2041 let node = &self.nodes[*node_id];
2042 let Some(element) = node.element_data() else {
2043 return false;
2044 };
2045 if element.name.ns != ns!(html) || element.name.local != local_name!("title") {
2046 return false;
2047 }
2048 node.parent
2049 .and_then(|parent_id| self.nodes.get(parent_id))
2050 .and_then(Node::element_data)
2051 .is_some_and(|parent| {
2052 parent.name.ns == ns!(html) && parent.name.local == local_name!("head")
2053 })
2054 })
2055 .map(|node_id| &self.nodes[node_id])
2056 }
2057
2058 pub fn with_text_input(
2059 &mut self,
2060 node_id: usize,
2061 cb: impl FnOnce(PlainEditorDriver<TextBrush>),
2062 ) {
2063 let Some(node) = self.nodes.get_mut(node_id) else {
2064 return;
2065 };
2066
2067 if let Some(text_input) = node
2068 .element_data_mut()
2069 .and_then(|el| el.text_input_data_mut())
2070 {
2071 let mut font_ctx = self.font_ctx.lock().unwrap();
2072 let layout_ctx = &mut self.layout_ctx;
2073 let driver = text_input.editor.driver(&mut font_ctx, layout_ctx);
2074 cb(driver)
2075 }
2076 }
2077
2078 pub(crate) fn clamp_text_input_scroll(&mut self, node_id: usize) {
2081 let Some(node) = self.nodes.get_mut(node_id) else {
2082 return;
2083 };
2084
2085 let content_box_width = node.final_layout.content_box_width();
2086 let content_box_height = node.final_layout.content_box_height();
2087
2088 if let Some(text_input) = node
2089 .element_data_mut()
2090 .and_then(|el| el.text_input_data_mut())
2091 {
2092 text_input.clamp_scroll_offset(content_box_width, content_box_height);
2093 }
2094 }
2095
2096 pub(crate) fn compute_has_canvas(&self) -> bool {
2097 TreeTraverser::new(self).any(|node_id| {
2098 let node = &self.nodes[node_id];
2099 let Some(element) = node.element_data() else {
2100 return false;
2101 };
2102 if element.name.local == local_name!("canvas") && element.has_attr(local_name!("src")) {
2103 return true;
2104 }
2105
2106 false
2107 })
2108 }
2109
2110 pub fn find_text_position(&self, x: f32, y: f32) -> Option<(usize, usize)> {
2116 let hit = self.hit(x, y)?;
2117 let hit_node = self.get_node(hit.node_id)?;
2118 let inline_root = hit_node.inline_root_ancestor()?;
2119 let byte_offset = inline_root.text_offset_at_point(hit.x, hit.y)?;
2120 Some((inline_root.id, byte_offset))
2121 }
2122
2123 pub fn set_text_selection(
2125 &mut self,
2126 anchor_node: usize,
2127 anchor_offset: usize,
2128 focus_node: usize,
2129 focus_offset: usize,
2130 ) {
2131 self.text_selection =
2132 TextSelection::new(anchor_node, anchor_offset, focus_node, focus_offset);
2133
2134 if let (Some(parent), Some(idx)) = self.anonymous_block_location(anchor_node) {
2136 self.text_selection
2137 .anchor
2138 .set_anonymous(parent, idx, anchor_offset);
2139 }
2140 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
2141 self.text_selection
2142 .focus
2143 .set_anonymous(parent, idx, focus_offset);
2144 }
2145 }
2146
2147 fn anonymous_block_location(&self, node_id: usize) -> (Option<usize>, Option<usize>) {
2150 let Some(node) = self.get_node(node_id) else {
2151 return (None, None);
2152 };
2153
2154 if !node.is_anonymous() {
2155 return (None, None);
2156 }
2157
2158 let Some(parent_id) = node.parent else {
2159 return (None, None);
2160 };
2161
2162 let Some(parent) = self.get_node(parent_id) else {
2163 return (Some(parent_id), None);
2164 };
2165
2166 let layout_children = parent.layout_children.borrow();
2167 let Some(children) = layout_children.as_ref() else {
2168 return (Some(parent_id), None);
2169 };
2170
2171 let mut anon_index = 0;
2173 for &child_id in children.iter() {
2174 if child_id == node_id {
2175 return (Some(parent_id), Some(anon_index));
2176 }
2177 if self.get_node(child_id).is_some_and(|n| n.is_anonymous()) {
2178 anon_index += 1;
2179 }
2180 }
2181
2182 (Some(parent_id), None)
2183 }
2184
2185 pub fn clear_text_selection(&mut self) {
2187 self.text_selection.clear();
2188 }
2189
2190 pub fn update_selection_focus(&mut self, focus_node: usize, focus_offset: usize) {
2192 if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
2194 self.text_selection
2195 .focus
2196 .set_anonymous(parent, idx, focus_offset);
2197 } else {
2198 self.text_selection.set_focus(focus_node, focus_offset);
2199 }
2200 }
2201
2202 pub fn extend_text_selection_to_point(&mut self, x: f32, y: f32) -> bool {
2205 if !self.text_selection.anchor.is_some() {
2206 return false;
2207 }
2208
2209 if let Some((node, offset)) = self.find_text_position(x, y) {
2210 self.update_selection_focus(node, offset);
2211 self.shell_provider.request_redraw();
2212 true
2213 } else {
2214 false
2215 }
2216 }
2217
2218 fn find_anonymous_block_by_index(
2220 &self,
2221 parent_id: usize,
2222 target_index: usize,
2223 ) -> Option<usize> {
2224 let parent = self.get_node(parent_id)?;
2225 let layout_children = parent.layout_children.borrow();
2226 let children = layout_children.as_ref()?;
2227
2228 children
2229 .iter()
2230 .filter(|&&child_id| self.get_node(child_id).is_some_and(|n| n.is_anonymous()))
2231 .nth(target_index)
2232 .copied()
2233 }
2234
2235 pub fn has_text_selection(&self) -> bool {
2237 self.text_selection.is_active()
2238 }
2239
2240 pub fn get_selected_text(&self) -> Option<String> {
2242 let ranges = self.get_text_selection_ranges();
2243 if ranges.is_empty() {
2244 return None;
2245 }
2246
2247 let mut result = String::new();
2248 for (node_id, start, end) in &ranges {
2249 let node = self.get_node(*node_id)?;
2250 let element_data = node.element_data()?;
2251 let inline_layout = element_data.inline_layout_data.as_ref()?;
2252
2253 if *end > inline_layout.text.len() {
2254 continue;
2255 }
2256
2257 if !result.is_empty() {
2258 result.push(' ');
2259 }
2260 result.push_str(&inline_layout.text[*start..*end]);
2261 }
2262
2263 if result.is_empty() {
2264 None
2265 } else {
2266 Some(result)
2267 }
2268 }
2269
2270 pub fn get_text_selection_ranges(&self) -> Vec<(usize, usize, usize)> {
2273 let lookup = |parent_id, idx| self.find_anonymous_block_by_index(parent_id, idx);
2274
2275 let anchor_node = match self.text_selection.anchor.resolve_node_id(lookup) {
2276 Some(id) => id,
2277 None => return Vec::new(),
2278 };
2279 let focus_node = match self.text_selection.focus.resolve_node_id(lookup) {
2280 Some(id) => id,
2281 None => return Vec::new(),
2282 };
2283
2284 let node_is_in_doc = |node_id: usize| {
2287 self.nodes
2288 .get(node_id)
2289 .is_some_and(|node| node.flags.is_in_document())
2290 };
2291 if !node_is_in_doc(anchor_node) || !node_is_in_doc(focus_node) {
2292 return Vec::new();
2293 }
2294
2295 if anchor_node == focus_node {
2297 let start = self
2298 .text_selection
2299 .anchor
2300 .offset
2301 .min(self.text_selection.focus.offset);
2302 let end = self
2303 .text_selection
2304 .anchor
2305 .offset
2306 .max(self.text_selection.focus.offset);
2307
2308 if start == end {
2309 return Vec::new();
2310 }
2311 return vec![(anchor_node, start, end)];
2312 }
2313
2314 let inline_roots = self.collect_inline_roots_in_range(anchor_node, focus_node);
2316 if inline_roots.is_empty() {
2317 return Vec::new();
2318 }
2319
2320 let first_in_roots = inline_roots[0];
2323
2324 let (first_node, first_offset, last_node, last_offset) =
2325 if first_in_roots == anchor_node || (first_in_roots != focus_node) {
2326 (
2328 anchor_node,
2329 self.text_selection.anchor.offset,
2330 focus_node,
2331 self.text_selection.focus.offset,
2332 )
2333 } else {
2334 (
2336 focus_node,
2337 self.text_selection.focus.offset,
2338 anchor_node,
2339 self.text_selection.anchor.offset,
2340 )
2341 };
2342
2343 let mut ranges = Vec::with_capacity(inline_roots.len());
2344
2345 for &node_id in &inline_roots {
2346 let Some(node) = self.get_node(node_id) else {
2347 continue;
2348 };
2349 let Some(element_data) = node.element_data() else {
2350 continue;
2351 };
2352 let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
2353 continue;
2354 };
2355
2356 let text_len = inline_layout.text.len();
2357
2358 if node_id == first_node && node_id == last_node {
2359 let start = first_offset.min(last_offset);
2360 let end = first_offset.max(last_offset);
2361 if start < end && end <= text_len {
2362 ranges.push((node_id, start, end));
2363 }
2364 } else if node_id == first_node {
2365 if first_offset < text_len {
2366 ranges.push((node_id, first_offset, text_len));
2367 }
2368 } else if node_id == last_node {
2369 if last_offset > 0 && last_offset <= text_len {
2370 ranges.push((node_id, 0, last_offset));
2371 }
2372 } else if text_len > 0 {
2373 ranges.push((node_id, 0, text_len));
2374 }
2375 }
2376
2377 ranges
2378 }
2379}
2380
2381pub struct BoundingRect {
2382 pub x: f64,
2383 pub y: f64,
2384 pub width: f64,
2385 pub height: f64,
2386}
2387
2388impl AsRef<BaseDocument> for BaseDocument {
2389 fn as_ref(&self) -> &BaseDocument {
2390 self
2391 }
2392}
2393
2394impl AsMut<BaseDocument> for BaseDocument {
2395 fn as_mut(&mut self) -> &mut BaseDocument {
2396 self
2397 }
2398}
2399
2400#[cfg(test)]
2401mod font_face_override_tests {
2402 use super::*;
2403 use crate::net::{FontFaceOverrides, Resource, ResourceLoadResponse};
2404
2405 #[test]
2421 fn font_face_overrides_alias_family_name() {
2422 const ALIAS: &str = "AliasedFamily";
2423
2424 let mut document = BaseDocument::new(DocumentConfig::default());
2425
2426 {
2428 let mut ctx = document.font_ctx.lock().unwrap();
2429 assert!(
2430 ctx.collection.family_id(ALIAS).is_none(),
2431 "alias must not exist before registration",
2432 );
2433 }
2434
2435 let response = ResourceLoadResponse {
2440 request_id: 0,
2441 node_id: None,
2442 resolved_url: Some(String::from("test://aliased-family")),
2443 result: Ok(Resource::Font(
2444 blitz_traits::net::Bytes::from_static(crate::BULLET_FONT),
2445 FontFaceOverrides {
2446 family_name: Some(String::from(ALIAS)),
2447 weight: Some(800.0),
2448 style: Some(parley::fontique::FontStyle::Italic),
2449 },
2450 )),
2451 };
2452 document.load_resource(response);
2453
2454 let mut ctx = document.font_ctx.lock().unwrap();
2457 let family_id = ctx
2458 .collection
2459 .family_id(ALIAS)
2460 .expect("CSS-declared family name should be registered as a family alias");
2461 let resolved_name = ctx
2462 .collection
2463 .family_name(family_id)
2464 .expect("family id should resolve back to a name");
2465 assert_eq!(
2466 resolved_name, ALIAS,
2467 "registered family should report the CSS-declared name, \
2468 not the font file's internal `name` table entry",
2469 );
2470 }
2471}