Skip to main content

apace_dom/node/
element.rs

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