Skip to main content

blitz_dom/node/
element.rs

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