1use cssparser::ParserInput;
2use linebender_resource_handle::Blob;
3use markup5ever::{LocalName, QualName, local_name};
4use selectors::matching::QuirksMode;
5use std::str::FromStr;
6use std::sync::Arc;
7use style::Atom;
8use style::parser::ParserContext;
9use style::properties::{Importance, PropertyDeclaration, PropertyId, SourcePropertyDeclaration};
10use style::stylesheets::{DocumentStyleSheet, Origin, UrlExtraData};
11use style::{
12 properties::{PropertyDeclarationBlock, parse_style_attribute},
13 servo_arc::Arc as ServoArc,
14 shared_lock::{Locked, SharedRwLock},
15 stylesheets::CssRuleType,
16};
17use style_traits::ParsingMode;
18use url::Url;
19
20use super::{Attribute, Attributes};
21use crate::Document;
22use crate::layout::table::TableContext;
23use crate::node::{TextBrush, TextInputData, TextLayout};
24
25#[cfg(feature = "custom-widget")]
26use super::custom_widget::CustomWidgetData;
27
28macro_rules! local_names {
29 ($($name:tt),+) => {
30 [$(local_name!($name),)+]
31 };
32}
33
34#[derive(Debug, Clone)]
35pub struct ElementData {
36 pub name: QualName,
38
39 pub id: Option<Atom>,
41
42 pub attrs: Attributes,
44
45 pub is_focussable: bool,
47
48 pub style_attribute: Option<ServoArc<Locked<PropertyDeclarationBlock>>>,
50
51 pub special_data: SpecialElementData,
57
58 pub background_images: Vec<Option<ImageResourceData>>,
59
60 pub mask_images: Vec<Option<ImageResourceData>>,
61
62 pub inline_layout_data: Option<Box<TextLayout>>,
64
65 pub list_item_data: Option<Box<ListItemLayout>>,
68
69 pub template_contents: Option<usize>,
71 }
74
75#[derive(Copy, Clone, Default)]
76#[non_exhaustive]
77pub enum SpecialElementType {
78 Stylesheet,
79 Image,
80 Canvas,
81 TableRoot,
82 TextInput,
83 CheckboxInput,
84 #[cfg(feature = "file-input")]
85 FileInput,
86 #[default]
87 None,
88}
89
90#[derive(Default)]
92pub enum SpecialElementData {
93 SubDocument(Box<dyn Document>),
95 #[cfg(feature = "custom-widget")]
97 CustomWidget(CustomWidgetData),
98 Stylesheet(DocumentStyleSheet),
100 Image(Box<ImageData>),
102 Canvas(CanvasData),
104 TableRoot(Arc<TableContext>),
106 TextInput(TextInputData),
108 CheckboxInput(bool),
110 #[cfg(feature = "file-input")]
112 FileInput(FileData),
113 #[default]
115 None,
116}
117
118impl Clone for SpecialElementData {
119 fn clone(&self) -> Self {
120 match self {
121 Self::SubDocument(_) => Self::None, #[cfg(feature = "custom-widget")]
123 Self::CustomWidget(_) => Self::None, Self::Stylesheet(data) => Self::Stylesheet(data.clone()),
125 Self::Image(data) => Self::Image(data.clone()),
126 Self::Canvas(data) => Self::Canvas(data.clone()),
127 Self::TableRoot(data) => Self::TableRoot(data.clone()),
128 Self::TextInput(data) => Self::TextInput(data.clone()),
129 Self::CheckboxInput(data) => Self::CheckboxInput(*data),
130 #[cfg(feature = "file-input")]
131 Self::FileInput(data) => Self::FileInput(data.clone()),
132 Self::None => Self::None,
133 }
134 }
135}
136
137impl SpecialElementData {
138 pub fn take(&mut self) -> Self {
139 std::mem::take(self)
140 }
141}
142
143impl ElementData {
144 pub fn new(name: QualName, attrs: Vec<Attribute>) -> Self {
145 let id_attr_atom = attrs
146 .iter()
147 .find(|attr| &attr.name.local == "id")
148 .map(|attr| attr.value.as_ref())
149 .map(|value: &str| Atom::from(value));
150
151 let mut data = ElementData {
152 name,
153 id: id_attr_atom,
154 attrs: Attributes::new(attrs),
155 is_focussable: false,
156 style_attribute: Default::default(),
157 inline_layout_data: None,
158 list_item_data: None,
159 special_data: SpecialElementData::None,
160 template_contents: None,
161 background_images: Vec::new(),
162 mask_images: Vec::new(),
163 };
164 data.flush_is_focussable();
165 data
166 }
167
168 pub fn attrs(&self) -> &[Attribute] {
169 &self.attrs
170 }
171
172 pub fn attr(&self, name: impl PartialEq<LocalName>) -> Option<&str> {
173 let attr = self.attrs.iter().find(|attr| name == attr.name.local)?;
174 Some(&attr.value)
175 }
176
177 pub fn attr_parsed<T: FromStr>(&self, name: impl PartialEq<LocalName>) -> Option<T> {
178 let attr = self.attrs.iter().find(|attr| name == attr.name.local)?;
179 attr.value.parse::<T>().ok()
180 }
181
182 pub fn has_attr(&self, name: impl PartialEq<LocalName>) -> bool {
184 self.attrs.iter().any(|attr| name == attr.name.local)
185 }
186
187 pub fn can_be_disabled(&self) -> bool {
188 local_names!("button", "input", "select", "textarea").contains(&self.name.local)
189 }
190
191 pub fn image_data(&self) -> Option<&ImageData> {
192 match &self.special_data {
193 SpecialElementData::Image(data) => Some(&**data),
194 _ => None,
195 }
196 }
197
198 pub fn image_data_mut(&mut self) -> Option<&mut ImageData> {
199 match self.special_data {
200 SpecialElementData::Image(ref mut data) => Some(&mut **data),
201 _ => None,
202 }
203 }
204
205 pub fn raster_image_data(&self) -> Option<&RasterImageData> {
206 match self.image_data()? {
207 ImageData::Raster(data) => Some(data),
208 _ => None,
209 }
210 }
211
212 pub fn raster_image_data_mut(&mut self) -> Option<&mut RasterImageData> {
213 match self.image_data_mut()? {
214 ImageData::Raster(data) => Some(data),
215 _ => None,
216 }
217 }
218
219 pub fn canvas_data(&self) -> Option<&CanvasData> {
220 match &self.special_data {
221 SpecialElementData::Canvas(data) => Some(data),
222 _ => None,
223 }
224 }
225
226 pub fn sub_doc_data(&self) -> Option<&dyn Document> {
227 match &self.special_data {
228 SpecialElementData::SubDocument(data) => Some(data.as_ref()),
229 _ => None,
230 }
231 }
232
233 pub fn sub_doc_data_mut(&mut self) -> Option<&mut dyn Document> {
234 match &mut self.special_data {
235 SpecialElementData::SubDocument(data) => Some(data.as_mut()),
236 _ => None,
237 }
238 }
239
240 #[cfg(feature = "svg")]
241 pub fn svg_data(&self) -> Option<&usvg::Tree> {
242 match self.image_data()? {
243 ImageData::Svg(data) => Some(&data.tree),
244 _ => None,
245 }
246 }
247
248 pub fn text_input_data(&self) -> Option<&TextInputData> {
249 match &self.special_data {
250 SpecialElementData::TextInput(data) => Some(data),
251 _ => None,
252 }
253 }
254
255 pub fn text_input_data_mut(&mut self) -> Option<&mut TextInputData> {
256 match &mut self.special_data {
257 SpecialElementData::TextInput(data) => Some(data),
258 _ => None,
259 }
260 }
261
262 #[cfg(feature = "custom-widget")]
263 pub fn custom_widget_data(&self) -> Option<&CustomWidgetData> {
264 match &self.special_data {
265 SpecialElementData::CustomWidget(data) => Some(data),
266 _ => None,
267 }
268 }
269
270 #[cfg(feature = "custom-widget")]
271 pub fn custom_widget_data_mut(&mut self) -> Option<&mut CustomWidgetData> {
272 match &mut self.special_data {
273 SpecialElementData::CustomWidget(data) => Some(data),
274 _ => None,
275 }
276 }
277
278 pub fn checkbox_input_checked(&self) -> Option<bool> {
279 match self.special_data {
280 SpecialElementData::CheckboxInput(checked) => Some(checked),
281 _ => None,
282 }
283 }
284
285 pub fn checkbox_input_checked_mut(&mut self) -> Option<&mut bool> {
286 match self.special_data {
287 SpecialElementData::CheckboxInput(ref mut checked) => Some(checked),
288 _ => None,
289 }
290 }
291
292 #[cfg(feature = "file-input")]
293 pub fn file_data(&self) -> Option<&FileData> {
294 match &self.special_data {
295 SpecialElementData::FileInput(data) => Some(data),
296 _ => None,
297 }
298 }
299
300 #[cfg(feature = "file-input")]
301 pub fn file_data_mut(&mut self) -> Option<&mut FileData> {
302 match &mut self.special_data {
303 SpecialElementData::FileInput(data) => Some(data),
304 _ => None,
305 }
306 }
307
308 pub fn flush_is_focussable(&mut self) {
309 let disabled: bool = self.attr_parsed(local_name!("disabled")).unwrap_or(false);
310 let tabindex: Option<i32> = self.attr_parsed(local_name!("tabindex"));
311 let contains_sub_document: bool = self.sub_doc_data().is_some();
312
313 self.is_focussable = contains_sub_document
314 || (!disabled
315 && match tabindex {
316 Some(index) => index >= 0,
317 None => {
318 if [local_name!("a"), local_name!("area")].contains(&self.name.local) {
325 self.attr(local_name!("href")).is_some()
326 } else {
327 const DEFAULT_FOCUSSABLE_ELEMENTS: [LocalName; 7] = [
328 local_name!("button"),
329 local_name!("input"),
330 local_name!("select"),
331 local_name!("textarea"),
332 local_name!("frame"),
333 local_name!("iframe"),
334 local_name!("summary"),
335 ];
336 DEFAULT_FOCUSSABLE_ELEMENTS.contains(&self.name.local)
337 }
338 }
339 })
340 }
341
342 pub fn flush_style_attribute(&mut self, guard: &SharedRwLock, url_extra_data: &UrlExtraData) {
343 self.style_attribute = self.attr(local_name!("style")).map(|style_str| {
344 ServoArc::new(guard.wrap(parse_style_attribute(
345 style_str,
346 url_extra_data,
347 None,
348 QuirksMode::NoQuirks,
349 CssRuleType::Style,
350 )))
351 });
352 }
353
354 pub fn set_style_property(
355 &mut self,
356 name: &str,
357 value: &str,
358 guard: &SharedRwLock,
359 url_extra_data: UrlExtraData,
360 ) -> bool {
361 let context = ParserContext::new(
362 Origin::Author,
363 &url_extra_data,
364 Some(CssRuleType::Style),
365 ParsingMode::DEFAULT,
366 QuirksMode::NoQuirks,
367 Default::default(),
368 None,
369 None,
370 Default::default(),
371 );
372
373 let Ok(property_id) = PropertyId::parse(name, &context) else {
374 #[cfg(feature = "tracing")]
375 tracing::warn!(property = name, "Unsupported property");
376 return false;
377 };
378 let mut source_property_declaration = SourcePropertyDeclaration::default();
379 let mut input = ParserInput::new(value);
380 let mut parser = style::values::Parser::new(&mut input);
381 let Ok(_) = PropertyDeclaration::parse_into(
382 &mut source_property_declaration,
383 property_id,
384 &context,
385 &mut parser,
386 ) else {
387 #[cfg(feature = "tracing")]
388 tracing::warn!(property = name, value, "Invalid property value");
389 return false;
390 };
391
392 if self.style_attribute.is_none() {
393 self.style_attribute = Some(ServoArc::new(guard.wrap(PropertyDeclarationBlock::new())));
394 }
395 self.style_attribute
396 .as_mut()
397 .unwrap()
398 .write_with(&mut guard.write())
399 .extend(source_property_declaration.drain(), Importance::Normal);
400
401 true
402 }
403
404 pub fn remove_style_property(
405 &mut self,
406 name: &str,
407 guard: &SharedRwLock,
408 url_extra_data: UrlExtraData,
409 ) -> bool {
410 let context = ParserContext::new(
411 Origin::Author,
412 &url_extra_data,
413 Some(CssRuleType::Style),
414 ParsingMode::DEFAULT,
415 QuirksMode::NoQuirks,
416 Default::default(),
417 None,
418 None,
419 Default::default(),
420 );
421 let Ok(property_id) = PropertyId::parse(name, &context) else {
422 #[cfg(feature = "tracing")]
423 tracing::warn!(property = name, "Unsupported property");
424 return false;
425 };
426
427 if let Some(style) = &mut self.style_attribute {
428 let mut guard = guard.write();
429 let style = style.write_with(&mut guard);
430 if let Some(index) = style.first_declaration_to_remove(&property_id) {
431 style.remove_property(&property_id, index);
432 return true;
433 }
434 }
435
436 false
437 }
438
439 pub fn set_sub_document(&mut self, sub_document: Box<dyn Document>) {
440 self.special_data = SpecialElementData::SubDocument(sub_document);
441 }
442
443 pub fn remove_sub_document(&mut self) {
444 self.special_data = SpecialElementData::None;
445 }
446
447 #[cfg(feature = "custom-widget")]
448 pub fn set_custom_widget(&mut self, widget: Box<dyn crate::Widget>) {
449 use crate::node::custom_widget::CustomWidgetData;
450 self.special_data = SpecialElementData::CustomWidget(CustomWidgetData::new(widget));
451 }
452
453 #[cfg(feature = "custom-widget")]
454 pub fn remove_custom_widget(&mut self) -> Vec<anyrender::ResourceId> {
455 let resource_ids = self
456 .custom_widget_data_mut()
457 .map(|widget_data| widget_data.take_resource_ids())
458 .unwrap_or_default();
459 self.special_data = SpecialElementData::None;
460 resource_ids
461 }
462
463 pub fn take_inline_layout(&mut self) -> Option<Box<TextLayout>> {
464 std::mem::take(&mut self.inline_layout_data)
465 }
466
467 pub fn is_submit_button(&self) -> bool {
468 if self.name.local != local_name!("button") {
469 return false;
470 }
471 let type_attr = self.attr(local_name!("type"));
472 let is_submit = type_attr == Some("submit");
473 let is_auto_submit = type_attr.is_none()
474 && self.attr(LocalName::from("command")).is_none()
475 && self.attr(LocalName::from("commandfor")).is_none();
476 is_submit || is_auto_submit
477 }
478}
479
480#[derive(Debug, Clone, PartialEq)]
481pub struct RasterImageData {
482 pub width: u32,
484 pub height: u32,
486 pub data: Blob<u8>,
488}
489impl RasterImageData {
490 pub fn new(width: u32, height: u32, data: Arc<Vec<u8>>) -> Self {
491 Self {
492 width,
493 height,
494 data: Blob::new(data),
495 }
496 }
497}
498
499#[cfg(feature = "svg")]
508#[derive(Debug, Clone)]
509pub struct SvgImageData {
510 pub tree: Arc<usvg::Tree>,
512 pub intrinsic_width: Option<f32>,
515 pub intrinsic_height: Option<f32>,
518}
519
520#[cfg(feature = "svg")]
521impl SvgImageData {
522 pub fn aspect_ratio(&self) -> f32 {
525 let size = self.tree.size();
526 size.width() / size.height()
527 }
528}
529
530#[derive(Debug, Clone)]
531pub enum ImageData {
532 Raster(RasterImageData),
533 #[cfg(feature = "svg")]
534 Svg(SvgImageData),
535 None,
536}
537
538#[derive(Debug, Clone, PartialEq)]
539pub enum Status {
540 Ok,
541 Error,
542 Loading,
543}
544
545#[derive(Debug, Clone)]
546pub struct ImageResourceData {
547 pub url: ServoArc<Url>,
549 pub status: Status,
551 pub image: ImageData,
553}
554
555impl ImageResourceData {
556 pub fn new(url: ServoArc<Url>) -> Self {
557 Self {
558 url,
559 status: Status::Loading,
560 image: ImageData::None,
561 }
562 }
563}
564
565#[derive(Debug, Clone)]
566pub struct CanvasData {
567 pub custom_paint_source_id: u64,
568}
569
570impl std::fmt::Debug for SpecialElementData {
571 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
572 match self {
573 SpecialElementData::SubDocument(_) => f.write_str("NodeSpecificData::SubDocument"),
574 #[cfg(feature = "custom-widget")]
575 SpecialElementData::CustomWidget(_) => f.write_str("NodeSpecificData::CustomWidget"),
576 SpecialElementData::Stylesheet(_) => f.write_str("NodeSpecificData::Stylesheet"),
577 SpecialElementData::Image(data) => match **data {
578 ImageData::Raster(_) => f.write_str("NodeSpecificData::Image(Raster)"),
579 #[cfg(feature = "svg")]
580 ImageData::Svg(_) => f.write_str("NodeSpecificData::Image(Svg)"),
581 ImageData::None => f.write_str("NodeSpecificData::Image(None)"),
582 },
583 SpecialElementData::Canvas(_) => f.write_str("NodeSpecificData::Canvas"),
584 SpecialElementData::TableRoot(_) => f.write_str("NodeSpecificData::TableRoot"),
585 SpecialElementData::TextInput(_) => f.write_str("NodeSpecificData::TextInput"),
586 SpecialElementData::CheckboxInput(_) => f.write_str("NodeSpecificData::CheckboxInput"),
587 #[cfg(feature = "file-input")]
588 SpecialElementData::FileInput(_) => f.write_str("NodeSpecificData::FileInput"),
589 SpecialElementData::None => f.write_str("NodeSpecificData::None"),
590 }
591 }
592}
593
594#[derive(Clone)]
595pub struct ListItemLayout {
596 pub marker: Marker,
597 pub position: ListItemLayoutPosition,
598}
599
600#[derive(Debug, PartialEq, Clone)]
603pub enum Marker {
604 Char(char),
605 String(String),
606}
607
608#[derive(Clone)]
610pub enum ListItemLayoutPosition {
611 Inside,
612 Outside(Box<parley::Layout<TextBrush>>),
613}
614
615impl std::fmt::Debug for ListItemLayout {
616 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
617 write!(f, "ListItemLayout - marker {:?}", self.marker)
618 }
619}
620
621#[cfg(feature = "file-input")]
622mod file_data {
623 use std::ops::{Deref, DerefMut};
624 use std::path::PathBuf;
625
626 #[derive(Clone, Debug)]
627 pub struct FileData(pub Vec<PathBuf>);
628 impl Deref for FileData {
629 type Target = Vec<PathBuf>;
630
631 fn deref(&self) -> &Self::Target {
632 &self.0
633 }
634 }
635 impl DerefMut for FileData {
636 fn deref_mut(&mut self) -> &mut Self::Target {
637 &mut self.0
638 }
639 }
640 impl From<Vec<PathBuf>> for FileData {
641 fn from(files: Vec<PathBuf>) -> Self {
642 Self(files)
643 }
644 }
645}
646#[cfg(feature = "file-input")]
647pub use file_data::FileData;
648
649#[cfg(test)]
650mod tests {
651 use super::TextInputData;
652 use parley::{FontContext, LayoutContext};
653
654 fn make_input(is_multiline: bool, text: &str) -> TextInputData {
656 let mut font_ctx = FontContext::new();
657 let mut layout_ctx = LayoutContext::new();
658 let mut data = TextInputData::new(is_multiline);
659 data.editor.set_scale(1.0);
660 data.editor.set_text(text);
661 data.editor
662 .driver(&mut font_ctx, &mut layout_ctx)
663 .refresh_layout();
664 data
665 }
666
667 #[test]
668 fn short_text_does_not_scroll() {
669 let mut data = make_input(false, "hi");
670 data.clamp_scroll_offset(1000.0, 100.0);
672 assert_eq!(data.scroll_offset, 0.0);
673 }
674
675 #[test]
676 fn single_line_scrolls_to_follow_caret() {
677 let text = "the quick brown fox jumps over the lazy dog repeatedly and at length";
678 let mut data = make_input(false, text);
679 let content_box_width = 40.0;
680 let content_box_height = 20.0;
681
682 data.editor
684 .driver(&mut FontContext::new(), &mut LayoutContext::new())
685 .move_to_text_end();
686 data.clamp_scroll_offset(content_box_width, content_box_height);
687
688 let layout_width = data.editor.try_layout().unwrap().full_width();
689 if layout_width > content_box_width {
690 assert!(
691 data.scroll_offset > 0.0,
692 "expected horizontal scroll for overflowing single-line input"
693 );
694 let caret = data.editor.cursor_geometry(1.5).unwrap();
696 assert!(caret.x1 as f32 <= data.scroll_offset + content_box_width + 0.5);
697 assert!(caret.x0 as f32 >= data.scroll_offset - 0.5);
698 }
699
700 data.editor
702 .driver(&mut FontContext::new(), &mut LayoutContext::new())
703 .move_to_text_start();
704 data.clamp_scroll_offset(content_box_width, content_box_height);
705 assert_eq!(data.scroll_offset, 0.0);
706 }
707
708 #[test]
709 fn multiline_scrolls_vertically_not_horizontally() {
710 let text = (0..40)
711 .map(|i| format!("line {i}"))
712 .collect::<Vec<_>>()
713 .join("\n");
714 let mut data = make_input(true, &text);
715 data.editor.set_width(Some(200.0));
717 data.editor
718 .driver(&mut FontContext::new(), &mut LayoutContext::new())
719 .refresh_layout();
720
721 let content_box_width = 200.0;
722 let content_box_height = 30.0;
723
724 data.editor
725 .driver(&mut FontContext::new(), &mut LayoutContext::new())
726 .move_to_text_end();
727 data.clamp_scroll_offset(content_box_width, content_box_height);
728
729 let layout_height = data.editor.try_layout().unwrap().height();
730 if layout_height > content_box_height {
731 assert!(
732 data.scroll_offset > 0.0,
733 "expected vertical scroll for overflowing multi-line input"
734 );
735 }
736 }
737
738 #[test]
739 fn scroll_by_clamps_and_bubbles() {
740 let text = (0..40)
741 .map(|i| format!("line {i}"))
742 .collect::<Vec<_>>()
743 .join("\n");
744 let mut data = make_input(true, &text);
745 data.editor.set_width(Some(200.0));
746 data.editor
747 .driver(&mut FontContext::new(), &mut LayoutContext::new())
748 .refresh_layout();
749
750 let content_box_width = 200.0;
751 let content_box_height = 30.0;
752 let max = data.max_scroll_offset(content_box_width, content_box_height);
753 assert!(max > 0.0, "test text should overflow the content box");
754
755 assert_eq!(data.scroll_offset, 0.0);
758 let bubbled = data.scroll_by(15.0, content_box_width, content_box_height);
759 assert_eq!(data.scroll_offset, 0.0);
760 assert_eq!(bubbled, 15.0);
761
762 let bubbled = data.scroll_by(-10.0, content_box_width, content_box_height);
764 assert_eq!(data.scroll_offset, 10.0);
765 assert_eq!(bubbled, 0.0);
766
767 let bubbled = data.scroll_by(-(max + 100.0), content_box_width, content_box_height);
771 assert_eq!(data.scroll_offset, max);
772 assert!((bubbled - (-110.0)).abs() < 1e-3);
773 }
774
775 #[test]
776 fn single_line_does_not_scroll_when_text_fits() {
777 let mut data = make_input(false, "hi");
778 let bubbled = data.scroll_by(-50.0, 1000.0, 100.0);
780 assert_eq!(data.scroll_offset, 0.0);
781 assert_eq!(bubbled, -50.0);
782 }
783}