Skip to main content

blitz_dom/node/
element.rs

1use blitz_traits::node_id::NodeId;
2use cssparser::ParserInput;
3use kurbo::{Affine, Rect as KurboRect};
4use linebender_resource_handle::Blob;
5use markup5ever::{LocalName, QualName, local_name};
6use selectors::matching::{ElementSelectorFlags, QuirksMode};
7use std::cell::Cell;
8use std::str::FromStr;
9use std::sync::Arc;
10use std::sync::atomic::AtomicBool;
11use style::Atom;
12use style::parser::ParserContext;
13use style::properties::{Importance, PropertyDeclaration, PropertyId, SourcePropertyDeclaration};
14use style::stylesheets::{DocumentStyleSheet, Origin, UrlExtraData};
15use style::values::computed::Display as StyloDisplay;
16use style::{
17    properties::{PropertyDeclarationBlock, parse_style_attribute},
18    servo_arc::Arc as ServoArc,
19    shared_lock::{Locked, SharedRwLock},
20    stylesheets::CssRuleType,
21};
22use style_dom::ElementState;
23use style_traits::ParsingMode;
24use taffy::{
25    Cache,
26    prelude::{Layout, Style},
27};
28use url::Url;
29
30use super::stylo_data::StyloData;
31#[cfg(feature = "svg")]
32use super::svg::SvgImageData;
33use super::{Attribute, Attributes};
34use crate::Document;
35use crate::layout::table::TableContext;
36use crate::node::{TextBrush, TextInputData, TextLayout};
37
38#[cfg(feature = "custom-widget")]
39use super::custom_widget::CustomWidgetData;
40
41macro_rules! local_names {
42    ($($name:tt),+) => {
43        [$(local_name!($name),)+]
44    };
45}
46
47pub struct ElementData {
48    /// The elements tag name, namespace and prefix
49    pub name: QualName,
50
51    /// The elements id attribute parsed as an atom (if it has one)
52    pub id: Option<Atom>,
53
54    /// The element's attributes
55    pub attrs: Attributes,
56
57    /// Whether the element is focussable
58    pub is_focussable: bool,
59
60    /// The element's parsed style attribute (used by stylo)
61    pub style_attribute: Option<ServoArc<Locked<PropertyDeclarationBlock>>>,
62
63    /// Heterogeneous data that depends on the element's type.
64    /// For example:
65    ///   - The image data for \<img\> elements.
66    ///   - The parley Layout for inline roots.
67    ///   - The text editor for input/textarea elements
68    pub special_data: SpecialElementData,
69
70    pub background_images: Vec<Option<ImageResourceData>>,
71
72    pub mask_images: Vec<Option<ImageResourceData>>,
73
74    /// Parley text layout (elements with inline inner display mode only)
75    pub inline_layout_data: Option<Box<TextLayout>>,
76
77    /// Data associated with display: list-item. Note that this display mode
78    /// does not exclude inline_layout_data
79    pub list_item_data: Option<Box<ListItemLayout>>,
80
81    /// The element's template contents (\<template\> elements only)
82    pub template_contents: Option<NodeId>,
83    // /// Whether the node is a [HTML integration point] (https://html.spec.whatwg.org/multipage/#html-integration-point)
84    // pub mathml_annotation_xml_integration_point: bool,
85
86    // ---------------------------------------------------------------------
87    // Fields moved from `Node`. These live on the element data so that the
88    // `Node` struct itself only carries tree-structure information.
89    // ---------------------------------------------------------------------
90    /// Style data from stylo, plus a lock guard that allows access to it.
91    pub stylo_element_data: StyloData,
92    pub selector_flags: Cell<ElementSelectorFlags>,
93    /// A clone of the document's shared style lock. Set when the owning
94    /// [`Node`](super::Node) is constructed.
95    pub guard: Option<SharedRwLock>,
96    pub element_state: ElementState,
97    pub has_snapshot: bool,
98    pub snapshot_handled: AtomicBool,
99    /// Whether any descendant of this node needs restyling.
100    /// Used by Stylo's incremental style traversal to skip unchanged subtrees.
101    pub dirty_descendants: AtomicBool,
102    /// Whether this node or any of its descendants may carry `RestyleDamage`.
103    /// Used by the damage propagation pass to skip unchanged subtrees.
104    pub damaged_descendants: AtomicBool,
105
106    // Pseudo element nodes
107    pub before: Option<NodeId>,
108    pub after: Option<NodeId>,
109
110    /// Detailed grid track sizing information from the most recent layout
111    /// (grid containers only). Used by devtools grid inspection.
112    pub detailed_grid_info: Option<Box<taffy::DetailedGridInfo<Atom>>>,
113
114    // Taffy layout data:
115    pub style: Style<Atom>,
116    pub display_constructed_as: StyloDisplay,
117    pub cache: Cache,
118    pub unrounded_layout: Layout,
119    pub final_layout: Layout,
120    pub scroll_offset: crate::Point<f64>,
121    pub scrollable_overflow: KurboRect,
122    pub transform: Option<Affine>,
123}
124
125/// Data specific to the [`Document`](super::super::Document) root node.
126///
127/// The document node participates in layout and styling like an element, so it
128/// carries the same style/layout fields that were previously stored directly on
129/// [`Node`](super::Node).
130pub struct DocumentData {
131    pub stylo_element_data: StyloData,
132    /// Selector flags deposited here by `apply_selector_flags` when a
133    /// `for_parent()` flag is applied while matching the root `<html>` element,
134    /// whose parent node is the document.
135    pub selector_flags: Cell<ElementSelectorFlags>,
136    /// A clone of the document's shared style lock. Set when the owning
137    /// [`Node`](super::Node) is constructed.
138    pub guard: Option<SharedRwLock>,
139    pub dirty_descendants: AtomicBool,
140    pub damaged_descendants: AtomicBool,
141    pub element_state: ElementState,
142    pub has_snapshot: bool,
143    pub snapshot_handled: AtomicBool,
144    pub style: Style<Atom>,
145    pub display_constructed_as: StyloDisplay,
146    pub cache: Cache,
147    pub unrounded_layout: Layout,
148    pub final_layout: Layout,
149    pub scroll_offset: crate::Point<f64>,
150    pub scrollable_overflow: KurboRect,
151    pub transform: Option<Affine>,
152}
153
154// Hand-written like `ElementData`'s, because `ElementSelectorFlags` does not
155// implement `Debug`. Every other field is still reported.
156impl std::fmt::Debug for DocumentData {
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        f.debug_struct("DocumentData")
159            .field("stylo_element_data", &self.stylo_element_data)
160            .field("guard", &self.guard)
161            .field("dirty_descendants", &self.dirty_descendants)
162            .field("damaged_descendants", &self.damaged_descendants)
163            .field("element_state", &self.element_state)
164            .field("has_snapshot", &self.has_snapshot)
165            .field("snapshot_handled", &self.snapshot_handled)
166            .field("style", &self.style)
167            .field("display_constructed_as", &self.display_constructed_as)
168            .field("cache", &self.cache)
169            .field("unrounded_layout", &self.unrounded_layout)
170            .field("final_layout", &self.final_layout)
171            .field("scroll_offset", &self.scroll_offset)
172            .field("scrollable_overflow", &self.scrollable_overflow)
173            .field("transform", &self.transform)
174            .finish_non_exhaustive()
175    }
176}
177
178impl DocumentData {
179    pub fn new() -> Self {
180        Self {
181            stylo_element_data: Default::default(),
182            selector_flags: Cell::new(ElementSelectorFlags::empty()),
183            guard: None,
184            dirty_descendants: AtomicBool::new(true),
185            damaged_descendants: AtomicBool::new(true),
186            element_state: ElementState::empty(),
187            has_snapshot: false,
188            snapshot_handled: AtomicBool::new(false),
189            style: Default::default(),
190            display_constructed_as: StyloDisplay::Block,
191            cache: Cache::new(),
192            unrounded_layout: Layout::new(),
193            final_layout: Layout::new(),
194            scroll_offset: crate::Point::ZERO,
195            scrollable_overflow: KurboRect::ZERO,
196            transform: None,
197        }
198    }
199}
200
201impl Default for DocumentData {
202    fn default() -> Self {
203        Self::new()
204    }
205}
206
207impl Clone for DocumentData {
208    fn clone(&self) -> Self {
209        // Runtime style/layout state is reset (the document node is not
210        // meaningfully cloneable), matching `ElementData`'s clone semantics.
211        Self {
212            guard: self.guard.clone(),
213            ..Self::new()
214        }
215    }
216}
217
218impl std::fmt::Debug for ElementData {
219    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220        f.debug_struct("ElementData")
221            .field("name", &self.name)
222            .field("id", &self.id)
223            .field("attrs", &self.attrs)
224            .field("is_focussable", &self.is_focussable)
225            .field("style_attribute", &self.style_attribute)
226            .field("special_data", &self.special_data)
227            .field("background_images", &self.background_images)
228            .field("mask_images", &self.mask_images)
229            .field("inline_layout_data", &self.inline_layout_data)
230            .field("list_item_data", &self.list_item_data)
231            .field("template_contents", &self.template_contents)
232            .field("element_state", &self.element_state)
233            .field("display_constructed_as", &self.display_constructed_as)
234            .finish_non_exhaustive()
235    }
236}
237
238impl Clone for ElementData {
239    /// Clones the *content* of the element (name, attributes, style attribute,
240    /// special data, etc.). Runtime style/layout state (stylo data, taffy
241    /// layout, caches, pseudo-element ids, ...) is reset to its default so that
242    /// the clone behaves like a freshly-created element that has not yet been
243    /// styled or laid out.
244    fn clone(&self) -> Self {
245        Self {
246            name: self.name.clone(),
247            id: self.id.clone(),
248            attrs: self.attrs.clone(),
249            is_focussable: self.is_focussable,
250            style_attribute: self.style_attribute.clone(),
251            special_data: self.special_data.clone(),
252            background_images: self.background_images.clone(),
253            mask_images: self.mask_images.clone(),
254            inline_layout_data: self.inline_layout_data.clone(),
255            list_item_data: self.list_item_data.clone(),
256            template_contents: self.template_contents,
257
258            // Runtime state: reset to defaults.
259            stylo_element_data: Default::default(),
260            selector_flags: Cell::new(ElementSelectorFlags::empty()),
261            guard: self.guard.clone(),
262            element_state: self.element_state,
263            has_snapshot: false,
264            snapshot_handled: AtomicBool::new(false),
265            dirty_descendants: AtomicBool::new(true),
266            damaged_descendants: AtomicBool::new(true),
267            before: None,
268            after: None,
269            detailed_grid_info: None,
270            style: Default::default(),
271            display_constructed_as: StyloDisplay::Block,
272            cache: Cache::new(),
273            unrounded_layout: Layout::new(),
274            final_layout: Layout::new(),
275            scroll_offset: crate::Point::ZERO,
276            scrollable_overflow: KurboRect::ZERO,
277            transform: None,
278        }
279    }
280}
281
282#[derive(Copy, Clone, Default)]
283#[non_exhaustive]
284pub enum SpecialElementType {
285    Stylesheet,
286    Image,
287    Canvas,
288    TableRoot,
289    TextInput,
290    CheckboxInput,
291    #[cfg(feature = "file-input")]
292    FileInput,
293    #[default]
294    None,
295}
296
297/// Heterogeneous data that depends on the element's type.
298#[derive(Default)]
299pub enum SpecialElementData {
300    /// A sub-document such an \<iframe\> or \<web-view\> element
301    SubDocument(Box<dyn Document>),
302    /// A custom widget
303    #[cfg(feature = "custom-widget")]
304    CustomWidget(CustomWidgetData),
305    /// A stylesheet
306    Stylesheet(DocumentStyleSheet),
307    /// An \<img\> element's image data
308    Image(Box<ImageData>),
309    /// A \<canvas\> element's custom paint source
310    Canvas(CanvasData),
311    /// Pre-computed table layout data
312    TableRoot(Arc<TableContext>),
313    /// Parley text editor (text inputs)
314    TextInput(TextInputData),
315    /// Checkbox checked state
316    CheckboxInput(bool),
317    /// Selected files
318    #[cfg(feature = "file-input")]
319    FileInput(FileData),
320    /// No data (for nodes that don't need any node-specific data)
321    #[default]
322    None,
323}
324
325impl Clone for SpecialElementData {
326    fn clone(&self) -> Self {
327        match self {
328            Self::SubDocument(_) => Self::None, // TODO
329            #[cfg(feature = "custom-widget")]
330            Self::CustomWidget(_) => Self::None, // TODO
331            Self::Stylesheet(data) => Self::Stylesheet(data.clone()),
332            Self::Image(data) => Self::Image(data.clone()),
333            Self::Canvas(data) => Self::Canvas(data.clone()),
334            Self::TableRoot(data) => Self::TableRoot(data.clone()),
335            Self::TextInput(data) => Self::TextInput(data.clone()),
336            Self::CheckboxInput(data) => Self::CheckboxInput(*data),
337            #[cfg(feature = "file-input")]
338            Self::FileInput(data) => Self::FileInput(data.clone()),
339            Self::None => Self::None,
340        }
341    }
342}
343
344impl SpecialElementData {
345    pub fn take(&mut self) -> Self {
346        std::mem::take(self)
347    }
348}
349
350impl ElementData {
351    pub fn new(name: QualName, attrs: Vec<Attribute>) -> Self {
352        let id_attr_atom = attrs
353            .iter()
354            .find(|attr| &attr.name.local == "id")
355            .map(|attr| attr.value.as_ref())
356            .map(|value: &str| Atom::from(value));
357
358        let mut data = ElementData {
359            name,
360            id: id_attr_atom,
361            attrs: Attributes::new(attrs),
362            is_focussable: false,
363            style_attribute: Default::default(),
364            inline_layout_data: None,
365            list_item_data: None,
366            special_data: SpecialElementData::None,
367            template_contents: None,
368            background_images: Vec::new(),
369            mask_images: Vec::new(),
370
371            stylo_element_data: Default::default(),
372            selector_flags: Cell::new(ElementSelectorFlags::empty()),
373            guard: None,
374            element_state: ElementState::empty(),
375            has_snapshot: false,
376            snapshot_handled: AtomicBool::new(false),
377            dirty_descendants: AtomicBool::new(true),
378            damaged_descendants: AtomicBool::new(true),
379            before: None,
380            after: None,
381            detailed_grid_info: None,
382            style: Default::default(),
383            display_constructed_as: StyloDisplay::Block,
384            cache: Cache::new(),
385            unrounded_layout: Layout::new(),
386            final_layout: Layout::new(),
387            scroll_offset: crate::Point::ZERO,
388            scrollable_overflow: KurboRect::ZERO,
389            transform: None,
390        };
391        data.flush_is_focussable();
392        data.flush_link_state();
393
394        // Mirror the `checked` attribute into the element state so that `:checked`
395        // selectors can be matched (and invalidated) from `ElementState`.
396        if data.name.local == local_name!("input")
397            && matches!(
398                data.attr(local_name!("type")),
399                Some("checkbox") | Some("radio")
400            )
401            && data.has_attr(local_name!("checked"))
402        {
403            data.element_state.insert(ElementState::CHECKED);
404        }
405
406        // The element state needs to be modified if the element can be disabled.
407        if data.can_be_disabled() {
408            data.element_state
409                .insert(match data.has_attr(local_name!("disabled")) {
410                    true => ElementState::DISABLED,
411                    false => ElementState::ENABLED,
412                });
413        }
414
415        data
416    }
417
418    pub fn attrs(&self) -> &[Attribute] {
419        &self.attrs
420    }
421
422    pub fn attr(&self, name: impl PartialEq<LocalName>) -> Option<&str> {
423        let attr = self.attrs.iter().find(|attr| name == attr.name.local)?;
424        Some(&attr.value)
425    }
426
427    pub fn attr_parsed<T: FromStr>(&self, name: impl PartialEq<LocalName>) -> Option<T> {
428        let attr = self.attrs.iter().find(|attr| name == attr.name.local)?;
429        attr.value.parse::<T>().ok()
430    }
431
432    /// Detects the presence of the attribute, treating *any* value as truthy.
433    pub fn has_attr(&self, name: impl PartialEq<LocalName>) -> bool {
434        self.attrs.iter().any(|attr| name == attr.name.local)
435    }
436
437    pub fn can_be_disabled(&self) -> bool {
438        local_names!("button", "input", "select", "textarea").contains(&self.name.local)
439    }
440
441    /// Whether this element is a link (an `<a>` or `<area>` element with an `href` attribute)
442    pub fn is_link(&self) -> bool {
443        (self.name.local == local_name!("a") || self.name.local == local_name!("area"))
444            && self.has_attr(local_name!("href"))
445    }
446
447    /// Sync the visitedness bits of `element_state` with the element's link-ness.
448    /// Blitz does not track browsing history, so all links are unvisited.
449    ///
450    /// Stylo's snapshot invalidation (`ElementWrapper::is_link`) determines link-ness
451    /// from these state bits, so they must be kept accurate for `:link`/`:any-link`
452    /// selectors to be correctly invalidated. Must be called whenever the `href`
453    /// attribute is added or removed.
454    pub fn flush_link_state(&mut self) {
455        self.element_state
456            .remove(ElementState::VISITED_OR_UNVISITED);
457        if self.is_link() {
458            self.element_state.insert(ElementState::UNVISITED);
459        }
460    }
461
462    pub fn image_data(&self) -> Option<&ImageData> {
463        match &self.special_data {
464            SpecialElementData::Image(data) => Some(&**data),
465            _ => None,
466        }
467    }
468
469    pub fn image_data_mut(&mut self) -> Option<&mut ImageData> {
470        match self.special_data {
471            SpecialElementData::Image(ref mut data) => Some(&mut **data),
472            _ => None,
473        }
474    }
475
476    pub fn raster_image_data(&self) -> Option<&RasterImageData> {
477        match self.image_data()? {
478            ImageData::Raster(data) => Some(data),
479            _ => None,
480        }
481    }
482
483    pub fn raster_image_data_mut(&mut self) -> Option<&mut RasterImageData> {
484        match self.image_data_mut()? {
485            ImageData::Raster(data) => Some(data),
486            _ => None,
487        }
488    }
489
490    pub fn canvas_data(&self) -> Option<&CanvasData> {
491        match &self.special_data {
492            SpecialElementData::Canvas(data) => Some(data),
493            _ => None,
494        }
495    }
496
497    pub fn sub_doc_data(&self) -> Option<&dyn Document> {
498        match &self.special_data {
499            SpecialElementData::SubDocument(data) => Some(data.as_ref()),
500            _ => None,
501        }
502    }
503
504    pub fn sub_doc_data_mut(&mut self) -> Option<&mut dyn Document> {
505        match &mut self.special_data {
506            SpecialElementData::SubDocument(data) => Some(data.as_mut()),
507            _ => None,
508        }
509    }
510
511    #[cfg(feature = "svg")]
512    pub fn svg_data(&self) -> Option<&usvg::Tree> {
513        match self.image_data()? {
514            ImageData::Svg(data) => Some(&data.tree),
515            _ => None,
516        }
517    }
518
519    pub fn text_input_data(&self) -> Option<&TextInputData> {
520        match &self.special_data {
521            SpecialElementData::TextInput(data) => Some(data),
522            _ => None,
523        }
524    }
525
526    pub fn text_input_data_mut(&mut self) -> Option<&mut TextInputData> {
527        match &mut self.special_data {
528            SpecialElementData::TextInput(data) => Some(data),
529            _ => None,
530        }
531    }
532
533    #[cfg(feature = "custom-widget")]
534    pub fn custom_widget_data(&self) -> Option<&CustomWidgetData> {
535        match &self.special_data {
536            SpecialElementData::CustomWidget(data) => Some(data),
537            _ => None,
538        }
539    }
540
541    #[cfg(feature = "custom-widget")]
542    pub fn custom_widget_data_mut(&mut self) -> Option<&mut CustomWidgetData> {
543        match &mut self.special_data {
544            SpecialElementData::CustomWidget(data) => Some(data),
545            _ => None,
546        }
547    }
548
549    pub fn checkbox_input_checked(&self) -> Option<bool> {
550        match self.special_data {
551            SpecialElementData::CheckboxInput(checked) => Some(checked),
552            _ => None,
553        }
554    }
555
556    pub fn checkbox_input_checked_mut(&mut self) -> Option<&mut bool> {
557        match self.special_data {
558            SpecialElementData::CheckboxInput(ref mut checked) => Some(checked),
559            _ => None,
560        }
561    }
562
563    /// Set the checked state of a checkbox/radio input, keeping the
564    /// `ElementState::CHECKED` bit (used for `:checked` selector matching and
565    /// invalidation) in sync with the special data.
566    pub fn set_checkbox_input_checked(&mut self, checked: bool) {
567        if let Some(is_checked) = self.checkbox_input_checked_mut() {
568            *is_checked = checked;
569        }
570        self.element_state.set(ElementState::CHECKED, checked);
571    }
572
573    #[cfg(feature = "file-input")]
574    pub fn file_data(&self) -> Option<&FileData> {
575        match &self.special_data {
576            SpecialElementData::FileInput(data) => Some(data),
577            _ => None,
578        }
579    }
580
581    #[cfg(feature = "file-input")]
582    pub fn file_data_mut(&mut self) -> Option<&mut FileData> {
583        match &mut self.special_data {
584            SpecialElementData::FileInput(data) => Some(data),
585            _ => None,
586        }
587    }
588
589    pub fn flush_is_focussable(&mut self) {
590        let disabled: bool = self.attr_parsed(local_name!("disabled")).unwrap_or(false);
591        let tabindex: Option<i32> = self.attr_parsed(local_name!("tabindex"));
592        let contains_sub_document: bool = self.sub_doc_data().is_some();
593
594        self.is_focussable = contains_sub_document
595            || (!disabled
596                && match tabindex {
597                    Some(index) => index >= 0,
598                    None => {
599                        // Some focusable HTML elements have a default tabindex value of 0 set under the hood by the user agent.
600                        // These elements are:
601                        //   - <a> or <area> with href attribute
602                        //   - <button>, <frame>, <iframe>, <input>, <object>, <select>, <textarea>, and SVG <a> element
603                        //   - <summary> element that provides summary for a <details> element.
604
605                        if [local_name!("a"), local_name!("area")].contains(&self.name.local) {
606                            self.attr(local_name!("href")).is_some()
607                        } else {
608                            const DEFAULT_FOCUSSABLE_ELEMENTS: [LocalName; 7] = [
609                                local_name!("button"),
610                                local_name!("input"),
611                                local_name!("select"),
612                                local_name!("textarea"),
613                                local_name!("frame"),
614                                local_name!("iframe"),
615                                local_name!("summary"),
616                            ];
617                            DEFAULT_FOCUSSABLE_ELEMENTS.contains(&self.name.local)
618                        }
619                    }
620                })
621    }
622
623    pub fn flush_style_attribute(&mut self, guard: &SharedRwLock, url_extra_data: &UrlExtraData) {
624        self.style_attribute = self.attr(local_name!("style")).map(|style_str| {
625            ServoArc::new(guard.wrap(parse_style_attribute(
626                style_str,
627                url_extra_data,
628                None,
629                QuirksMode::NoQuirks,
630                CssRuleType::Style,
631            )))
632        });
633    }
634
635    pub fn set_style_property(
636        &mut self,
637        name: &str,
638        value: &str,
639        guard: &SharedRwLock,
640        url_extra_data: UrlExtraData,
641    ) -> bool {
642        let context = ParserContext::new(
643            Origin::Author,
644            &url_extra_data,
645            Some(CssRuleType::Style),
646            ParsingMode::DEFAULT,
647            QuirksMode::NoQuirks,
648            /* namespaces = */ Default::default(),
649            None,
650            None,
651            /* attr_taint = */ Default::default(),
652        );
653
654        let Ok(property_id) = PropertyId::parse(name, &context) else {
655            #[cfg(feature = "tracing")]
656            tracing::warn!(property = name, "Unsupported property");
657            return false;
658        };
659        let mut source_property_declaration = SourcePropertyDeclaration::default();
660        let mut input = ParserInput::new(value);
661        let mut parser = style::values::Parser::new(&mut input);
662        let Ok(_) = PropertyDeclaration::parse_into(
663            &mut source_property_declaration,
664            property_id,
665            &context,
666            &mut parser,
667        ) else {
668            #[cfg(feature = "tracing")]
669            tracing::warn!(property = name, value, "Invalid property value");
670            return false;
671        };
672
673        if self.style_attribute.is_none() {
674            self.style_attribute = Some(ServoArc::new(guard.wrap(PropertyDeclarationBlock::new())));
675        }
676        self.style_attribute
677            .as_mut()
678            .unwrap()
679            .write_with(&mut guard.write())
680            .extend(source_property_declaration.drain(), Importance::Normal);
681
682        true
683    }
684
685    pub fn remove_style_property(
686        &mut self,
687        name: &str,
688        guard: &SharedRwLock,
689        url_extra_data: UrlExtraData,
690    ) -> bool {
691        let context = ParserContext::new(
692            Origin::Author,
693            &url_extra_data,
694            Some(CssRuleType::Style),
695            ParsingMode::DEFAULT,
696            QuirksMode::NoQuirks,
697            /* namespaces = */ Default::default(),
698            None,
699            None,
700            /* attr_taint = */ Default::default(),
701        );
702        let Ok(property_id) = PropertyId::parse(name, &context) else {
703            #[cfg(feature = "tracing")]
704            tracing::warn!(property = name, "Unsupported property");
705            return false;
706        };
707
708        if let Some(style) = &mut self.style_attribute {
709            let mut guard = guard.write();
710            let style = style.write_with(&mut guard);
711            if let Some(index) = style.first_declaration_to_remove(&property_id) {
712                style.remove_property(&property_id, index);
713                return true;
714            }
715        }
716
717        false
718    }
719
720    pub fn set_sub_document(&mut self, sub_document: Box<dyn Document>) {
721        self.special_data = SpecialElementData::SubDocument(sub_document);
722    }
723
724    pub fn remove_sub_document(&mut self) {
725        self.special_data = SpecialElementData::None;
726    }
727
728    #[cfg(feature = "custom-widget")]
729    pub fn set_custom_widget(&mut self, widget: Box<dyn crate::Widget>) {
730        use crate::node::custom_widget::CustomWidgetData;
731        self.special_data = SpecialElementData::CustomWidget(CustomWidgetData::new(widget));
732    }
733
734    #[cfg(feature = "custom-widget")]
735    pub fn remove_custom_widget(&mut self) -> Vec<anyrender::ResourceId> {
736        let resource_ids = self
737            .custom_widget_data_mut()
738            .map(|widget_data| widget_data.take_resource_ids())
739            .unwrap_or_default();
740        self.special_data = SpecialElementData::None;
741        resource_ids
742    }
743
744    pub fn take_inline_layout(&mut self) -> Option<Box<TextLayout>> {
745        std::mem::take(&mut self.inline_layout_data)
746    }
747
748    pub fn is_submit_button(&self) -> bool {
749        if self.name.local != local_name!("button") {
750            return false;
751        }
752        let type_attr = self.attr(local_name!("type"));
753        let is_submit = type_attr == Some("submit");
754        let is_auto_submit = type_attr.is_none()
755            && self.attr(LocalName::from("command")).is_none()
756            && self.attr(LocalName::from("commandfor")).is_none();
757        is_submit || is_auto_submit
758    }
759}
760
761#[derive(Debug, Clone, PartialEq)]
762pub struct RasterImageData {
763    /// The width of the image
764    pub width: u32,
765    /// The height of the image
766    pub height: u32,
767    /// The raw image data in RGBA8 format
768    pub data: Blob<u8>,
769}
770impl RasterImageData {
771    pub fn new(width: u32, height: u32, data: Arc<Vec<u8>>) -> Self {
772        Self {
773            width,
774            height,
775            data: Blob::new(data),
776        }
777    }
778}
779
780#[derive(Debug, Clone)]
781pub enum ImageData {
782    Raster(RasterImageData),
783    #[cfg(feature = "svg")]
784    Svg(SvgImageData),
785    None,
786}
787
788#[derive(Debug, Clone, PartialEq)]
789pub enum Status {
790    Ok,
791    Error,
792    Loading,
793}
794
795#[derive(Debug, Clone)]
796pub struct ImageResourceData {
797    /// The url of the background image
798    pub url: ServoArc<Url>,
799    /// The loading status of the background image
800    pub status: Status,
801    /// The image data
802    pub image: ImageData,
803}
804
805impl ImageResourceData {
806    pub fn new(url: ServoArc<Url>) -> Self {
807        Self {
808            url,
809            status: Status::Loading,
810            image: ImageData::None,
811        }
812    }
813}
814
815#[derive(Debug, Clone)]
816pub struct CanvasData {
817    pub custom_paint_source_id: u64,
818}
819
820impl std::fmt::Debug for SpecialElementData {
821    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
822        match self {
823            SpecialElementData::SubDocument(_) => f.write_str("NodeSpecificData::SubDocument"),
824            #[cfg(feature = "custom-widget")]
825            SpecialElementData::CustomWidget(_) => f.write_str("NodeSpecificData::CustomWidget"),
826            SpecialElementData::Stylesheet(_) => f.write_str("NodeSpecificData::Stylesheet"),
827            SpecialElementData::Image(data) => match **data {
828                ImageData::Raster(_) => f.write_str("NodeSpecificData::Image(Raster)"),
829                #[cfg(feature = "svg")]
830                ImageData::Svg(_) => f.write_str("NodeSpecificData::Image(Svg)"),
831                ImageData::None => f.write_str("NodeSpecificData::Image(None)"),
832            },
833            SpecialElementData::Canvas(_) => f.write_str("NodeSpecificData::Canvas"),
834            SpecialElementData::TableRoot(_) => f.write_str("NodeSpecificData::TableRoot"),
835            SpecialElementData::TextInput(_) => f.write_str("NodeSpecificData::TextInput"),
836            SpecialElementData::CheckboxInput(_) => f.write_str("NodeSpecificData::CheckboxInput"),
837            #[cfg(feature = "file-input")]
838            SpecialElementData::FileInput(_) => f.write_str("NodeSpecificData::FileInput"),
839            SpecialElementData::None => f.write_str("NodeSpecificData::None"),
840        }
841    }
842}
843
844#[derive(Clone)]
845pub struct ListItemLayout {
846    pub marker: Marker,
847    pub position: ListItemLayoutPosition,
848}
849
850//We seperate chars from strings in order to optimise rendering - ie not needing to
851//construct a whole parley layout for simple char markers
852#[derive(Debug, PartialEq, Clone)]
853pub enum Marker {
854    Char(char),
855    String(String),
856}
857
858//Value depends on list-style-position, determining whether a seperate layout is created for it
859#[derive(Clone)]
860pub enum ListItemLayoutPosition {
861    Inside,
862    Outside(Box<parley::Layout<TextBrush>>),
863}
864
865impl std::fmt::Debug for ListItemLayout {
866    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
867        write!(f, "ListItemLayout - marker {:?}", self.marker)
868    }
869}
870
871#[cfg(feature = "file-input")]
872mod file_data {
873    use std::ops::{Deref, DerefMut};
874    use std::path::PathBuf;
875
876    #[derive(Clone, Debug)]
877    pub struct FileData(pub Vec<PathBuf>);
878    impl Deref for FileData {
879        type Target = Vec<PathBuf>;
880
881        fn deref(&self) -> &Self::Target {
882            &self.0
883        }
884    }
885    impl DerefMut for FileData {
886        fn deref_mut(&mut self) -> &mut Self::Target {
887            &mut self.0
888        }
889    }
890    impl From<Vec<PathBuf>> for FileData {
891        fn from(files: Vec<PathBuf>) -> Self {
892            Self(files)
893        }
894    }
895}
896#[cfg(feature = "file-input")]
897pub use file_data::FileData;
898
899#[cfg(test)]
900mod tests {
901    use super::TextInputData;
902    use parley::{FontContext, LayoutContext};
903
904    /// Build a [`TextInputData`] with the given text laid out at scale 1.0.
905    fn make_input(is_multiline: bool, text: &str) -> TextInputData {
906        let mut font_ctx = FontContext::new();
907        let mut layout_ctx = LayoutContext::new();
908        let mut data = TextInputData::new(is_multiline);
909        data.editor.set_scale(1.0);
910        data.editor.set_text(text);
911        data.editor
912            .driver(&mut font_ctx, &mut layout_ctx)
913            .refresh_layout();
914        data
915    }
916
917    #[test]
918    fn short_text_does_not_scroll() {
919        let mut data = make_input(false, "hi");
920        // A wide content box that comfortably fits the text.
921        data.clamp_scroll_offset(1000.0, 100.0);
922        assert_eq!(data.scroll_offset, 0.0);
923    }
924
925    #[test]
926    fn single_line_scrolls_to_follow_caret() {
927        let text = "the quick brown fox jumps over the lazy dog repeatedly and at length";
928        let mut data = make_input(false, text);
929        let content_box_width = 40.0;
930        let content_box_height = 20.0;
931
932        // Caret at the end of a string that overflows a narrow input should scroll right.
933        data.editor
934            .driver(&mut FontContext::new(), &mut LayoutContext::new())
935            .move_to_text_end();
936        data.clamp_scroll_offset(content_box_width, content_box_height);
937
938        let layout_width = data.editor.try_layout().unwrap().full_width();
939        if layout_width > content_box_width {
940            assert!(
941                data.scroll_offset > 0.0,
942                "expected horizontal scroll for overflowing single-line input"
943            );
944            // The caret must be within the visible region after scrolling.
945            let caret = data.editor.cursor_geometry(1.5).unwrap();
946            assert!(caret.x1 as f32 <= data.scroll_offset + content_box_width + 0.5);
947            assert!(caret.x0 as f32 >= data.scroll_offset - 0.5);
948        }
949
950        // Moving the caret back to the start should reset the scroll offset.
951        data.editor
952            .driver(&mut FontContext::new(), &mut LayoutContext::new())
953            .move_to_text_start();
954        data.clamp_scroll_offset(content_box_width, content_box_height);
955        assert_eq!(data.scroll_offset, 0.0);
956    }
957
958    #[test]
959    fn multiline_scrolls_vertically_not_horizontally() {
960        let text = (0..40)
961            .map(|i| format!("line {i}"))
962            .collect::<Vec<_>>()
963            .join("\n");
964        let mut data = make_input(true, &text);
965        // Constrain the width so wrapping is well-defined.
966        data.editor.set_width(Some(200.0));
967        data.editor
968            .driver(&mut FontContext::new(), &mut LayoutContext::new())
969            .refresh_layout();
970
971        let content_box_width = 200.0;
972        let content_box_height = 30.0;
973
974        data.editor
975            .driver(&mut FontContext::new(), &mut LayoutContext::new())
976            .move_to_text_end();
977        data.clamp_scroll_offset(content_box_width, content_box_height);
978
979        let layout_height = data.editor.try_layout().unwrap().height();
980        if layout_height > content_box_height {
981            assert!(
982                data.scroll_offset > 0.0,
983                "expected vertical scroll for overflowing multi-line input"
984            );
985        }
986    }
987
988    #[test]
989    fn scroll_by_clamps_and_bubbles() {
990        let text = (0..40)
991            .map(|i| format!("line {i}"))
992            .collect::<Vec<_>>()
993            .join("\n");
994        let mut data = make_input(true, &text);
995        data.editor.set_width(Some(200.0));
996        data.editor
997            .driver(&mut FontContext::new(), &mut LayoutContext::new())
998            .refresh_layout();
999
1000        let content_box_width = 200.0;
1001        let content_box_height = 30.0;
1002        let max = data.max_scroll_offset(content_box_width, content_box_height);
1003        assert!(max > 0.0, "test text should overflow the content box");
1004
1005        // Scrolling up (positive delta decreases offset) while already at the top is a no-op and
1006        // the whole delta bubbles.
1007        assert_eq!(data.scroll_offset, 0.0);
1008        let bubbled = data.scroll_by(15.0, content_box_width, content_box_height);
1009        assert_eq!(data.scroll_offset, 0.0);
1010        assert_eq!(bubbled, 15.0);
1011
1012        // Scrolling down moves the offset and consumes the delta.
1013        let bubbled = data.scroll_by(-10.0, content_box_width, content_box_height);
1014        assert_eq!(data.scroll_offset, 10.0);
1015        assert_eq!(bubbled, 0.0);
1016
1017        // Scrolling past the end clamps to the maximum and bubbles the remainder. Starting at
1018        // offset 10 with max headroom of `max - 10`, a delta of `-(max + 100)` consumes
1019        // `max - 10` and bubbles the rest (`-110`).
1020        let bubbled = data.scroll_by(-(max + 100.0), content_box_width, content_box_height);
1021        assert_eq!(data.scroll_offset, max);
1022        assert!((bubbled - (-110.0)).abs() < 1e-3);
1023    }
1024
1025    #[test]
1026    fn single_line_does_not_scroll_when_text_fits() {
1027        let mut data = make_input(false, "hi");
1028        // Wide content box; nothing to scroll, so all delta bubbles.
1029        let bubbled = data.scroll_by(-50.0, 1000.0, 100.0);
1030        assert_eq!(data.scroll_offset, 0.0);
1031        assert_eq!(bubbled, -50.0);
1032    }
1033}