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