1use apace_traits::node_id::NodeId;
2use cssparser::Parser;
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::{Importance, PropertyDeclaration, PropertyId, SourcePropertyDeclaration};
14use style::stylesheets::{DocumentStyleSheet, Origin, UrlExtraData};
15use style::values::computed::Display as StyloDisplay;
16use style::{
17 properties::{PropertyDeclarationBlock, parse_style_attribute},
18 servo_arc::Arc as ServoArc,
19 shared_lock::{Locked, SharedRwLock},
20 stylesheets::CssRuleType,
21};
22use style_dom::ElementState;
23use style_traits::ParsingMode;
24use takia::{Cache, prelude::Layout};
25use thin_vec::ThinVec;
26use url::Url;
27
28use super::stylo_data::StyloData;
29#[cfg(feature = "svg")]
30use super::svg::SvgImageData;
31use super::{Attribute, Attributes};
32use crate::Document;
33use crate::layout::table::TableContext;
34use crate::node::{TextBrush, TextInputData, TextLayout};
35
36#[cfg(feature = "custom-widget")]
37use super::custom_widget::CustomWidgetData;
38
39macro_rules! local_names {
40 ($($name:tt),+) => {
41 [$(local_name!($name),)+]
42 };
43}
44
45pub(crate) fn parse_classes(value: &str) -> ThinVec<Atom> {
46 value.split_ascii_whitespace().map(Atom::from).collect()
47}
48
49pub struct ElementData {
50 pub name: QualName,
52
53 pub id: Option<Atom>,
55
56 pub classes: ThinVec<Atom>,
61
62 pub attrs: Attributes,
64
65 pub is_focussable: bool,
67
68 pub style_attribute: Option<ServoArc<Locked<PropertyDeclarationBlock>>>,
70
71 pub special_data: SpecialElementData,
77
78 pub background_images: ThinVec<Option<ImageResourceData>>,
79
80 pub mask_images: ThinVec<Option<ImageResourceData>>,
81
82 pub inline_layout_data: Option<Box<TextLayout>>,
84
85 pub list_item_data: Option<Box<ListItemLayout>>,
88
89 pub template_contents: Option<NodeId>,
91 pub stylo_element_data: StyloData,
100 pub selector_flags: Cell<ElementSelectorFlags>,
101 pub guard: Option<SharedRwLock>,
104 pub element_state: ElementState,
105 pub has_snapshot: bool,
106 pub snapshot_handled: AtomicBool,
107 pub dirty_descendants: AtomicBool,
110 pub damaged_descendants: AtomicBool,
113 pub may_have_animations: AtomicBool,
117
118 pub before: Option<NodeId>,
120 pub after: Option<NodeId>,
121
122 pub detailed_layout_info: takia::DetailedLayoutInfo<Atom>,
126
127 pub display_constructed_as: StyloDisplay,
129 pub layout_data: Option<Box<LayoutData>>,
131 pub transform: Option<Box<Affine>>,
132}
133
134#[derive(Debug, Clone)]
140pub struct LayoutData {
141 pub cache: Cache,
142 pub unrounded_layout: Layout,
143 pub final_layout: Layout,
144 pub scroll_offset: crate::Point<f64>,
145 pub scrollable_overflow: KurboRect,
146 #[cfg(feature = "writing-mode")]
149 pub writing_mode: stylo_takia::WritingMode,
150}
151
152impl LayoutData {
153 pub const fn new() -> Self {
154 Self {
155 cache: Cache::new(),
156 unrounded_layout: Layout::new(),
157 final_layout: Layout::new(),
158 scroll_offset: crate::Point::ZERO,
159 scrollable_overflow: KurboRect::ZERO,
160 #[cfg(feature = "writing-mode")]
161 writing_mode: stylo_takia::WritingMode::empty(),
162 }
163 }
164}
165
166impl Default for LayoutData {
167 fn default() -> Self {
168 Self::new()
169 }
170}
171
172pub(crate) static DEFAULT_LAYOUT_DATA: LayoutData = LayoutData::new();
175
176impl ElementData {
177 #[inline]
180 pub fn layout_data(&self) -> &LayoutData {
181 self.layout_data.as_deref().unwrap_or(&DEFAULT_LAYOUT_DATA)
182 }
183
184 #[inline]
187 pub fn layout_data_mut(&mut self) -> &mut LayoutData {
188 self.layout_data.get_or_insert_default()
189 }
190}
191
192impl DocumentData {
193 #[inline]
196 pub fn layout_data(&self) -> &LayoutData {
197 self.layout_data.as_deref().unwrap_or(&DEFAULT_LAYOUT_DATA)
198 }
199
200 #[inline]
203 pub fn layout_data_mut(&mut self) -> &mut LayoutData {
204 self.layout_data.get_or_insert_default()
205 }
206}
207
208pub struct DocumentData {
214 pub stylo_element_data: StyloData,
215 pub selector_flags: Cell<ElementSelectorFlags>,
219 pub guard: Option<SharedRwLock>,
222 pub dirty_descendants: AtomicBool,
223 pub damaged_descendants: AtomicBool,
224 pub element_state: ElementState,
225 pub has_snapshot: bool,
226 pub snapshot_handled: AtomicBool,
227 pub display_constructed_as: StyloDisplay,
228 pub layout_data: Option<Box<LayoutData>>,
230 pub transform: Option<Box<Affine>>,
231}
232
233impl std::fmt::Debug for DocumentData {
236 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237 f.debug_struct("DocumentData")
238 .field("stylo_element_data", &self.stylo_element_data)
239 .field("guard", &self.guard)
240 .field("dirty_descendants", &self.dirty_descendants)
241 .field("damaged_descendants", &self.damaged_descendants)
242 .field("element_state", &self.element_state)
243 .field("has_snapshot", &self.has_snapshot)
244 .field("snapshot_handled", &self.snapshot_handled)
245 .field("display_constructed_as", &self.display_constructed_as)
246 .field("layout_data", &self.layout_data)
247 .field("transform", &self.transform)
248 .finish_non_exhaustive()
249 }
250}
251
252impl DocumentData {
253 pub fn new() -> Self {
254 Self {
255 stylo_element_data: Default::default(),
256 selector_flags: Cell::new(ElementSelectorFlags::empty()),
257 guard: None,
258 dirty_descendants: AtomicBool::new(true),
259 damaged_descendants: AtomicBool::new(true),
260 element_state: ElementState::empty(),
261 has_snapshot: false,
262 snapshot_handled: AtomicBool::new(false),
263 display_constructed_as: StyloDisplay::Block,
264 layout_data: None,
265 transform: None,
266 }
267 }
268}
269
270impl Default for DocumentData {
271 fn default() -> Self {
272 Self::new()
273 }
274}
275
276impl Clone for DocumentData {
277 fn clone(&self) -> Self {
278 Self {
281 guard: self.guard.clone(),
282 ..Self::new()
283 }
284 }
285}
286
287impl std::fmt::Debug for ElementData {
288 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289 f.debug_struct("ElementData")
290 .field("name", &self.name)
291 .field("id", &self.id)
292 .field("attrs", &self.attrs)
293 .field("is_focussable", &self.is_focussable)
294 .field("style_attribute", &self.style_attribute)
295 .field("special_data", &self.special_data)
296 .field("background_images", &self.background_images)
297 .field("mask_images", &self.mask_images)
298 .field("inline_layout_data", &self.inline_layout_data)
299 .field("list_item_data", &self.list_item_data)
300 .field("template_contents", &self.template_contents)
301 .field("element_state", &self.element_state)
302 .field("display_constructed_as", &self.display_constructed_as)
303 .finish_non_exhaustive()
304 }
305}
306
307impl Clone for ElementData {
308 fn clone(&self) -> Self {
314 Self {
315 name: self.name.clone(),
316 id: self.id.clone(),
317 classes: self.classes.clone(),
318 attrs: self.attrs.clone(),
319 is_focussable: self.is_focussable,
320 style_attribute: self.style_attribute.clone(),
321 special_data: self.special_data.clone(),
322 background_images: self.background_images.clone(),
323 mask_images: self.mask_images.clone(),
324 inline_layout_data: self.inline_layout_data.clone(),
325 list_item_data: self.list_item_data.clone(),
326 template_contents: self.template_contents,
327
328 stylo_element_data: Default::default(),
330 selector_flags: Cell::new(ElementSelectorFlags::empty()),
331 guard: self.guard.clone(),
332 element_state: self.element_state,
333 has_snapshot: false,
334 snapshot_handled: AtomicBool::new(false),
335 dirty_descendants: AtomicBool::new(true),
336 damaged_descendants: AtomicBool::new(true),
337 may_have_animations: AtomicBool::new(false),
338 before: None,
339 after: None,
340 detailed_layout_info: takia::DetailedLayoutInfo::None,
341 display_constructed_as: StyloDisplay::Block,
342 layout_data: None,
343 transform: None,
344 }
345 }
346}
347
348#[derive(Copy, Clone, Default)]
349#[non_exhaustive]
350pub enum SpecialElementType {
351 Stylesheet,
352 Image,
353 Canvas,
354 TableRoot,
355 TextInput,
356 CheckboxInput,
357 #[cfg(feature = "file-input")]
358 FileInput,
359 #[default]
360 None,
361}
362
363#[derive(Default)]
365pub enum SpecialElementData {
366 SubDocument(Box<dyn Document>),
368 #[cfg(feature = "custom-widget")]
370 CustomWidget(CustomWidgetData),
371 Stylesheet(DocumentStyleSheet),
373 Image(Box<ImageData>),
375 Canvas(CanvasData),
377 TableRoot(Arc<TableContext>),
379 TextInput(TextInputData),
381 CheckboxInput(bool),
383 #[cfg(feature = "file-input")]
385 FileInput(FileData),
386 #[default]
388 None,
389}
390
391impl Clone for SpecialElementData {
392 fn clone(&self) -> Self {
393 match self {
394 Self::SubDocument(_) => Self::None, #[cfg(feature = "custom-widget")]
396 Self::CustomWidget(_) => Self::None, Self::Stylesheet(data) => Self::Stylesheet(data.clone()),
398 Self::Image(data) => Self::Image(data.clone()),
399 Self::Canvas(data) => Self::Canvas(data.clone()),
400 Self::TableRoot(data) => Self::TableRoot(data.clone()),
401 Self::TextInput(data) => Self::TextInput(data.clone()),
402 Self::CheckboxInput(data) => Self::CheckboxInput(*data),
403 #[cfg(feature = "file-input")]
404 Self::FileInput(data) => Self::FileInput(data.clone()),
405 Self::None => Self::None,
406 }
407 }
408}
409
410impl SpecialElementData {
411 pub fn take(&mut self) -> Self {
412 std::mem::take(self)
413 }
414}
415
416impl ElementData {
417 pub fn new(name: QualName, attrs: Vec<Attribute>) -> Self {
418 let id_attr_atom = attrs
419 .iter()
420 .find(|attr| &attr.name.local == "id")
421 .map(|attr| attr.value.as_ref())
422 .map(|value: &str| Atom::from(value));
423 let classes = attrs
424 .iter()
425 .find(|attr| &attr.name.local == "class")
426 .map(|attr| parse_classes(&attr.value))
427 .unwrap_or_default();
428
429 let mut data = ElementData {
430 name,
431 id: id_attr_atom,
432 classes,
433 attrs: Attributes::new(attrs),
434 is_focussable: false,
435 style_attribute: Default::default(),
436 inline_layout_data: None,
437 list_item_data: None,
438 special_data: SpecialElementData::None,
439 template_contents: None,
440 background_images: ThinVec::new(),
441 mask_images: ThinVec::new(),
442
443 stylo_element_data: Default::default(),
444 selector_flags: Cell::new(ElementSelectorFlags::empty()),
445 guard: None,
446 element_state: ElementState::empty(),
447 has_snapshot: false,
448 snapshot_handled: AtomicBool::new(false),
449 dirty_descendants: AtomicBool::new(true),
450 damaged_descendants: AtomicBool::new(true),
451 may_have_animations: AtomicBool::new(false),
452 before: None,
453 after: None,
454 detailed_layout_info: takia::DetailedLayoutInfo::None,
455 display_constructed_as: StyloDisplay::Block,
456 layout_data: None,
457 transform: None,
458 };
459 data.flush_is_focussable();
460 data.flush_link_state();
461
462 if data.name.local == local_name!("input")
465 && matches!(
466 data.attr(local_name!("type")),
467 Some("checkbox") | Some("radio")
468 )
469 && data.has_attr(local_name!("checked"))
470 {
471 data.element_state.insert(ElementState::CHECKED);
472 }
473
474 if data.can_be_disabled() {
476 data.element_state
477 .insert(match data.has_attr(local_name!("disabled")) {
478 true => ElementState::DISABLED,
479 false => ElementState::ENABLED,
480 });
481 }
482
483 data.flush_open_state();
484
485 data
486 }
487
488 pub fn attrs(&self) -> &[Attribute] {
489 &self.attrs
490 }
491
492 pub fn attr(&self, name: impl PartialEq<LocalName>) -> Option<&str> {
493 let attr = self.attrs.iter().find(|attr| name == attr.name.local)?;
494 Some(&attr.value)
495 }
496
497 pub fn attr_parsed<T: FromStr>(&self, name: impl PartialEq<LocalName>) -> Option<T> {
498 let attr = self.attrs.iter().find(|attr| name == attr.name.local)?;
499 attr.value.parse::<T>().ok()
500 }
501
502 pub fn has_attr(&self, name: impl PartialEq<LocalName>) -> bool {
504 self.attrs.iter().any(|attr| name == attr.name.local)
505 }
506
507 pub fn can_be_disabled(&self) -> bool {
508 local_names!("button", "input", "select", "textarea").contains(&self.name.local)
509 }
510
511 pub fn is_link(&self) -> bool {
513 (self.name.local == local_name!("a") || self.name.local == local_name!("area"))
514 && self.has_attr(local_name!("href"))
515 }
516
517 pub fn flush_link_state(&mut self) {
525 self.element_state
526 .remove(ElementState::VISITED_OR_UNVISITED);
527 if self.is_link() {
528 self.element_state.insert(ElementState::UNVISITED);
529 }
530 }
531
532 pub fn flush_open_state(&mut self) {
533 let is_open = matches!(
534 self.name.local,
535 local_name!("details") | local_name!("dialog")
536 ) && self.has_attr(local_name!("open"));
537 self.element_state.set(ElementState::OPEN, is_open);
538 }
539
540 pub fn image_data(&self) -> Option<&ImageData> {
541 match &self.special_data {
542 SpecialElementData::Image(data) => Some(&**data),
543 _ => None,
544 }
545 }
546
547 pub fn image_data_mut(&mut self) -> Option<&mut ImageData> {
548 match self.special_data {
549 SpecialElementData::Image(ref mut data) => Some(&mut **data),
550 _ => None,
551 }
552 }
553
554 pub fn raster_image_data(&self) -> Option<&RasterImageData> {
555 match self.image_data()? {
556 ImageData::Raster(data) => Some(data),
557 _ => None,
558 }
559 }
560
561 pub fn raster_image_data_mut(&mut self) -> Option<&mut RasterImageData> {
562 match self.image_data_mut()? {
563 ImageData::Raster(data) => Some(data),
564 _ => None,
565 }
566 }
567
568 pub fn canvas_data(&self) -> Option<&CanvasData> {
569 match &self.special_data {
570 SpecialElementData::Canvas(data) => Some(data),
571 _ => None,
572 }
573 }
574
575 pub fn sub_doc_data(&self) -> Option<&dyn Document> {
576 match &self.special_data {
577 SpecialElementData::SubDocument(data) => Some(data.as_ref()),
578 _ => None,
579 }
580 }
581
582 pub fn sub_doc_data_mut(&mut self) -> Option<&mut dyn Document> {
583 match &mut self.special_data {
584 SpecialElementData::SubDocument(data) => Some(data.as_mut()),
585 _ => None,
586 }
587 }
588
589 #[cfg(feature = "svg")]
590 pub fn svg_data(&self) -> Option<&usvg::Tree> {
591 match self.image_data()? {
592 ImageData::Svg(data) => Some(&data.tree),
593 _ => None,
594 }
595 }
596
597 pub fn text_input_data(&self) -> Option<&TextInputData> {
598 match &self.special_data {
599 SpecialElementData::TextInput(data) => Some(data),
600 _ => None,
601 }
602 }
603
604 pub fn text_input_data_mut(&mut self) -> Option<&mut TextInputData> {
605 match &mut self.special_data {
606 SpecialElementData::TextInput(data) => Some(data),
607 _ => None,
608 }
609 }
610
611 #[cfg(feature = "custom-widget")]
612 pub fn custom_widget_data(&self) -> Option<&CustomWidgetData> {
613 match &self.special_data {
614 SpecialElementData::CustomWidget(data) => Some(data),
615 _ => None,
616 }
617 }
618
619 #[cfg(feature = "custom-widget")]
620 pub fn custom_widget_data_mut(&mut self) -> Option<&mut CustomWidgetData> {
621 match &mut self.special_data {
622 SpecialElementData::CustomWidget(data) => Some(data),
623 _ => None,
624 }
625 }
626
627 pub fn checkbox_input_checked(&self) -> Option<bool> {
628 match self.special_data {
629 SpecialElementData::CheckboxInput(checked) => Some(checked),
630 _ => None,
631 }
632 }
633
634 pub fn checkbox_input_indeterminate(&self) -> Option<bool> {
636 (self.name.local == local_name!("input")
637 && self.attr(local_name!("type")) == Some("checkbox"))
638 .then(|| self.element_state.contains(ElementState::INDETERMINATE))
639 }
640
641 pub fn set_checkbox_input_indeterminate(&mut self, value: bool) {
642 if self.checkbox_input_indeterminate().is_some() {
643 self.element_state.set(ElementState::INDETERMINATE, value);
644 }
645 }
646
647 pub fn checkbox_input_checked_mut(&mut self) -> Option<&mut bool> {
648 match self.special_data {
649 SpecialElementData::CheckboxInput(ref mut checked) => Some(checked),
650 _ => None,
651 }
652 }
653
654 pub fn set_checkbox_input_checked(&mut self, checked: bool) {
658 if let Some(is_checked) = self.checkbox_input_checked_mut() {
659 *is_checked = checked;
660 }
661 self.element_state.set(ElementState::CHECKED, checked);
662 }
663
664 #[cfg(feature = "file-input")]
665 pub fn file_data(&self) -> Option<&FileData> {
666 match &self.special_data {
667 SpecialElementData::FileInput(data) => Some(data),
668 _ => None,
669 }
670 }
671
672 #[cfg(feature = "file-input")]
673 pub fn file_data_mut(&mut self) -> Option<&mut FileData> {
674 match &mut self.special_data {
675 SpecialElementData::FileInput(data) => Some(data),
676 _ => None,
677 }
678 }
679
680 pub fn flush_is_focussable(&mut self) {
681 let disabled: bool = self.attr_parsed(local_name!("disabled")).unwrap_or(false);
682 let tabindex: Option<i32> = self.attr_parsed(local_name!("tabindex"));
683 let contains_sub_document: bool = self.sub_doc_data().is_some();
684
685 self.is_focussable = contains_sub_document
686 || (!disabled
687 && match tabindex {
688 Some(index) => index >= 0,
689 None => {
690 if [local_name!("a"), local_name!("area")].contains(&self.name.local) {
697 self.attr(local_name!("href")).is_some()
698 } else {
699 const DEFAULT_FOCUSSABLE_ELEMENTS: [LocalName; 7] = [
700 local_name!("button"),
701 local_name!("input"),
702 local_name!("select"),
703 local_name!("textarea"),
704 local_name!("frame"),
705 local_name!("iframe"),
706 local_name!("summary"),
707 ];
708 DEFAULT_FOCUSSABLE_ELEMENTS.contains(&self.name.local)
709 }
710 }
711 })
712 }
713
714 pub fn flush_style_attribute(&mut self, guard: &SharedRwLock, url_extra_data: &UrlExtraData) {
715 self.style_attribute = self.attr(local_name!("style")).map(|style_str| {
716 ServoArc::new(guard.wrap(parse_style_attribute(
717 style_str,
718 url_extra_data,
719 None,
720 QuirksMode::NoQuirks,
721 CssRuleType::Style,
722 )))
723 });
724 }
725
726 pub fn set_style_property(
727 &mut self,
728 name: &str,
729 value: &str,
730 guard: &SharedRwLock,
731 url_extra_data: UrlExtraData,
732 ) -> bool {
733 let mut context = ParserContext::new(
734 Origin::Author,
735 &url_extra_data,
736 Some(CssRuleType::Style),
737 ParsingMode::DEFAULT,
738 QuirksMode::NoQuirks,
739 Default::default(),
740 None,
741 None,
742 Default::default(),
743 );
744
745 let Ok(property_id) = PropertyId::parse(name, &context) else {
746 #[cfg(feature = "tracing")]
747 tracing::warn!(property = name, "Unsupported property");
748 return false;
749 };
750 let mut source_property_declaration = SourcePropertyDeclaration::default();
751 let mut parser = Parser::new(value);
752 let Ok(_) = PropertyDeclaration::parse_into(
753 &mut source_property_declaration,
754 property_id,
755 &mut context,
756 &mut parser,
757 ) else {
758 #[cfg(feature = "tracing")]
759 tracing::warn!(property = name, value, "Invalid property value");
760 return false;
761 };
762
763 self.mutable_style_attribute(guard)
764 .write_with(&mut guard.write())
765 .extend(source_property_declaration.drain(), Importance::Normal);
766
767 true
768 }
769
770 fn mutable_style_attribute(
774 &mut self,
775 guard: &SharedRwLock,
776 ) -> &ServoArc<Locked<PropertyDeclarationBlock>> {
777 let cloned = match &self.style_attribute {
778 None => Some(PropertyDeclarationBlock::new()),
779 Some(arc) => {
780 let read = guard.read();
781 let block = arc.read_with(&read);
782 block
783 .immutable
784 .load(std::sync::atomic::Ordering::Relaxed)
785 .then(|| block.clone())
786 }
787 };
788 if let Some(block) = cloned {
789 self.style_attribute = Some(ServoArc::new(guard.wrap(block)));
790 }
791 self.style_attribute.as_ref().unwrap()
792 }
793
794 pub fn remove_style_property(
795 &mut self,
796 name: &str,
797 guard: &SharedRwLock,
798 url_extra_data: UrlExtraData,
799 ) -> bool {
800 let context = ParserContext::new(
801 Origin::Author,
802 &url_extra_data,
803 Some(CssRuleType::Style),
804 ParsingMode::DEFAULT,
805 QuirksMode::NoQuirks,
806 Default::default(),
807 None,
808 None,
809 Default::default(),
810 );
811 let Ok(property_id) = PropertyId::parse(name, &context) else {
812 #[cfg(feature = "tracing")]
813 tracing::warn!(property = name, "Unsupported property");
814 return false;
815 };
816
817 if self.style_attribute.is_none() {
818 return false;
819 }
820 let style = self.mutable_style_attribute(guard);
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 false
829 }
830
831 pub fn set_sub_document(&mut self, sub_document: Box<dyn Document>) {
832 self.special_data = SpecialElementData::SubDocument(sub_document);
833 }
834
835 pub fn remove_sub_document(&mut self) {
836 self.special_data = SpecialElementData::None;
837 }
838
839 #[cfg(feature = "custom-widget")]
840 pub fn set_custom_widget(&mut self, mut widget: Box<dyn crate::Widget>) {
841 use crate::node::custom_widget::CustomWidgetData;
842 for attr in self.attrs.iter() {
843 widget.attribute_changed(&attr.name.local, None, Some(&attr.value));
844 }
845 self.special_data = SpecialElementData::CustomWidget(CustomWidgetData::new(widget));
846 }
847
848 #[cfg(feature = "custom-widget")]
849 pub fn remove_custom_widget(&mut self) -> Vec<anyrender::ResourceId> {
850 let resource_ids = self
851 .custom_widget_data_mut()
852 .map(|widget_data| widget_data.take_resource_ids())
853 .unwrap_or_default();
854 self.special_data = SpecialElementData::None;
855 resource_ids
856 }
857
858 pub fn take_inline_layout(&mut self) -> Option<Box<TextLayout>> {
859 std::mem::take(&mut self.inline_layout_data)
860 }
861
862 pub(crate) fn is_button_input(&self) -> bool {
863 self.name.local == local_name!("input")
864 && matches!(
865 self.attr(local_name!("type")),
866 Some("button" | "submit" | "reset")
867 )
868 }
869
870 pub fn is_submit_button(&self) -> bool {
871 if self.name.local != local_name!("button") {
872 return false;
873 }
874 let type_attr = self.attr(local_name!("type"));
875 let is_submit = type_attr == Some("submit");
876 let is_auto_submit = type_attr.is_none()
877 && self.attr(LocalName::from("command")).is_none()
878 && self.attr(LocalName::from("commandfor")).is_none();
879 is_submit || is_auto_submit
880 }
881}
882
883#[derive(Debug, Clone, PartialEq)]
884pub struct RasterImageData {
885 pub width: u32,
887 pub height: u32,
889 pub data: Blob<u8>,
891}
892impl RasterImageData {
893 pub fn new(width: u32, height: u32, data: Arc<Vec<u8>>) -> Self {
894 Self {
895 width,
896 height,
897 data: Blob::new(data),
898 }
899 }
900}
901
902#[derive(Debug, Clone)]
903pub enum ImageData {
904 Raster(RasterImageData),
905 #[cfg(feature = "svg")]
906 Svg(SvgImageData),
907 None,
908}
909
910#[derive(Debug, Clone, PartialEq)]
911pub enum Status {
912 Ok,
913 Error,
914 Loading,
915}
916
917#[derive(Debug, Clone)]
918pub struct ImageResourceData {
919 pub url: ServoArc<Url>,
921 pub status: Status,
923 pub image: ImageData,
925}
926
927impl ImageResourceData {
928 pub fn new(url: ServoArc<Url>) -> Self {
929 Self {
930 url,
931 status: Status::Loading,
932 image: ImageData::None,
933 }
934 }
935}
936
937#[derive(Debug, Clone)]
938pub struct CanvasData {
939 pub custom_paint_source_id: u64,
940}
941
942impl std::fmt::Debug for SpecialElementData {
943 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
944 match self {
945 SpecialElementData::SubDocument(_) => f.write_str("NodeSpecificData::SubDocument"),
946 #[cfg(feature = "custom-widget")]
947 SpecialElementData::CustomWidget(_) => f.write_str("NodeSpecificData::CustomWidget"),
948 SpecialElementData::Stylesheet(_) => f.write_str("NodeSpecificData::Stylesheet"),
949 SpecialElementData::Image(data) => match **data {
950 ImageData::Raster(_) => f.write_str("NodeSpecificData::Image(Raster)"),
951 #[cfg(feature = "svg")]
952 ImageData::Svg(_) => f.write_str("NodeSpecificData::Image(Svg)"),
953 ImageData::None => f.write_str("NodeSpecificData::Image(None)"),
954 },
955 SpecialElementData::Canvas(_) => f.write_str("NodeSpecificData::Canvas"),
956 SpecialElementData::TableRoot(_) => f.write_str("NodeSpecificData::TableRoot"),
957 SpecialElementData::TextInput(_) => f.write_str("NodeSpecificData::TextInput"),
958 SpecialElementData::CheckboxInput(_) => f.write_str("NodeSpecificData::CheckboxInput"),
959 #[cfg(feature = "file-input")]
960 SpecialElementData::FileInput(_) => f.write_str("NodeSpecificData::FileInput"),
961 SpecialElementData::None => f.write_str("NodeSpecificData::None"),
962 }
963 }
964}
965
966#[derive(Clone)]
967pub struct ListItemLayout {
968 pub marker: Marker,
969 pub position: ListItemLayoutPosition,
970}
971
972#[derive(Debug, PartialEq, Clone)]
975pub enum Marker {
976 Char(char),
977 String(String),
978}
979
980#[derive(Clone)]
982pub enum ListItemLayoutPosition {
983 Inside,
984 Outside(Box<parley::Layout<TextBrush>>),
985}
986
987impl std::fmt::Debug for ListItemLayout {
988 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
989 write!(f, "ListItemLayout - marker {:?}", self.marker)
990 }
991}
992
993#[cfg(feature = "file-input")]
994mod file_data {
995 use std::ops::{Deref, DerefMut};
996 use std::path::PathBuf;
997
998 #[derive(Clone, Debug)]
999 pub struct FileData(pub Vec<PathBuf>);
1000 impl Deref for FileData {
1001 type Target = Vec<PathBuf>;
1002
1003 fn deref(&self) -> &Self::Target {
1004 &self.0
1005 }
1006 }
1007 impl DerefMut for FileData {
1008 fn deref_mut(&mut self) -> &mut Self::Target {
1009 &mut self.0
1010 }
1011 }
1012 impl From<Vec<PathBuf>> for FileData {
1013 fn from(files: Vec<PathBuf>) -> Self {
1014 Self(files)
1015 }
1016 }
1017}
1018#[cfg(feature = "file-input")]
1019pub use file_data::FileData;
1020
1021#[cfg(test)]
1022mod tests {
1023 use super::TextInputData;
1024 use parley::{FontContext, LayoutContext};
1025
1026 fn make_input(is_multiline: bool, text: &str) -> TextInputData {
1028 let mut font_ctx = FontContext::new();
1029 let mut layout_ctx = LayoutContext::new();
1030 let mut data = TextInputData::new(is_multiline);
1031 data.editor.set_scale(1.0);
1032 data.editor.set_text(text);
1033 data.editor
1034 .driver(&mut font_ctx, &mut layout_ctx)
1035 .refresh_layout();
1036 data
1037 }
1038
1039 #[test]
1040 fn short_text_does_not_scroll() {
1041 let mut data = make_input(false, "hi");
1042 data.clamp_scroll_offset(1000.0, 100.0);
1044 assert_eq!(data.scroll_offset, 0.0);
1045 }
1046
1047 #[test]
1048 fn single_line_scrolls_to_follow_caret() {
1049 let text = "the quick brown fox jumps over the lazy dog repeatedly and at length";
1050 let mut data = make_input(false, text);
1051 let content_box_width = 40.0;
1052 let content_box_height = 20.0;
1053
1054 data.editor
1056 .driver(&mut FontContext::new(), &mut LayoutContext::new())
1057 .move_to_text_end();
1058 data.clamp_scroll_offset(content_box_width, content_box_height);
1059
1060 let layout_width = data.editor.try_layout().unwrap().full_width();
1061 if layout_width > content_box_width {
1062 assert!(
1063 data.scroll_offset > 0.0,
1064 "expected horizontal scroll for overflowing single-line input"
1065 );
1066 let caret = data.editor.cursor_geometry(1.5).unwrap();
1068 assert!(caret.x1 as f32 <= data.scroll_offset + content_box_width + 0.5);
1069 assert!(caret.x0 as f32 >= data.scroll_offset - 0.5);
1070 }
1071
1072 data.editor
1074 .driver(&mut FontContext::new(), &mut LayoutContext::new())
1075 .move_to_text_start();
1076 data.clamp_scroll_offset(content_box_width, content_box_height);
1077 assert_eq!(data.scroll_offset, 0.0);
1078 }
1079
1080 #[test]
1081 fn multiline_scrolls_vertically_not_horizontally() {
1082 let text = (0..40)
1083 .map(|i| format!("line {i}"))
1084 .collect::<Vec<_>>()
1085 .join("\n");
1086 let mut data = make_input(true, &text);
1087 data.editor.set_width(Some(200.0));
1089 data.editor
1090 .driver(&mut FontContext::new(), &mut LayoutContext::new())
1091 .refresh_layout();
1092
1093 let content_box_width = 200.0;
1094 let content_box_height = 30.0;
1095
1096 data.editor
1097 .driver(&mut FontContext::new(), &mut LayoutContext::new())
1098 .move_to_text_end();
1099 data.clamp_scroll_offset(content_box_width, content_box_height);
1100
1101 let layout_height = data.editor.try_layout().unwrap().height();
1102 if layout_height > content_box_height {
1103 assert!(
1104 data.scroll_offset > 0.0,
1105 "expected vertical scroll for overflowing multi-line input"
1106 );
1107 }
1108 }
1109
1110 #[test]
1111 fn scroll_by_clamps_and_bubbles() {
1112 let text = (0..40)
1113 .map(|i| format!("line {i}"))
1114 .collect::<Vec<_>>()
1115 .join("\n");
1116 let mut data = make_input(true, &text);
1117 data.editor.set_width(Some(200.0));
1118 data.editor
1119 .driver(&mut FontContext::new(), &mut LayoutContext::new())
1120 .refresh_layout();
1121
1122 let content_box_width = 200.0;
1123 let content_box_height = 30.0;
1124 let max = data.max_scroll_offset(content_box_width, content_box_height);
1125 assert!(max > 0.0, "test text should overflow the content box");
1126
1127 assert_eq!(data.scroll_offset, 0.0);
1130 let bubbled = data.scroll_by(15.0, content_box_width, content_box_height);
1131 assert_eq!(data.scroll_offset, 0.0);
1132 assert_eq!(bubbled, 15.0);
1133
1134 let bubbled = data.scroll_by(-10.0, content_box_width, content_box_height);
1136 assert_eq!(data.scroll_offset, 10.0);
1137 assert_eq!(bubbled, 0.0);
1138
1139 let bubbled = data.scroll_by(-(max + 100.0), content_box_width, content_box_height);
1143 assert_eq!(data.scroll_offset, max);
1144 assert!((bubbled - (-110.0)).abs() < 1e-3);
1145 }
1146
1147 #[test]
1148 fn single_line_does_not_scroll_when_text_fits() {
1149 let mut data = make_input(false, "hi");
1150 let bubbled = data.scroll_by(-50.0, 1000.0, 100.0);
1152 assert_eq!(data.scroll_offset, 0.0);
1153 assert_eq!(bubbled, -50.0);
1154 }
1155}