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::{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 #[cfg(feature = "file-input")]
387 FileInput,
388 #[default]
389 None,
390}
391
392#[derive(Default)]
394pub enum SpecialElementData {
395 SubDocument(Box<dyn Document>),
397 #[cfg(feature = "custom-widget")]
399 CustomWidget(CustomWidgetData),
400 #[cfg(feature = "shadow-dom")]
402 CustomElement(CustomElementData),
403 Stylesheet(DocumentStyleSheet),
405 Image(Box<ImageData>),
407 Canvas(CanvasData),
409 TableRoot(Arc<TableContext>),
411 TextInput(TextInputData),
413 CheckboxInput(bool),
415 #[cfg(feature = "file-input")]
417 FileInput(FileData),
418 #[default]
420 None,
421}
422
423impl Clone for SpecialElementData {
424 fn clone(&self) -> Self {
425 match self {
426 Self::SubDocument(_) => Self::None, #[cfg(feature = "custom-widget")]
428 Self::CustomWidget(_) => Self::None, #[cfg(feature = "shadow-dom")]
430 Self::CustomElement(_) => Self::None, Self::Stylesheet(data) => Self::Stylesheet(data.clone()),
432 Self::Image(data) => Self::Image(data.clone()),
433 Self::Canvas(data) => Self::Canvas(data.clone()),
434 Self::TableRoot(data) => Self::TableRoot(data.clone()),
435 Self::TextInput(data) => Self::TextInput(data.clone()),
436 Self::CheckboxInput(data) => Self::CheckboxInput(*data),
437 #[cfg(feature = "file-input")]
438 Self::FileInput(data) => Self::FileInput(data.clone()),
439 Self::None => Self::None,
440 }
441 }
442}
443
444impl SpecialElementData {
445 pub fn take(&mut self) -> Self {
446 std::mem::take(self)
447 }
448}
449
450static EMPTY_CACHE: Cache = Cache::new();
459
460impl ElementData {
461 #[inline]
464 pub fn cache(&self) -> &Cache {
465 self.cache.as_deref().unwrap_or(&EMPTY_CACHE)
466 }
467
468 #[inline]
473 pub fn cache_mut(&mut self) -> &mut Cache {
474 self.cache.get_or_insert_with(|| Box::new(Cache::new()))
475 }
476
477 #[inline]
484 pub fn cache_release(&mut self) {
485 self.cache = None;
486 }
487
488 pub fn new(name: QualName, attrs: Vec<Attribute>) -> Self {
489 let id_attr_atom = attrs
490 .iter()
491 .find(|attr| &attr.name.local == "id")
492 .map(|attr| attr.value.as_ref())
493 .map(|value: &str| Atom::from(value));
494
495 let mut data = ElementData {
496 name,
497 id: id_attr_atom,
498 attrs: Attributes::new(attrs),
499 is_focussable: false,
500 style_attribute: Default::default(),
501 inline_layout_data: None,
502 list_item_data: None,
503 special_data: SpecialElementData::None,
504 template_contents: None,
505 shadow_root: None,
506 assigned_slot: None,
507 background_images: Vec::new(),
508 mask_images: Vec::new(),
509
510 stylo_element_data: Default::default(),
511 selector_flags: Cell::new(ElementSelectorFlags::empty()),
512 guard: None,
513 element_state: ElementState::empty(),
514 has_snapshot: false,
515 snapshot_handled: AtomicBool::new(false),
516 dirty_descendants: AtomicBool::new(true),
517 before: None,
518 after: None,
519 detailed_grid_info: None,
520 style: Default::default(),
521 style_source: None,
522 subtree_hoists: false,
523 display_constructed_as: StyloDisplay::Block,
524 cache: None,
525 unrounded_layout: Layout::new(),
526 final_layout: Layout::new(),
527 scroll_offset: crate::Point::ZERO,
528 scrollable_overflow: KurboRect::ZERO,
529 transform: None,
530 };
531 data.flush_is_focussable();
532
533 if data.can_be_disabled() {
535 data.element_state
536 .insert(match data.has_attr(local_name!("disabled")) {
537 true => ElementState::DISABLED,
538 false => ElementState::ENABLED,
539 });
540 }
541
542 data
543 }
544
545 pub fn attrs(&self) -> &[Attribute] {
546 &self.attrs
547 }
548
549 pub fn attr(&self, name: impl PartialEq<LocalName>) -> Option<&str> {
550 let attr = self.attrs.iter().find(|attr| name == attr.name.local)?;
551 Some(&attr.value)
552 }
553
554 pub fn attr_parsed<T: FromStr>(&self, name: impl PartialEq<LocalName>) -> Option<T> {
555 let attr = self.attrs.iter().find(|attr| name == attr.name.local)?;
556 attr.value.parse::<T>().ok()
557 }
558
559 pub fn has_attr(&self, name: impl PartialEq<LocalName>) -> bool {
561 self.attrs.iter().any(|attr| name == attr.name.local)
562 }
563
564 pub fn can_be_disabled(&self) -> bool {
565 local_names!("button", "input", "select", "textarea").contains(&self.name.local)
566 }
567
568 pub fn image_data(&self) -> Option<&ImageData> {
569 match &self.special_data {
570 SpecialElementData::Image(data) => Some(&**data),
571 _ => None,
572 }
573 }
574
575 pub fn image_data_mut(&mut self) -> Option<&mut ImageData> {
576 match self.special_data {
577 SpecialElementData::Image(ref mut data) => Some(&mut **data),
578 _ => None,
579 }
580 }
581
582 pub fn raster_image_data(&self) -> Option<&RasterImageData> {
583 match self.image_data()? {
584 ImageData::Raster(data) => Some(data),
585 _ => None,
586 }
587 }
588
589 pub fn raster_image_data_mut(&mut self) -> Option<&mut RasterImageData> {
590 match self.image_data_mut()? {
591 ImageData::Raster(data) => Some(data),
592 _ => None,
593 }
594 }
595
596 pub fn canvas_data(&self) -> Option<&CanvasData> {
597 match &self.special_data {
598 SpecialElementData::Canvas(data) => Some(data),
599 _ => None,
600 }
601 }
602
603 pub fn sub_doc_data(&self) -> Option<&dyn Document> {
604 match &self.special_data {
605 SpecialElementData::SubDocument(data) => Some(data.as_ref()),
606 _ => None,
607 }
608 }
609
610 pub fn sub_doc_data_mut(&mut self) -> Option<&mut dyn Document> {
611 match &mut self.special_data {
612 SpecialElementData::SubDocument(data) => Some(data.as_mut()),
613 _ => None,
614 }
615 }
616
617 #[cfg(feature = "svg")]
618 pub fn svg_data(&self) -> Option<&usvg::Tree> {
619 match self.image_data()? {
620 ImageData::Svg(data) => Some(&data.tree),
621 _ => None,
622 }
623 }
624
625 pub fn text_input_data(&self) -> Option<&TextInputData> {
626 match &self.special_data {
627 SpecialElementData::TextInput(data) => Some(data),
628 _ => None,
629 }
630 }
631
632 pub fn text_input_data_mut(&mut self) -> Option<&mut TextInputData> {
633 match &mut self.special_data {
634 SpecialElementData::TextInput(data) => Some(data),
635 _ => None,
636 }
637 }
638
639 #[cfg(feature = "custom-widget")]
640 pub fn custom_widget_data(&self) -> Option<&CustomWidgetData> {
641 match &self.special_data {
642 SpecialElementData::CustomWidget(data) => Some(data),
643 _ => None,
644 }
645 }
646
647 #[cfg(feature = "custom-widget")]
648 pub fn custom_widget_data_mut(&mut self) -> Option<&mut CustomWidgetData> {
649 match &mut self.special_data {
650 SpecialElementData::CustomWidget(data) => Some(data),
651 _ => None,
652 }
653 }
654
655 #[cfg(feature = "shadow-dom")]
656 pub fn custom_element_data(&self) -> Option<&CustomElementData> {
657 match &self.special_data {
658 SpecialElementData::CustomElement(data) => Some(data),
659 _ => None,
660 }
661 }
662
663 #[cfg(feature = "shadow-dom")]
664 pub fn custom_element_data_mut(&mut self) -> Option<&mut CustomElementData> {
665 match &mut self.special_data {
666 SpecialElementData::CustomElement(data) => Some(data),
667 _ => None,
668 }
669 }
670
671 pub fn checkbox_input_checked(&self) -> Option<bool> {
672 match self.special_data {
673 SpecialElementData::CheckboxInput(checked) => Some(checked),
674 _ => None,
675 }
676 }
677
678 pub fn checkbox_input_checked_mut(&mut self) -> Option<&mut bool> {
679 match self.special_data {
680 SpecialElementData::CheckboxInput(ref mut checked) => Some(checked),
681 _ => None,
682 }
683 }
684
685 #[cfg(feature = "file-input")]
686 pub fn file_data(&self) -> Option<&FileData> {
687 match &self.special_data {
688 SpecialElementData::FileInput(data) => Some(data),
689 _ => None,
690 }
691 }
692
693 #[cfg(feature = "file-input")]
694 pub fn file_data_mut(&mut self) -> Option<&mut FileData> {
695 match &mut self.special_data {
696 SpecialElementData::FileInput(data) => Some(data),
697 _ => None,
698 }
699 }
700
701 pub fn flush_is_focussable(&mut self) {
702 let disabled: bool = self.attr_parsed(local_name!("disabled")).unwrap_or(false);
703 let tabindex: Option<i32> = self.attr_parsed(local_name!("tabindex"));
704 let contains_sub_document: bool = self.sub_doc_data().is_some();
705
706 self.is_focussable = contains_sub_document
707 || (!disabled
708 && match tabindex {
709 Some(index) => index >= 0,
710 None => {
711 if [local_name!("a"), local_name!("area")].contains(&self.name.local) {
718 self.attr(local_name!("href")).is_some()
719 } else {
720 const DEFAULT_FOCUSSABLE_ELEMENTS: [LocalName; 7] = [
721 local_name!("button"),
722 local_name!("input"),
723 local_name!("select"),
724 local_name!("textarea"),
725 local_name!("frame"),
726 local_name!("iframe"),
727 local_name!("summary"),
728 ];
729 DEFAULT_FOCUSSABLE_ELEMENTS.contains(&self.name.local)
730 }
731 }
732 })
733 }
734
735 pub fn flush_style_attribute(&mut self, guard: &SharedRwLock, url_extra_data: &UrlExtraData) {
736 self.style_attribute = self.attr(local_name!("style")).map(|style_str| {
737 ServoArc::new(guard.wrap(parse_style_attribute(
738 style_str,
739 url_extra_data,
740 None,
741 QuirksMode::NoQuirks,
742 CssRuleType::Style,
743 )))
744 });
745 }
746
747 pub fn set_style_property(
748 &mut self,
749 name: &str,
750 value: &str,
751 guard: &SharedRwLock,
752 url_extra_data: UrlExtraData,
753 ) -> bool {
754 let context = ParserContext::new(
755 Origin::Author,
756 &url_extra_data,
757 Some(CssRuleType::Style),
758 ParsingMode::DEFAULT,
759 QuirksMode::NoQuirks,
760 Default::default(),
761 None,
762 None,
763 Default::default(),
764 );
765
766 let Ok(property_id) = PropertyId::parse(name, &context) else {
767 #[cfg(feature = "tracing")]
768 tracing::warn!(property = name, "Unsupported property");
769 return false;
770 };
771 let mut source_property_declaration = SourcePropertyDeclaration::default();
772 let mut input = ParserInput::new(value);
773 let mut parser = style::values::Parser::new(&mut input);
774 let Ok(_) = PropertyDeclaration::parse_into(
775 &mut source_property_declaration,
776 property_id,
777 &context,
778 &mut parser,
779 ) else {
780 #[cfg(feature = "tracing")]
781 tracing::warn!(property = name, value, "Invalid property value");
782 return false;
783 };
784
785 if self.style_attribute.is_none() {
786 self.style_attribute = Some(ServoArc::new(guard.wrap(PropertyDeclarationBlock::new())));
787 }
788 self.style_attribute
789 .as_mut()
790 .unwrap()
791 .write_with(&mut guard.write())
792 .extend(source_property_declaration.drain(), Importance::Normal);
793
794 true
795 }
796
797 pub fn remove_style_property(
798 &mut self,
799 name: &str,
800 guard: &SharedRwLock,
801 url_extra_data: UrlExtraData,
802 ) -> bool {
803 let context = ParserContext::new(
804 Origin::Author,
805 &url_extra_data,
806 Some(CssRuleType::Style),
807 ParsingMode::DEFAULT,
808 QuirksMode::NoQuirks,
809 Default::default(),
810 None,
811 None,
812 Default::default(),
813 );
814 let Ok(property_id) = PropertyId::parse(name, &context) else {
815 #[cfg(feature = "tracing")]
816 tracing::warn!(property = name, "Unsupported property");
817 return false;
818 };
819
820 if let Some(style) = &mut self.style_attribute {
821 let mut guard = guard.write();
822 let style = style.write_with(&mut guard);
823 if let Some(index) = style.first_declaration_to_remove(&property_id) {
824 style.remove_property(&property_id, index);
825 return true;
826 }
827 }
828
829 false
830 }
831
832 pub fn set_sub_document(&mut self, sub_document: Box<dyn Document>) {
833 self.special_data = SpecialElementData::SubDocument(sub_document);
834 }
835
836 pub fn remove_sub_document(&mut self) {
837 self.special_data = SpecialElementData::None;
838 }
839
840 #[cfg(feature = "custom-widget")]
841 pub fn set_custom_widget(&mut self, widget: Box<dyn crate::Widget>) {
842 use crate::node::custom_widget::CustomWidgetData;
843 self.special_data = SpecialElementData::CustomWidget(CustomWidgetData::new(widget));
844 }
845
846 #[cfg(feature = "custom-widget")]
847 pub fn remove_custom_widget(&mut self) -> Vec<anyrender::ResourceId> {
848 let resource_ids = self
849 .custom_widget_data_mut()
850 .map(|widget_data| widget_data.take_resource_ids())
851 .unwrap_or_default();
852 self.special_data = SpecialElementData::None;
853 resource_ids
854 }
855
856 pub fn take_inline_layout(&mut self) -> Option<Box<TextLayout>> {
857 std::mem::take(&mut self.inline_layout_data)
858 }
859
860 pub fn is_submit_button(&self) -> bool {
861 if self.name.local != local_name!("button") {
862 return false;
863 }
864 let type_attr = self.attr(local_name!("type"));
865 let is_submit = type_attr == Some("submit");
866 let is_auto_submit = type_attr.is_none()
867 && self.attr(LocalName::from("command")).is_none()
868 && self.attr(LocalName::from("commandfor")).is_none();
869 is_submit || is_auto_submit
870 }
871}
872
873#[derive(Debug, Clone, PartialEq)]
874pub struct RasterImageData {
875 pub width: u32,
877 pub height: u32,
879 pub data: Blob<u8>,
881}
882impl RasterImageData {
883 pub fn new(width: u32, height: u32, data: Arc<Vec<u8>>) -> Self {
884 Self {
885 width,
886 height,
887 data: Blob::new(data),
888 }
889 }
890}
891
892#[cfg(feature = "svg")]
902#[derive(Debug, Clone)]
903pub struct SvgImageData {
904 pub tree: Arc<usvg::Tree>,
906}
907
908#[cfg(feature = "svg")]
909impl SvgImageData {
910 pub fn intrinsic_width(&self) -> Option<f32> {
913 use usvg::svgtypes::LengthUnit;
914 let declared = self
915 .tree
916 .intrinsic_dimensions()
917 .width
918 .is_some_and(|len| len.unit != LengthUnit::Percent);
919 declared.then(|| self.tree.size().width())
920 }
921
922 pub fn intrinsic_height(&self) -> Option<f32> {
925 use usvg::svgtypes::LengthUnit;
926 let declared = self
927 .tree
928 .intrinsic_dimensions()
929 .height
930 .is_some_and(|len| len.unit != LengthUnit::Percent);
931 declared.then(|| self.tree.size().height())
932 }
933
934 pub fn viewbox_aspect_ratio(&self) -> Option<f32> {
936 self.tree
937 .intrinsic_dimensions()
938 .view_box
939 .map(|vb| vb.width() / vb.height())
940 }
941
942 pub fn resolved_width(&self, container_width: Option<f32>) -> Option<f32> {
951 use usvg::svgtypes::LengthUnit;
952 match self.tree.intrinsic_dimensions().width {
953 Some(len) if len.unit != LengthUnit::Percent => Some(self.tree.size().width()),
954 Some(len) => container_width.map(|cw| cw * (len.number as f32) / 100.0),
955 None => None,
956 }
957 }
958
959 pub fn resolved_height(&self, container_height: Option<f32>) -> Option<f32> {
962 use usvg::svgtypes::LengthUnit;
963 match self.tree.intrinsic_dimensions().height {
964 Some(len) if len.unit != LengthUnit::Percent => Some(self.tree.size().height()),
965 Some(len) => container_height.map(|ch| ch * (len.number as f32) / 100.0),
966 None => None,
967 }
968 }
969
970 pub fn aspect_ratio(&self) -> f32 {
975 match (self.intrinsic_width(), self.intrinsic_height()) {
976 (Some(w), Some(h)) => w / h,
977 _ => self.viewbox_aspect_ratio().unwrap_or_else(|| {
978 let size = self.tree.size();
979 size.width() / size.height()
980 }),
981 }
982 }
983
984 pub fn intrinsic_size(&self) -> (f32, f32) {
989 let aspect_ratio = self.aspect_ratio();
990 match (self.intrinsic_width(), self.intrinsic_height()) {
991 (Some(w), Some(h)) => (w, h),
992 (Some(w), None) => (w, w / aspect_ratio),
993 (None, Some(h)) => (h * aspect_ratio, h),
994 (None, None) => {
995 if self.viewbox_aspect_ratio().is_some() {
999 let scale = (300.0 / aspect_ratio).min(150.0);
1000 (scale * aspect_ratio, scale)
1001 } else {
1002 let size = self.tree.size();
1003 (size.width(), size.height())
1004 }
1005 }
1006 }
1007 }
1008}
1009
1010#[derive(Debug, Clone)]
1011pub enum ImageData {
1012 Raster(RasterImageData),
1013 #[cfg(feature = "svg")]
1014 Svg(SvgImageData),
1015 None,
1016}
1017
1018#[derive(Debug, Clone, PartialEq)]
1019pub enum Status {
1020 Ok,
1021 Error,
1022 Loading,
1023}
1024
1025#[derive(Debug, Clone)]
1026pub struct ImageResourceData {
1027 pub url: ServoArc<Url>,
1029 pub status: Status,
1031 pub image: ImageData,
1033}
1034
1035impl ImageResourceData {
1036 pub fn new(url: ServoArc<Url>) -> Self {
1037 Self {
1038 url,
1039 status: Status::Loading,
1040 image: ImageData::None,
1041 }
1042 }
1043}
1044
1045#[derive(Debug, Clone)]
1046pub struct CanvasData {
1047 pub custom_paint_source_id: u64,
1048}
1049
1050impl std::fmt::Debug for SpecialElementData {
1051 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1052 match self {
1053 SpecialElementData::SubDocument(_) => f.write_str("NodeSpecificData::SubDocument"),
1054 #[cfg(feature = "custom-widget")]
1055 SpecialElementData::CustomWidget(_) => f.write_str("NodeSpecificData::CustomWidget"),
1056 #[cfg(feature = "shadow-dom")]
1057 SpecialElementData::CustomElement(_) => f.write_str("NodeSpecificData::CustomElement"),
1058 SpecialElementData::Stylesheet(_) => f.write_str("NodeSpecificData::Stylesheet"),
1059 SpecialElementData::Image(data) => match **data {
1060 ImageData::Raster(_) => f.write_str("NodeSpecificData::Image(Raster)"),
1061 #[cfg(feature = "svg")]
1062 ImageData::Svg(_) => f.write_str("NodeSpecificData::Image(Svg)"),
1063 ImageData::None => f.write_str("NodeSpecificData::Image(None)"),
1064 },
1065 SpecialElementData::Canvas(_) => f.write_str("NodeSpecificData::Canvas"),
1066 SpecialElementData::TableRoot(_) => f.write_str("NodeSpecificData::TableRoot"),
1067 SpecialElementData::TextInput(_) => f.write_str("NodeSpecificData::TextInput"),
1068 SpecialElementData::CheckboxInput(_) => f.write_str("NodeSpecificData::CheckboxInput"),
1069 #[cfg(feature = "file-input")]
1070 SpecialElementData::FileInput(_) => f.write_str("NodeSpecificData::FileInput"),
1071 SpecialElementData::None => f.write_str("NodeSpecificData::None"),
1072 }
1073 }
1074}
1075
1076#[derive(Clone)]
1077pub struct ListItemLayout {
1078 pub marker: Marker,
1079 pub position: ListItemLayoutPosition,
1080}
1081
1082#[derive(Debug, PartialEq, Clone)]
1085pub enum Marker {
1086 Char(char),
1087 String(String),
1088}
1089
1090#[derive(Clone)]
1092pub enum ListItemLayoutPosition {
1093 Inside,
1094 Outside(Box<parley::Layout<TextBrush>>),
1095}
1096
1097impl std::fmt::Debug for ListItemLayout {
1098 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1099 write!(f, "ListItemLayout - marker {:?}", self.marker)
1100 }
1101}
1102
1103#[cfg(feature = "file-input")]
1104mod file_data {
1105 use std::ops::{Deref, DerefMut};
1106 use std::path::PathBuf;
1107
1108 #[derive(Clone, Debug)]
1109 pub struct FileData(pub Vec<PathBuf>);
1110 impl Deref for FileData {
1111 type Target = Vec<PathBuf>;
1112
1113 fn deref(&self) -> &Self::Target {
1114 &self.0
1115 }
1116 }
1117 impl DerefMut for FileData {
1118 fn deref_mut(&mut self) -> &mut Self::Target {
1119 &mut self.0
1120 }
1121 }
1122 impl From<Vec<PathBuf>> for FileData {
1123 fn from(files: Vec<PathBuf>) -> Self {
1124 Self(files)
1125 }
1126 }
1127}
1128#[cfg(feature = "file-input")]
1129pub use file_data::FileData;
1130
1131#[cfg(test)]
1132mod tests {
1133 use super::{ElementData, TextInputData};
1134 use parley::{FontContext, LayoutContext};
1135
1136 #[test]
1149 fn element_data_stays_out_of_the_large_size_classes() {
1150 let size = std::mem::size_of::<ElementData>();
1151 assert!(
1152 size <= 1536,
1153 "ElementData grew to {size} bytes. It is allocated per element, so \
1154 this is a multiplier on the whole DOM's footprint: at 2848 bytes \
1155 it was 814 MB of a live instance. Box the new field instead, as \
1156 `cache` is."
1157 );
1158 }
1159
1160 #[test]
1165 fn an_unlaid_out_element_carries_no_cache() {
1166 let data = ElementData::new(
1167 markup5ever::QualName::new(None, markup5ever::ns!(html), "div".into()),
1168 Vec::new(),
1169 );
1170 assert!(
1171 std::mem::size_of_val(&data) < std::mem::size_of::<taffy::Cache>() * 2,
1172 "the cache must not be inline: a fresh element should be smaller \
1173 than two caches"
1174 );
1175 }
1176
1177 fn make_input(is_multiline: bool, text: &str) -> TextInputData {
1179 let mut font_ctx = FontContext::new();
1180 let mut layout_ctx = LayoutContext::new();
1181 let mut data = TextInputData::new(is_multiline);
1182 data.editor.set_scale(1.0);
1183 data.editor.set_text(text);
1184 data.editor
1185 .driver(&mut font_ctx, &mut layout_ctx)
1186 .refresh_layout();
1187 data
1188 }
1189
1190 #[test]
1191 fn short_text_does_not_scroll() {
1192 let mut data = make_input(false, "hi");
1193 data.clamp_scroll_offset(1000.0, 100.0);
1195 assert_eq!(data.scroll_offset, 0.0);
1196 }
1197
1198 #[test]
1199 fn single_line_scrolls_to_follow_caret() {
1200 let text = "the quick brown fox jumps over the lazy dog repeatedly and at length";
1201 let mut data = make_input(false, text);
1202 let content_box_width = 40.0;
1203 let content_box_height = 20.0;
1204
1205 data.editor
1207 .driver(&mut FontContext::new(), &mut LayoutContext::new())
1208 .move_to_text_end();
1209 data.clamp_scroll_offset(content_box_width, content_box_height);
1210
1211 let layout_width = data.editor.try_layout().unwrap().full_width();
1212 if layout_width > content_box_width {
1213 assert!(
1214 data.scroll_offset > 0.0,
1215 "expected horizontal scroll for overflowing single-line input"
1216 );
1217 let caret = data.editor.cursor_geometry(1.5).unwrap();
1219 assert!(caret.x1 as f32 <= data.scroll_offset + content_box_width + 0.5);
1220 assert!(caret.x0 as f32 >= data.scroll_offset - 0.5);
1221 }
1222
1223 data.editor
1225 .driver(&mut FontContext::new(), &mut LayoutContext::new())
1226 .move_to_text_start();
1227 data.clamp_scroll_offset(content_box_width, content_box_height);
1228 assert_eq!(data.scroll_offset, 0.0);
1229 }
1230
1231 #[test]
1232 fn multiline_scrolls_vertically_not_horizontally() {
1233 let text = (0..40)
1234 .map(|i| format!("line {i}"))
1235 .collect::<Vec<_>>()
1236 .join("\n");
1237 let mut data = make_input(true, &text);
1238 data.editor.set_width(Some(200.0));
1240 data.editor
1241 .driver(&mut FontContext::new(), &mut LayoutContext::new())
1242 .refresh_layout();
1243
1244 let content_box_width = 200.0;
1245 let content_box_height = 30.0;
1246
1247 data.editor
1248 .driver(&mut FontContext::new(), &mut LayoutContext::new())
1249 .move_to_text_end();
1250 data.clamp_scroll_offset(content_box_width, content_box_height);
1251
1252 let layout_height = data.editor.try_layout().unwrap().height();
1253 if layout_height > content_box_height {
1254 assert!(
1255 data.scroll_offset > 0.0,
1256 "expected vertical scroll for overflowing multi-line input"
1257 );
1258 }
1259 }
1260
1261 #[cfg(target_os = "macos")]
1266 #[test]
1267 fn scroll_by_clamps_and_bubbles() {
1268 let text = (0..40)
1269 .map(|i| format!("line {i}"))
1270 .collect::<Vec<_>>()
1271 .join("\n");
1272 let mut data = make_input(true, &text);
1273 data.editor.set_width(Some(200.0));
1274 data.editor
1275 .driver(&mut FontContext::new(), &mut LayoutContext::new())
1276 .refresh_layout();
1277
1278 let content_box_width = 200.0;
1279 let content_box_height = 30.0;
1280 let max = data.max_scroll_offset(content_box_width, content_box_height);
1281 assert!(max > 0.0, "test text should overflow the content box");
1282
1283 assert_eq!(data.scroll_offset, 0.0);
1286 let bubbled = data.scroll_by(15.0, content_box_width, content_box_height);
1287 assert_eq!(data.scroll_offset, 0.0);
1288 assert_eq!(bubbled, 15.0);
1289
1290 let bubbled = data.scroll_by(-10.0, content_box_width, content_box_height);
1292 assert_eq!(data.scroll_offset, 10.0);
1293 assert_eq!(bubbled, 0.0);
1294
1295 let bubbled = data.scroll_by(-(max + 100.0), content_box_width, content_box_height);
1299 assert_eq!(data.scroll_offset, max);
1300 assert!((bubbled - (-110.0)).abs() < 1e-3);
1301 }
1302
1303 #[test]
1304 fn single_line_does_not_scroll_when_text_fits() {
1305 let mut data = make_input(false, "hi");
1306 let bubbled = data.scroll_by(-50.0, 1000.0, 100.0);
1308 assert_eq!(data.scroll_offset, 0.0);
1309 assert_eq!(bubbled, -50.0);
1310 }
1311}