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::{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 taffy::{
25 Cache,
26 prelude::{Layout, Style},
27};
28use url::Url;
29
30use super::stylo_data::StyloData;
31#[cfg(feature = "svg")]
32use super::svg::SvgImageData;
33use super::{Attribute, Attributes};
34use crate::Document;
35use crate::layout::table::TableContext;
36use crate::node::{TextBrush, TextInputData, TextLayout};
37
38#[cfg(feature = "custom-widget")]
39use super::custom_widget::CustomWidgetData;
40
41macro_rules! local_names {
42 ($($name:tt),+) => {
43 [$(local_name!($name),)+]
44 };
45}
46
47pub struct ElementData {
48 pub name: QualName,
50
51 pub id: Option<Atom>,
53
54 pub attrs: Attributes,
56
57 pub is_focussable: bool,
59
60 pub style_attribute: Option<ServoArc<Locked<PropertyDeclarationBlock>>>,
62
63 pub special_data: SpecialElementData,
69
70 pub background_images: Vec<Option<ImageResourceData>>,
71
72 pub mask_images: Vec<Option<ImageResourceData>>,
73
74 pub inline_layout_data: Option<Box<TextLayout>>,
76
77 pub list_item_data: Option<Box<ListItemLayout>>,
80
81 pub template_contents: Option<NodeId>,
83 pub stylo_element_data: StyloData,
92 pub selector_flags: Cell<ElementSelectorFlags>,
93 pub guard: Option<SharedRwLock>,
96 pub element_state: ElementState,
97 pub has_snapshot: bool,
98 pub snapshot_handled: AtomicBool,
99 pub dirty_descendants: AtomicBool,
102 pub damaged_descendants: AtomicBool,
105
106 pub before: Option<NodeId>,
108 pub after: Option<NodeId>,
109
110 pub detailed_grid_info: Option<Box<taffy::DetailedGridInfo<Atom>>>,
113
114 pub style: Style<Atom>,
116 pub display_constructed_as: StyloDisplay,
117 pub cache: Cache,
118 pub unrounded_layout: Layout,
119 pub final_layout: Layout,
120 pub scroll_offset: crate::Point<f64>,
121 pub scrollable_overflow: KurboRect,
122 pub transform: Option<Affine>,
123}
124
125pub struct DocumentData {
131 pub stylo_element_data: StyloData,
132 pub selector_flags: Cell<ElementSelectorFlags>,
136 pub guard: Option<SharedRwLock>,
139 pub dirty_descendants: AtomicBool,
140 pub damaged_descendants: AtomicBool,
141 pub element_state: ElementState,
142 pub has_snapshot: bool,
143 pub snapshot_handled: AtomicBool,
144 pub style: Style<Atom>,
145 pub display_constructed_as: StyloDisplay,
146 pub cache: Cache,
147 pub unrounded_layout: Layout,
148 pub final_layout: Layout,
149 pub scroll_offset: crate::Point<f64>,
150 pub scrollable_overflow: KurboRect,
151 pub transform: Option<Affine>,
152}
153
154impl std::fmt::Debug for DocumentData {
157 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158 f.debug_struct("DocumentData")
159 .field("stylo_element_data", &self.stylo_element_data)
160 .field("guard", &self.guard)
161 .field("dirty_descendants", &self.dirty_descendants)
162 .field("damaged_descendants", &self.damaged_descendants)
163 .field("element_state", &self.element_state)
164 .field("has_snapshot", &self.has_snapshot)
165 .field("snapshot_handled", &self.snapshot_handled)
166 .field("style", &self.style)
167 .field("display_constructed_as", &self.display_constructed_as)
168 .field("cache", &self.cache)
169 .field("unrounded_layout", &self.unrounded_layout)
170 .field("final_layout", &self.final_layout)
171 .field("scroll_offset", &self.scroll_offset)
172 .field("scrollable_overflow", &self.scrollable_overflow)
173 .field("transform", &self.transform)
174 .finish_non_exhaustive()
175 }
176}
177
178impl DocumentData {
179 pub fn new() -> Self {
180 Self {
181 stylo_element_data: Default::default(),
182 selector_flags: Cell::new(ElementSelectorFlags::empty()),
183 guard: None,
184 dirty_descendants: AtomicBool::new(true),
185 damaged_descendants: AtomicBool::new(true),
186 element_state: ElementState::empty(),
187 has_snapshot: false,
188 snapshot_handled: AtomicBool::new(false),
189 style: Default::default(),
190 display_constructed_as: StyloDisplay::Block,
191 cache: Cache::new(),
192 unrounded_layout: Layout::new(),
193 final_layout: Layout::new(),
194 scroll_offset: crate::Point::ZERO,
195 scrollable_overflow: KurboRect::ZERO,
196 transform: None,
197 }
198 }
199}
200
201impl Default for DocumentData {
202 fn default() -> Self {
203 Self::new()
204 }
205}
206
207impl Clone for DocumentData {
208 fn clone(&self) -> Self {
209 Self {
212 guard: self.guard.clone(),
213 ..Self::new()
214 }
215 }
216}
217
218impl std::fmt::Debug for ElementData {
219 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220 f.debug_struct("ElementData")
221 .field("name", &self.name)
222 .field("id", &self.id)
223 .field("attrs", &self.attrs)
224 .field("is_focussable", &self.is_focussable)
225 .field("style_attribute", &self.style_attribute)
226 .field("special_data", &self.special_data)
227 .field("background_images", &self.background_images)
228 .field("mask_images", &self.mask_images)
229 .field("inline_layout_data", &self.inline_layout_data)
230 .field("list_item_data", &self.list_item_data)
231 .field("template_contents", &self.template_contents)
232 .field("element_state", &self.element_state)
233 .field("display_constructed_as", &self.display_constructed_as)
234 .finish_non_exhaustive()
235 }
236}
237
238impl Clone for ElementData {
239 fn clone(&self) -> Self {
245 Self {
246 name: self.name.clone(),
247 id: self.id.clone(),
248 attrs: self.attrs.clone(),
249 is_focussable: self.is_focussable,
250 style_attribute: self.style_attribute.clone(),
251 special_data: self.special_data.clone(),
252 background_images: self.background_images.clone(),
253 mask_images: self.mask_images.clone(),
254 inline_layout_data: self.inline_layout_data.clone(),
255 list_item_data: self.list_item_data.clone(),
256 template_contents: self.template_contents,
257
258 stylo_element_data: Default::default(),
260 selector_flags: Cell::new(ElementSelectorFlags::empty()),
261 guard: self.guard.clone(),
262 element_state: self.element_state,
263 has_snapshot: false,
264 snapshot_handled: AtomicBool::new(false),
265 dirty_descendants: AtomicBool::new(true),
266 damaged_descendants: AtomicBool::new(true),
267 before: None,
268 after: None,
269 detailed_grid_info: None,
270 style: Default::default(),
271 display_constructed_as: StyloDisplay::Block,
272 cache: Cache::new(),
273 unrounded_layout: Layout::new(),
274 final_layout: Layout::new(),
275 scroll_offset: crate::Point::ZERO,
276 scrollable_overflow: KurboRect::ZERO,
277 transform: None,
278 }
279 }
280}
281
282#[derive(Copy, Clone, Default)]
283#[non_exhaustive]
284pub enum SpecialElementType {
285 Stylesheet,
286 Image,
287 Canvas,
288 TableRoot,
289 TextInput,
290 CheckboxInput,
291 #[cfg(feature = "file-input")]
292 FileInput,
293 #[default]
294 None,
295}
296
297#[derive(Default)]
299pub enum SpecialElementData {
300 SubDocument(Box<dyn Document>),
302 #[cfg(feature = "custom-widget")]
304 CustomWidget(CustomWidgetData),
305 Stylesheet(DocumentStyleSheet),
307 Image(Box<ImageData>),
309 Canvas(CanvasData),
311 TableRoot(Arc<TableContext>),
313 TextInput(TextInputData),
315 CheckboxInput(bool),
317 #[cfg(feature = "file-input")]
319 FileInput(FileData),
320 #[default]
322 None,
323}
324
325impl Clone for SpecialElementData {
326 fn clone(&self) -> Self {
327 match self {
328 Self::SubDocument(_) => Self::None, #[cfg(feature = "custom-widget")]
330 Self::CustomWidget(_) => Self::None, Self::Stylesheet(data) => Self::Stylesheet(data.clone()),
332 Self::Image(data) => Self::Image(data.clone()),
333 Self::Canvas(data) => Self::Canvas(data.clone()),
334 Self::TableRoot(data) => Self::TableRoot(data.clone()),
335 Self::TextInput(data) => Self::TextInput(data.clone()),
336 Self::CheckboxInput(data) => Self::CheckboxInput(*data),
337 #[cfg(feature = "file-input")]
338 Self::FileInput(data) => Self::FileInput(data.clone()),
339 Self::None => Self::None,
340 }
341 }
342}
343
344impl SpecialElementData {
345 pub fn take(&mut self) -> Self {
346 std::mem::take(self)
347 }
348}
349
350impl ElementData {
351 pub fn new(name: QualName, attrs: Vec<Attribute>) -> Self {
352 let id_attr_atom = attrs
353 .iter()
354 .find(|attr| &attr.name.local == "id")
355 .map(|attr| attr.value.as_ref())
356 .map(|value: &str| Atom::from(value));
357
358 let mut data = ElementData {
359 name,
360 id: id_attr_atom,
361 attrs: Attributes::new(attrs),
362 is_focussable: false,
363 style_attribute: Default::default(),
364 inline_layout_data: None,
365 list_item_data: None,
366 special_data: SpecialElementData::None,
367 template_contents: None,
368 background_images: Vec::new(),
369 mask_images: Vec::new(),
370
371 stylo_element_data: Default::default(),
372 selector_flags: Cell::new(ElementSelectorFlags::empty()),
373 guard: None,
374 element_state: ElementState::empty(),
375 has_snapshot: false,
376 snapshot_handled: AtomicBool::new(false),
377 dirty_descendants: AtomicBool::new(true),
378 damaged_descendants: AtomicBool::new(true),
379 before: None,
380 after: None,
381 detailed_grid_info: None,
382 style: Default::default(),
383 display_constructed_as: StyloDisplay::Block,
384 cache: Cache::new(),
385 unrounded_layout: Layout::new(),
386 final_layout: Layout::new(),
387 scroll_offset: crate::Point::ZERO,
388 scrollable_overflow: KurboRect::ZERO,
389 transform: None,
390 };
391 data.flush_is_focussable();
392 data.flush_link_state();
393
394 if data.name.local == local_name!("input")
397 && matches!(
398 data.attr(local_name!("type")),
399 Some("checkbox") | Some("radio")
400 )
401 && data.has_attr(local_name!("checked"))
402 {
403 data.element_state.insert(ElementState::CHECKED);
404 }
405
406 if data.can_be_disabled() {
408 data.element_state
409 .insert(match data.has_attr(local_name!("disabled")) {
410 true => ElementState::DISABLED,
411 false => ElementState::ENABLED,
412 });
413 }
414
415 data
416 }
417
418 pub fn attrs(&self) -> &[Attribute] {
419 &self.attrs
420 }
421
422 pub fn attr(&self, name: impl PartialEq<LocalName>) -> Option<&str> {
423 let attr = self.attrs.iter().find(|attr| name == attr.name.local)?;
424 Some(&attr.value)
425 }
426
427 pub fn attr_parsed<T: FromStr>(&self, name: impl PartialEq<LocalName>) -> Option<T> {
428 let attr = self.attrs.iter().find(|attr| name == attr.name.local)?;
429 attr.value.parse::<T>().ok()
430 }
431
432 pub fn has_attr(&self, name: impl PartialEq<LocalName>) -> bool {
434 self.attrs.iter().any(|attr| name == attr.name.local)
435 }
436
437 pub fn can_be_disabled(&self) -> bool {
438 local_names!("button", "input", "select", "textarea").contains(&self.name.local)
439 }
440
441 pub fn is_link(&self) -> bool {
443 (self.name.local == local_name!("a") || self.name.local == local_name!("area"))
444 && self.has_attr(local_name!("href"))
445 }
446
447 pub fn flush_link_state(&mut self) {
455 self.element_state
456 .remove(ElementState::VISITED_OR_UNVISITED);
457 if self.is_link() {
458 self.element_state.insert(ElementState::UNVISITED);
459 }
460 }
461
462 pub fn image_data(&self) -> Option<&ImageData> {
463 match &self.special_data {
464 SpecialElementData::Image(data) => Some(&**data),
465 _ => None,
466 }
467 }
468
469 pub fn image_data_mut(&mut self) -> Option<&mut ImageData> {
470 match self.special_data {
471 SpecialElementData::Image(ref mut data) => Some(&mut **data),
472 _ => None,
473 }
474 }
475
476 pub fn raster_image_data(&self) -> Option<&RasterImageData> {
477 match self.image_data()? {
478 ImageData::Raster(data) => Some(data),
479 _ => None,
480 }
481 }
482
483 pub fn raster_image_data_mut(&mut self) -> Option<&mut RasterImageData> {
484 match self.image_data_mut()? {
485 ImageData::Raster(data) => Some(data),
486 _ => None,
487 }
488 }
489
490 pub fn canvas_data(&self) -> Option<&CanvasData> {
491 match &self.special_data {
492 SpecialElementData::Canvas(data) => Some(data),
493 _ => None,
494 }
495 }
496
497 pub fn sub_doc_data(&self) -> Option<&dyn Document> {
498 match &self.special_data {
499 SpecialElementData::SubDocument(data) => Some(data.as_ref()),
500 _ => None,
501 }
502 }
503
504 pub fn sub_doc_data_mut(&mut self) -> Option<&mut dyn Document> {
505 match &mut self.special_data {
506 SpecialElementData::SubDocument(data) => Some(data.as_mut()),
507 _ => None,
508 }
509 }
510
511 #[cfg(feature = "svg")]
512 pub fn svg_data(&self) -> Option<&usvg::Tree> {
513 match self.image_data()? {
514 ImageData::Svg(data) => Some(&data.tree),
515 _ => None,
516 }
517 }
518
519 pub fn text_input_data(&self) -> Option<&TextInputData> {
520 match &self.special_data {
521 SpecialElementData::TextInput(data) => Some(data),
522 _ => None,
523 }
524 }
525
526 pub fn text_input_data_mut(&mut self) -> Option<&mut TextInputData> {
527 match &mut self.special_data {
528 SpecialElementData::TextInput(data) => Some(data),
529 _ => None,
530 }
531 }
532
533 #[cfg(feature = "custom-widget")]
534 pub fn custom_widget_data(&self) -> Option<&CustomWidgetData> {
535 match &self.special_data {
536 SpecialElementData::CustomWidget(data) => Some(data),
537 _ => None,
538 }
539 }
540
541 #[cfg(feature = "custom-widget")]
542 pub fn custom_widget_data_mut(&mut self) -> Option<&mut CustomWidgetData> {
543 match &mut self.special_data {
544 SpecialElementData::CustomWidget(data) => Some(data),
545 _ => None,
546 }
547 }
548
549 pub fn checkbox_input_checked(&self) -> Option<bool> {
550 match self.special_data {
551 SpecialElementData::CheckboxInput(checked) => Some(checked),
552 _ => None,
553 }
554 }
555
556 pub fn checkbox_input_checked_mut(&mut self) -> Option<&mut bool> {
557 match self.special_data {
558 SpecialElementData::CheckboxInput(ref mut checked) => Some(checked),
559 _ => None,
560 }
561 }
562
563 pub fn set_checkbox_input_checked(&mut self, checked: bool) {
567 if let Some(is_checked) = self.checkbox_input_checked_mut() {
568 *is_checked = checked;
569 }
570 self.element_state.set(ElementState::CHECKED, checked);
571 }
572
573 #[cfg(feature = "file-input")]
574 pub fn file_data(&self) -> Option<&FileData> {
575 match &self.special_data {
576 SpecialElementData::FileInput(data) => Some(data),
577 _ => None,
578 }
579 }
580
581 #[cfg(feature = "file-input")]
582 pub fn file_data_mut(&mut self) -> Option<&mut FileData> {
583 match &mut self.special_data {
584 SpecialElementData::FileInput(data) => Some(data),
585 _ => None,
586 }
587 }
588
589 pub fn flush_is_focussable(&mut self) {
590 let disabled: bool = self.attr_parsed(local_name!("disabled")).unwrap_or(false);
591 let tabindex: Option<i32> = self.attr_parsed(local_name!("tabindex"));
592 let contains_sub_document: bool = self.sub_doc_data().is_some();
593
594 self.is_focussable = contains_sub_document
595 || (!disabled
596 && match tabindex {
597 Some(index) => index >= 0,
598 None => {
599 if [local_name!("a"), local_name!("area")].contains(&self.name.local) {
606 self.attr(local_name!("href")).is_some()
607 } else {
608 const DEFAULT_FOCUSSABLE_ELEMENTS: [LocalName; 7] = [
609 local_name!("button"),
610 local_name!("input"),
611 local_name!("select"),
612 local_name!("textarea"),
613 local_name!("frame"),
614 local_name!("iframe"),
615 local_name!("summary"),
616 ];
617 DEFAULT_FOCUSSABLE_ELEMENTS.contains(&self.name.local)
618 }
619 }
620 })
621 }
622
623 pub fn flush_style_attribute(&mut self, guard: &SharedRwLock, url_extra_data: &UrlExtraData) {
624 self.style_attribute = self.attr(local_name!("style")).map(|style_str| {
625 ServoArc::new(guard.wrap(parse_style_attribute(
626 style_str,
627 url_extra_data,
628 None,
629 QuirksMode::NoQuirks,
630 CssRuleType::Style,
631 )))
632 });
633 }
634
635 pub fn set_style_property(
636 &mut self,
637 name: &str,
638 value: &str,
639 guard: &SharedRwLock,
640 url_extra_data: UrlExtraData,
641 ) -> bool {
642 let context = ParserContext::new(
643 Origin::Author,
644 &url_extra_data,
645 Some(CssRuleType::Style),
646 ParsingMode::DEFAULT,
647 QuirksMode::NoQuirks,
648 Default::default(),
649 None,
650 None,
651 Default::default(),
652 );
653
654 let Ok(property_id) = PropertyId::parse(name, &context) else {
655 #[cfg(feature = "tracing")]
656 tracing::warn!(property = name, "Unsupported property");
657 return false;
658 };
659 let mut source_property_declaration = SourcePropertyDeclaration::default();
660 let mut input = ParserInput::new(value);
661 let mut parser = style::values::Parser::new(&mut input);
662 let Ok(_) = PropertyDeclaration::parse_into(
663 &mut source_property_declaration,
664 property_id,
665 &context,
666 &mut parser,
667 ) else {
668 #[cfg(feature = "tracing")]
669 tracing::warn!(property = name, value, "Invalid property value");
670 return false;
671 };
672
673 if self.style_attribute.is_none() {
674 self.style_attribute = Some(ServoArc::new(guard.wrap(PropertyDeclarationBlock::new())));
675 }
676 self.style_attribute
677 .as_mut()
678 .unwrap()
679 .write_with(&mut guard.write())
680 .extend(source_property_declaration.drain(), Importance::Normal);
681
682 true
683 }
684
685 pub fn remove_style_property(
686 &mut self,
687 name: &str,
688 guard: &SharedRwLock,
689 url_extra_data: UrlExtraData,
690 ) -> bool {
691 let context = ParserContext::new(
692 Origin::Author,
693 &url_extra_data,
694 Some(CssRuleType::Style),
695 ParsingMode::DEFAULT,
696 QuirksMode::NoQuirks,
697 Default::default(),
698 None,
699 None,
700 Default::default(),
701 );
702 let Ok(property_id) = PropertyId::parse(name, &context) else {
703 #[cfg(feature = "tracing")]
704 tracing::warn!(property = name, "Unsupported property");
705 return false;
706 };
707
708 if let Some(style) = &mut self.style_attribute {
709 let mut guard = guard.write();
710 let style = style.write_with(&mut guard);
711 if let Some(index) = style.first_declaration_to_remove(&property_id) {
712 style.remove_property(&property_id, index);
713 return true;
714 }
715 }
716
717 false
718 }
719
720 pub fn set_sub_document(&mut self, sub_document: Box<dyn Document>) {
721 self.special_data = SpecialElementData::SubDocument(sub_document);
722 }
723
724 pub fn remove_sub_document(&mut self) {
725 self.special_data = SpecialElementData::None;
726 }
727
728 #[cfg(feature = "custom-widget")]
729 pub fn set_custom_widget(&mut self, widget: Box<dyn crate::Widget>) {
730 use crate::node::custom_widget::CustomWidgetData;
731 self.special_data = SpecialElementData::CustomWidget(CustomWidgetData::new(widget));
732 }
733
734 #[cfg(feature = "custom-widget")]
735 pub fn remove_custom_widget(&mut self) -> Vec<anyrender::ResourceId> {
736 let resource_ids = self
737 .custom_widget_data_mut()
738 .map(|widget_data| widget_data.take_resource_ids())
739 .unwrap_or_default();
740 self.special_data = SpecialElementData::None;
741 resource_ids
742 }
743
744 pub fn take_inline_layout(&mut self) -> Option<Box<TextLayout>> {
745 std::mem::take(&mut self.inline_layout_data)
746 }
747
748 pub fn is_submit_button(&self) -> bool {
749 if self.name.local != local_name!("button") {
750 return false;
751 }
752 let type_attr = self.attr(local_name!("type"));
753 let is_submit = type_attr == Some("submit");
754 let is_auto_submit = type_attr.is_none()
755 && self.attr(LocalName::from("command")).is_none()
756 && self.attr(LocalName::from("commandfor")).is_none();
757 is_submit || is_auto_submit
758 }
759}
760
761#[derive(Debug, Clone, PartialEq)]
762pub struct RasterImageData {
763 pub width: u32,
765 pub height: u32,
767 pub data: Blob<u8>,
769}
770impl RasterImageData {
771 pub fn new(width: u32, height: u32, data: Arc<Vec<u8>>) -> Self {
772 Self {
773 width,
774 height,
775 data: Blob::new(data),
776 }
777 }
778}
779
780#[derive(Debug, Clone)]
781pub enum ImageData {
782 Raster(RasterImageData),
783 #[cfg(feature = "svg")]
784 Svg(SvgImageData),
785 None,
786}
787
788#[derive(Debug, Clone, PartialEq)]
789pub enum Status {
790 Ok,
791 Error,
792 Loading,
793}
794
795#[derive(Debug, Clone)]
796pub struct ImageResourceData {
797 pub url: ServoArc<Url>,
799 pub status: Status,
801 pub image: ImageData,
803}
804
805impl ImageResourceData {
806 pub fn new(url: ServoArc<Url>) -> Self {
807 Self {
808 url,
809 status: Status::Loading,
810 image: ImageData::None,
811 }
812 }
813}
814
815#[derive(Debug, Clone)]
816pub struct CanvasData {
817 pub custom_paint_source_id: u64,
818}
819
820impl std::fmt::Debug for SpecialElementData {
821 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
822 match self {
823 SpecialElementData::SubDocument(_) => f.write_str("NodeSpecificData::SubDocument"),
824 #[cfg(feature = "custom-widget")]
825 SpecialElementData::CustomWidget(_) => f.write_str("NodeSpecificData::CustomWidget"),
826 SpecialElementData::Stylesheet(_) => f.write_str("NodeSpecificData::Stylesheet"),
827 SpecialElementData::Image(data) => match **data {
828 ImageData::Raster(_) => f.write_str("NodeSpecificData::Image(Raster)"),
829 #[cfg(feature = "svg")]
830 ImageData::Svg(_) => f.write_str("NodeSpecificData::Image(Svg)"),
831 ImageData::None => f.write_str("NodeSpecificData::Image(None)"),
832 },
833 SpecialElementData::Canvas(_) => f.write_str("NodeSpecificData::Canvas"),
834 SpecialElementData::TableRoot(_) => f.write_str("NodeSpecificData::TableRoot"),
835 SpecialElementData::TextInput(_) => f.write_str("NodeSpecificData::TextInput"),
836 SpecialElementData::CheckboxInput(_) => f.write_str("NodeSpecificData::CheckboxInput"),
837 #[cfg(feature = "file-input")]
838 SpecialElementData::FileInput(_) => f.write_str("NodeSpecificData::FileInput"),
839 SpecialElementData::None => f.write_str("NodeSpecificData::None"),
840 }
841 }
842}
843
844#[derive(Clone)]
845pub struct ListItemLayout {
846 pub marker: Marker,
847 pub position: ListItemLayoutPosition,
848}
849
850#[derive(Debug, PartialEq, Clone)]
853pub enum Marker {
854 Char(char),
855 String(String),
856}
857
858#[derive(Clone)]
860pub enum ListItemLayoutPosition {
861 Inside,
862 Outside(Box<parley::Layout<TextBrush>>),
863}
864
865impl std::fmt::Debug for ListItemLayout {
866 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
867 write!(f, "ListItemLayout - marker {:?}", self.marker)
868 }
869}
870
871#[cfg(feature = "file-input")]
872mod file_data {
873 use std::ops::{Deref, DerefMut};
874 use std::path::PathBuf;
875
876 #[derive(Clone, Debug)]
877 pub struct FileData(pub Vec<PathBuf>);
878 impl Deref for FileData {
879 type Target = Vec<PathBuf>;
880
881 fn deref(&self) -> &Self::Target {
882 &self.0
883 }
884 }
885 impl DerefMut for FileData {
886 fn deref_mut(&mut self) -> &mut Self::Target {
887 &mut self.0
888 }
889 }
890 impl From<Vec<PathBuf>> for FileData {
891 fn from(files: Vec<PathBuf>) -> Self {
892 Self(files)
893 }
894 }
895}
896#[cfg(feature = "file-input")]
897pub use file_data::FileData;
898
899#[cfg(test)]
900mod tests {
901 use super::TextInputData;
902 use parley::{FontContext, LayoutContext};
903
904 fn make_input(is_multiline: bool, text: &str) -> TextInputData {
906 let mut font_ctx = FontContext::new();
907 let mut layout_ctx = LayoutContext::new();
908 let mut data = TextInputData::new(is_multiline);
909 data.editor.set_scale(1.0);
910 data.editor.set_text(text);
911 data.editor
912 .driver(&mut font_ctx, &mut layout_ctx)
913 .refresh_layout();
914 data
915 }
916
917 #[test]
918 fn short_text_does_not_scroll() {
919 let mut data = make_input(false, "hi");
920 data.clamp_scroll_offset(1000.0, 100.0);
922 assert_eq!(data.scroll_offset, 0.0);
923 }
924
925 #[test]
926 fn single_line_scrolls_to_follow_caret() {
927 let text = "the quick brown fox jumps over the lazy dog repeatedly and at length";
928 let mut data = make_input(false, text);
929 let content_box_width = 40.0;
930 let content_box_height = 20.0;
931
932 data.editor
934 .driver(&mut FontContext::new(), &mut LayoutContext::new())
935 .move_to_text_end();
936 data.clamp_scroll_offset(content_box_width, content_box_height);
937
938 let layout_width = data.editor.try_layout().unwrap().full_width();
939 if layout_width > content_box_width {
940 assert!(
941 data.scroll_offset > 0.0,
942 "expected horizontal scroll for overflowing single-line input"
943 );
944 let caret = data.editor.cursor_geometry(1.5).unwrap();
946 assert!(caret.x1 as f32 <= data.scroll_offset + content_box_width + 0.5);
947 assert!(caret.x0 as f32 >= data.scroll_offset - 0.5);
948 }
949
950 data.editor
952 .driver(&mut FontContext::new(), &mut LayoutContext::new())
953 .move_to_text_start();
954 data.clamp_scroll_offset(content_box_width, content_box_height);
955 assert_eq!(data.scroll_offset, 0.0);
956 }
957
958 #[test]
959 fn multiline_scrolls_vertically_not_horizontally() {
960 let text = (0..40)
961 .map(|i| format!("line {i}"))
962 .collect::<Vec<_>>()
963 .join("\n");
964 let mut data = make_input(true, &text);
965 data.editor.set_width(Some(200.0));
967 data.editor
968 .driver(&mut FontContext::new(), &mut LayoutContext::new())
969 .refresh_layout();
970
971 let content_box_width = 200.0;
972 let content_box_height = 30.0;
973
974 data.editor
975 .driver(&mut FontContext::new(), &mut LayoutContext::new())
976 .move_to_text_end();
977 data.clamp_scroll_offset(content_box_width, content_box_height);
978
979 let layout_height = data.editor.try_layout().unwrap().height();
980 if layout_height > content_box_height {
981 assert!(
982 data.scroll_offset > 0.0,
983 "expected vertical scroll for overflowing multi-line input"
984 );
985 }
986 }
987
988 #[test]
989 fn scroll_by_clamps_and_bubbles() {
990 let text = (0..40)
991 .map(|i| format!("line {i}"))
992 .collect::<Vec<_>>()
993 .join("\n");
994 let mut data = make_input(true, &text);
995 data.editor.set_width(Some(200.0));
996 data.editor
997 .driver(&mut FontContext::new(), &mut LayoutContext::new())
998 .refresh_layout();
999
1000 let content_box_width = 200.0;
1001 let content_box_height = 30.0;
1002 let max = data.max_scroll_offset(content_box_width, content_box_height);
1003 assert!(max > 0.0, "test text should overflow the content box");
1004
1005 assert_eq!(data.scroll_offset, 0.0);
1008 let bubbled = data.scroll_by(15.0, content_box_width, content_box_height);
1009 assert_eq!(data.scroll_offset, 0.0);
1010 assert_eq!(bubbled, 15.0);
1011
1012 let bubbled = data.scroll_by(-10.0, content_box_width, content_box_height);
1014 assert_eq!(data.scroll_offset, 10.0);
1015 assert_eq!(bubbled, 0.0);
1016
1017 let bubbled = data.scroll_by(-(max + 100.0), content_box_width, content_box_height);
1021 assert_eq!(data.scroll_offset, max);
1022 assert!((bubbled - (-110.0)).abs() < 1e-3);
1023 }
1024
1025 #[test]
1026 fn single_line_does_not_scroll_when_text_fits() {
1027 let mut data = make_input(false, "hi");
1028 let bubbled = data.scroll_by(-50.0, 1000.0, 100.0);
1030 assert_eq!(data.scroll_offset, 0.0);
1031 assert_eq!(bubbled, -50.0);
1032 }
1033}