Skip to main content

blitz_dom/node/
element.rs

1use crate::layout::cache::Cache;
2use blitz_traits::node_id::NodeId;
3use cssparser::ParserInput;
4use kurbo::{Affine, Rect as KurboRect};
5use linebender_resource_handle::Blob;
6use markup5ever::{LocalName, QualName, local_name};
7use selectors::matching::{ElementSelectorFlags, QuirksMode};
8use std::cell::Cell;
9use std::str::FromStr;
10use std::sync::Arc;
11use std::sync::atomic::AtomicBool;
12use style::Atom;
13use style::parser::ParserContext;
14use style::properties::ComputedValues;
15use style::properties::{Importance, PropertyDeclaration, PropertyId, SourcePropertyDeclaration};
16use style::stylesheets::{DocumentStyleSheet, Origin, UrlExtraData};
17use style::values::computed::Display as StyloDisplay;
18use style::{
19    properties::{PropertyDeclarationBlock, parse_style_attribute},
20    servo_arc::Arc as ServoArc,
21    shared_lock::{Locked, SharedRwLock},
22    stylesheets::CssRuleType,
23};
24use style_dom::ElementState;
25use style_traits::ParsingMode;
26use taffy::prelude::{Layout, Style};
27use url::Url;
28
29use super::stylo_data::StyloData;
30use super::{Attribute, Attributes};
31use crate::Document;
32use crate::layout::table::TableContext;
33use crate::node::{SelectData, TextBrush, TextInputData, TextLayout};
34
35#[cfg(feature = "shadow-dom")]
36use super::custom_element::CustomElementData;
37#[cfg(feature = "custom-widget")]
38use super::custom_widget::CustomWidgetData;
39
40macro_rules! local_names {
41    ($($name:tt),+) => {
42        [$(local_name!($name),)+]
43    };
44}
45
46pub struct ElementData {
47    /// The elements tag name, namespace and prefix
48    pub name: QualName,
49
50    /// The elements id attribute parsed as an atom (if it has one)
51    pub id: Option<Atom>,
52
53    /// The element's attributes
54    pub attrs: Attributes,
55
56    /// Raw event handler content attributes. Script-assigned handlers live in
57    /// the script runtime instead. Absent on elements without on* attributes.
58    pub inline_event_attributes: Option<Box<Vec<Attribute>>>,
59
60    /// Whether the element is focussable
61    pub is_focussable: bool,
62
63    /// The element's parsed style attribute (used by stylo)
64    pub style_attribute: Option<ServoArc<Locked<PropertyDeclarationBlock>>>,
65
66    /// Heterogeneous data that depends on the element's type.
67    /// For example:
68    ///   - The image data for \<img\> elements.
69    ///   - The parley Layout for inline roots.
70    ///   - The text editor for input/textarea elements
71    pub special_data: SpecialElementData,
72
73    /// DOM geometry owned by an inline SVG root, rebuilt with its image.
74    #[cfg(feature = "svg")]
75    pub(crate) svg_geometry: Option<Arc<crate::layout::svg_geometry::SvgGeometry>>,
76
77    pub background_images: Vec<Option<ImageResourceData>>,
78
79    pub mask_images: Vec<Option<ImageResourceData>>,
80
81    /// Parley text layout (elements with inline inner display mode only)
82    pub inline_layout_data: Option<Box<TextLayout>>,
83
84    /// Data associated with display: list-item. Note that this display mode
85    /// does not exclude inline_layout_data
86    pub list_item_data: Option<Box<ListItemLayout>>,
87
88    /// The element's template contents (\<template\> elements only)
89    pub template_contents: Option<NodeId>,
90
91    /// The node id of the shadow root attached to this element (if it is a
92    /// shadow host). The shadow root node's children form the shadow tree.
93    pub shadow_root: Option<NodeId>,
94
95    /// If this element is a light-DOM child of a shadow host, the node id of
96    /// the `<slot>` element it has been assigned to in the host's shadow tree
97    /// (if any). Recomputed during slot assignment.
98    pub assigned_slot: Option<NodeId>,
99    // /// Whether the node is a [HTML integration point] (https://html.spec.whatwg.org/multipage/#html-integration-point)
100    // pub mathml_annotation_xml_integration_point: bool,
101
102    // ---------------------------------------------------------------------
103    // Fields moved from `Node`. These live on the element data so that the
104    // `Node` struct itself only carries tree-structure information.
105    // ---------------------------------------------------------------------
106    /// Style data from stylo, plus a lock guard that allows access to it.
107    pub stylo_element_data: StyloData,
108    pub selector_flags: Cell<ElementSelectorFlags>,
109    /// A clone of the document's shared style lock. Set when the owning
110    /// [`Node`](super::Node) is constructed.
111    pub guard: Option<SharedRwLock>,
112    pub element_state: ElementState,
113    pub has_snapshot: bool,
114    pub snapshot_handled: AtomicBool,
115    /// Whether any descendant of this node needs restyling.
116    /// Used by Stylo's incremental style traversal to skip unchanged subtrees.
117    pub dirty_descendants: AtomicBool,
118
119    // Pseudo element nodes
120    pub before: Option<NodeId>,
121    pub after: Option<NodeId>,
122
123    /// Detailed grid track sizing information from the most recent layout
124    /// (grid containers only). Used by devtools grid inspection.
125    pub detailed_grid_info: Option<Box<taffy::DetailedGridInfo>>,
126
127    // Taffy layout data:
128    pub style: Style<Atom>,
129    /// Whether flushing this subtree contributes anything to an ancestor's
130    /// paint order: a hoisted `position: fixed` node, or a descendant that
131    /// pushes into an ancestor's stacking context.
132    ///
133    /// Set while flushing, read to decide whether a subtree with no damage can
134    /// be skipped entirely. Without it the walk cannot be skipped at all: an
135    /// ancestor rebuilds its stacking context from scratch, so a subtree that
136    /// feeds it and is not walked simply vanishes from paint.
137    pub subtree_hoists: bool,
138
139    /// The computed values [`style`](Self::style) was built from, held alive.
140    ///
141    /// A taffy `Style` does not own its `calc()` values. `stylo_taffy` stores a
142    /// raw pointer to the stylo `CalcLengthPercentage`, and that value lives
143    /// inside these `ComputedValues`. Drop them while the taffy style survives
144    /// and the next layout dereferences freed memory. Keeping the arc here
145    /// keeps the pointee alive, and comparing its identity is how a restyle is
146    /// told apart from a no-op. See `flush_styles_to_layout_impl`.
147    pub style_source: Option<ServoArc<ComputedValues>>,
148    /// For a node outside the box tree (an `<option>`, SVG content), which is
149    /// never flushed and so never gets a `style_source`: the computed values
150    /// damage propagation last saw, so only a real restyle counts as one.
151    pub unflushed_style_seen: Option<ServoArc<ComputedValues>>,
152    pub display_constructed_as: StyloDisplay,
153    /// Taffy's layout cache, allocated on first use.
154    ///
155    /// `Box`ed rather than inline because it is **1616 bytes, 57% of this
156    /// whole struct**, and most nodes never use it. A `heap` census of a
157    /// running instance found 264,891 live allocations in the 3KB size class
158    /// totalling 814 MB, which is `ElementData` at 2848 bytes each; the cache
159    /// accounts for 428 MB of that.
160    ///
161    /// The cache is two fixed-size arrays (`measure_entries` and
162    /// `measure_inputs`, `CACHE_SIZE` slots each) plus a final-layout entry.
163    /// It is created for every node that can be addressed by taffy, but a
164    /// `display: none` subtree, a node that never reaches layout, and any
165    /// element before its first measurement all hold an entirely empty one.
166    ///
167    /// `None` means "no cache yet", which is observationally identical to an
168    /// empty cache: a lookup against an absent cache misses exactly as it
169    /// would against a present-but-empty one. Only the mutable accessor
170    /// allocates, so a node pays the 1616 bytes at the moment taffy first
171    /// stores a measurement for it, and never before.
172    cache: Option<Box<Cache>>,
173    pub unrounded_layout: Layout,
174    pub final_layout: Layout,
175    pub scroll_offset: crate::Point<f64>,
176    pub scrollable_overflow: KurboRect,
177    pub transform: Option<Affine>,
178}
179
180/// Data specific to the [`Document`](super::super::Document) root node.
181///
182/// The document node participates in layout and styling like an element, so it
183/// carries the same style/layout fields that were previously stored directly on
184/// [`Node`](super::Node).
185pub struct DocumentData {
186    pub content_type: &'static str,
187    /// Ownership index for detached documents. The live document retains its
188    /// existing ID map. Lookup verifies ancestry and current tree order.
189    pub ids: std::collections::HashMap<String, Vec<NodeId>>,
190    /// 0 is standards, 1 is limited quirks, and 2 is quirks.
191    pub quirks_mode: u8,
192    pub stylo_element_data: StyloData,
193    /// Selector flags deposited here by `apply_selector_flags` when a
194    /// `for_parent()` flag is applied while matching the root `<html>` element,
195    /// whose parent node is the document.
196    pub selector_flags: Cell<ElementSelectorFlags>,
197    /// A clone of the document's shared style lock. Set when the owning
198    /// [`Node`](super::Node) is constructed.
199    pub guard: Option<SharedRwLock>,
200    pub dirty_descendants: AtomicBool,
201    pub element_state: ElementState,
202    pub has_snapshot: bool,
203    pub snapshot_handled: AtomicBool,
204    pub style: Style<Atom>,
205    /// See [`ElementData::subtree_hoists`].
206    pub subtree_hoists: bool,
207
208    /// See [`ElementData::style_source`]. The document node is styled and laid
209    /// out like an element, so it carries the same hazard.
210    pub style_source: Option<ServoArc<ComputedValues>>,
211    pub display_constructed_as: StyloDisplay,
212    /// Taffy's layout cache, allocated on first use.
213    ///
214    /// `Box`ed rather than inline because it is **1616 bytes, 57% of this
215    /// whole struct**, and most nodes never use it. A `heap` census of a
216    /// running instance found 264,891 live allocations in the 3KB size class
217    /// totalling 814 MB, which is `ElementData` at 2848 bytes each; the cache
218    /// accounts for 428 MB of that.
219    ///
220    /// The cache is two fixed-size arrays (`measure_entries` and
221    /// `measure_inputs`, `CACHE_SIZE` slots each) plus a final-layout entry.
222    /// It is created for every node that can be addressed by taffy, but a
223    /// `display: none` subtree, a node that never reaches layout, and any
224    /// element before its first measurement all hold an entirely empty one.
225    ///
226    /// `None` means "no cache yet", which is observationally identical to an
227    /// empty cache: a lookup against an absent cache misses exactly as it
228    /// would against a present-but-empty one. Only the mutable accessor
229    /// allocates, so a node pays the 1616 bytes at the moment taffy first
230    /// stores a measurement for it, and never before.
231    cache: Option<Box<Cache>>,
232    pub unrounded_layout: Layout,
233    pub final_layout: Layout,
234    pub scroll_offset: crate::Point<f64>,
235    pub scrollable_overflow: KurboRect,
236    pub transform: Option<Affine>,
237}
238
239// Hand-written like `ElementData`'s, because `ElementSelectorFlags` does not
240// implement `Debug`. Every other field is still reported.
241impl std::fmt::Debug for DocumentData {
242    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243        f.debug_struct("DocumentData")
244            .field("content_type", &self.content_type)
245            .field("ids", &self.ids)
246            .field("quirks_mode", &self.quirks_mode)
247            .field("stylo_element_data", &self.stylo_element_data)
248            .field("guard", &self.guard)
249            .field("dirty_descendants", &self.dirty_descendants)
250            .field("element_state", &self.element_state)
251            .field("has_snapshot", &self.has_snapshot)
252            .field("snapshot_handled", &self.snapshot_handled)
253            .field("style", &self.style)
254            .field("display_constructed_as", &self.display_constructed_as)
255            .field("cache", &self.cache)
256            .field("unrounded_layout", &self.unrounded_layout)
257            .field("final_layout", &self.final_layout)
258            .field("scroll_offset", &self.scroll_offset)
259            .field("scrollable_overflow", &self.scrollable_overflow)
260            .field("transform", &self.transform)
261            .finish_non_exhaustive()
262    }
263}
264
265impl DocumentData {
266    /// As [`ElementData::cache`](ElementData::cache). There is only one
267    /// document node, so this saves nothing by itself; it exists so both node
268    /// kinds present the same accessor and the forwarding on
269    /// [`Node`](super::Node) stays uniform.
270    #[inline]
271    pub fn cache(&self) -> &Cache {
272        self.cache.as_deref().unwrap_or(&EMPTY_CACHE)
273    }
274
275    #[inline]
276    pub fn cache_mut(&mut self) -> &mut Cache {
277        self.cache.get_or_insert_with(|| Box::new(Cache::new()))
278    }
279
280    #[inline]
281    pub fn cache_release(&mut self) {
282        self.cache = None;
283    }
284
285    pub fn new() -> Self {
286        Self {
287            content_type: "text/html",
288            ids: Default::default(),
289            quirks_mode: 0,
290            stylo_element_data: Default::default(),
291            selector_flags: Cell::new(ElementSelectorFlags::empty()),
292            guard: None,
293            dirty_descendants: AtomicBool::new(true),
294            element_state: ElementState::empty(),
295            has_snapshot: false,
296            snapshot_handled: AtomicBool::new(false),
297            style: Default::default(),
298            style_source: None,
299            subtree_hoists: false,
300            display_constructed_as: StyloDisplay::Block,
301            cache: None,
302            unrounded_layout: Layout::new(),
303            final_layout: Layout::new(),
304            scroll_offset: crate::Point::ZERO,
305            scrollable_overflow: KurboRect::ZERO,
306            transform: None,
307        }
308    }
309}
310
311impl Default for DocumentData {
312    fn default() -> Self {
313        Self::new()
314    }
315}
316
317impl Clone for DocumentData {
318    fn clone(&self) -> Self {
319        // Runtime style/layout state is reset (the document node is not
320        // meaningfully cloneable), matching `ElementData`'s clone semantics.
321        Self {
322            content_type: self.content_type,
323            quirks_mode: self.quirks_mode,
324            guard: self.guard.clone(),
325            ..Self::new()
326        }
327    }
328}
329
330impl std::fmt::Debug for ElementData {
331    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
332        f.debug_struct("ElementData")
333            .field("name", &self.name)
334            .field("id", &self.id)
335            .field("attrs", &self.attrs)
336            .field("is_focussable", &self.is_focussable)
337            .field("style_attribute", &self.style_attribute)
338            .field("special_data", &self.special_data)
339            .field("background_images", &self.background_images)
340            .field("mask_images", &self.mask_images)
341            .field("inline_layout_data", &self.inline_layout_data)
342            .field("list_item_data", &self.list_item_data)
343            .field("template_contents", &self.template_contents)
344            .field("element_state", &self.element_state)
345            .field("display_constructed_as", &self.display_constructed_as)
346            .finish_non_exhaustive()
347    }
348}
349
350impl Clone for ElementData {
351    /// Clones the *content* of the element (name, attributes, style attribute,
352    /// special data, etc.). Runtime style/layout state (stylo data, taffy
353    /// layout, caches, pseudo-element ids, ...) is reset to its default so that
354    /// the clone behaves like a freshly-created element that has not yet been
355    /// styled or laid out.
356    fn clone(&self) -> Self {
357        Self {
358            name: self.name.clone(),
359            id: self.id.clone(),
360            attrs: self.attrs.clone(),
361            inline_event_attributes: self.inline_event_attributes.clone(),
362            is_focussable: self.is_focussable,
363            style_attribute: self.style_attribute.clone(),
364            special_data: self.special_data.clone(),
365            #[cfg(feature = "svg")]
366            svg_geometry: None,
367            background_images: self.background_images.clone(),
368            mask_images: self.mask_images.clone(),
369            inline_layout_data: self.inline_layout_data.clone(),
370            list_item_data: self.list_item_data.clone(),
371            template_contents: None,
372
373            // Runtime state: reset to defaults.
374            //
375            // A shadow root belongs to exactly one host, so a clone must not
376            // claim its original's, and slot assignment is recomputed from the
377            // flattened tree on the next resolve.
378            shadow_root: None,
379            assigned_slot: None,
380            stylo_element_data: Default::default(),
381            selector_flags: Cell::new(ElementSelectorFlags::empty()),
382            guard: self.guard.clone(),
383            element_state: self.element_state,
384            has_snapshot: false,
385            snapshot_handled: AtomicBool::new(false),
386            dirty_descendants: AtomicBool::new(true),
387            before: None,
388            after: None,
389            detailed_grid_info: None,
390            style: Default::default(),
391            style_source: None,
392            unflushed_style_seen: None,
393            subtree_hoists: false,
394            display_constructed_as: StyloDisplay::Block,
395            cache: None,
396            unrounded_layout: Layout::new(),
397            final_layout: Layout::new(),
398            scroll_offset: crate::Point::ZERO,
399            scrollable_overflow: KurboRect::ZERO,
400            transform: None,
401        }
402    }
403}
404
405#[derive(Copy, Clone, Default)]
406#[non_exhaustive]
407pub enum SpecialElementType {
408    Stylesheet,
409    Image,
410    Canvas,
411    TableRoot,
412    TextInput,
413    CheckboxInput,
414    Select,
415    #[cfg(feature = "file-input")]
416    FileInput,
417    #[default]
418    None,
419}
420
421/// Heterogeneous data that depends on the element's type.
422#[derive(Default)]
423pub enum SpecialElementData {
424    /// A sub-document such an \<iframe\> or \<web-view\> element
425    SubDocument(Box<dyn Document>),
426    /// A custom widget
427    #[cfg(feature = "custom-widget")]
428    CustomWidget(CustomWidgetData),
429    /// A custom element (a Rust object that controls an attached shadow DOM)
430    #[cfg(feature = "shadow-dom")]
431    CustomElement(CustomElementData),
432    /// A stylesheet
433    Stylesheet(DocumentStyleSheet),
434    /// An \<img\> element's image data
435    Image(Box<ImageData>),
436    /// A \<canvas\> element's custom paint source
437    Canvas(CanvasData),
438    /// Pre-computed table layout data
439    TableRoot(Arc<TableContext>),
440    /// Parley text editor (text inputs)
441    TextInput(TextInputData),
442    /// Checkbox checked state
443    CheckboxInput(bool),
444    /// A \<select\> element's selectedness and open state
445    Select(SelectData),
446    /// Selected files
447    #[cfg(feature = "file-input")]
448    FileInput(FileData),
449    /// No data (for nodes that don't need any node-specific data)
450    #[default]
451    None,
452}
453
454impl Clone for SpecialElementData {
455    fn clone(&self) -> Self {
456        match self {
457            Self::SubDocument(_) => Self::None, // TODO
458            #[cfg(feature = "custom-widget")]
459            Self::CustomWidget(_) => Self::None, // TODO
460            #[cfg(feature = "shadow-dom")]
461            Self::CustomElement(_) => Self::None, // TODO
462            Self::Stylesheet(data) => Self::Stylesheet(data.clone()),
463            Self::Image(data) => Self::Image(data.clone()),
464            Self::Canvas(data) => Self::Canvas(data.clone()),
465            Self::TableRoot(data) => Self::TableRoot(data.clone()),
466            Self::TextInput(data) => Self::TextInput(data.clone()),
467            Self::CheckboxInput(data) => Self::CheckboxInput(*data),
468            Self::Select(data) => Self::Select(data.clone()),
469            #[cfg(feature = "file-input")]
470            Self::FileInput(data) => Self::FileInput(data.clone()),
471            Self::None => Self::None,
472        }
473    }
474}
475
476impl SpecialElementData {
477    pub fn take(&mut self) -> Self {
478        std::mem::take(self)
479    }
480}
481
482/// A shared empty cache, handed out when a node has never been laid out.
483///
484/// Returning a reference to this rather than allocating keeps `cache()` an
485/// infallible `&Cache` for callers, which is what lets the lazy allocation stay
486/// invisible above this module. It is only ever read: a lookup against it
487/// misses, which is the same answer an owned empty cache would give. Nothing
488/// can write through a shared reference, so the `is_empty` fast path inside
489/// taffy stays correct for every node pointing here.
490static EMPTY_CACHE: Cache = Cache::new();
491
492impl ElementData {
493    /// This node's layout cache, or a shared empty one if it has never been
494    /// laid out. See the [`cache`](Self::cache) field for why it is optional.
495    #[inline]
496    pub fn cache(&self) -> &Cache {
497        self.cache.as_deref().unwrap_or(&EMPTY_CACHE)
498    }
499
500    /// This node's layout cache, allocating it if this is the first use.
501    ///
502    /// Every caller of this is taffy storing or clearing a measurement, which
503    /// is exactly the moment a node stops being one that never lays out.
504    #[inline]
505    pub fn cache_mut(&mut self) -> &mut Cache {
506        self.cache.get_or_insert_with(|| Box::new(Cache::new()))
507    }
508
509    /// Drop the cache entirely rather than emptying it in place.
510    ///
511    /// `cache_clear` runs on every node whose layout is invalidated, and the
512    /// old code zeroed 1616 bytes there. Releasing the box instead returns the
513    /// memory, and a node that is cleared and never re-measured (a subtree that
514    /// became `display: none`, say) stops paying for the cache at all.
515    #[inline]
516    pub fn cache_release(&mut self) {
517        self.cache = None;
518    }
519
520    pub fn new(name: QualName, attrs: Vec<Attribute>) -> Self {
521        let handlers: Vec<_> = attrs
522            .iter()
523            .filter(|attr| {
524                attr.name.ns == markup5ever::ns!() && attr.name.local.as_ref().starts_with("on")
525            })
526            .cloned()
527            .collect();
528        let inline_event_attributes = (!handlers.is_empty()).then(|| Box::new(handlers));
529        let id_attr_atom = attrs
530            .iter()
531            .find(|attr| &attr.name.local == "id")
532            .map(|attr| attr.value.as_ref())
533            .map(|value: &str| Atom::from(value));
534
535        let mut data = ElementData {
536            name,
537            id: id_attr_atom,
538            attrs: Attributes::new(attrs),
539            inline_event_attributes,
540            is_focussable: false,
541            style_attribute: Default::default(),
542            inline_layout_data: None,
543            list_item_data: None,
544            special_data: SpecialElementData::None,
545            #[cfg(feature = "svg")]
546            svg_geometry: None,
547            template_contents: None,
548            shadow_root: None,
549            assigned_slot: None,
550            background_images: Vec::new(),
551            mask_images: Vec::new(),
552
553            stylo_element_data: Default::default(),
554            selector_flags: Cell::new(ElementSelectorFlags::empty()),
555            guard: None,
556            element_state: ElementState::empty(),
557            has_snapshot: false,
558            snapshot_handled: AtomicBool::new(false),
559            dirty_descendants: AtomicBool::new(true),
560            before: None,
561            after: None,
562            detailed_grid_info: None,
563            style: Default::default(),
564            style_source: None,
565            unflushed_style_seen: None,
566            subtree_hoists: false,
567            display_constructed_as: StyloDisplay::Block,
568            cache: None,
569            unrounded_layout: Layout::new(),
570            final_layout: Layout::new(),
571            scroll_offset: crate::Point::ZERO,
572            scrollable_overflow: KurboRect::ZERO,
573            transform: None,
574        };
575        data.flush_is_focussable();
576
577        // The element state needs to be modified if the element can be disabled.
578        if data.can_be_disabled() {
579            data.element_state
580                .insert(match data.has_attr(local_name!("disabled")) {
581                    true => ElementState::DISABLED,
582                    false => ElementState::ENABLED,
583                });
584        }
585
586        data
587    }
588
589    pub fn attrs(&self) -> &[Attribute] {
590        &self.attrs
591    }
592
593    pub fn attr(&self, name: impl PartialEq<LocalName>) -> Option<&str> {
594        let attr = self
595            .attrs
596            .iter()
597            .find(|attr| attr.name.ns == markup5ever::ns!() && name == attr.name.local)?;
598        Some(&attr.value)
599    }
600
601    /// SVG links accept xlink:href, with an unnamespaced href taking precedence.
602    pub fn link_href(&self) -> Option<&str> {
603        self.attr(local_name!("href")).or_else(|| {
604            if self.name.ns != markup5ever::ns!(svg) {
605                return None;
606            }
607            self.attrs()
608                .iter()
609                .find(|attr| {
610                    attr.name.ns == markup5ever::ns!(xlink)
611                        && attr.name.local == local_name!("href")
612                })
613                .map(|attr| attr.value.as_ref())
614        })
615    }
616
617    pub fn attr_parsed<T: FromStr>(&self, name: impl PartialEq<LocalName>) -> Option<T> {
618        let attr = self
619            .attrs
620            .iter()
621            .find(|attr| attr.name.ns == markup5ever::ns!() && name == attr.name.local)?;
622        attr.value.parse::<T>().ok()
623    }
624
625    /// Detects the presence of the attribute, treating *any* value as truthy.
626    pub fn has_attr(&self, name: impl PartialEq<LocalName>) -> bool {
627        self.attrs
628            .iter()
629            .any(|attr| attr.name.ns == markup5ever::ns!() && name == attr.name.local)
630    }
631
632    pub fn can_be_disabled(&self) -> bool {
633        local_names!("button", "input", "select", "textarea").contains(&self.name.local)
634    }
635
636    pub fn image_data(&self) -> Option<&ImageData> {
637        match &self.special_data {
638            SpecialElementData::Image(data) => Some(&**data),
639            _ => None,
640        }
641    }
642
643    pub fn image_data_mut(&mut self) -> Option<&mut ImageData> {
644        match self.special_data {
645            SpecialElementData::Image(ref mut data) => Some(&mut **data),
646            _ => None,
647        }
648    }
649
650    pub fn raster_image_data(&self) -> Option<&RasterImageData> {
651        match self.image_data()? {
652            ImageData::Raster(data) => Some(data),
653            _ => None,
654        }
655    }
656
657    pub fn raster_image_data_mut(&mut self) -> Option<&mut RasterImageData> {
658        match self.image_data_mut()? {
659            ImageData::Raster(data) => Some(data),
660            _ => None,
661        }
662    }
663
664    pub fn canvas_data(&self) -> Option<&CanvasData> {
665        match &self.special_data {
666            SpecialElementData::Canvas(data) => Some(data),
667            _ => None,
668        }
669    }
670
671    pub fn sub_doc_data(&self) -> Option<&dyn Document> {
672        match &self.special_data {
673            SpecialElementData::SubDocument(data) => Some(data.as_ref()),
674            _ => None,
675        }
676    }
677
678    pub fn sub_doc_data_mut(&mut self) -> Option<&mut dyn Document> {
679        match &mut self.special_data {
680            SpecialElementData::SubDocument(data) => Some(data.as_mut()),
681            _ => None,
682        }
683    }
684
685    #[cfg(feature = "svg")]
686    pub fn svg_data(&self) -> Option<&usvg::Tree> {
687        match self.image_data()? {
688            ImageData::Svg(data) => Some(&data.tree),
689            _ => None,
690        }
691    }
692
693    pub fn text_input_data(&self) -> Option<&TextInputData> {
694        match &self.special_data {
695            SpecialElementData::TextInput(data) => Some(data),
696            _ => None,
697        }
698    }
699
700    pub fn text_input_data_mut(&mut self) -> Option<&mut TextInputData> {
701        match &mut self.special_data {
702            SpecialElementData::TextInput(data) => Some(data),
703            _ => None,
704        }
705    }
706
707    #[cfg(feature = "custom-widget")]
708    pub fn custom_widget_data(&self) -> Option<&CustomWidgetData> {
709        match &self.special_data {
710            SpecialElementData::CustomWidget(data) => Some(data),
711            _ => None,
712        }
713    }
714
715    #[cfg(feature = "custom-widget")]
716    pub fn custom_widget_data_mut(&mut self) -> Option<&mut CustomWidgetData> {
717        match &mut self.special_data {
718            SpecialElementData::CustomWidget(data) => Some(data),
719            _ => None,
720        }
721    }
722
723    #[cfg(feature = "shadow-dom")]
724    pub fn custom_element_data(&self) -> Option<&CustomElementData> {
725        match &self.special_data {
726            SpecialElementData::CustomElement(data) => Some(data),
727            _ => None,
728        }
729    }
730
731    #[cfg(feature = "shadow-dom")]
732    pub fn custom_element_data_mut(&mut self) -> Option<&mut CustomElementData> {
733        match &mut self.special_data {
734            SpecialElementData::CustomElement(data) => Some(data),
735            _ => None,
736        }
737    }
738
739    pub fn checkbox_input_checked(&self) -> Option<bool> {
740        match self.special_data {
741            SpecialElementData::CheckboxInput(checked) => Some(checked),
742            _ => None,
743        }
744    }
745
746    pub fn checkbox_input_checked_mut(&mut self) -> Option<&mut bool> {
747        match self.special_data {
748            SpecialElementData::CheckboxInput(ref mut checked) => Some(checked),
749            _ => None,
750        }
751    }
752
753    /// The live state of a `<select>`, or `None` for anything else.
754    ///
755    /// Deliberately not modelled the way `checkbox_input_checked` is: that one
756    /// hands back an `Option<bool>` whose `None` means both "not a checkbox"
757    /// and, to every caller that reaches for `unwrap_or(false)`, "unchecked".
758    /// A select's answer to "what is selected" is a whole vector, so the
759    /// borrow is handed out instead and the two questions stay apart.
760    pub fn select_data(&self) -> Option<&SelectData> {
761        match &self.special_data {
762            SpecialElementData::Select(data) => Some(data),
763            _ => None,
764        }
765    }
766
767    pub fn select_data_mut(&mut self) -> Option<&mut SelectData> {
768        match &mut self.special_data {
769            SpecialElementData::Select(data) => Some(data),
770            _ => None,
771        }
772    }
773
774    #[cfg(feature = "file-input")]
775    pub fn file_data(&self) -> Option<&FileData> {
776        match &self.special_data {
777            SpecialElementData::FileInput(data) => Some(data),
778            _ => None,
779        }
780    }
781
782    #[cfg(feature = "file-input")]
783    pub fn file_data_mut(&mut self) -> Option<&mut FileData> {
784        match &mut self.special_data {
785            SpecialElementData::FileInput(data) => Some(data),
786            _ => None,
787        }
788    }
789
790    pub fn flush_is_focussable(&mut self) {
791        let disabled: bool = self.attr_parsed(local_name!("disabled")).unwrap_or(false);
792        let tabindex: Option<i32> = self.attr_parsed(local_name!("tabindex"));
793        let contains_sub_document: bool = self.sub_doc_data().is_some();
794
795        self.is_focussable = contains_sub_document
796            || (!disabled
797                && match tabindex {
798                    Some(index) => index >= 0,
799                    None => {
800                        // Some focusable HTML elements have a default tabindex value of 0 set under the hood by the user agent.
801                        // These elements are:
802                        //   - <a> or <area> with href attribute
803                        //   - <button>, <frame>, <iframe>, <input>, <object>, <select>, <textarea>, and SVG <a> element
804                        //   - <summary> element that provides summary for a <details> element.
805
806                        if [local_name!("a"), local_name!("area")].contains(&self.name.local) {
807                            self.attr(local_name!("href")).is_some()
808                        } else {
809                            const DEFAULT_FOCUSSABLE_ELEMENTS: [LocalName; 7] = [
810                                local_name!("button"),
811                                local_name!("input"),
812                                local_name!("select"),
813                                local_name!("textarea"),
814                                local_name!("frame"),
815                                local_name!("iframe"),
816                                local_name!("summary"),
817                            ];
818                            DEFAULT_FOCUSSABLE_ELEMENTS.contains(&self.name.local)
819                        }
820                    }
821                })
822    }
823
824    pub fn flush_style_attribute(&mut self, guard: &SharedRwLock, url_extra_data: &UrlExtraData) {
825        self.style_attribute = self.attr(local_name!("style")).map(|style_str| {
826            ServoArc::new(guard.wrap(parse_style_attribute(
827                style_str,
828                url_extra_data,
829                None,
830                QuirksMode::NoQuirks,
831                CssRuleType::Style,
832            )))
833        });
834    }
835
836    pub fn set_style_property(
837        &mut self,
838        name: &str,
839        value: &str,
840        guard: &SharedRwLock,
841        url_extra_data: UrlExtraData,
842    ) -> bool {
843        let context = ParserContext::new(
844            Origin::Author,
845            &url_extra_data,
846            Some(CssRuleType::Style),
847            ParsingMode::DEFAULT,
848            QuirksMode::NoQuirks,
849            /* namespaces = */ Default::default(),
850            None,
851            None,
852            /* attr_taint = */ Default::default(),
853        );
854
855        let Ok(property_id) = PropertyId::parse(name, &context) else {
856            #[cfg(feature = "tracing")]
857            tracing::warn!(property = name, "Unsupported property");
858            return false;
859        };
860        let mut source_property_declaration = SourcePropertyDeclaration::default();
861        let mut input = ParserInput::new(value);
862        let mut parser = style::values::Parser::new(&mut input);
863        let Ok(_) = PropertyDeclaration::parse_into(
864            &mut source_property_declaration,
865            property_id,
866            &context,
867            &mut parser,
868        ) else {
869            #[cfg(feature = "tracing")]
870            tracing::warn!(property = name, value, "Invalid property value");
871            return false;
872        };
873
874        if self.style_attribute.is_none() {
875            self.style_attribute = Some(ServoArc::new(guard.wrap(PropertyDeclarationBlock::new())));
876        }
877        self.style_attribute
878            .as_mut()
879            .unwrap()
880            .write_with(&mut guard.write())
881            .extend(source_property_declaration.drain(), Importance::Normal);
882
883        true
884    }
885
886    pub fn remove_style_property(
887        &mut self,
888        name: &str,
889        guard: &SharedRwLock,
890        url_extra_data: UrlExtraData,
891    ) -> bool {
892        let context = ParserContext::new(
893            Origin::Author,
894            &url_extra_data,
895            Some(CssRuleType::Style),
896            ParsingMode::DEFAULT,
897            QuirksMode::NoQuirks,
898            /* namespaces = */ Default::default(),
899            None,
900            None,
901            /* attr_taint = */ Default::default(),
902        );
903        let Ok(property_id) = PropertyId::parse(name, &context) else {
904            #[cfg(feature = "tracing")]
905            tracing::warn!(property = name, "Unsupported property");
906            return false;
907        };
908
909        if let Some(style) = &mut self.style_attribute {
910            let mut guard = guard.write();
911            let style = style.write_with(&mut guard);
912            if let Some(index) = style.first_declaration_to_remove(&property_id) {
913                style.remove_property(&property_id, index);
914                return true;
915            }
916        }
917
918        false
919    }
920
921    pub fn set_sub_document(&mut self, sub_document: Box<dyn Document>) {
922        self.special_data = SpecialElementData::SubDocument(sub_document);
923    }
924
925    pub fn remove_sub_document(&mut self) {
926        self.special_data = SpecialElementData::None;
927    }
928
929    #[cfg(feature = "custom-widget")]
930    pub fn set_custom_widget(&mut self, widget: Box<dyn crate::Widget>) {
931        use crate::node::custom_widget::CustomWidgetData;
932        self.special_data = SpecialElementData::CustomWidget(CustomWidgetData::new(widget));
933    }
934
935    #[cfg(feature = "custom-widget")]
936    pub fn remove_custom_widget(&mut self) -> Vec<anyrender::ResourceId> {
937        let resource_ids = self
938            .custom_widget_data_mut()
939            .map(|widget_data| widget_data.take_resource_ids())
940            .unwrap_or_default();
941        self.special_data = SpecialElementData::None;
942        resource_ids
943    }
944
945    pub fn take_inline_layout(&mut self) -> Option<Box<TextLayout>> {
946        std::mem::take(&mut self.inline_layout_data)
947    }
948
949    pub fn is_submit_button(&self) -> bool {
950        if self.name.local != local_name!("button") {
951            return false;
952        }
953        let type_attr = self.attr(local_name!("type"));
954        let is_submit = type_attr == Some("submit");
955        let is_auto_submit = type_attr.is_none()
956            && self.attr(LocalName::from("command")).is_none()
957            && self.attr(LocalName::from("commandfor")).is_none();
958        is_submit || is_auto_submit
959    }
960}
961
962#[derive(Debug, Clone, PartialEq)]
963pub struct RasterImageData {
964    /// The width of the image
965    pub width: u32,
966    /// The height of the image
967    pub height: u32,
968    /// The raw image data in RGBA8 format
969    pub data: Blob<u8>,
970}
971impl RasterImageData {
972    pub fn new(width: u32, height: u32, data: Arc<Vec<u8>>) -> Self {
973        Self {
974            width,
975            height,
976            data: Blob::new(data),
977        }
978    }
979}
980
981/// A parsed SVG image.
982///
983/// usvg always resolves the root `<svg>` to a concrete [`usvg::Tree::size`],
984/// falling back to the `viewBox` size when `width`/`height` are absent or given
985/// as percentages. For CSS sizing purposes, however, such an SVG has *no*
986/// intrinsic width/height (only an intrinsic aspect ratio). The accessors on
987/// this type resolve the CSS intrinsic dimensions lazily from
988/// [`usvg::Tree::intrinsic_dimensions`], which preserves what was actually
989/// declared on the root element.
990#[cfg(feature = "svg")]
991#[derive(Debug, Clone)]
992pub struct SvgImageData {
993    /// The parsed SVG tree.
994    pub tree: Arc<usvg::Tree>,
995}
996
997#[cfg(feature = "svg")]
998impl SvgImageData {
999    /// The intrinsic width in CSS px, present only when the root `<svg>`
1000    /// declared an absolute (non-percentage) `width`.
1001    pub fn intrinsic_width(&self) -> Option<f32> {
1002        use usvg::svgtypes::LengthUnit;
1003        let declared = self
1004            .tree
1005            .intrinsic_dimensions()
1006            .width
1007            .is_some_and(|len| len.unit != LengthUnit::Percent);
1008        declared.then(|| self.tree.size().width())
1009    }
1010
1011    /// The intrinsic height in CSS px, present only when the root `<svg>`
1012    /// declared an absolute (non-percentage) `height`.
1013    pub fn intrinsic_height(&self) -> Option<f32> {
1014        use usvg::svgtypes::LengthUnit;
1015        let declared = self
1016            .tree
1017            .intrinsic_dimensions()
1018            .height
1019            .is_some_and(|len| len.unit != LengthUnit::Percent);
1020        declared.then(|| self.tree.size().height())
1021    }
1022
1023    /// The aspect ratio of the root `<svg>`'s `viewBox`, if it declares one.
1024    pub fn viewbox_aspect_ratio(&self) -> Option<f32> {
1025        self.tree
1026            .intrinsic_dimensions()
1027            .view_box
1028            .map(|vb| vb.width() / vb.height())
1029    }
1030
1031    /// The root `width` attribute resolved against a containing block width:
1032    /// percentages resolve against the containing block (`None` if it is
1033    /// indefinite) and an absent attribute is `None`.
1034    ///
1035    /// This is only appropriate for an inline `<svg>` element, where the
1036    /// attributes behave as presentation attributes. SVG used as an image
1037    /// (e.g. `<img src>` or a background) must use [`Self::intrinsic_width`],
1038    /// as its intrinsic dimensions are context-free per CSS.
1039    pub fn resolved_width(&self, container_width: Option<f32>) -> Option<f32> {
1040        use usvg::svgtypes::LengthUnit;
1041        match self.tree.intrinsic_dimensions().width {
1042            Some(len) if len.unit != LengthUnit::Percent => Some(self.tree.size().width()),
1043            Some(len) => container_width.map(|cw| cw * (len.number as f32) / 100.0),
1044            None => None,
1045        }
1046    }
1047
1048    /// The root `height` attribute resolved against a containing block height.
1049    /// See [`Self::resolved_width`].
1050    pub fn resolved_height(&self, container_height: Option<f32>) -> Option<f32> {
1051        use usvg::svgtypes::LengthUnit;
1052        match self.tree.intrinsic_dimensions().height {
1053            Some(len) if len.unit != LengthUnit::Percent => Some(self.tree.size().height()),
1054            Some(len) => container_height.map(|ch| ch * (len.number as f32) / 100.0),
1055            None => None,
1056        }
1057    }
1058
1059    /// The intrinsic aspect ratio of the SVG: the ratio of its declared
1060    /// `width`/`height` when both are absolute lengths, otherwise the
1061    /// `viewBox` ratio, otherwise the ratio of the resolved
1062    /// [`usvg::Tree::size`] (which is always non-zero).
1063    pub fn aspect_ratio(&self) -> f32 {
1064        match (self.intrinsic_width(), self.intrinsic_height()) {
1065            (Some(w), Some(h)) => w / h,
1066            _ => self.viewbox_aspect_ratio().unwrap_or_else(|| {
1067                let size = self.tree.size();
1068                size.width() / size.height()
1069            }),
1070        }
1071    }
1072
1073    /// The intrinsic dimensions of the SVG resolved per CSS replaced element
1074    /// sizing: a missing dimension is computed from the declared one and the
1075    /// intrinsic aspect ratio; if neither is declared, the resolved
1076    /// [`usvg::Tree::size`] is used as a fallback.
1077    pub fn intrinsic_size(&self) -> (f32, f32) {
1078        let aspect_ratio = self.aspect_ratio();
1079        match (self.intrinsic_width(), self.intrinsic_height()) {
1080            (Some(w), Some(h)) => (w, h),
1081            (Some(w), None) => (w, w / aspect_ratio),
1082            (None, Some(h)) => (h * aspect_ratio, h),
1083            (None, None) => {
1084                // No intrinsic dimensions. If there is an intrinsic aspect ratio, apply
1085                // the CSS default sizing algorithm: contain within the default object
1086                // size of 300x150. Otherwise fall back to the resolved tree size.
1087                if self.viewbox_aspect_ratio().is_some() {
1088                    let scale = (300.0 / aspect_ratio).min(150.0);
1089                    (scale * aspect_ratio, scale)
1090                } else {
1091                    let size = self.tree.size();
1092                    (size.width(), size.height())
1093                }
1094            }
1095        }
1096    }
1097}
1098
1099#[derive(Debug, Clone)]
1100pub enum ImageData {
1101    Raster(RasterImageData),
1102    #[cfg(feature = "svg")]
1103    Svg(SvgImageData),
1104    None,
1105}
1106
1107#[derive(Debug, Clone, PartialEq)]
1108pub enum Status {
1109    Ok,
1110    Error,
1111    Loading,
1112}
1113
1114#[derive(Debug, Clone)]
1115pub struct ImageResourceData {
1116    /// The url of the background image
1117    pub url: ServoArc<Url>,
1118    /// The loading status of the background image
1119    pub status: Status,
1120    /// The image data
1121    pub image: ImageData,
1122}
1123
1124impl ImageResourceData {
1125    pub fn new(url: ServoArc<Url>) -> Self {
1126        Self {
1127            url,
1128            status: Status::Loading,
1129            image: ImageData::None,
1130        }
1131    }
1132}
1133
1134#[derive(Debug, Clone)]
1135pub struct CanvasData {
1136    pub custom_paint_source_id: u64,
1137}
1138
1139impl std::fmt::Debug for SpecialElementData {
1140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1141        match self {
1142            SpecialElementData::SubDocument(_) => f.write_str("NodeSpecificData::SubDocument"),
1143            #[cfg(feature = "custom-widget")]
1144            SpecialElementData::CustomWidget(_) => f.write_str("NodeSpecificData::CustomWidget"),
1145            #[cfg(feature = "shadow-dom")]
1146            SpecialElementData::CustomElement(_) => f.write_str("NodeSpecificData::CustomElement"),
1147            SpecialElementData::Stylesheet(_) => f.write_str("NodeSpecificData::Stylesheet"),
1148            SpecialElementData::Image(data) => match **data {
1149                ImageData::Raster(_) => f.write_str("NodeSpecificData::Image(Raster)"),
1150                #[cfg(feature = "svg")]
1151                ImageData::Svg(_) => f.write_str("NodeSpecificData::Image(Svg)"),
1152                ImageData::None => f.write_str("NodeSpecificData::Image(None)"),
1153            },
1154            SpecialElementData::Canvas(_) => f.write_str("NodeSpecificData::Canvas"),
1155            SpecialElementData::TableRoot(_) => f.write_str("NodeSpecificData::TableRoot"),
1156            SpecialElementData::TextInput(_) => f.write_str("NodeSpecificData::TextInput"),
1157            SpecialElementData::CheckboxInput(_) => f.write_str("NodeSpecificData::CheckboxInput"),
1158            SpecialElementData::Select(data) => {
1159                write!(f, "NodeSpecificData::Select({data:?})")
1160            }
1161            #[cfg(feature = "file-input")]
1162            SpecialElementData::FileInput(_) => f.write_str("NodeSpecificData::FileInput"),
1163            SpecialElementData::None => f.write_str("NodeSpecificData::None"),
1164        }
1165    }
1166}
1167
1168#[derive(Clone)]
1169pub struct ListItemLayout {
1170    pub marker: Marker,
1171    pub position: ListItemLayoutPosition,
1172}
1173
1174//We seperate chars from strings in order to optimise rendering - ie not needing to
1175//construct a whole parley layout for simple char markers
1176#[derive(Debug, PartialEq, Clone)]
1177pub enum Marker {
1178    Char(char),
1179    String(String),
1180}
1181
1182//Value depends on list-style-position, determining whether a seperate layout is created for it
1183#[derive(Clone)]
1184pub enum ListItemLayoutPosition {
1185    Inside,
1186    Outside(Box<parley::Layout<TextBrush>>),
1187}
1188
1189impl std::fmt::Debug for ListItemLayout {
1190    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1191        write!(f, "ListItemLayout - marker {:?}", self.marker)
1192    }
1193}
1194
1195#[cfg(feature = "file-input")]
1196mod file_data {
1197    use std::ops::{Deref, DerefMut};
1198    use std::path::PathBuf;
1199
1200    #[derive(Clone, Debug)]
1201    pub struct FileData(pub Vec<PathBuf>);
1202    impl Deref for FileData {
1203        type Target = Vec<PathBuf>;
1204
1205        fn deref(&self) -> &Self::Target {
1206            &self.0
1207        }
1208    }
1209    impl DerefMut for FileData {
1210        fn deref_mut(&mut self) -> &mut Self::Target {
1211            &mut self.0
1212        }
1213    }
1214    impl From<Vec<PathBuf>> for FileData {
1215        fn from(files: Vec<PathBuf>) -> Self {
1216            Self(files)
1217        }
1218    }
1219}
1220#[cfg(feature = "file-input")]
1221pub use file_data::FileData;
1222
1223#[cfg(test)]
1224mod tests {
1225    use super::{ElementData, TextInputData};
1226    use parley::{FontContext, LayoutContext};
1227
1228    /// `ElementData` is allocated once per element, so its size is a direct
1229    /// multiplier on the DOM's footprint.
1230    ///
1231    /// A `heap` census of a running instance found 264,891 live allocations in
1232    /// the 3KB malloc class totalling **814 MB**, which was this struct at
1233    /// 2848 bytes each. Boxing the taffy cache took it to 1240 and out of that
1234    /// size class entirely.
1235    ///
1236    /// The bound is deliberately loose and one-sided: it does not care what
1237    /// the exact number is, only that nobody puts a kilobyte back inline
1238    /// without noticing. If a field genuinely has to grow past this, raise the
1239    /// bound and say why, rather than deleting the test.
1240    #[test]
1241    fn element_data_stays_out_of_the_large_size_classes() {
1242        let size = std::mem::size_of::<ElementData>();
1243        assert!(
1244            size <= 1536,
1245            "ElementData grew to {size} bytes. It is allocated per element, so \
1246             this is a multiplier on the whole DOM's footprint: at 2848 bytes \
1247             it was 814 MB of a live instance. Box the new field instead, as \
1248             `cache` is."
1249        );
1250    }
1251
1252    /// The cache is what was boxed, so its absence has to stay cheap.
1253    ///
1254    /// If this ever fails, the `Option<Box<Cache>>` has been flattened back to
1255    /// an inline `Cache` and the 428 MB is back.
1256    #[test]
1257    fn an_unlaid_out_element_carries_no_cache() {
1258        let data = ElementData::new(
1259            markup5ever::QualName::new(None, markup5ever::ns!(html), "div".into()),
1260            Vec::new(),
1261        );
1262        assert!(
1263            std::mem::size_of_val(&data) < std::mem::size_of::<taffy::Cache>() * 2,
1264            "the cache must not be inline: a fresh element should be smaller \
1265             than two caches"
1266        );
1267    }
1268
1269    /// Build a [`TextInputData`] with the given text laid out at scale 1.0.
1270    fn make_input(is_multiline: bool, text: &str) -> TextInputData {
1271        let mut font_ctx = FontContext::new();
1272        let mut layout_ctx = LayoutContext::new();
1273        let mut data = TextInputData::new(is_multiline);
1274        data.editor.set_scale(1.0);
1275        data.editor.set_text(text);
1276        data.editor
1277            .driver(&mut font_ctx, &mut layout_ctx)
1278            .refresh_layout();
1279        data
1280    }
1281
1282    #[test]
1283    fn short_text_does_not_scroll() {
1284        let mut data = make_input(false, "hi");
1285        // A wide content box that comfortably fits the text.
1286        data.clamp_scroll_offset(1000.0, 100.0);
1287        assert_eq!(data.scroll_offset, 0.0);
1288    }
1289
1290    #[test]
1291    fn single_line_scrolls_to_follow_caret() {
1292        let text = "the quick brown fox jumps over the lazy dog repeatedly and at length";
1293        let mut data = make_input(false, text);
1294        let content_box_width = 40.0;
1295        let content_box_height = 20.0;
1296
1297        // Caret at the end of a string that overflows a narrow input should scroll right.
1298        data.editor
1299            .driver(&mut FontContext::new(), &mut LayoutContext::new())
1300            .move_to_text_end();
1301        data.clamp_scroll_offset(content_box_width, content_box_height);
1302
1303        let layout_width = data.editor.try_layout().unwrap().full_width();
1304        if layout_width > content_box_width {
1305            assert!(
1306                data.scroll_offset > 0.0,
1307                "expected horizontal scroll for overflowing single-line input"
1308            );
1309            // The caret must be within the visible region after scrolling.
1310            let caret = data.editor.cursor_geometry(1.5).unwrap();
1311            assert!(caret.x1 as f32 <= data.scroll_offset + content_box_width + 0.5);
1312            assert!(caret.x0 as f32 >= data.scroll_offset - 0.5);
1313        }
1314
1315        // Moving the caret back to the start should reset the scroll offset.
1316        data.editor
1317            .driver(&mut FontContext::new(), &mut LayoutContext::new())
1318            .move_to_text_start();
1319        data.clamp_scroll_offset(content_box_width, content_box_height);
1320        assert_eq!(data.scroll_offset, 0.0);
1321    }
1322
1323    #[test]
1324    fn multiline_scrolls_vertically_not_horizontally() {
1325        let text = (0..40)
1326            .map(|i| format!("line {i}"))
1327            .collect::<Vec<_>>()
1328            .join("\n");
1329        let mut data = make_input(true, &text);
1330        // Constrain the width so wrapping is well-defined.
1331        data.editor.set_width(Some(200.0));
1332        data.editor
1333            .driver(&mut FontContext::new(), &mut LayoutContext::new())
1334            .refresh_layout();
1335
1336        let content_box_width = 200.0;
1337        let content_box_height = 30.0;
1338
1339        data.editor
1340            .driver(&mut FontContext::new(), &mut LayoutContext::new())
1341            .move_to_text_end();
1342        data.clamp_scroll_offset(content_box_width, content_box_height);
1343
1344        let layout_height = data.editor.try_layout().unwrap().height();
1345        if layout_height > content_box_height {
1346            assert!(
1347                data.scroll_offset > 0.0,
1348                "expected vertical scroll for overflowing multi-line input"
1349            );
1350        }
1351    }
1352
1353    /// macOS only: the scrollable extent it clamps against comes from shaped
1354    /// text, so with no font registered there is nothing to scroll and the
1355    /// clamp has no work to do. See the target-scoped `parley` dev-dependency
1356    /// in this crate's manifest.
1357    #[cfg(target_os = "macos")]
1358    #[test]
1359    fn scroll_by_clamps_and_bubbles() {
1360        let text = (0..40)
1361            .map(|i| format!("line {i}"))
1362            .collect::<Vec<_>>()
1363            .join("\n");
1364        let mut data = make_input(true, &text);
1365        data.editor.set_width(Some(200.0));
1366        data.editor
1367            .driver(&mut FontContext::new(), &mut LayoutContext::new())
1368            .refresh_layout();
1369
1370        let content_box_width = 200.0;
1371        let content_box_height = 30.0;
1372        let max = data.max_scroll_offset(content_box_width, content_box_height);
1373        assert!(max > 0.0, "test text should overflow the content box");
1374
1375        // Scrolling up (positive delta decreases offset) while already at the top is a no-op and
1376        // the whole delta bubbles.
1377        assert_eq!(data.scroll_offset, 0.0);
1378        let bubbled = data.scroll_by(15.0, content_box_width, content_box_height);
1379        assert_eq!(data.scroll_offset, 0.0);
1380        assert_eq!(bubbled, 15.0);
1381
1382        // Scrolling down moves the offset and consumes the delta.
1383        let bubbled = data.scroll_by(-10.0, content_box_width, content_box_height);
1384        assert_eq!(data.scroll_offset, 10.0);
1385        assert_eq!(bubbled, 0.0);
1386
1387        // Scrolling past the end clamps to the maximum and bubbles the remainder. Starting at
1388        // offset 10 with max headroom of `max - 10`, a delta of `-(max + 100)` consumes
1389        // `max - 10` and bubbles the rest (`-110`).
1390        let bubbled = data.scroll_by(-(max + 100.0), content_box_width, content_box_height);
1391        assert_eq!(data.scroll_offset, max);
1392        assert!((bubbled - (-110.0)).abs() < 1e-3);
1393    }
1394
1395    #[test]
1396    fn single_line_does_not_scroll_when_text_fits() {
1397        let mut data = make_input(false, "hi");
1398        // Wide content box; nothing to scroll, so all delta bubbles.
1399        let bubbled = data.scroll_by(-50.0, 1000.0, 100.0);
1400        assert_eq!(data.scroll_offset, 0.0);
1401        assert_eq!(bubbled, -50.0);
1402    }
1403}