1use blitz_traits::node_id::NodeId;
2use cssparser::ParserInput;
3use kurbo::{Affine, Rect as KurboRect};
4use linebender_resource_handle::Blob;
5use markup5ever::{LocalName, QualName, local_name};
6use selectors::matching::{ElementSelectorFlags, QuirksMode};
7use std::cell::Cell;
8use std::str::FromStr;
9use std::sync::Arc;
10use std::sync::atomic::AtomicBool;
11use style::Atom;
12use style::parser::ParserContext;
13use style::properties::ComputedValues;
14use style::properties::{Importance, PropertyDeclaration, PropertyId, SourcePropertyDeclaration};
15use style::stylesheets::{DocumentStyleSheet, Origin, UrlExtraData};
16use style::values::computed::Display as StyloDisplay;
17use style::{
18 properties::{PropertyDeclarationBlock, parse_style_attribute},
19 servo_arc::Arc as ServoArc,
20 shared_lock::{Locked, SharedRwLock},
21 stylesheets::CssRuleType,
22};
23use style_dom::ElementState;
24use style_traits::ParsingMode;
25use taffy::{
26 Cache,
27 prelude::{Layout, Style},
28};
29use url::Url;
30
31use super::stylo_data::StyloData;
32use super::{Attribute, Attributes};
33use crate::Document;
34use crate::layout::table::TableContext;
35use crate::node::{SelectData, TextBrush, TextInputData, TextLayout};
36
37#[cfg(feature = "shadow-dom")]
38use super::custom_element::CustomElementData;
39#[cfg(feature = "custom-widget")]
40use super::custom_widget::CustomWidgetData;
41
42macro_rules! local_names {
43 ($($name:tt),+) => {
44 [$(local_name!($name),)+]
45 };
46}
47
48pub struct ElementData {
49 pub name: QualName,
51
52 pub id: Option<Atom>,
54
55 pub attrs: Attributes,
57
58 pub is_focussable: bool,
60
61 pub style_attribute: Option<ServoArc<Locked<PropertyDeclarationBlock>>>,
63
64 pub special_data: SpecialElementData,
70
71 pub background_images: Vec<Option<ImageResourceData>>,
72
73 pub mask_images: Vec<Option<ImageResourceData>>,
74
75 pub inline_layout_data: Option<Box<TextLayout>>,
77
78 pub list_item_data: Option<Box<ListItemLayout>>,
81
82 pub template_contents: Option<NodeId>,
84
85 pub shadow_root: Option<NodeId>,
88
89 pub assigned_slot: Option<NodeId>,
93 pub stylo_element_data: StyloData,
102 pub selector_flags: Cell<ElementSelectorFlags>,
103 pub guard: Option<SharedRwLock>,
106 pub element_state: ElementState,
107 pub has_snapshot: bool,
108 pub snapshot_handled: AtomicBool,
109 pub dirty_descendants: AtomicBool,
112
113 pub before: Option<NodeId>,
115 pub after: Option<NodeId>,
116
117 pub detailed_grid_info: Option<Box<taffy::DetailedGridInfo>>,
120
121 pub style: Style<Atom>,
123 pub subtree_hoists: bool,
132
133 pub style_source: Option<ServoArc<ComputedValues>>,
142 pub display_constructed_as: StyloDisplay,
143 cache: Option<Box<Cache>>,
163 pub unrounded_layout: Layout,
164 pub final_layout: Layout,
165 pub scroll_offset: crate::Point<f64>,
166 pub scrollable_overflow: KurboRect,
167 pub transform: Option<Affine>,
168}
169
170pub struct DocumentData {
176 pub stylo_element_data: StyloData,
177 pub selector_flags: Cell<ElementSelectorFlags>,
181 pub guard: Option<SharedRwLock>,
184 pub dirty_descendants: AtomicBool,
185 pub element_state: ElementState,
186 pub has_snapshot: bool,
187 pub snapshot_handled: AtomicBool,
188 pub style: Style<Atom>,
189 pub subtree_hoists: bool,
191
192 pub style_source: Option<ServoArc<ComputedValues>>,
195 pub display_constructed_as: StyloDisplay,
196 cache: Option<Box<Cache>>,
216 pub unrounded_layout: Layout,
217 pub final_layout: Layout,
218 pub scroll_offset: crate::Point<f64>,
219 pub scrollable_overflow: KurboRect,
220 pub transform: Option<Affine>,
221}
222
223impl std::fmt::Debug for DocumentData {
226 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227 f.debug_struct("DocumentData")
228 .field("stylo_element_data", &self.stylo_element_data)
229 .field("guard", &self.guard)
230 .field("dirty_descendants", &self.dirty_descendants)
231 .field("element_state", &self.element_state)
232 .field("has_snapshot", &self.has_snapshot)
233 .field("snapshot_handled", &self.snapshot_handled)
234 .field("style", &self.style)
235 .field("display_constructed_as", &self.display_constructed_as)
236 .field("cache", &self.cache)
237 .field("unrounded_layout", &self.unrounded_layout)
238 .field("final_layout", &self.final_layout)
239 .field("scroll_offset", &self.scroll_offset)
240 .field("scrollable_overflow", &self.scrollable_overflow)
241 .field("transform", &self.transform)
242 .finish_non_exhaustive()
243 }
244}
245
246impl DocumentData {
247 #[inline]
252 pub fn cache(&self) -> &Cache {
253 self.cache.as_deref().unwrap_or(&EMPTY_CACHE)
254 }
255
256 #[inline]
257 pub fn cache_mut(&mut self) -> &mut Cache {
258 self.cache.get_or_insert_with(|| Box::new(Cache::new()))
259 }
260
261 #[inline]
262 pub fn cache_release(&mut self) {
263 self.cache = None;
264 }
265
266 pub fn new() -> Self {
267 Self {
268 stylo_element_data: Default::default(),
269 selector_flags: Cell::new(ElementSelectorFlags::empty()),
270 guard: None,
271 dirty_descendants: AtomicBool::new(true),
272 element_state: ElementState::empty(),
273 has_snapshot: false,
274 snapshot_handled: AtomicBool::new(false),
275 style: Default::default(),
276 style_source: None,
277 subtree_hoists: false,
278 display_constructed_as: StyloDisplay::Block,
279 cache: None,
280 unrounded_layout: Layout::new(),
281 final_layout: Layout::new(),
282 scroll_offset: crate::Point::ZERO,
283 scrollable_overflow: KurboRect::ZERO,
284 transform: None,
285 }
286 }
287}
288
289impl Default for DocumentData {
290 fn default() -> Self {
291 Self::new()
292 }
293}
294
295impl Clone for DocumentData {
296 fn clone(&self) -> Self {
297 Self {
300 guard: self.guard.clone(),
301 ..Self::new()
302 }
303 }
304}
305
306impl std::fmt::Debug for ElementData {
307 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
308 f.debug_struct("ElementData")
309 .field("name", &self.name)
310 .field("id", &self.id)
311 .field("attrs", &self.attrs)
312 .field("is_focussable", &self.is_focussable)
313 .field("style_attribute", &self.style_attribute)
314 .field("special_data", &self.special_data)
315 .field("background_images", &self.background_images)
316 .field("mask_images", &self.mask_images)
317 .field("inline_layout_data", &self.inline_layout_data)
318 .field("list_item_data", &self.list_item_data)
319 .field("template_contents", &self.template_contents)
320 .field("element_state", &self.element_state)
321 .field("display_constructed_as", &self.display_constructed_as)
322 .finish_non_exhaustive()
323 }
324}
325
326impl Clone for ElementData {
327 fn clone(&self) -> Self {
333 Self {
334 name: self.name.clone(),
335 id: self.id.clone(),
336 attrs: self.attrs.clone(),
337 is_focussable: self.is_focussable,
338 style_attribute: self.style_attribute.clone(),
339 special_data: self.special_data.clone(),
340 background_images: self.background_images.clone(),
341 mask_images: self.mask_images.clone(),
342 inline_layout_data: self.inline_layout_data.clone(),
343 list_item_data: self.list_item_data.clone(),
344 template_contents: self.template_contents,
345
346 shadow_root: None,
352 assigned_slot: None,
353 stylo_element_data: Default::default(),
354 selector_flags: Cell::new(ElementSelectorFlags::empty()),
355 guard: self.guard.clone(),
356 element_state: self.element_state,
357 has_snapshot: false,
358 snapshot_handled: AtomicBool::new(false),
359 dirty_descendants: AtomicBool::new(true),
360 before: None,
361 after: None,
362 detailed_grid_info: None,
363 style: Default::default(),
364 style_source: None,
365 subtree_hoists: false,
366 display_constructed_as: StyloDisplay::Block,
367 cache: None,
368 unrounded_layout: Layout::new(),
369 final_layout: Layout::new(),
370 scroll_offset: crate::Point::ZERO,
371 scrollable_overflow: KurboRect::ZERO,
372 transform: None,
373 }
374 }
375}
376
377#[derive(Copy, Clone, Default)]
378#[non_exhaustive]
379pub enum SpecialElementType {
380 Stylesheet,
381 Image,
382 Canvas,
383 TableRoot,
384 TextInput,
385 CheckboxInput,
386 Select,
387 #[cfg(feature = "file-input")]
388 FileInput,
389 #[default]
390 None,
391}
392
393#[derive(Default)]
395pub enum SpecialElementData {
396 SubDocument(Box<dyn Document>),
398 #[cfg(feature = "custom-widget")]
400 CustomWidget(CustomWidgetData),
401 #[cfg(feature = "shadow-dom")]
403 CustomElement(CustomElementData),
404 Stylesheet(DocumentStyleSheet),
406 Image(Box<ImageData>),
408 Canvas(CanvasData),
410 TableRoot(Arc<TableContext>),
412 TextInput(TextInputData),
414 CheckboxInput(bool),
416 Select(SelectData),
418 #[cfg(feature = "file-input")]
420 FileInput(FileData),
421 #[default]
423 None,
424}
425
426impl Clone for SpecialElementData {
427 fn clone(&self) -> Self {
428 match self {
429 Self::SubDocument(_) => Self::None, #[cfg(feature = "custom-widget")]
431 Self::CustomWidget(_) => Self::None, #[cfg(feature = "shadow-dom")]
433 Self::CustomElement(_) => Self::None, Self::Stylesheet(data) => Self::Stylesheet(data.clone()),
435 Self::Image(data) => Self::Image(data.clone()),
436 Self::Canvas(data) => Self::Canvas(data.clone()),
437 Self::TableRoot(data) => Self::TableRoot(data.clone()),
438 Self::TextInput(data) => Self::TextInput(data.clone()),
439 Self::CheckboxInput(data) => Self::CheckboxInput(*data),
440 Self::Select(data) => Self::Select(data.clone()),
441 #[cfg(feature = "file-input")]
442 Self::FileInput(data) => Self::FileInput(data.clone()),
443 Self::None => Self::None,
444 }
445 }
446}
447
448impl SpecialElementData {
449 pub fn take(&mut self) -> Self {
450 std::mem::take(self)
451 }
452}
453
454static EMPTY_CACHE: Cache = Cache::new();
463
464impl ElementData {
465 #[inline]
468 pub fn cache(&self) -> &Cache {
469 self.cache.as_deref().unwrap_or(&EMPTY_CACHE)
470 }
471
472 #[inline]
477 pub fn cache_mut(&mut self) -> &mut Cache {
478 self.cache.get_or_insert_with(|| Box::new(Cache::new()))
479 }
480
481 #[inline]
488 pub fn cache_release(&mut self) {
489 self.cache = None;
490 }
491
492 pub fn new(name: QualName, attrs: Vec<Attribute>) -> Self {
493 let id_attr_atom = attrs
494 .iter()
495 .find(|attr| &attr.name.local == "id")
496 .map(|attr| attr.value.as_ref())
497 .map(|value: &str| Atom::from(value));
498
499 let mut data = ElementData {
500 name,
501 id: id_attr_atom,
502 attrs: Attributes::new(attrs),
503 is_focussable: false,
504 style_attribute: Default::default(),
505 inline_layout_data: None,
506 list_item_data: None,
507 special_data: SpecialElementData::None,
508 template_contents: None,
509 shadow_root: None,
510 assigned_slot: None,
511 background_images: Vec::new(),
512 mask_images: Vec::new(),
513
514 stylo_element_data: Default::default(),
515 selector_flags: Cell::new(ElementSelectorFlags::empty()),
516 guard: None,
517 element_state: ElementState::empty(),
518 has_snapshot: false,
519 snapshot_handled: AtomicBool::new(false),
520 dirty_descendants: AtomicBool::new(true),
521 before: None,
522 after: None,
523 detailed_grid_info: None,
524 style: Default::default(),
525 style_source: None,
526 subtree_hoists: false,
527 display_constructed_as: StyloDisplay::Block,
528 cache: None,
529 unrounded_layout: Layout::new(),
530 final_layout: Layout::new(),
531 scroll_offset: crate::Point::ZERO,
532 scrollable_overflow: KurboRect::ZERO,
533 transform: None,
534 };
535 data.flush_is_focussable();
536
537 if data.can_be_disabled() {
539 data.element_state
540 .insert(match data.has_attr(local_name!("disabled")) {
541 true => ElementState::DISABLED,
542 false => ElementState::ENABLED,
543 });
544 }
545
546 data
547 }
548
549 pub fn attrs(&self) -> &[Attribute] {
550 &self.attrs
551 }
552
553 pub fn attr(&self, name: impl PartialEq<LocalName>) -> Option<&str> {
554 let attr = self.attrs.iter().find(|attr| name == attr.name.local)?;
555 Some(&attr.value)
556 }
557
558 pub fn attr_parsed<T: FromStr>(&self, name: impl PartialEq<LocalName>) -> Option<T> {
559 let attr = self.attrs.iter().find(|attr| name == attr.name.local)?;
560 attr.value.parse::<T>().ok()
561 }
562
563 pub fn has_attr(&self, name: impl PartialEq<LocalName>) -> bool {
565 self.attrs.iter().any(|attr| name == attr.name.local)
566 }
567
568 pub fn can_be_disabled(&self) -> bool {
569 local_names!("button", "input", "select", "textarea").contains(&self.name.local)
570 }
571
572 pub fn image_data(&self) -> Option<&ImageData> {
573 match &self.special_data {
574 SpecialElementData::Image(data) => Some(&**data),
575 _ => None,
576 }
577 }
578
579 pub fn image_data_mut(&mut self) -> Option<&mut ImageData> {
580 match self.special_data {
581 SpecialElementData::Image(ref mut data) => Some(&mut **data),
582 _ => None,
583 }
584 }
585
586 pub fn raster_image_data(&self) -> Option<&RasterImageData> {
587 match self.image_data()? {
588 ImageData::Raster(data) => Some(data),
589 _ => None,
590 }
591 }
592
593 pub fn raster_image_data_mut(&mut self) -> Option<&mut RasterImageData> {
594 match self.image_data_mut()? {
595 ImageData::Raster(data) => Some(data),
596 _ => None,
597 }
598 }
599
600 pub fn canvas_data(&self) -> Option<&CanvasData> {
601 match &self.special_data {
602 SpecialElementData::Canvas(data) => Some(data),
603 _ => None,
604 }
605 }
606
607 pub fn sub_doc_data(&self) -> Option<&dyn Document> {
608 match &self.special_data {
609 SpecialElementData::SubDocument(data) => Some(data.as_ref()),
610 _ => None,
611 }
612 }
613
614 pub fn sub_doc_data_mut(&mut self) -> Option<&mut dyn Document> {
615 match &mut self.special_data {
616 SpecialElementData::SubDocument(data) => Some(data.as_mut()),
617 _ => None,
618 }
619 }
620
621 #[cfg(feature = "svg")]
622 pub fn svg_data(&self) -> Option<&usvg::Tree> {
623 match self.image_data()? {
624 ImageData::Svg(data) => Some(&data.tree),
625 _ => None,
626 }
627 }
628
629 pub fn text_input_data(&self) -> Option<&TextInputData> {
630 match &self.special_data {
631 SpecialElementData::TextInput(data) => Some(data),
632 _ => None,
633 }
634 }
635
636 pub fn text_input_data_mut(&mut self) -> Option<&mut TextInputData> {
637 match &mut self.special_data {
638 SpecialElementData::TextInput(data) => Some(data),
639 _ => None,
640 }
641 }
642
643 #[cfg(feature = "custom-widget")]
644 pub fn custom_widget_data(&self) -> Option<&CustomWidgetData> {
645 match &self.special_data {
646 SpecialElementData::CustomWidget(data) => Some(data),
647 _ => None,
648 }
649 }
650
651 #[cfg(feature = "custom-widget")]
652 pub fn custom_widget_data_mut(&mut self) -> Option<&mut CustomWidgetData> {
653 match &mut self.special_data {
654 SpecialElementData::CustomWidget(data) => Some(data),
655 _ => None,
656 }
657 }
658
659 #[cfg(feature = "shadow-dom")]
660 pub fn custom_element_data(&self) -> Option<&CustomElementData> {
661 match &self.special_data {
662 SpecialElementData::CustomElement(data) => Some(data),
663 _ => None,
664 }
665 }
666
667 #[cfg(feature = "shadow-dom")]
668 pub fn custom_element_data_mut(&mut self) -> Option<&mut CustomElementData> {
669 match &mut self.special_data {
670 SpecialElementData::CustomElement(data) => Some(data),
671 _ => None,
672 }
673 }
674
675 pub fn checkbox_input_checked(&self) -> Option<bool> {
676 match self.special_data {
677 SpecialElementData::CheckboxInput(checked) => Some(checked),
678 _ => None,
679 }
680 }
681
682 pub fn checkbox_input_checked_mut(&mut self) -> Option<&mut bool> {
683 match self.special_data {
684 SpecialElementData::CheckboxInput(ref mut checked) => Some(checked),
685 _ => None,
686 }
687 }
688
689 pub fn select_data(&self) -> Option<&SelectData> {
697 match &self.special_data {
698 SpecialElementData::Select(data) => Some(data),
699 _ => None,
700 }
701 }
702
703 pub fn select_data_mut(&mut self) -> Option<&mut SelectData> {
704 match &mut self.special_data {
705 SpecialElementData::Select(data) => Some(data),
706 _ => None,
707 }
708 }
709
710 #[cfg(feature = "file-input")]
711 pub fn file_data(&self) -> Option<&FileData> {
712 match &self.special_data {
713 SpecialElementData::FileInput(data) => Some(data),
714 _ => None,
715 }
716 }
717
718 #[cfg(feature = "file-input")]
719 pub fn file_data_mut(&mut self) -> Option<&mut FileData> {
720 match &mut self.special_data {
721 SpecialElementData::FileInput(data) => Some(data),
722 _ => None,
723 }
724 }
725
726 pub fn flush_is_focussable(&mut self) {
727 let disabled: bool = self.attr_parsed(local_name!("disabled")).unwrap_or(false);
728 let tabindex: Option<i32> = self.attr_parsed(local_name!("tabindex"));
729 let contains_sub_document: bool = self.sub_doc_data().is_some();
730
731 self.is_focussable = contains_sub_document
732 || (!disabled
733 && match tabindex {
734 Some(index) => index >= 0,
735 None => {
736 if [local_name!("a"), local_name!("area")].contains(&self.name.local) {
743 self.attr(local_name!("href")).is_some()
744 } else {
745 const DEFAULT_FOCUSSABLE_ELEMENTS: [LocalName; 7] = [
746 local_name!("button"),
747 local_name!("input"),
748 local_name!("select"),
749 local_name!("textarea"),
750 local_name!("frame"),
751 local_name!("iframe"),
752 local_name!("summary"),
753 ];
754 DEFAULT_FOCUSSABLE_ELEMENTS.contains(&self.name.local)
755 }
756 }
757 })
758 }
759
760 pub fn flush_style_attribute(&mut self, guard: &SharedRwLock, url_extra_data: &UrlExtraData) {
761 self.style_attribute = self.attr(local_name!("style")).map(|style_str| {
762 ServoArc::new(guard.wrap(parse_style_attribute(
763 style_str,
764 url_extra_data,
765 None,
766 QuirksMode::NoQuirks,
767 CssRuleType::Style,
768 )))
769 });
770 }
771
772 pub fn set_style_property(
773 &mut self,
774 name: &str,
775 value: &str,
776 guard: &SharedRwLock,
777 url_extra_data: UrlExtraData,
778 ) -> bool {
779 let context = ParserContext::new(
780 Origin::Author,
781 &url_extra_data,
782 Some(CssRuleType::Style),
783 ParsingMode::DEFAULT,
784 QuirksMode::NoQuirks,
785 Default::default(),
786 None,
787 None,
788 Default::default(),
789 );
790
791 let Ok(property_id) = PropertyId::parse(name, &context) else {
792 #[cfg(feature = "tracing")]
793 tracing::warn!(property = name, "Unsupported property");
794 return false;
795 };
796 let mut source_property_declaration = SourcePropertyDeclaration::default();
797 let mut input = ParserInput::new(value);
798 let mut parser = style::values::Parser::new(&mut input);
799 let Ok(_) = PropertyDeclaration::parse_into(
800 &mut source_property_declaration,
801 property_id,
802 &context,
803 &mut parser,
804 ) else {
805 #[cfg(feature = "tracing")]
806 tracing::warn!(property = name, value, "Invalid property value");
807 return false;
808 };
809
810 if self.style_attribute.is_none() {
811 self.style_attribute = Some(ServoArc::new(guard.wrap(PropertyDeclarationBlock::new())));
812 }
813 self.style_attribute
814 .as_mut()
815 .unwrap()
816 .write_with(&mut guard.write())
817 .extend(source_property_declaration.drain(), Importance::Normal);
818
819 true
820 }
821
822 pub fn remove_style_property(
823 &mut self,
824 name: &str,
825 guard: &SharedRwLock,
826 url_extra_data: UrlExtraData,
827 ) -> bool {
828 let context = ParserContext::new(
829 Origin::Author,
830 &url_extra_data,
831 Some(CssRuleType::Style),
832 ParsingMode::DEFAULT,
833 QuirksMode::NoQuirks,
834 Default::default(),
835 None,
836 None,
837 Default::default(),
838 );
839 let Ok(property_id) = PropertyId::parse(name, &context) else {
840 #[cfg(feature = "tracing")]
841 tracing::warn!(property = name, "Unsupported property");
842 return false;
843 };
844
845 if let Some(style) = &mut self.style_attribute {
846 let mut guard = guard.write();
847 let style = style.write_with(&mut guard);
848 if let Some(index) = style.first_declaration_to_remove(&property_id) {
849 style.remove_property(&property_id, index);
850 return true;
851 }
852 }
853
854 false
855 }
856
857 pub fn set_sub_document(&mut self, sub_document: Box<dyn Document>) {
858 self.special_data = SpecialElementData::SubDocument(sub_document);
859 }
860
861 pub fn remove_sub_document(&mut self) {
862 self.special_data = SpecialElementData::None;
863 }
864
865 #[cfg(feature = "custom-widget")]
866 pub fn set_custom_widget(&mut self, widget: Box<dyn crate::Widget>) {
867 use crate::node::custom_widget::CustomWidgetData;
868 self.special_data = SpecialElementData::CustomWidget(CustomWidgetData::new(widget));
869 }
870
871 #[cfg(feature = "custom-widget")]
872 pub fn remove_custom_widget(&mut self) -> Vec<anyrender::ResourceId> {
873 let resource_ids = self
874 .custom_widget_data_mut()
875 .map(|widget_data| widget_data.take_resource_ids())
876 .unwrap_or_default();
877 self.special_data = SpecialElementData::None;
878 resource_ids
879 }
880
881 pub fn take_inline_layout(&mut self) -> Option<Box<TextLayout>> {
882 std::mem::take(&mut self.inline_layout_data)
883 }
884
885 pub fn is_submit_button(&self) -> bool {
886 if self.name.local != local_name!("button") {
887 return false;
888 }
889 let type_attr = self.attr(local_name!("type"));
890 let is_submit = type_attr == Some("submit");
891 let is_auto_submit = type_attr.is_none()
892 && self.attr(LocalName::from("command")).is_none()
893 && self.attr(LocalName::from("commandfor")).is_none();
894 is_submit || is_auto_submit
895 }
896}
897
898#[derive(Debug, Clone, PartialEq)]
899pub struct RasterImageData {
900 pub width: u32,
902 pub height: u32,
904 pub data: Blob<u8>,
906}
907impl RasterImageData {
908 pub fn new(width: u32, height: u32, data: Arc<Vec<u8>>) -> Self {
909 Self {
910 width,
911 height,
912 data: Blob::new(data),
913 }
914 }
915}
916
917#[cfg(feature = "svg")]
927#[derive(Debug, Clone)]
928pub struct SvgImageData {
929 pub tree: Arc<usvg::Tree>,
931}
932
933#[cfg(feature = "svg")]
934impl SvgImageData {
935 pub fn intrinsic_width(&self) -> Option<f32> {
938 use usvg::svgtypes::LengthUnit;
939 let declared = self
940 .tree
941 .intrinsic_dimensions()
942 .width
943 .is_some_and(|len| len.unit != LengthUnit::Percent);
944 declared.then(|| self.tree.size().width())
945 }
946
947 pub fn intrinsic_height(&self) -> Option<f32> {
950 use usvg::svgtypes::LengthUnit;
951 let declared = self
952 .tree
953 .intrinsic_dimensions()
954 .height
955 .is_some_and(|len| len.unit != LengthUnit::Percent);
956 declared.then(|| self.tree.size().height())
957 }
958
959 pub fn viewbox_aspect_ratio(&self) -> Option<f32> {
961 self.tree
962 .intrinsic_dimensions()
963 .view_box
964 .map(|vb| vb.width() / vb.height())
965 }
966
967 pub fn resolved_width(&self, container_width: Option<f32>) -> Option<f32> {
976 use usvg::svgtypes::LengthUnit;
977 match self.tree.intrinsic_dimensions().width {
978 Some(len) if len.unit != LengthUnit::Percent => Some(self.tree.size().width()),
979 Some(len) => container_width.map(|cw| cw * (len.number as f32) / 100.0),
980 None => None,
981 }
982 }
983
984 pub fn resolved_height(&self, container_height: Option<f32>) -> Option<f32> {
987 use usvg::svgtypes::LengthUnit;
988 match self.tree.intrinsic_dimensions().height {
989 Some(len) if len.unit != LengthUnit::Percent => Some(self.tree.size().height()),
990 Some(len) => container_height.map(|ch| ch * (len.number as f32) / 100.0),
991 None => None,
992 }
993 }
994
995 pub fn aspect_ratio(&self) -> f32 {
1000 match (self.intrinsic_width(), self.intrinsic_height()) {
1001 (Some(w), Some(h)) => w / h,
1002 _ => self.viewbox_aspect_ratio().unwrap_or_else(|| {
1003 let size = self.tree.size();
1004 size.width() / size.height()
1005 }),
1006 }
1007 }
1008
1009 pub fn intrinsic_size(&self) -> (f32, f32) {
1014 let aspect_ratio = self.aspect_ratio();
1015 match (self.intrinsic_width(), self.intrinsic_height()) {
1016 (Some(w), Some(h)) => (w, h),
1017 (Some(w), None) => (w, w / aspect_ratio),
1018 (None, Some(h)) => (h * aspect_ratio, h),
1019 (None, None) => {
1020 if self.viewbox_aspect_ratio().is_some() {
1024 let scale = (300.0 / aspect_ratio).min(150.0);
1025 (scale * aspect_ratio, scale)
1026 } else {
1027 let size = self.tree.size();
1028 (size.width(), size.height())
1029 }
1030 }
1031 }
1032 }
1033}
1034
1035#[derive(Debug, Clone)]
1036pub enum ImageData {
1037 Raster(RasterImageData),
1038 #[cfg(feature = "svg")]
1039 Svg(SvgImageData),
1040 None,
1041}
1042
1043#[derive(Debug, Clone, PartialEq)]
1044pub enum Status {
1045 Ok,
1046 Error,
1047 Loading,
1048}
1049
1050#[derive(Debug, Clone)]
1051pub struct ImageResourceData {
1052 pub url: ServoArc<Url>,
1054 pub status: Status,
1056 pub image: ImageData,
1058}
1059
1060impl ImageResourceData {
1061 pub fn new(url: ServoArc<Url>) -> Self {
1062 Self {
1063 url,
1064 status: Status::Loading,
1065 image: ImageData::None,
1066 }
1067 }
1068}
1069
1070#[derive(Debug, Clone)]
1071pub struct CanvasData {
1072 pub custom_paint_source_id: u64,
1073}
1074
1075impl std::fmt::Debug for SpecialElementData {
1076 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1077 match self {
1078 SpecialElementData::SubDocument(_) => f.write_str("NodeSpecificData::SubDocument"),
1079 #[cfg(feature = "custom-widget")]
1080 SpecialElementData::CustomWidget(_) => f.write_str("NodeSpecificData::CustomWidget"),
1081 #[cfg(feature = "shadow-dom")]
1082 SpecialElementData::CustomElement(_) => f.write_str("NodeSpecificData::CustomElement"),
1083 SpecialElementData::Stylesheet(_) => f.write_str("NodeSpecificData::Stylesheet"),
1084 SpecialElementData::Image(data) => match **data {
1085 ImageData::Raster(_) => f.write_str("NodeSpecificData::Image(Raster)"),
1086 #[cfg(feature = "svg")]
1087 ImageData::Svg(_) => f.write_str("NodeSpecificData::Image(Svg)"),
1088 ImageData::None => f.write_str("NodeSpecificData::Image(None)"),
1089 },
1090 SpecialElementData::Canvas(_) => f.write_str("NodeSpecificData::Canvas"),
1091 SpecialElementData::TableRoot(_) => f.write_str("NodeSpecificData::TableRoot"),
1092 SpecialElementData::TextInput(_) => f.write_str("NodeSpecificData::TextInput"),
1093 SpecialElementData::CheckboxInput(_) => f.write_str("NodeSpecificData::CheckboxInput"),
1094 SpecialElementData::Select(data) => {
1095 write!(f, "NodeSpecificData::Select({data:?})")
1096 }
1097 #[cfg(feature = "file-input")]
1098 SpecialElementData::FileInput(_) => f.write_str("NodeSpecificData::FileInput"),
1099 SpecialElementData::None => f.write_str("NodeSpecificData::None"),
1100 }
1101 }
1102}
1103
1104#[derive(Clone)]
1105pub struct ListItemLayout {
1106 pub marker: Marker,
1107 pub position: ListItemLayoutPosition,
1108}
1109
1110#[derive(Debug, PartialEq, Clone)]
1113pub enum Marker {
1114 Char(char),
1115 String(String),
1116}
1117
1118#[derive(Clone)]
1120pub enum ListItemLayoutPosition {
1121 Inside,
1122 Outside(Box<parley::Layout<TextBrush>>),
1123}
1124
1125impl std::fmt::Debug for ListItemLayout {
1126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1127 write!(f, "ListItemLayout - marker {:?}", self.marker)
1128 }
1129}
1130
1131#[cfg(feature = "file-input")]
1132mod file_data {
1133 use std::ops::{Deref, DerefMut};
1134 use std::path::PathBuf;
1135
1136 #[derive(Clone, Debug)]
1137 pub struct FileData(pub Vec<PathBuf>);
1138 impl Deref for FileData {
1139 type Target = Vec<PathBuf>;
1140
1141 fn deref(&self) -> &Self::Target {
1142 &self.0
1143 }
1144 }
1145 impl DerefMut for FileData {
1146 fn deref_mut(&mut self) -> &mut Self::Target {
1147 &mut self.0
1148 }
1149 }
1150 impl From<Vec<PathBuf>> for FileData {
1151 fn from(files: Vec<PathBuf>) -> Self {
1152 Self(files)
1153 }
1154 }
1155}
1156#[cfg(feature = "file-input")]
1157pub use file_data::FileData;
1158
1159#[cfg(test)]
1160mod tests {
1161 use super::{ElementData, TextInputData};
1162 use parley::{FontContext, LayoutContext};
1163
1164 #[test]
1177 fn element_data_stays_out_of_the_large_size_classes() {
1178 let size = std::mem::size_of::<ElementData>();
1179 assert!(
1180 size <= 1536,
1181 "ElementData grew to {size} bytes. It is allocated per element, so \
1182 this is a multiplier on the whole DOM's footprint: at 2848 bytes \
1183 it was 814 MB of a live instance. Box the new field instead, as \
1184 `cache` is."
1185 );
1186 }
1187
1188 #[test]
1193 fn an_unlaid_out_element_carries_no_cache() {
1194 let data = ElementData::new(
1195 markup5ever::QualName::new(None, markup5ever::ns!(html), "div".into()),
1196 Vec::new(),
1197 );
1198 assert!(
1199 std::mem::size_of_val(&data) < std::mem::size_of::<taffy::Cache>() * 2,
1200 "the cache must not be inline: a fresh element should be smaller \
1201 than two caches"
1202 );
1203 }
1204
1205 fn make_input(is_multiline: bool, text: &str) -> TextInputData {
1207 let mut font_ctx = FontContext::new();
1208 let mut layout_ctx = LayoutContext::new();
1209 let mut data = TextInputData::new(is_multiline);
1210 data.editor.set_scale(1.0);
1211 data.editor.set_text(text);
1212 data.editor
1213 .driver(&mut font_ctx, &mut layout_ctx)
1214 .refresh_layout();
1215 data
1216 }
1217
1218 #[test]
1219 fn short_text_does_not_scroll() {
1220 let mut data = make_input(false, "hi");
1221 data.clamp_scroll_offset(1000.0, 100.0);
1223 assert_eq!(data.scroll_offset, 0.0);
1224 }
1225
1226 #[test]
1227 fn single_line_scrolls_to_follow_caret() {
1228 let text = "the quick brown fox jumps over the lazy dog repeatedly and at length";
1229 let mut data = make_input(false, text);
1230 let content_box_width = 40.0;
1231 let content_box_height = 20.0;
1232
1233 data.editor
1235 .driver(&mut FontContext::new(), &mut LayoutContext::new())
1236 .move_to_text_end();
1237 data.clamp_scroll_offset(content_box_width, content_box_height);
1238
1239 let layout_width = data.editor.try_layout().unwrap().full_width();
1240 if layout_width > content_box_width {
1241 assert!(
1242 data.scroll_offset > 0.0,
1243 "expected horizontal scroll for overflowing single-line input"
1244 );
1245 let caret = data.editor.cursor_geometry(1.5).unwrap();
1247 assert!(caret.x1 as f32 <= data.scroll_offset + content_box_width + 0.5);
1248 assert!(caret.x0 as f32 >= data.scroll_offset - 0.5);
1249 }
1250
1251 data.editor
1253 .driver(&mut FontContext::new(), &mut LayoutContext::new())
1254 .move_to_text_start();
1255 data.clamp_scroll_offset(content_box_width, content_box_height);
1256 assert_eq!(data.scroll_offset, 0.0);
1257 }
1258
1259 #[test]
1260 fn multiline_scrolls_vertically_not_horizontally() {
1261 let text = (0..40)
1262 .map(|i| format!("line {i}"))
1263 .collect::<Vec<_>>()
1264 .join("\n");
1265 let mut data = make_input(true, &text);
1266 data.editor.set_width(Some(200.0));
1268 data.editor
1269 .driver(&mut FontContext::new(), &mut LayoutContext::new())
1270 .refresh_layout();
1271
1272 let content_box_width = 200.0;
1273 let content_box_height = 30.0;
1274
1275 data.editor
1276 .driver(&mut FontContext::new(), &mut LayoutContext::new())
1277 .move_to_text_end();
1278 data.clamp_scroll_offset(content_box_width, content_box_height);
1279
1280 let layout_height = data.editor.try_layout().unwrap().height();
1281 if layout_height > content_box_height {
1282 assert!(
1283 data.scroll_offset > 0.0,
1284 "expected vertical scroll for overflowing multi-line input"
1285 );
1286 }
1287 }
1288
1289 #[cfg(target_os = "macos")]
1294 #[test]
1295 fn scroll_by_clamps_and_bubbles() {
1296 let text = (0..40)
1297 .map(|i| format!("line {i}"))
1298 .collect::<Vec<_>>()
1299 .join("\n");
1300 let mut data = make_input(true, &text);
1301 data.editor.set_width(Some(200.0));
1302 data.editor
1303 .driver(&mut FontContext::new(), &mut LayoutContext::new())
1304 .refresh_layout();
1305
1306 let content_box_width = 200.0;
1307 let content_box_height = 30.0;
1308 let max = data.max_scroll_offset(content_box_width, content_box_height);
1309 assert!(max > 0.0, "test text should overflow the content box");
1310
1311 assert_eq!(data.scroll_offset, 0.0);
1314 let bubbled = data.scroll_by(15.0, content_box_width, content_box_height);
1315 assert_eq!(data.scroll_offset, 0.0);
1316 assert_eq!(bubbled, 15.0);
1317
1318 let bubbled = data.scroll_by(-10.0, content_box_width, content_box_height);
1320 assert_eq!(data.scroll_offset, 10.0);
1321 assert_eq!(bubbled, 0.0);
1322
1323 let bubbled = data.scroll_by(-(max + 100.0), content_box_width, content_box_height);
1327 assert_eq!(data.scroll_offset, max);
1328 assert!((bubbled - (-110.0)).abs() < 1e-3);
1329 }
1330
1331 #[test]
1332 fn single_line_does_not_scroll_when_text_fits() {
1333 let mut data = make_input(false, "hi");
1334 let bubbled = data.scroll_by(-50.0, 1000.0, 100.0);
1336 assert_eq!(data.scroll_offset, 0.0);
1337 assert_eq!(bubbled, -50.0);
1338 }
1339}