Skip to main content

blitz_dom/node/
element.rs

1use cssparser::ParserInput;
2use linebender_resource_handle::Blob;
3use markup5ever::{LocalName, QualName, local_name};
4use parley::{ContentWidths, FontContext, LayoutContext};
5use selectors::matching::QuirksMode;
6use std::str::FromStr;
7use std::sync::Arc;
8use style::Atom;
9use style::parser::ParserContext;
10use style::properties::{Importance, PropertyDeclaration, PropertyId, SourcePropertyDeclaration};
11use style::stylesheets::{DocumentStyleSheet, Origin, UrlExtraData};
12use style::{
13    properties::{PropertyDeclarationBlock, parse_style_attribute},
14    servo_arc::Arc as ServoArc,
15    shared_lock::{Locked, SharedRwLock},
16    stylesheets::CssRuleType,
17};
18use style_traits::ParsingMode;
19use url::Url;
20
21use super::{Attribute, Attributes};
22use crate::Document;
23use crate::layout::table::TableContext;
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    /// The elements tag name, namespace and prefix
37    pub name: QualName,
38
39    /// The elements id attribute parsed as an atom (if it has one)
40    pub id: Option<Atom>,
41
42    /// The element's attributes
43    pub attrs: Attributes,
44
45    /// Whether the element is focussable
46    pub is_focussable: bool,
47
48    /// The element's parsed style attribute (used by stylo)
49    pub style_attribute: Option<ServoArc<Locked<PropertyDeclarationBlock>>>,
50
51    /// Heterogeneous data that depends on the element's type.
52    /// For example:
53    ///   - The image data for \<img\> elements.
54    ///   - The parley Layout for inline roots.
55    ///   - The text editor for input/textarea elements
56    pub special_data: SpecialElementData,
57
58    pub background_images: Vec<Option<BackgroundImageData>>,
59
60    /// Parley text layout (elements with inline inner display mode only)
61    pub inline_layout_data: Option<Box<TextLayout>>,
62
63    /// Data associated with display: list-item. Note that this display mode
64    /// does not exclude inline_layout_data
65    pub list_item_data: Option<Box<ListItemLayout>>,
66
67    /// The element's template contents (\<template\> elements only)
68    pub template_contents: Option<usize>,
69    // /// Whether the node is a [HTML integration point] (https://html.spec.whatwg.org/multipage/#html-integration-point)
70    // pub mathml_annotation_xml_integration_point: bool,
71}
72
73#[derive(Copy, Clone, Default)]
74#[non_exhaustive]
75pub enum SpecialElementType {
76    Stylesheet,
77    Image,
78    Canvas,
79    TableRoot,
80    TextInput,
81    CheckboxInput,
82    #[cfg(feature = "file_input")]
83    FileInput,
84    #[default]
85    None,
86}
87
88/// Heterogeneous data that depends on the element's type.
89#[derive(Default)]
90pub enum SpecialElementData {
91    /// A sub-document such an \<iframe\> or \<web-view\> element
92    SubDocument(Box<dyn Document>),
93    /// A custom widget
94    #[cfg(feature = "custom-widget")]
95    CustomWidget(CustomWidgetData),
96    /// A stylesheet
97    Stylesheet(DocumentStyleSheet),
98    /// An \<img\> element's image data
99    Image(Box<ImageData>),
100    /// A \<canvas\> element's custom paint source
101    Canvas(CanvasData),
102    /// Pre-computed table layout data
103    TableRoot(Arc<TableContext>),
104    /// Parley text editor (text inputs)
105    TextInput(TextInputData),
106    /// Checkbox checked state
107    CheckboxInput(bool),
108    /// Selected files
109    #[cfg(feature = "file_input")]
110    FileInput(FileData),
111    /// No data (for nodes that don't need any node-specific data)
112    #[default]
113    None,
114}
115
116impl Clone for SpecialElementData {
117    fn clone(&self) -> Self {
118        match self {
119            Self::SubDocument(_) => Self::None, // TODO
120            #[cfg(feature = "custom-widget")]
121            Self::CustomWidget(_) => Self::None, // TODO
122            Self::Stylesheet(data) => Self::Stylesheet(data.clone()),
123            Self::Image(data) => Self::Image(data.clone()),
124            Self::Canvas(data) => Self::Canvas(data.clone()),
125            Self::TableRoot(data) => Self::TableRoot(data.clone()),
126            Self::TextInput(data) => Self::TextInput(data.clone()),
127            Self::CheckboxInput(data) => Self::CheckboxInput(*data),
128            #[cfg(feature = "file_input")]
129            Self::FileInput(data) => Self::FileInput(data.clone()),
130            Self::None => Self::None,
131        }
132    }
133}
134
135impl SpecialElementData {
136    pub fn take(&mut self) -> Self {
137        std::mem::take(self)
138    }
139}
140
141impl ElementData {
142    pub fn new(name: QualName, attrs: Vec<Attribute>) -> Self {
143        let id_attr_atom = attrs
144            .iter()
145            .find(|attr| &attr.name.local == "id")
146            .map(|attr| attr.value.as_ref())
147            .map(|value: &str| Atom::from(value));
148
149        let mut data = ElementData {
150            name,
151            id: id_attr_atom,
152            attrs: Attributes::new(attrs),
153            is_focussable: false,
154            style_attribute: Default::default(),
155            inline_layout_data: None,
156            list_item_data: None,
157            special_data: SpecialElementData::None,
158            template_contents: None,
159            background_images: Vec::new(),
160        };
161        data.flush_is_focussable();
162        data
163    }
164
165    pub fn attrs(&self) -> &[Attribute] {
166        &self.attrs
167    }
168
169    pub fn attr(&self, name: impl PartialEq<LocalName>) -> Option<&str> {
170        let attr = self.attrs.iter().find(|attr| name == attr.name.local)?;
171        Some(&attr.value)
172    }
173
174    pub fn attr_parsed<T: FromStr>(&self, name: impl PartialEq<LocalName>) -> Option<T> {
175        let attr = self.attrs.iter().find(|attr| name == attr.name.local)?;
176        attr.value.parse::<T>().ok()
177    }
178
179    /// Detects the presence of the attribute, treating *any* value as truthy.
180    pub fn has_attr(&self, name: impl PartialEq<LocalName>) -> bool {
181        self.attrs.iter().any(|attr| name == attr.name.local)
182    }
183
184    pub fn can_be_disabled(&self) -> bool {
185        local_names!("button", "input", "select", "textarea").contains(&self.name.local)
186    }
187
188    pub fn image_data(&self) -> Option<&ImageData> {
189        match &self.special_data {
190            SpecialElementData::Image(data) => Some(&**data),
191            _ => None,
192        }
193    }
194
195    pub fn image_data_mut(&mut self) -> Option<&mut ImageData> {
196        match self.special_data {
197            SpecialElementData::Image(ref mut data) => Some(&mut **data),
198            _ => None,
199        }
200    }
201
202    pub fn raster_image_data(&self) -> Option<&RasterImageData> {
203        match self.image_data()? {
204            ImageData::Raster(data) => Some(data),
205            _ => None,
206        }
207    }
208
209    pub fn raster_image_data_mut(&mut self) -> Option<&mut RasterImageData> {
210        match self.image_data_mut()? {
211            ImageData::Raster(data) => Some(data),
212            _ => None,
213        }
214    }
215
216    pub fn canvas_data(&self) -> Option<&CanvasData> {
217        match &self.special_data {
218            SpecialElementData::Canvas(data) => Some(data),
219            _ => None,
220        }
221    }
222
223    pub fn sub_doc_data(&self) -> Option<&dyn Document> {
224        match &self.special_data {
225            SpecialElementData::SubDocument(data) => Some(data.as_ref()),
226            _ => None,
227        }
228    }
229
230    pub fn sub_doc_data_mut(&mut self) -> Option<&mut dyn Document> {
231        match &mut self.special_data {
232            SpecialElementData::SubDocument(data) => Some(data.as_mut()),
233            _ => None,
234        }
235    }
236
237    #[cfg(feature = "svg")]
238    pub fn svg_data(&self) -> Option<&usvg::Tree> {
239        match self.image_data()? {
240            ImageData::Svg(data) => Some(data),
241            _ => None,
242        }
243    }
244
245    pub fn text_input_data(&self) -> Option<&TextInputData> {
246        match &self.special_data {
247            SpecialElementData::TextInput(data) => Some(data),
248            _ => None,
249        }
250    }
251
252    pub fn text_input_data_mut(&mut self) -> Option<&mut TextInputData> {
253        match &mut self.special_data {
254            SpecialElementData::TextInput(data) => Some(data),
255            _ => None,
256        }
257    }
258
259    #[cfg(feature = "custom-widget")]
260    pub fn custom_widget_data(&self) -> Option<&CustomWidgetData> {
261        match &self.special_data {
262            SpecialElementData::CustomWidget(data) => Some(data),
263            _ => None,
264        }
265    }
266
267    #[cfg(feature = "custom-widget")]
268    pub fn custom_widget_data_mut(&mut self) -> Option<&mut CustomWidgetData> {
269        match &mut self.special_data {
270            SpecialElementData::CustomWidget(data) => Some(data),
271            _ => None,
272        }
273    }
274
275    pub fn checkbox_input_checked(&self) -> Option<bool> {
276        match self.special_data {
277            SpecialElementData::CheckboxInput(checked) => Some(checked),
278            _ => None,
279        }
280    }
281
282    pub fn checkbox_input_checked_mut(&mut self) -> Option<&mut bool> {
283        match self.special_data {
284            SpecialElementData::CheckboxInput(ref mut checked) => Some(checked),
285            _ => None,
286        }
287    }
288
289    #[cfg(feature = "file_input")]
290    pub fn file_data(&self) -> Option<&FileData> {
291        match &self.special_data {
292            SpecialElementData::FileInput(data) => Some(data),
293            _ => None,
294        }
295    }
296
297    #[cfg(feature = "file_input")]
298    pub fn file_data_mut(&mut self) -> Option<&mut FileData> {
299        match &mut self.special_data {
300            SpecialElementData::FileInput(data) => Some(data),
301            _ => None,
302        }
303    }
304
305    pub fn flush_is_focussable(&mut self) {
306        let disabled: bool = self.attr_parsed(local_name!("disabled")).unwrap_or(false);
307        let tabindex: Option<i32> = self.attr_parsed(local_name!("tabindex"));
308        let contains_sub_document: bool = self.sub_doc_data().is_some();
309
310        self.is_focussable = contains_sub_document
311            || (!disabled
312                && match tabindex {
313                    Some(index) => index >= 0,
314                    None => {
315                        // Some focusable HTML elements have a default tabindex value of 0 set under the hood by the user agent.
316                        // These elements are:
317                        //   - <a> or <area> with href attribute
318                        //   - <button>, <frame>, <iframe>, <input>, <object>, <select>, <textarea>, and SVG <a> element
319                        //   - <summary> element that provides summary for a <details> element.
320
321                        if [local_name!("a"), local_name!("area")].contains(&self.name.local) {
322                            self.attr(local_name!("href")).is_some()
323                        } else {
324                            const DEFAULT_FOCUSSABLE_ELEMENTS: [LocalName; 6] = [
325                                local_name!("button"),
326                                local_name!("input"),
327                                local_name!("select"),
328                                local_name!("textarea"),
329                                local_name!("frame"),
330                                local_name!("iframe"),
331                            ];
332                            DEFAULT_FOCUSSABLE_ELEMENTS.contains(&self.name.local)
333                        }
334                    }
335                })
336    }
337
338    pub fn flush_style_attribute(&mut self, guard: &SharedRwLock, url_extra_data: &UrlExtraData) {
339        self.style_attribute = self.attr(local_name!("style")).map(|style_str| {
340            ServoArc::new(guard.wrap(parse_style_attribute(
341                style_str,
342                url_extra_data,
343                None,
344                QuirksMode::NoQuirks,
345                CssRuleType::Style,
346            )))
347        });
348    }
349
350    pub fn set_style_property(
351        &mut self,
352        name: &str,
353        value: &str,
354        guard: &SharedRwLock,
355        url_extra_data: UrlExtraData,
356    ) -> bool {
357        let context = ParserContext::new(
358            Origin::Author,
359            &url_extra_data,
360            Some(CssRuleType::Style),
361            ParsingMode::DEFAULT,
362            QuirksMode::NoQuirks,
363            /* namespaces = */ Default::default(),
364            None,
365            None,
366            /* attr_taint = */ Default::default(),
367        );
368
369        let Ok(property_id) = PropertyId::parse(name, &context) else {
370            #[cfg(feature = "tracing")]
371            tracing::warn!(property = name, "Unsupported property");
372            return false;
373        };
374        let mut source_property_declaration = SourcePropertyDeclaration::default();
375        let mut input = ParserInput::new(value);
376        let mut parser = style::values::Parser::new(&mut input);
377        let Ok(_) = PropertyDeclaration::parse_into(
378            &mut source_property_declaration,
379            property_id,
380            &context,
381            &mut parser,
382        ) else {
383            #[cfg(feature = "tracing")]
384            tracing::warn!(property = name, value, "Invalid property value");
385            return false;
386        };
387
388        if self.style_attribute.is_none() {
389            self.style_attribute = Some(ServoArc::new(guard.wrap(PropertyDeclarationBlock::new())));
390        }
391        self.style_attribute
392            .as_mut()
393            .unwrap()
394            .write_with(&mut guard.write())
395            .extend(source_property_declaration.drain(), Importance::Normal);
396
397        true
398    }
399
400    pub fn remove_style_property(
401        &mut self,
402        name: &str,
403        guard: &SharedRwLock,
404        url_extra_data: UrlExtraData,
405    ) -> bool {
406        let context = ParserContext::new(
407            Origin::Author,
408            &url_extra_data,
409            Some(CssRuleType::Style),
410            ParsingMode::DEFAULT,
411            QuirksMode::NoQuirks,
412            /* namespaces = */ Default::default(),
413            None,
414            None,
415            /* attr_taint = */ Default::default(),
416        );
417        let Ok(property_id) = PropertyId::parse(name, &context) else {
418            #[cfg(feature = "tracing")]
419            tracing::warn!(property = name, "Unsupported property");
420            return false;
421        };
422
423        if let Some(style) = &mut self.style_attribute {
424            let mut guard = guard.write();
425            let style = style.write_with(&mut guard);
426            if let Some(index) = style.first_declaration_to_remove(&property_id) {
427                style.remove_property(&property_id, index);
428                return true;
429            }
430        }
431
432        false
433    }
434
435    pub fn set_sub_document(&mut self, sub_document: Box<dyn Document>) {
436        self.special_data = SpecialElementData::SubDocument(sub_document);
437    }
438
439    pub fn remove_sub_document(&mut self) {
440        self.special_data = SpecialElementData::None;
441    }
442
443    #[cfg(feature = "custom-widget")]
444    pub fn set_custom_widget(&mut self, widget: Box<dyn crate::Widget>) {
445        use crate::node::custom_widget::CustomWidgetData;
446        self.special_data = SpecialElementData::CustomWidget(CustomWidgetData::new(widget));
447    }
448
449    #[cfg(feature = "custom-widget")]
450    pub fn remove_custom_widget(&mut self) -> Vec<anyrender::ResourceId> {
451        let resource_ids = self
452            .custom_widget_data_mut()
453            .map(|widget_data| widget_data.take_resource_ids())
454            .unwrap_or_default();
455        self.special_data = SpecialElementData::None;
456        resource_ids
457    }
458
459    pub fn take_inline_layout(&mut self) -> Option<Box<TextLayout>> {
460        std::mem::take(&mut self.inline_layout_data)
461    }
462
463    pub fn is_submit_button(&self) -> bool {
464        if self.name.local != local_name!("button") {
465            return false;
466        }
467        let type_attr = self.attr(local_name!("type"));
468        let is_submit = type_attr == Some("submit");
469        let is_auto_submit = type_attr.is_none()
470            && self.attr(LocalName::from("command")).is_none()
471            && self.attr(LocalName::from("commandfor")).is_none();
472        is_submit || is_auto_submit
473    }
474}
475
476#[derive(Debug, Clone, PartialEq)]
477pub struct RasterImageData {
478    /// The width of the image
479    pub width: u32,
480    /// The height of the image
481    pub height: u32,
482    /// The raw image data in RGBA8 format
483    pub data: Blob<u8>,
484}
485impl RasterImageData {
486    pub fn new(width: u32, height: u32, data: Arc<Vec<u8>>) -> Self {
487        Self {
488            width,
489            height,
490            data: Blob::new(data),
491        }
492    }
493}
494
495#[derive(Debug, Clone)]
496pub enum ImageData {
497    Raster(RasterImageData),
498    #[cfg(feature = "svg")]
499    Svg(Arc<usvg::Tree>),
500    None,
501}
502#[cfg(feature = "svg")]
503impl From<usvg::Tree> for ImageData {
504    fn from(value: usvg::Tree) -> Self {
505        Self::Svg(Arc::new(value))
506    }
507}
508
509#[derive(Debug, Clone, PartialEq)]
510pub enum Status {
511    Ok,
512    Error,
513    Loading,
514}
515
516#[derive(Debug, Clone)]
517pub struct BackgroundImageData {
518    /// The url of the background image
519    pub url: ServoArc<Url>,
520    /// The loading status of the background image
521    pub status: Status,
522    /// The image data
523    pub image: ImageData,
524}
525
526impl BackgroundImageData {
527    pub fn new(url: ServoArc<Url>) -> Self {
528        Self {
529            url,
530            status: Status::Loading,
531            image: ImageData::None,
532        }
533    }
534}
535
536pub struct TextInputData {
537    /// A parley TextEditor instance
538    pub editor: Box<parley::PlainEditor<TextBrush>>,
539    /// Whether the input is a singleline or multiline input
540    pub is_multiline: bool,
541}
542
543// FIXME: Implement Clone for PlainEditor
544impl Clone for TextInputData {
545    fn clone(&self) -> Self {
546        TextInputData::new(self.is_multiline)
547    }
548}
549
550impl TextInputData {
551    pub fn new(is_multiline: bool) -> Self {
552        let editor = Box::new(parley::PlainEditor::new(16.0));
553        Self {
554            editor,
555            is_multiline,
556        }
557    }
558
559    pub fn set_text(
560        &mut self,
561        font_ctx: &mut FontContext,
562        layout_ctx: &mut LayoutContext<TextBrush>,
563        text: &str,
564    ) {
565        if self.editor.text() != text {
566            self.editor.set_text(text);
567            self.editor.driver(font_ctx, layout_ctx).refresh_layout();
568        }
569    }
570}
571
572#[derive(Debug, Clone)]
573pub struct CanvasData {
574    pub custom_paint_source_id: u64,
575}
576
577impl std::fmt::Debug for SpecialElementData {
578    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
579        match self {
580            SpecialElementData::SubDocument(_) => f.write_str("NodeSpecificData::SubDocument"),
581            #[cfg(feature = "custom-widget")]
582            SpecialElementData::CustomWidget(_) => f.write_str("NodeSpecificData::CustomWidget"),
583            SpecialElementData::Stylesheet(_) => f.write_str("NodeSpecificData::Stylesheet"),
584            SpecialElementData::Image(data) => match **data {
585                ImageData::Raster(_) => f.write_str("NodeSpecificData::Image(Raster)"),
586                #[cfg(feature = "svg")]
587                ImageData::Svg(_) => f.write_str("NodeSpecificData::Image(Svg)"),
588                ImageData::None => f.write_str("NodeSpecificData::Image(None)"),
589            },
590            SpecialElementData::Canvas(_) => f.write_str("NodeSpecificData::Canvas"),
591            SpecialElementData::TableRoot(_) => f.write_str("NodeSpecificData::TableRoot"),
592            SpecialElementData::TextInput(_) => f.write_str("NodeSpecificData::TextInput"),
593            SpecialElementData::CheckboxInput(_) => f.write_str("NodeSpecificData::CheckboxInput"),
594            #[cfg(feature = "file_input")]
595            SpecialElementData::FileInput(_) => f.write_str("NodeSpecificData::FileInput"),
596            SpecialElementData::None => f.write_str("NodeSpecificData::None"),
597        }
598    }
599}
600
601#[derive(Clone)]
602pub struct ListItemLayout {
603    pub marker: Marker,
604    pub position: ListItemLayoutPosition,
605}
606
607//We seperate chars from strings in order to optimise rendering - ie not needing to
608//construct a whole parley layout for simple char markers
609#[derive(Debug, PartialEq, Clone)]
610pub enum Marker {
611    Char(char),
612    String(String),
613}
614
615//Value depends on list-style-position, determining whether a seperate layout is created for it
616#[derive(Clone)]
617pub enum ListItemLayoutPosition {
618    Inside,
619    Outside(Box<parley::Layout<TextBrush>>),
620}
621
622impl std::fmt::Debug for ListItemLayout {
623    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
624        write!(f, "ListItemLayout - marker {:?}", self.marker)
625    }
626}
627
628#[derive(Debug, Clone, Copy, Default, PartialEq)]
629/// Parley Brush type for Blitz which contains the Blitz node id
630pub struct TextBrush {
631    /// The node id for the span
632    pub id: usize,
633}
634
635impl TextBrush {
636    pub(crate) fn from_id(id: usize) -> Self {
637        Self { id }
638    }
639}
640
641#[derive(Clone, Default)]
642pub struct TextLayout {
643    pub text: String,
644    pub content_widths: Option<ContentWidths>,
645    pub layout: parley::layout::Layout<TextBrush>,
646}
647
648impl TextLayout {
649    pub fn new() -> Self {
650        Default::default()
651    }
652
653    pub fn content_widths(&mut self) -> ContentWidths {
654        *self
655            .content_widths
656            .get_or_insert_with(|| self.layout.calculate_content_widths())
657    }
658}
659
660impl std::fmt::Debug for TextLayout {
661    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
662        write!(f, "TextLayout")
663    }
664}
665
666#[cfg(feature = "file_input")]
667mod file_data {
668    use std::ops::{Deref, DerefMut};
669    use std::path::PathBuf;
670
671    #[derive(Clone, Debug)]
672    pub struct FileData(pub Vec<PathBuf>);
673    impl Deref for FileData {
674        type Target = Vec<PathBuf>;
675
676        fn deref(&self) -> &Self::Target {
677            &self.0
678        }
679    }
680    impl DerefMut for FileData {
681        fn deref_mut(&mut self) -> &mut Self::Target {
682            &mut self.0
683        }
684    }
685    impl From<Vec<PathBuf>> for FileData {
686        fn from(files: Vec<PathBuf>) -> Self {
687            Self(files)
688        }
689    }
690}
691#[cfg(feature = "file_input")]
692pub use file_data::FileData;