blitz_dom/node/
element.rs

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