Skip to main content

bliss_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    ) {
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            eprintln!("Warning: unsupported property {name}");
345            return;
346        };
347        let mut source_property_declaration = SourcePropertyDeclaration::default();
348        let mut input = ParserInput::new(value);
349        let mut parser = style::values::Parser::new(&mut input);
350        let Ok(_) = PropertyDeclaration::parse_into(
351            &mut source_property_declaration,
352            property_id,
353            &context,
354            &mut parser,
355        ) else {
356            eprintln!("Warning: invalid property value for {name}: {value}");
357            return;
358        };
359
360        if self.style_attribute.is_none() {
361            self.style_attribute = Some(ServoArc::new(guard.wrap(PropertyDeclarationBlock::new())));
362        }
363        self.style_attribute
364            .as_mut()
365            .unwrap()
366            .write_with(&mut guard.write())
367            .extend(source_property_declaration.drain(), Importance::Normal);
368    }
369
370    pub fn remove_style_property(
371        &mut self,
372        name: &str,
373        guard: &SharedRwLock,
374        url_extra_data: UrlExtraData,
375    ) {
376        let context = ParserContext::new(
377            Origin::Author,
378            &url_extra_data,
379            Some(CssRuleType::Style),
380            ParsingMode::DEFAULT,
381            QuirksMode::NoQuirks,
382            /* namespaces = */ Default::default(),
383            None,
384            None,
385        );
386        let Ok(property_id) = PropertyId::parse(name, &context) else {
387            eprintln!("Warning: unsupported property {name}");
388            return;
389        };
390
391        if let Some(style) = &mut self.style_attribute {
392            let mut guard = guard.write();
393            let style = style.write_with(&mut guard);
394            if let Some(index) = style.first_declaration_to_remove(&property_id) {
395                style.remove_property(&property_id, index);
396            }
397        }
398    }
399
400    pub fn set_sub_document(&mut self, sub_document: Box<dyn Document>) {
401        self.special_data = SpecialElementData::SubDocument(sub_document);
402    }
403
404    pub fn remove_sub_document(&mut self) {
405        self.special_data = SpecialElementData::None;
406    }
407
408    pub fn take_inline_layout(&mut self) -> Option<Box<TextLayout>> {
409        std::mem::take(&mut self.inline_layout_data)
410    }
411
412    pub fn is_submit_button(&self) -> bool {
413        if self.name.local != local_name!("button") {
414            return false;
415        }
416        let type_attr = self.attr(local_name!("type"));
417        let is_submit = type_attr == Some("submit");
418        let is_auto_submit = type_attr.is_none()
419            && self.attr(LocalName::from("command")).is_none()
420            && self.attr(LocalName::from("commandfor")).is_none();
421        is_submit || is_auto_submit
422    }
423}
424
425#[derive(Debug, Clone, PartialEq)]
426pub struct RasterImageData {
427    /// The width of the image
428    pub width: u32,
429    /// The height of the image
430    pub height: u32,
431    /// The raw image data in RGBA8 format
432    pub data: Blob<u8>,
433}
434impl RasterImageData {
435    pub fn new(width: u32, height: u32, data: Arc<Vec<u8>>) -> Self {
436        Self {
437            width,
438            height,
439            data: Blob::new(data),
440        }
441    }
442}
443
444#[derive(Debug, Clone)]
445pub enum ImageData {
446    Raster(RasterImageData),
447    #[cfg(feature = "svg")]
448    Svg(Arc<usvg::Tree>),
449    None,
450}
451#[cfg(feature = "svg")]
452impl From<usvg::Tree> for ImageData {
453    fn from(value: usvg::Tree) -> Self {
454        Self::Svg(Arc::new(value))
455    }
456}
457
458#[derive(Debug, Clone, PartialEq)]
459pub enum Status {
460    Ok,
461    Error,
462    Loading,
463}
464
465#[derive(Debug, Clone)]
466pub struct BackgroundImageData {
467    /// The url of the background image
468    pub url: ServoArc<Url>,
469    /// The loading status of the background image
470    pub status: Status,
471    /// The image data
472    pub image: ImageData,
473}
474
475impl BackgroundImageData {
476    pub fn new(url: ServoArc<Url>) -> Self {
477        Self {
478            url,
479            status: Status::Loading,
480            image: ImageData::None,
481        }
482    }
483}
484
485pub struct TextInputData {
486    /// A parley TextEditor instance
487    pub editor: Box<parley::PlainEditor<TextBrush>>,
488    /// Whether the input is a singleline or multiline input
489    pub is_multiline: bool,
490}
491
492// FIXME: Implement Clone for PlainEditor
493impl Clone for TextInputData {
494    fn clone(&self) -> Self {
495        TextInputData::new(self.is_multiline)
496    }
497}
498
499impl TextInputData {
500    pub fn new(is_multiline: bool) -> Self {
501        let editor = Box::new(parley::PlainEditor::new(16.0));
502        Self {
503            editor,
504            is_multiline,
505        }
506    }
507
508    pub fn set_text(
509        &mut self,
510        font_ctx: &mut FontContext,
511        layout_ctx: &mut LayoutContext<TextBrush>,
512        text: &str,
513    ) {
514        if self.editor.text() != text {
515            self.editor.set_text(text);
516            self.editor.driver(font_ctx, layout_ctx).refresh_layout();
517        }
518    }
519}
520
521#[derive(Debug, Clone)]
522pub struct CanvasData {
523    pub custom_paint_source_id: u64,
524}
525
526impl std::fmt::Debug for SpecialElementData {
527    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
528        match self {
529            SpecialElementData::SubDocument(_) => f.write_str("NodeSpecificData::SubDocument"),
530            SpecialElementData::Stylesheet(_) => f.write_str("NodeSpecificData::Stylesheet"),
531            SpecialElementData::Image(data) => match **data {
532                ImageData::Raster(_) => f.write_str("NodeSpecificData::Image(Raster)"),
533                #[cfg(feature = "svg")]
534                ImageData::Svg(_) => f.write_str("NodeSpecificData::Image(Svg)"),
535                ImageData::None => f.write_str("NodeSpecificData::Image(None)"),
536            },
537            SpecialElementData::Canvas(_) => f.write_str("NodeSpecificData::Canvas"),
538            SpecialElementData::TableRoot(_) => f.write_str("NodeSpecificData::TableRoot"),
539            SpecialElementData::TextInput(_) => f.write_str("NodeSpecificData::TextInput"),
540            SpecialElementData::CheckboxInput(_) => f.write_str("NodeSpecificData::CheckboxInput"),
541            #[cfg(feature = "file_input")]
542            SpecialElementData::FileInput(_) => f.write_str("NodeSpecificData::FileInput"),
543            SpecialElementData::None => f.write_str("NodeSpecificData::None"),
544        }
545    }
546}
547
548#[derive(Clone)]
549pub struct ListItemLayout {
550    pub marker: Marker,
551    pub position: ListItemLayoutPosition,
552}
553
554//We seperate chars from strings in order to optimise rendering - ie not needing to
555//construct a whole parley layout for simple char markers
556#[derive(Debug, PartialEq, Clone)]
557pub enum Marker {
558    Char(char),
559    String(String),
560}
561
562//Value depends on list-style-position, determining whether a seperate layout is created for it
563#[derive(Clone)]
564pub enum ListItemLayoutPosition {
565    Inside,
566    Outside(Box<parley::Layout<TextBrush>>),
567}
568
569impl std::fmt::Debug for ListItemLayout {
570    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
571        write!(f, "ListItemLayout - marker {:?}", self.marker)
572    }
573}
574
575#[derive(Debug, Clone, Copy, Default, PartialEq)]
576/// Parley Brush type for Bliss which contains the Bliss node id
577pub struct TextBrush {
578    /// The node id for the span
579    pub id: usize,
580}
581
582impl TextBrush {
583    pub(crate) fn from_id(id: usize) -> Self {
584        Self { id }
585    }
586}
587
588#[derive(Clone, Default)]
589pub struct TextLayout {
590    pub text: String,
591    pub content_widths: Option<ContentWidths>,
592    pub layout: parley::layout::Layout<TextBrush>,
593}
594
595impl TextLayout {
596    pub fn new() -> Self {
597        Default::default()
598    }
599
600    pub fn content_widths(&mut self) -> ContentWidths {
601        *self
602            .content_widths
603            .get_or_insert_with(|| self.layout.calculate_content_widths())
604    }
605}
606
607impl std::fmt::Debug for TextLayout {
608    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
609        write!(f, "TextLayout")
610    }
611}
612
613#[cfg(feature = "file_input")]
614mod file_data {
615    use std::ops::{Deref, DerefMut};
616    use std::path::PathBuf;
617
618    #[derive(Clone, Debug)]
619    pub struct FileData(pub Vec<PathBuf>);
620    impl Deref for FileData {
621        type Target = Vec<PathBuf>;
622
623        fn deref(&self) -> &Self::Target {
624            &self.0
625        }
626    }
627    impl DerefMut for FileData {
628        fn deref_mut(&mut self) -> &mut Self::Target {
629            &mut self.0
630        }
631    }
632    impl From<Vec<PathBuf>> for FileData {
633        fn from(files: Vec<PathBuf>) -> Self {
634            Self(files)
635        }
636    }
637}
638#[cfg(feature = "file_input")]
639pub use file_data::FileData;