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