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