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