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