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    /// 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    Select,
387    #[cfg(feature = "file-input")]
388    FileInput,
389    #[default]
390    None,
391}
392
393/// Heterogeneous data that depends on the element's type.
394#[derive(Default)]
395pub enum SpecialElementData {
396    /// A sub-document such an \<iframe\> or \<web-view\> element
397    SubDocument(Box<dyn Document>),
398    /// A custom widget
399    #[cfg(feature = "custom-widget")]
400    CustomWidget(CustomWidgetData),
401    /// A custom element (a Rust object that controls an attached shadow DOM)
402    #[cfg(feature = "shadow-dom")]
403    CustomElement(CustomElementData),
404    /// A stylesheet
405    Stylesheet(DocumentStyleSheet),
406    /// An \<img\> element's image data
407    Image(Box<ImageData>),
408    /// A \<canvas\> element's custom paint source
409    Canvas(CanvasData),
410    /// Pre-computed table layout data
411    TableRoot(Arc<TableContext>),
412    /// Parley text editor (text inputs)
413    TextInput(TextInputData),
414    /// Checkbox checked state
415    CheckboxInput(bool),
416    /// A \<select\> element's selectedness and open state
417    Select(SelectData),
418    /// Selected files
419    #[cfg(feature = "file-input")]
420    FileInput(FileData),
421    /// No data (for nodes that don't need any node-specific data)
422    #[default]
423    None,
424}
425
426impl Clone for SpecialElementData {
427    fn clone(&self) -> Self {
428        match self {
429            Self::SubDocument(_) => Self::None, // TODO
430            #[cfg(feature = "custom-widget")]
431            Self::CustomWidget(_) => Self::None, // TODO
432            #[cfg(feature = "shadow-dom")]
433            Self::CustomElement(_) => Self::None, // TODO
434            Self::Stylesheet(data) => Self::Stylesheet(data.clone()),
435            Self::Image(data) => Self::Image(data.clone()),
436            Self::Canvas(data) => Self::Canvas(data.clone()),
437            Self::TableRoot(data) => Self::TableRoot(data.clone()),
438            Self::TextInput(data) => Self::TextInput(data.clone()),
439            Self::CheckboxInput(data) => Self::CheckboxInput(*data),
440            Self::Select(data) => Self::Select(data.clone()),
441            #[cfg(feature = "file-input")]
442            Self::FileInput(data) => Self::FileInput(data.clone()),
443            Self::None => Self::None,
444        }
445    }
446}
447
448impl SpecialElementData {
449    pub fn take(&mut self) -> Self {
450        std::mem::take(self)
451    }
452}
453
454/// A shared empty cache, handed out when a node has never been laid out.
455///
456/// Returning a reference to this rather than allocating keeps `cache()` an
457/// infallible `&Cache` for callers, which is what lets the lazy allocation stay
458/// invisible above this module. It is only ever read: a lookup against it
459/// misses, which is the same answer an owned empty cache would give. Nothing
460/// can write through a shared reference, so the `is_empty` fast path inside
461/// taffy stays correct for every node pointing here.
462static EMPTY_CACHE: Cache = Cache::new();
463
464impl ElementData {
465    /// This node's layout cache, or a shared empty one if it has never been
466    /// laid out. See the [`cache`](Self::cache) field for why it is optional.
467    #[inline]
468    pub fn cache(&self) -> &Cache {
469        self.cache.as_deref().unwrap_or(&EMPTY_CACHE)
470    }
471
472    /// This node's layout cache, allocating it if this is the first use.
473    ///
474    /// Every caller of this is taffy storing or clearing a measurement, which
475    /// is exactly the moment a node stops being one that never lays out.
476    #[inline]
477    pub fn cache_mut(&mut self) -> &mut Cache {
478        self.cache.get_or_insert_with(|| Box::new(Cache::new()))
479    }
480
481    /// Drop the cache entirely rather than emptying it in place.
482    ///
483    /// `cache_clear` runs on every node whose layout is invalidated, and the
484    /// old code zeroed 1616 bytes there. Releasing the box instead returns the
485    /// memory, and a node that is cleared and never re-measured (a subtree that
486    /// became `display: none`, say) stops paying for the cache at all.
487    #[inline]
488    pub fn cache_release(&mut self) {
489        self.cache = None;
490    }
491
492    pub fn new(name: QualName, attrs: Vec<Attribute>) -> Self {
493        let id_attr_atom = attrs
494            .iter()
495            .find(|attr| &attr.name.local == "id")
496            .map(|attr| attr.value.as_ref())
497            .map(|value: &str| Atom::from(value));
498
499        let mut data = ElementData {
500            name,
501            id: id_attr_atom,
502            attrs: Attributes::new(attrs),
503            is_focussable: false,
504            style_attribute: Default::default(),
505            inline_layout_data: None,
506            list_item_data: None,
507            special_data: SpecialElementData::None,
508            template_contents: None,
509            shadow_root: None,
510            assigned_slot: None,
511            background_images: Vec::new(),
512            mask_images: Vec::new(),
513
514            stylo_element_data: Default::default(),
515            selector_flags: Cell::new(ElementSelectorFlags::empty()),
516            guard: None,
517            element_state: ElementState::empty(),
518            has_snapshot: false,
519            snapshot_handled: AtomicBool::new(false),
520            dirty_descendants: AtomicBool::new(true),
521            before: None,
522            after: None,
523            detailed_grid_info: None,
524            style: Default::default(),
525            style_source: None,
526            subtree_hoists: false,
527            display_constructed_as: StyloDisplay::Block,
528            cache: None,
529            unrounded_layout: Layout::new(),
530            final_layout: Layout::new(),
531            scroll_offset: crate::Point::ZERO,
532            scrollable_overflow: KurboRect::ZERO,
533            transform: None,
534        };
535        data.flush_is_focussable();
536
537        // The element state needs to be modified if the element can be disabled.
538        if data.can_be_disabled() {
539            data.element_state
540                .insert(match data.has_attr(local_name!("disabled")) {
541                    true => ElementState::DISABLED,
542                    false => ElementState::ENABLED,
543                });
544        }
545
546        data
547    }
548
549    pub fn attrs(&self) -> &[Attribute] {
550        &self.attrs
551    }
552
553    pub fn attr(&self, name: impl PartialEq<LocalName>) -> Option<&str> {
554        let attr = self.attrs.iter().find(|attr| name == attr.name.local)?;
555        Some(&attr.value)
556    }
557
558    pub fn attr_parsed<T: FromStr>(&self, name: impl PartialEq<LocalName>) -> Option<T> {
559        let attr = self.attrs.iter().find(|attr| name == attr.name.local)?;
560        attr.value.parse::<T>().ok()
561    }
562
563    /// Detects the presence of the attribute, treating *any* value as truthy.
564    pub fn has_attr(&self, name: impl PartialEq<LocalName>) -> bool {
565        self.attrs.iter().any(|attr| name == attr.name.local)
566    }
567
568    pub fn can_be_disabled(&self) -> bool {
569        local_names!("button", "input", "select", "textarea").contains(&self.name.local)
570    }
571
572    pub fn image_data(&self) -> Option<&ImageData> {
573        match &self.special_data {
574            SpecialElementData::Image(data) => Some(&**data),
575            _ => None,
576        }
577    }
578
579    pub fn image_data_mut(&mut self) -> Option<&mut ImageData> {
580        match self.special_data {
581            SpecialElementData::Image(ref mut data) => Some(&mut **data),
582            _ => None,
583        }
584    }
585
586    pub fn raster_image_data(&self) -> Option<&RasterImageData> {
587        match self.image_data()? {
588            ImageData::Raster(data) => Some(data),
589            _ => None,
590        }
591    }
592
593    pub fn raster_image_data_mut(&mut self) -> Option<&mut RasterImageData> {
594        match self.image_data_mut()? {
595            ImageData::Raster(data) => Some(data),
596            _ => None,
597        }
598    }
599
600    pub fn canvas_data(&self) -> Option<&CanvasData> {
601        match &self.special_data {
602            SpecialElementData::Canvas(data) => Some(data),
603            _ => None,
604        }
605    }
606
607    pub fn sub_doc_data(&self) -> Option<&dyn Document> {
608        match &self.special_data {
609            SpecialElementData::SubDocument(data) => Some(data.as_ref()),
610            _ => None,
611        }
612    }
613
614    pub fn sub_doc_data_mut(&mut self) -> Option<&mut dyn Document> {
615        match &mut self.special_data {
616            SpecialElementData::SubDocument(data) => Some(data.as_mut()),
617            _ => None,
618        }
619    }
620
621    #[cfg(feature = "svg")]
622    pub fn svg_data(&self) -> Option<&usvg::Tree> {
623        match self.image_data()? {
624            ImageData::Svg(data) => Some(&data.tree),
625            _ => None,
626        }
627    }
628
629    pub fn text_input_data(&self) -> Option<&TextInputData> {
630        match &self.special_data {
631            SpecialElementData::TextInput(data) => Some(data),
632            _ => None,
633        }
634    }
635
636    pub fn text_input_data_mut(&mut self) -> Option<&mut TextInputData> {
637        match &mut self.special_data {
638            SpecialElementData::TextInput(data) => Some(data),
639            _ => None,
640        }
641    }
642
643    #[cfg(feature = "custom-widget")]
644    pub fn custom_widget_data(&self) -> Option<&CustomWidgetData> {
645        match &self.special_data {
646            SpecialElementData::CustomWidget(data) => Some(data),
647            _ => None,
648        }
649    }
650
651    #[cfg(feature = "custom-widget")]
652    pub fn custom_widget_data_mut(&mut self) -> Option<&mut CustomWidgetData> {
653        match &mut self.special_data {
654            SpecialElementData::CustomWidget(data) => Some(data),
655            _ => None,
656        }
657    }
658
659    #[cfg(feature = "shadow-dom")]
660    pub fn custom_element_data(&self) -> Option<&CustomElementData> {
661        match &self.special_data {
662            SpecialElementData::CustomElement(data) => Some(data),
663            _ => None,
664        }
665    }
666
667    #[cfg(feature = "shadow-dom")]
668    pub fn custom_element_data_mut(&mut self) -> Option<&mut CustomElementData> {
669        match &mut self.special_data {
670            SpecialElementData::CustomElement(data) => Some(data),
671            _ => None,
672        }
673    }
674
675    pub fn checkbox_input_checked(&self) -> Option<bool> {
676        match self.special_data {
677            SpecialElementData::CheckboxInput(checked) => Some(checked),
678            _ => None,
679        }
680    }
681
682    pub fn checkbox_input_checked_mut(&mut self) -> Option<&mut bool> {
683        match self.special_data {
684            SpecialElementData::CheckboxInput(ref mut checked) => Some(checked),
685            _ => None,
686        }
687    }
688
689    /// The live state of a `<select>`, or `None` for anything else.
690    ///
691    /// Deliberately not modelled the way `checkbox_input_checked` is: that one
692    /// hands back an `Option<bool>` whose `None` means both "not a checkbox"
693    /// and, to every caller that reaches for `unwrap_or(false)`, "unchecked".
694    /// A select's answer to "what is selected" is a whole vector, so the
695    /// borrow is handed out instead and the two questions stay apart.
696    pub fn select_data(&self) -> Option<&SelectData> {
697        match &self.special_data {
698            SpecialElementData::Select(data) => Some(data),
699            _ => None,
700        }
701    }
702
703    pub fn select_data_mut(&mut self) -> Option<&mut SelectData> {
704        match &mut self.special_data {
705            SpecialElementData::Select(data) => Some(data),
706            _ => None,
707        }
708    }
709
710    #[cfg(feature = "file-input")]
711    pub fn file_data(&self) -> Option<&FileData> {
712        match &self.special_data {
713            SpecialElementData::FileInput(data) => Some(data),
714            _ => None,
715        }
716    }
717
718    #[cfg(feature = "file-input")]
719    pub fn file_data_mut(&mut self) -> Option<&mut FileData> {
720        match &mut self.special_data {
721            SpecialElementData::FileInput(data) => Some(data),
722            _ => None,
723        }
724    }
725
726    pub fn flush_is_focussable(&mut self) {
727        let disabled: bool = self.attr_parsed(local_name!("disabled")).unwrap_or(false);
728        let tabindex: Option<i32> = self.attr_parsed(local_name!("tabindex"));
729        let contains_sub_document: bool = self.sub_doc_data().is_some();
730
731        self.is_focussable = contains_sub_document
732            || (!disabled
733                && match tabindex {
734                    Some(index) => index >= 0,
735                    None => {
736                        // Some focusable HTML elements have a default tabindex value of 0 set under the hood by the user agent.
737                        // These elements are:
738                        //   - <a> or <area> with href attribute
739                        //   - <button>, <frame>, <iframe>, <input>, <object>, <select>, <textarea>, and SVG <a> element
740                        //   - <summary> element that provides summary for a <details> element.
741
742                        if [local_name!("a"), local_name!("area")].contains(&self.name.local) {
743                            self.attr(local_name!("href")).is_some()
744                        } else {
745                            const DEFAULT_FOCUSSABLE_ELEMENTS: [LocalName; 7] = [
746                                local_name!("button"),
747                                local_name!("input"),
748                                local_name!("select"),
749                                local_name!("textarea"),
750                                local_name!("frame"),
751                                local_name!("iframe"),
752                                local_name!("summary"),
753                            ];
754                            DEFAULT_FOCUSSABLE_ELEMENTS.contains(&self.name.local)
755                        }
756                    }
757                })
758    }
759
760    pub fn flush_style_attribute(&mut self, guard: &SharedRwLock, url_extra_data: &UrlExtraData) {
761        self.style_attribute = self.attr(local_name!("style")).map(|style_str| {
762            ServoArc::new(guard.wrap(parse_style_attribute(
763                style_str,
764                url_extra_data,
765                None,
766                QuirksMode::NoQuirks,
767                CssRuleType::Style,
768            )))
769        });
770    }
771
772    pub fn set_style_property(
773        &mut self,
774        name: &str,
775        value: &str,
776        guard: &SharedRwLock,
777        url_extra_data: UrlExtraData,
778    ) -> bool {
779        let context = ParserContext::new(
780            Origin::Author,
781            &url_extra_data,
782            Some(CssRuleType::Style),
783            ParsingMode::DEFAULT,
784            QuirksMode::NoQuirks,
785            /* namespaces = */ Default::default(),
786            None,
787            None,
788            /* attr_taint = */ Default::default(),
789        );
790
791        let Ok(property_id) = PropertyId::parse(name, &context) else {
792            #[cfg(feature = "tracing")]
793            tracing::warn!(property = name, "Unsupported property");
794            return false;
795        };
796        let mut source_property_declaration = SourcePropertyDeclaration::default();
797        let mut input = ParserInput::new(value);
798        let mut parser = style::values::Parser::new(&mut input);
799        let Ok(_) = PropertyDeclaration::parse_into(
800            &mut source_property_declaration,
801            property_id,
802            &context,
803            &mut parser,
804        ) else {
805            #[cfg(feature = "tracing")]
806            tracing::warn!(property = name, value, "Invalid property value");
807            return false;
808        };
809
810        if self.style_attribute.is_none() {
811            self.style_attribute = Some(ServoArc::new(guard.wrap(PropertyDeclarationBlock::new())));
812        }
813        self.style_attribute
814            .as_mut()
815            .unwrap()
816            .write_with(&mut guard.write())
817            .extend(source_property_declaration.drain(), Importance::Normal);
818
819        true
820    }
821
822    pub fn remove_style_property(
823        &mut self,
824        name: &str,
825        guard: &SharedRwLock,
826        url_extra_data: UrlExtraData,
827    ) -> bool {
828        let context = ParserContext::new(
829            Origin::Author,
830            &url_extra_data,
831            Some(CssRuleType::Style),
832            ParsingMode::DEFAULT,
833            QuirksMode::NoQuirks,
834            /* namespaces = */ Default::default(),
835            None,
836            None,
837            /* attr_taint = */ Default::default(),
838        );
839        let Ok(property_id) = PropertyId::parse(name, &context) else {
840            #[cfg(feature = "tracing")]
841            tracing::warn!(property = name, "Unsupported property");
842            return false;
843        };
844
845        if let Some(style) = &mut self.style_attribute {
846            let mut guard = guard.write();
847            let style = style.write_with(&mut guard);
848            if let Some(index) = style.first_declaration_to_remove(&property_id) {
849                style.remove_property(&property_id, index);
850                return true;
851            }
852        }
853
854        false
855    }
856
857    pub fn set_sub_document(&mut self, sub_document: Box<dyn Document>) {
858        self.special_data = SpecialElementData::SubDocument(sub_document);
859    }
860
861    pub fn remove_sub_document(&mut self) {
862        self.special_data = SpecialElementData::None;
863    }
864
865    #[cfg(feature = "custom-widget")]
866    pub fn set_custom_widget(&mut self, widget: Box<dyn crate::Widget>) {
867        use crate::node::custom_widget::CustomWidgetData;
868        self.special_data = SpecialElementData::CustomWidget(CustomWidgetData::new(widget));
869    }
870
871    #[cfg(feature = "custom-widget")]
872    pub fn remove_custom_widget(&mut self) -> Vec<anyrender::ResourceId> {
873        let resource_ids = self
874            .custom_widget_data_mut()
875            .map(|widget_data| widget_data.take_resource_ids())
876            .unwrap_or_default();
877        self.special_data = SpecialElementData::None;
878        resource_ids
879    }
880
881    pub fn take_inline_layout(&mut self) -> Option<Box<TextLayout>> {
882        std::mem::take(&mut self.inline_layout_data)
883    }
884
885    pub fn is_submit_button(&self) -> bool {
886        if self.name.local != local_name!("button") {
887            return false;
888        }
889        let type_attr = self.attr(local_name!("type"));
890        let is_submit = type_attr == Some("submit");
891        let is_auto_submit = type_attr.is_none()
892            && self.attr(LocalName::from("command")).is_none()
893            && self.attr(LocalName::from("commandfor")).is_none();
894        is_submit || is_auto_submit
895    }
896}
897
898#[derive(Debug, Clone, PartialEq)]
899pub struct RasterImageData {
900    /// The width of the image
901    pub width: u32,
902    /// The height of the image
903    pub height: u32,
904    /// The raw image data in RGBA8 format
905    pub data: Blob<u8>,
906}
907impl RasterImageData {
908    pub fn new(width: u32, height: u32, data: Arc<Vec<u8>>) -> Self {
909        Self {
910            width,
911            height,
912            data: Blob::new(data),
913        }
914    }
915}
916
917/// A parsed SVG image.
918///
919/// usvg always resolves the root `<svg>` to a concrete [`usvg::Tree::size`],
920/// falling back to the `viewBox` size when `width`/`height` are absent or given
921/// as percentages. For CSS sizing purposes, however, such an SVG has *no*
922/// intrinsic width/height (only an intrinsic aspect ratio). The accessors on
923/// this type resolve the CSS intrinsic dimensions lazily from
924/// [`usvg::Tree::intrinsic_dimensions`], which preserves what was actually
925/// declared on the root element.
926#[cfg(feature = "svg")]
927#[derive(Debug, Clone)]
928pub struct SvgImageData {
929    /// The parsed SVG tree.
930    pub tree: Arc<usvg::Tree>,
931}
932
933#[cfg(feature = "svg")]
934impl SvgImageData {
935    /// The intrinsic width in CSS px, present only when the root `<svg>`
936    /// declared an absolute (non-percentage) `width`.
937    pub fn intrinsic_width(&self) -> Option<f32> {
938        use usvg::svgtypes::LengthUnit;
939        let declared = self
940            .tree
941            .intrinsic_dimensions()
942            .width
943            .is_some_and(|len| len.unit != LengthUnit::Percent);
944        declared.then(|| self.tree.size().width())
945    }
946
947    /// The intrinsic height in CSS px, present only when the root `<svg>`
948    /// declared an absolute (non-percentage) `height`.
949    pub fn intrinsic_height(&self) -> Option<f32> {
950        use usvg::svgtypes::LengthUnit;
951        let declared = self
952            .tree
953            .intrinsic_dimensions()
954            .height
955            .is_some_and(|len| len.unit != LengthUnit::Percent);
956        declared.then(|| self.tree.size().height())
957    }
958
959    /// The aspect ratio of the root `<svg>`'s `viewBox`, if it declares one.
960    pub fn viewbox_aspect_ratio(&self) -> Option<f32> {
961        self.tree
962            .intrinsic_dimensions()
963            .view_box
964            .map(|vb| vb.width() / vb.height())
965    }
966
967    /// The root `width` attribute resolved against a containing block width:
968    /// percentages resolve against the containing block (`None` if it is
969    /// indefinite) and an absent attribute is `None`.
970    ///
971    /// This is only appropriate for an inline `<svg>` element, where the
972    /// attributes behave as presentation attributes. SVG used as an image
973    /// (e.g. `<img src>` or a background) must use [`Self::intrinsic_width`],
974    /// as its intrinsic dimensions are context-free per CSS.
975    pub fn resolved_width(&self, container_width: Option<f32>) -> Option<f32> {
976        use usvg::svgtypes::LengthUnit;
977        match self.tree.intrinsic_dimensions().width {
978            Some(len) if len.unit != LengthUnit::Percent => Some(self.tree.size().width()),
979            Some(len) => container_width.map(|cw| cw * (len.number as f32) / 100.0),
980            None => None,
981        }
982    }
983
984    /// The root `height` attribute resolved against a containing block height.
985    /// See [`Self::resolved_width`].
986    pub fn resolved_height(&self, container_height: Option<f32>) -> Option<f32> {
987        use usvg::svgtypes::LengthUnit;
988        match self.tree.intrinsic_dimensions().height {
989            Some(len) if len.unit != LengthUnit::Percent => Some(self.tree.size().height()),
990            Some(len) => container_height.map(|ch| ch * (len.number as f32) / 100.0),
991            None => None,
992        }
993    }
994
995    /// The intrinsic aspect ratio of the SVG: the ratio of its declared
996    /// `width`/`height` when both are absolute lengths, otherwise the
997    /// `viewBox` ratio, otherwise the ratio of the resolved
998    /// [`usvg::Tree::size`] (which is always non-zero).
999    pub fn aspect_ratio(&self) -> f32 {
1000        match (self.intrinsic_width(), self.intrinsic_height()) {
1001            (Some(w), Some(h)) => w / h,
1002            _ => self.viewbox_aspect_ratio().unwrap_or_else(|| {
1003                let size = self.tree.size();
1004                size.width() / size.height()
1005            }),
1006        }
1007    }
1008
1009    /// The intrinsic dimensions of the SVG resolved per CSS replaced element
1010    /// sizing: a missing dimension is computed from the declared one and the
1011    /// intrinsic aspect ratio; if neither is declared, the resolved
1012    /// [`usvg::Tree::size`] is used as a fallback.
1013    pub fn intrinsic_size(&self) -> (f32, f32) {
1014        let aspect_ratio = self.aspect_ratio();
1015        match (self.intrinsic_width(), self.intrinsic_height()) {
1016            (Some(w), Some(h)) => (w, h),
1017            (Some(w), None) => (w, w / aspect_ratio),
1018            (None, Some(h)) => (h * aspect_ratio, h),
1019            (None, None) => {
1020                // No intrinsic dimensions. If there is an intrinsic aspect ratio, apply
1021                // the CSS default sizing algorithm: contain within the default object
1022                // size of 300x150. Otherwise fall back to the resolved tree size.
1023                if self.viewbox_aspect_ratio().is_some() {
1024                    let scale = (300.0 / aspect_ratio).min(150.0);
1025                    (scale * aspect_ratio, scale)
1026                } else {
1027                    let size = self.tree.size();
1028                    (size.width(), size.height())
1029                }
1030            }
1031        }
1032    }
1033}
1034
1035#[derive(Debug, Clone)]
1036pub enum ImageData {
1037    Raster(RasterImageData),
1038    #[cfg(feature = "svg")]
1039    Svg(SvgImageData),
1040    None,
1041}
1042
1043#[derive(Debug, Clone, PartialEq)]
1044pub enum Status {
1045    Ok,
1046    Error,
1047    Loading,
1048}
1049
1050#[derive(Debug, Clone)]
1051pub struct ImageResourceData {
1052    /// The url of the background image
1053    pub url: ServoArc<Url>,
1054    /// The loading status of the background image
1055    pub status: Status,
1056    /// The image data
1057    pub image: ImageData,
1058}
1059
1060impl ImageResourceData {
1061    pub fn new(url: ServoArc<Url>) -> Self {
1062        Self {
1063            url,
1064            status: Status::Loading,
1065            image: ImageData::None,
1066        }
1067    }
1068}
1069
1070#[derive(Debug, Clone)]
1071pub struct CanvasData {
1072    pub custom_paint_source_id: u64,
1073}
1074
1075impl std::fmt::Debug for SpecialElementData {
1076    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1077        match self {
1078            SpecialElementData::SubDocument(_) => f.write_str("NodeSpecificData::SubDocument"),
1079            #[cfg(feature = "custom-widget")]
1080            SpecialElementData::CustomWidget(_) => f.write_str("NodeSpecificData::CustomWidget"),
1081            #[cfg(feature = "shadow-dom")]
1082            SpecialElementData::CustomElement(_) => f.write_str("NodeSpecificData::CustomElement"),
1083            SpecialElementData::Stylesheet(_) => f.write_str("NodeSpecificData::Stylesheet"),
1084            SpecialElementData::Image(data) => match **data {
1085                ImageData::Raster(_) => f.write_str("NodeSpecificData::Image(Raster)"),
1086                #[cfg(feature = "svg")]
1087                ImageData::Svg(_) => f.write_str("NodeSpecificData::Image(Svg)"),
1088                ImageData::None => f.write_str("NodeSpecificData::Image(None)"),
1089            },
1090            SpecialElementData::Canvas(_) => f.write_str("NodeSpecificData::Canvas"),
1091            SpecialElementData::TableRoot(_) => f.write_str("NodeSpecificData::TableRoot"),
1092            SpecialElementData::TextInput(_) => f.write_str("NodeSpecificData::TextInput"),
1093            SpecialElementData::CheckboxInput(_) => f.write_str("NodeSpecificData::CheckboxInput"),
1094            SpecialElementData::Select(data) => {
1095                write!(f, "NodeSpecificData::Select({data:?})")
1096            }
1097            #[cfg(feature = "file-input")]
1098            SpecialElementData::FileInput(_) => f.write_str("NodeSpecificData::FileInput"),
1099            SpecialElementData::None => f.write_str("NodeSpecificData::None"),
1100        }
1101    }
1102}
1103
1104#[derive(Clone)]
1105pub struct ListItemLayout {
1106    pub marker: Marker,
1107    pub position: ListItemLayoutPosition,
1108}
1109
1110//We seperate chars from strings in order to optimise rendering - ie not needing to
1111//construct a whole parley layout for simple char markers
1112#[derive(Debug, PartialEq, Clone)]
1113pub enum Marker {
1114    Char(char),
1115    String(String),
1116}
1117
1118//Value depends on list-style-position, determining whether a seperate layout is created for it
1119#[derive(Clone)]
1120pub enum ListItemLayoutPosition {
1121    Inside,
1122    Outside(Box<parley::Layout<TextBrush>>),
1123}
1124
1125impl std::fmt::Debug for ListItemLayout {
1126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1127        write!(f, "ListItemLayout - marker {:?}", self.marker)
1128    }
1129}
1130
1131#[cfg(feature = "file-input")]
1132mod file_data {
1133    use std::ops::{Deref, DerefMut};
1134    use std::path::PathBuf;
1135
1136    #[derive(Clone, Debug)]
1137    pub struct FileData(pub Vec<PathBuf>);
1138    impl Deref for FileData {
1139        type Target = Vec<PathBuf>;
1140
1141        fn deref(&self) -> &Self::Target {
1142            &self.0
1143        }
1144    }
1145    impl DerefMut for FileData {
1146        fn deref_mut(&mut self) -> &mut Self::Target {
1147            &mut self.0
1148        }
1149    }
1150    impl From<Vec<PathBuf>> for FileData {
1151        fn from(files: Vec<PathBuf>) -> Self {
1152            Self(files)
1153        }
1154    }
1155}
1156#[cfg(feature = "file-input")]
1157pub use file_data::FileData;
1158
1159#[cfg(test)]
1160mod tests {
1161    use super::{ElementData, TextInputData};
1162    use parley::{FontContext, LayoutContext};
1163
1164    /// `ElementData` is allocated once per element, so its size is a direct
1165    /// multiplier on the DOM's footprint.
1166    ///
1167    /// A `heap` census of a running instance found 264,891 live allocations in
1168    /// the 3KB malloc class totalling **814 MB**, which was this struct at
1169    /// 2848 bytes each. Boxing the taffy cache took it to 1240 and out of that
1170    /// size class entirely.
1171    ///
1172    /// The bound is deliberately loose and one-sided: it does not care what
1173    /// the exact number is, only that nobody puts a kilobyte back inline
1174    /// without noticing. If a field genuinely has to grow past this, raise the
1175    /// bound and say why, rather than deleting the test.
1176    #[test]
1177    fn element_data_stays_out_of_the_large_size_classes() {
1178        let size = std::mem::size_of::<ElementData>();
1179        assert!(
1180            size <= 1536,
1181            "ElementData grew to {size} bytes. It is allocated per element, so \
1182             this is a multiplier on the whole DOM's footprint: at 2848 bytes \
1183             it was 814 MB of a live instance. Box the new field instead, as \
1184             `cache` is."
1185        );
1186    }
1187
1188    /// The cache is what was boxed, so its absence has to stay cheap.
1189    ///
1190    /// If this ever fails, the `Option<Box<Cache>>` has been flattened back to
1191    /// an inline `Cache` and the 428 MB is back.
1192    #[test]
1193    fn an_unlaid_out_element_carries_no_cache() {
1194        let data = ElementData::new(
1195            markup5ever::QualName::new(None, markup5ever::ns!(html), "div".into()),
1196            Vec::new(),
1197        );
1198        assert!(
1199            std::mem::size_of_val(&data) < std::mem::size_of::<taffy::Cache>() * 2,
1200            "the cache must not be inline: a fresh element should be smaller \
1201             than two caches"
1202        );
1203    }
1204
1205    /// Build a [`TextInputData`] with the given text laid out at scale 1.0.
1206    fn make_input(is_multiline: bool, text: &str) -> TextInputData {
1207        let mut font_ctx = FontContext::new();
1208        let mut layout_ctx = LayoutContext::new();
1209        let mut data = TextInputData::new(is_multiline);
1210        data.editor.set_scale(1.0);
1211        data.editor.set_text(text);
1212        data.editor
1213            .driver(&mut font_ctx, &mut layout_ctx)
1214            .refresh_layout();
1215        data
1216    }
1217
1218    #[test]
1219    fn short_text_does_not_scroll() {
1220        let mut data = make_input(false, "hi");
1221        // A wide content box that comfortably fits the text.
1222        data.clamp_scroll_offset(1000.0, 100.0);
1223        assert_eq!(data.scroll_offset, 0.0);
1224    }
1225
1226    #[test]
1227    fn single_line_scrolls_to_follow_caret() {
1228        let text = "the quick brown fox jumps over the lazy dog repeatedly and at length";
1229        let mut data = make_input(false, text);
1230        let content_box_width = 40.0;
1231        let content_box_height = 20.0;
1232
1233        // Caret at the end of a string that overflows a narrow input should scroll right.
1234        data.editor
1235            .driver(&mut FontContext::new(), &mut LayoutContext::new())
1236            .move_to_text_end();
1237        data.clamp_scroll_offset(content_box_width, content_box_height);
1238
1239        let layout_width = data.editor.try_layout().unwrap().full_width();
1240        if layout_width > content_box_width {
1241            assert!(
1242                data.scroll_offset > 0.0,
1243                "expected horizontal scroll for overflowing single-line input"
1244            );
1245            // The caret must be within the visible region after scrolling.
1246            let caret = data.editor.cursor_geometry(1.5).unwrap();
1247            assert!(caret.x1 as f32 <= data.scroll_offset + content_box_width + 0.5);
1248            assert!(caret.x0 as f32 >= data.scroll_offset - 0.5);
1249        }
1250
1251        // Moving the caret back to the start should reset the scroll offset.
1252        data.editor
1253            .driver(&mut FontContext::new(), &mut LayoutContext::new())
1254            .move_to_text_start();
1255        data.clamp_scroll_offset(content_box_width, content_box_height);
1256        assert_eq!(data.scroll_offset, 0.0);
1257    }
1258
1259    #[test]
1260    fn multiline_scrolls_vertically_not_horizontally() {
1261        let text = (0..40)
1262            .map(|i| format!("line {i}"))
1263            .collect::<Vec<_>>()
1264            .join("\n");
1265        let mut data = make_input(true, &text);
1266        // Constrain the width so wrapping is well-defined.
1267        data.editor.set_width(Some(200.0));
1268        data.editor
1269            .driver(&mut FontContext::new(), &mut LayoutContext::new())
1270            .refresh_layout();
1271
1272        let content_box_width = 200.0;
1273        let content_box_height = 30.0;
1274
1275        data.editor
1276            .driver(&mut FontContext::new(), &mut LayoutContext::new())
1277            .move_to_text_end();
1278        data.clamp_scroll_offset(content_box_width, content_box_height);
1279
1280        let layout_height = data.editor.try_layout().unwrap().height();
1281        if layout_height > content_box_height {
1282            assert!(
1283                data.scroll_offset > 0.0,
1284                "expected vertical scroll for overflowing multi-line input"
1285            );
1286        }
1287    }
1288
1289    /// macOS only: the scrollable extent it clamps against comes from shaped
1290    /// text, so with no font registered there is nothing to scroll and the
1291    /// clamp has no work to do. See the target-scoped `parley` dev-dependency
1292    /// in this crate's manifest.
1293    #[cfg(target_os = "macos")]
1294    #[test]
1295    fn scroll_by_clamps_and_bubbles() {
1296        let text = (0..40)
1297            .map(|i| format!("line {i}"))
1298            .collect::<Vec<_>>()
1299            .join("\n");
1300        let mut data = make_input(true, &text);
1301        data.editor.set_width(Some(200.0));
1302        data.editor
1303            .driver(&mut FontContext::new(), &mut LayoutContext::new())
1304            .refresh_layout();
1305
1306        let content_box_width = 200.0;
1307        let content_box_height = 30.0;
1308        let max = data.max_scroll_offset(content_box_width, content_box_height);
1309        assert!(max > 0.0, "test text should overflow the content box");
1310
1311        // Scrolling up (positive delta decreases offset) while already at the top is a no-op and
1312        // the whole delta bubbles.
1313        assert_eq!(data.scroll_offset, 0.0);
1314        let bubbled = data.scroll_by(15.0, content_box_width, content_box_height);
1315        assert_eq!(data.scroll_offset, 0.0);
1316        assert_eq!(bubbled, 15.0);
1317
1318        // Scrolling down moves the offset and consumes the delta.
1319        let bubbled = data.scroll_by(-10.0, content_box_width, content_box_height);
1320        assert_eq!(data.scroll_offset, 10.0);
1321        assert_eq!(bubbled, 0.0);
1322
1323        // Scrolling past the end clamps to the maximum and bubbles the remainder. Starting at
1324        // offset 10 with max headroom of `max - 10`, a delta of `-(max + 100)` consumes
1325        // `max - 10` and bubbles the rest (`-110`).
1326        let bubbled = data.scroll_by(-(max + 100.0), content_box_width, content_box_height);
1327        assert_eq!(data.scroll_offset, max);
1328        assert!((bubbled - (-110.0)).abs() < 1e-3);
1329    }
1330
1331    #[test]
1332    fn single_line_does_not_scroll_when_text_fits() {
1333        let mut data = make_input(false, "hi");
1334        // Wide content box; nothing to scroll, so all delta bubbles.
1335        let bubbled = data.scroll_by(-50.0, 1000.0, 100.0);
1336        assert_eq!(data.scroll_offset, 0.0);
1337        assert_eq!(bubbled, -50.0);
1338    }
1339}