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,3395 @@
|
|
|
1
|
+
use crate::NodeTree;
|
|
2
|
+
use crate::events::{DragMode, handle_dom_event};
|
|
3
|
+
use crate::layout::construct::ConstructionTask;
|
|
4
|
+
use crate::layout::damage::ALL_DAMAGE;
|
|
5
|
+
use crate::mutator::ViewportMut;
|
|
6
|
+
use crate::net::{
|
|
7
|
+
Resource, ResourceHandler, ResourceLoadResponse, StylesheetHandler, StylesheetLoader,
|
|
8
|
+
};
|
|
9
|
+
use crate::node::{ImageData, NodeFlags, RasterImageData, SpecialElementData, Status, TextBrush};
|
|
10
|
+
use crate::scrolling::ScrollAnimationState;
|
|
11
|
+
use crate::selection::TextSelection;
|
|
12
|
+
use crate::stylo_device::{DeviceChanges, make_device};
|
|
13
|
+
use crate::stylo_to_cursor_icon::stylo_to_cursor_icon;
|
|
14
|
+
use crate::traversal::TreeTraverser;
|
|
15
|
+
use crate::url::DocumentUrl;
|
|
16
|
+
use crate::util::ImageType;
|
|
17
|
+
use crate::{
|
|
18
|
+
DEFAULT_CSS, DocumentConfig, DocumentMutator, DummyHtmlParserProvider, ElementData,
|
|
19
|
+
EventDriver, HtmlParserProvider, Node, NodeData, NoopEventHandler, StyleThreading,
|
|
20
|
+
TextNodeData,
|
|
21
|
+
};
|
|
22
|
+
use blitz_traits::devtools::DevtoolSettings;
|
|
23
|
+
use blitz_traits::events::{DomEvent, HitResult, UiEvent};
|
|
24
|
+
use blitz_traits::navigation::{DummyNavigationProvider, NavigationProvider};
|
|
25
|
+
use blitz_traits::net::{AbortSignal, DummyNetProvider, NetProvider, Request};
|
|
26
|
+
use blitz_traits::node_id::NodeId;
|
|
27
|
+
use blitz_traits::shell::{DummyShellProvider, ShellProvider, Viewport};
|
|
28
|
+
use cursor_icon::CursorIcon;
|
|
29
|
+
use linebender_resource_handle::Blob;
|
|
30
|
+
use markup5ever::{LocalName, local_name};
|
|
31
|
+
use parley::{FontContext, PlainEditorDriver};
|
|
32
|
+
use selectors::{Element, matching::QuirksMode};
|
|
33
|
+
use smallvec::SmallVec;
|
|
34
|
+
use std::any::Any;
|
|
35
|
+
use std::cell::RefCell;
|
|
36
|
+
use std::collections::{BTreeMap, Bound, HashMap, HashSet};
|
|
37
|
+
use std::ops::{Deref, DerefMut};
|
|
38
|
+
use std::rc::Rc;
|
|
39
|
+
use std::str::FromStr;
|
|
40
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
41
|
+
use std::sync::mpsc::{Receiver, Sender, channel};
|
|
42
|
+
use std::sync::{Arc, LazyLock, Mutex, MutexGuard, OnceLock, RwLockReadGuard, RwLockWriteGuard};
|
|
43
|
+
use std::task::{Context as TaskContext, Waker};
|
|
44
|
+
use style::Atom;
|
|
45
|
+
use style::animation::DocumentAnimationSet;
|
|
46
|
+
use style::attr::{AttrIdentifier, AttrValue};
|
|
47
|
+
use style::computed_value_flags::ComputedValueFlags;
|
|
48
|
+
use style::data::{ElementData as StyloElementData, ElementStyles};
|
|
49
|
+
use style::invalidation::element::restyle_hints::RestyleHint;
|
|
50
|
+
use style::media_queries::MediaType;
|
|
51
|
+
use style::properties::ComputedValues;
|
|
52
|
+
use style::properties::style_structs::Font;
|
|
53
|
+
use style::selector_parser::ServoElementSnapshot;
|
|
54
|
+
use style::servo_arc::Arc as ServoArc;
|
|
55
|
+
use style::values::GenericAtomIdent;
|
|
56
|
+
use style::values::computed::UserSelect;
|
|
57
|
+
use style::values::computed::ui::CursorKind;
|
|
58
|
+
use style::{
|
|
59
|
+
device::Device,
|
|
60
|
+
dom::{TDocument, TNode},
|
|
61
|
+
media_queries::MediaList,
|
|
62
|
+
selector_parser::SnapshotMap,
|
|
63
|
+
shared_lock::{SharedRwLock, StylesheetGuards},
|
|
64
|
+
stylesheets::{AllowImportRules, DocumentStyleSheet, Origin, Stylesheet},
|
|
65
|
+
stylist::Stylist,
|
|
66
|
+
};
|
|
67
|
+
use style_dom::ElementState;
|
|
68
|
+
use thin_vec::ThinVec;
|
|
69
|
+
use url::Url;
|
|
70
|
+
use web_time::Instant;
|
|
71
|
+
|
|
72
|
+
#[cfg(feature = "parallel-construct")]
|
|
73
|
+
use thread_local::ThreadLocal;
|
|
74
|
+
|
|
75
|
+
/// A user-agent stylesheet, parsed with a read-only lock so that it can be shared between documents.
|
|
76
|
+
///
|
|
77
|
+
/// Documents with the same user-agent stylesheets (in the same order) also share Stylo's
|
|
78
|
+
/// cached user-agent cascade data. `@import` rules are ignored.
|
|
79
|
+
#[derive(Clone, Debug, PartialEq)]
|
|
80
|
+
pub struct UserAgentStylesheet(DocumentStyleSheet);
|
|
81
|
+
|
|
82
|
+
impl UserAgentStylesheet {
|
|
83
|
+
pub fn parse(css: &str) -> Self {
|
|
84
|
+
let lock = SharedRwLock::read_only();
|
|
85
|
+
let data = Stylesheet::from_str(
|
|
86
|
+
css,
|
|
87
|
+
DocumentUrl::default().url_extra_data(),
|
|
88
|
+
Origin::UserAgent,
|
|
89
|
+
ServoArc::new(lock.wrap(MediaList::empty())),
|
|
90
|
+
lock,
|
|
91
|
+
None,
|
|
92
|
+
None,
|
|
93
|
+
QuirksMode::NoQuirks,
|
|
94
|
+
AllowImportRules::No,
|
|
95
|
+
);
|
|
96
|
+
Self(DocumentStyleSheet(ServoArc::new(data)))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// The default user-agent stylesheet ([`DEFAULT_CSS`]), parsed once.
|
|
100
|
+
pub fn default_sheet() -> Self {
|
|
101
|
+
static SHEET: LazyLock<UserAgentStylesheet> =
|
|
102
|
+
LazyLock::new(|| UserAgentStylesheet::parse(DEFAULT_CSS));
|
|
103
|
+
SHEET.clone()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
pub fn sheet(&self) -> &DocumentStyleSheet {
|
|
107
|
+
&self.0
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
pub enum DocGuard<'a> {
|
|
112
|
+
Ref(&'a BaseDocument),
|
|
113
|
+
RefCell(std::cell::Ref<'a, BaseDocument>),
|
|
114
|
+
RwLock(RwLockReadGuard<'a, BaseDocument>),
|
|
115
|
+
Mutex(MutexGuard<'a, BaseDocument>),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
impl Deref for DocGuard<'_> {
|
|
119
|
+
type Target = BaseDocument;
|
|
120
|
+
#[inline(always)]
|
|
121
|
+
fn deref(&self) -> &Self::Target {
|
|
122
|
+
match self {
|
|
123
|
+
Self::Ref(base_document) => base_document,
|
|
124
|
+
Self::RefCell(refcell_guard) => refcell_guard,
|
|
125
|
+
Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
|
|
126
|
+
Self::Mutex(mutex_guard) => mutex_guard,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
pub enum DocGuardMut<'a> {
|
|
132
|
+
Ref(&'a mut BaseDocument),
|
|
133
|
+
RefCell(std::cell::RefMut<'a, BaseDocument>),
|
|
134
|
+
RwLock(RwLockWriteGuard<'a, BaseDocument>),
|
|
135
|
+
Mutex(MutexGuard<'a, BaseDocument>),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
impl Deref for DocGuardMut<'_> {
|
|
139
|
+
type Target = BaseDocument;
|
|
140
|
+
#[inline(always)]
|
|
141
|
+
fn deref(&self) -> &Self::Target {
|
|
142
|
+
match self {
|
|
143
|
+
Self::Ref(base_document) => base_document,
|
|
144
|
+
Self::RefCell(refcell_guard) => refcell_guard,
|
|
145
|
+
Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
|
|
146
|
+
Self::Mutex(mutex_guard) => mutex_guard,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
impl DerefMut for DocGuardMut<'_> {
|
|
152
|
+
#[inline(always)]
|
|
153
|
+
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
154
|
+
match self {
|
|
155
|
+
Self::Ref(base_document) => base_document,
|
|
156
|
+
Self::RefCell(refcell_guard) => &mut *refcell_guard,
|
|
157
|
+
Self::RwLock(rw_lock_read_guard) => &mut *rw_lock_read_guard,
|
|
158
|
+
Self::Mutex(mutex_guard) => &mut *mutex_guard,
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Abstraction over wrappers around [`BaseDocument`] to allow for them all to
|
|
164
|
+
/// be driven by [`blitz-shell`](https://docs.rs/blitz-shell)
|
|
165
|
+
pub trait Document: Any + 'static {
|
|
166
|
+
fn inner(&self) -> DocGuard<'_>;
|
|
167
|
+
fn inner_mut(&mut self) -> DocGuardMut<'_>;
|
|
168
|
+
|
|
169
|
+
/// Update the [`Document`] in response to a [`UiEvent`] (click, keypress, etc)
|
|
170
|
+
fn handle_ui_event(&mut self, event: UiEvent) {
|
|
171
|
+
let mut doc = self.inner_mut();
|
|
172
|
+
let mut driver = EventDriver::new(&mut *doc, NoopEventHandler);
|
|
173
|
+
driver.handle_ui_event(event);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// Poll any pending async operations, and flush changes to the underlying [`BaseDocument`]
|
|
177
|
+
fn poll(&mut self, task_context: Option<TaskContext>) -> bool {
|
|
178
|
+
// Default implementation does nothing
|
|
179
|
+
let _ = task_context;
|
|
180
|
+
false
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// Get the [`Document`]'s id
|
|
184
|
+
fn id(&self) -> usize {
|
|
185
|
+
self.inner().id
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
pub struct PlainDocument(pub BaseDocument);
|
|
190
|
+
impl Document for PlainDocument {
|
|
191
|
+
fn inner(&self) -> DocGuard<'_> {
|
|
192
|
+
DocGuard::Ref(&self.0)
|
|
193
|
+
}
|
|
194
|
+
fn inner_mut(&mut self) -> DocGuardMut<'_> {
|
|
195
|
+
DocGuardMut::Ref(&mut self.0)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
impl Document for BaseDocument {
|
|
200
|
+
fn inner(&self) -> DocGuard<'_> {
|
|
201
|
+
DocGuard::Ref(self)
|
|
202
|
+
}
|
|
203
|
+
fn inner_mut(&mut self) -> DocGuardMut<'_> {
|
|
204
|
+
DocGuardMut::Ref(self)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
impl Document for Rc<RefCell<BaseDocument>> {
|
|
209
|
+
fn inner(&self) -> DocGuard<'_> {
|
|
210
|
+
DocGuard::RefCell(self.borrow())
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
fn inner_mut(&mut self) -> DocGuardMut<'_> {
|
|
214
|
+
DocGuardMut::RefCell(self.borrow_mut())
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
pub enum DocumentEvent {
|
|
219
|
+
ResourceLoad(ResourceLoadResponse),
|
|
220
|
+
/// A navigation originating from within an iframe's sub-document
|
|
221
|
+
/// (e.g. a link click), to be applied to the iframe identified by `node_id`.
|
|
222
|
+
NavigateIframe {
|
|
223
|
+
node_id: NodeId,
|
|
224
|
+
url: Url,
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
pub struct BaseDocument {
|
|
229
|
+
/// ID of the document
|
|
230
|
+
id: usize,
|
|
231
|
+
|
|
232
|
+
// Config
|
|
233
|
+
/// Base url for resolving linked resources (stylesheets, images, fonts, etc)
|
|
234
|
+
pub(crate) url: DocumentUrl,
|
|
235
|
+
// Devtool settings. Currently used to render debug overlays
|
|
236
|
+
pub(crate) devtool_settings: DevtoolSettings,
|
|
237
|
+
// Viewport details such as the dimensions, HiDPI scale, and zoom factor,
|
|
238
|
+
pub(crate) viewport: Viewport,
|
|
239
|
+
pub(crate) layout_viewport: Option<(f32, f32)>,
|
|
240
|
+
pub(crate) round_layout: bool,
|
|
241
|
+
/// CSS media type used to evaluate `@media` rules.
|
|
242
|
+
pub(crate) media_type: MediaType,
|
|
243
|
+
/// Changes to the stylist [`Device`] that have been requested since the
|
|
244
|
+
/// last [`resolve`](Self::resolve), to be applied (coalesced into a single
|
|
245
|
+
/// device rebuild) at the start of the next resolve.
|
|
246
|
+
pub(crate) pending_device_changes: DeviceChanges,
|
|
247
|
+
/// Strategy for Stylo's style traversal during `resolve`.
|
|
248
|
+
pub(crate) style_threading: StyleThreading,
|
|
249
|
+
/// Whether incremental layout is enabled for this document. When disabled,
|
|
250
|
+
/// every node is treated as damaged on every `resolve`, so the same damage
|
|
251
|
+
/// pipeline reconstructs and relays out the whole tree.
|
|
252
|
+
pub(crate) incremental_layout: bool,
|
|
253
|
+
/// How deeply this document is nested within other documents
|
|
254
|
+
/// (0 for a root document). Used to limit `<iframe>` nesting depth.
|
|
255
|
+
pub(crate) subdocument_depth: usize,
|
|
256
|
+
|
|
257
|
+
// Events
|
|
258
|
+
pub(crate) tx: Sender<DocumentEvent>,
|
|
259
|
+
// rx will always be Some, except temporarily while processing events
|
|
260
|
+
pub(crate) rx: Option<Receiver<DocumentEvent>>,
|
|
261
|
+
|
|
262
|
+
/// A slotmap-backed tree of nodes
|
|
263
|
+
///
|
|
264
|
+
/// We pin the tree to a guarantee to the nodes it creates that the tree is stable in memory.
|
|
265
|
+
/// There is no way to create the tree - publicly or privately - that would invalidate that invariant.
|
|
266
|
+
pub(crate) nodes: Box<NodeTree>,
|
|
267
|
+
|
|
268
|
+
/// The id of the root node (a Document node)
|
|
269
|
+
pub(crate) root_node_id: NodeId,
|
|
270
|
+
|
|
271
|
+
// Stylo
|
|
272
|
+
/// The Stylo engine
|
|
273
|
+
pub(crate) stylist: Stylist,
|
|
274
|
+
pub(crate) animations: DocumentAnimationSet,
|
|
275
|
+
/// Stylo shared lock
|
|
276
|
+
pub(crate) guard: SharedRwLock,
|
|
277
|
+
/// Stylo invalidation map. We insert into this map prior to mutating nodes.
|
|
278
|
+
pub(crate) snapshots: SnapshotMap,
|
|
279
|
+
|
|
280
|
+
// Parley contexts
|
|
281
|
+
/// A Parley font context
|
|
282
|
+
pub(crate) font_ctx: Arc<Mutex<parley::FontContext>>,
|
|
283
|
+
#[cfg(feature = "parallel-construct")]
|
|
284
|
+
/// Thread-and-document-local copies to the font context
|
|
285
|
+
pub(crate) thread_font_contexts: ThreadLocal<RefCell<Box<FontContext>>>,
|
|
286
|
+
/// A Parley layout context
|
|
287
|
+
pub(crate) layout_ctx: parley::LayoutContext<TextBrush>,
|
|
288
|
+
|
|
289
|
+
/// The real (non-anonymous) node which is currently hovered (if any).
|
|
290
|
+
/// This is never a layout-generated (anonymous) node, so it remains valid
|
|
291
|
+
/// across box-tree reconstruction.
|
|
292
|
+
pub(crate) hover_node_id: Option<NodeId>,
|
|
293
|
+
/// The precise (may be anonymous) layout node under the pointer (if any).
|
|
294
|
+
/// This can be invalidated by box-tree reconstruction, and is re-resolved against
|
|
295
|
+
/// fresh layout at the end of every `resolve` pass.
|
|
296
|
+
pub(crate) hover_hit_node_id: Option<NodeId>,
|
|
297
|
+
/// Whether the node which is currently hovered is a text node/span
|
|
298
|
+
pub(crate) hover_node_is_text: bool,
|
|
299
|
+
/// The last known pointer position in client coordinates (viewport-relative, unscrolled).
|
|
300
|
+
pub(crate) last_client_pointer_position: Option<taffy::Point<f32>>,
|
|
301
|
+
/// The node which is currently focussed (if any)
|
|
302
|
+
pub(crate) focus_node_id: Option<NodeId>,
|
|
303
|
+
/// The node which is currently active (if any)
|
|
304
|
+
pub(crate) active_node_id: Option<NodeId>,
|
|
305
|
+
/// The node which recieved a mousedown event (if any)
|
|
306
|
+
pub(crate) mousedown_node_id: Option<NodeId>,
|
|
307
|
+
/// The last time a mousedown was made (for double-click detection)
|
|
308
|
+
pub(crate) last_mousedown_time: Option<Instant>,
|
|
309
|
+
/// The position where mousedown occurred (for selection drags and double-click detection)
|
|
310
|
+
pub(crate) mousedown_position: taffy::Point<f32>,
|
|
311
|
+
/// How many clicks have been made in quick succession
|
|
312
|
+
pub(crate) click_count: u16,
|
|
313
|
+
/// Whether we're currently in a text selection drag (moved 2px+ from mousedown)
|
|
314
|
+
pub(crate) drag_mode: DragMode,
|
|
315
|
+
/// The scrollbar thumb currently under the pointer, if any
|
|
316
|
+
pub(crate) hovered_scrollbar: Option<crate::node::ScrollbarRef>,
|
|
317
|
+
/// When each scroll container's overlay scrollbars were last shown
|
|
318
|
+
/// (scrolled, or the pointer left the thumb); drives their fade-out
|
|
319
|
+
pub(crate) scrollbar_activity: HashMap<NodeId, Instant>,
|
|
320
|
+
/// Whether and what kind of scroll animation is currently in progress
|
|
321
|
+
pub(crate) scroll_animation: ScrollAnimationState,
|
|
322
|
+
|
|
323
|
+
/// Text selection state (for non-input text)
|
|
324
|
+
pub(crate) text_selection: TextSelection,
|
|
325
|
+
|
|
326
|
+
// TODO: collapse animating state into a bitflags
|
|
327
|
+
/// Whether there are active CSS animations/transitions (so we should re-render every frame)
|
|
328
|
+
pub(crate) has_active_animations: bool,
|
|
329
|
+
/// Whether there is a `<canvas>` element in the DOM (so we should re-render every frame)
|
|
330
|
+
pub(crate) has_canvas: bool,
|
|
331
|
+
/// Whether there are subdocuments that are animating (so we should re-render every frame)
|
|
332
|
+
pub(crate) subdoc_is_animating: bool,
|
|
333
|
+
|
|
334
|
+
/// Map of id attribute values to node IDs for fast lookups.
|
|
335
|
+
/// May contain multiple nodes for the same id: `get_element_by_id`
|
|
336
|
+
/// returns the first in tree order.
|
|
337
|
+
pub(crate) nodes_to_id: HashMap<String, SmallVec<[NodeId; 1]>>,
|
|
338
|
+
/// Map of `<style>` and `<link>` node IDs to their associated stylesheet
|
|
339
|
+
pub(crate) nodes_to_stylesheet: BTreeMap<NodeId, DocumentStyleSheet>,
|
|
340
|
+
/// Incremented whenever `nodes_to_stylesheet` changes (a stylesheet is
|
|
341
|
+
/// added, replaced or removed), so that CSSOM wrappers can detect that
|
|
342
|
+
/// their cached rule data is stale.
|
|
343
|
+
pub(crate) stylesheet_generation: u64,
|
|
344
|
+
/// Map from form control node ID's to their associated forms node ID's
|
|
345
|
+
pub(crate) controls_to_form: HashMap<NodeId, NodeId>,
|
|
346
|
+
/// Nodes that contain sub documents
|
|
347
|
+
pub(crate) sub_document_nodes: HashSet<NodeId>,
|
|
348
|
+
/// Load state (abort controller and in-flight request id) for each
|
|
349
|
+
/// `<iframe>` element whose sub-document is loaded automatically
|
|
350
|
+
pub(crate) iframe_loads: HashMap<NodeId, crate::iframe::IframeLoad>,
|
|
351
|
+
/// Set of changed nodes for updating the accessibility tree
|
|
352
|
+
pub(crate) changed_nodes: HashSet<NodeId>,
|
|
353
|
+
/// Set of changed nodes for updating the accessibility tree
|
|
354
|
+
pub(crate) deferred_construction_nodes: Vec<ConstructionTask>,
|
|
355
|
+
|
|
356
|
+
/// Nodes that contain custom widgets
|
|
357
|
+
#[cfg(feature = "custom-widget")]
|
|
358
|
+
pub(crate) custom_widget_nodes: HashSet<NodeId>,
|
|
359
|
+
/// Rendering resources allocated by custom widgets that should be deallocated during the next render
|
|
360
|
+
#[cfg(feature = "custom-widget")]
|
|
361
|
+
pub(crate) pending_resource_deallocations: Vec<anyrender::ResourceId>,
|
|
362
|
+
|
|
363
|
+
/// Cache of loaded images, keyed by URL. Allows reusing images across multiple
|
|
364
|
+
/// elements without re-fetching from the network.
|
|
365
|
+
pub(crate) image_cache: HashMap<String, ImageData>,
|
|
366
|
+
|
|
367
|
+
/// Tracks in-flight image requests. When an image is being fetched, additional
|
|
368
|
+
/// requests for the same URL are queued here instead of starting new fetches.
|
|
369
|
+
/// Value is a list of (node_id, image_type) pairs waiting for the image.
|
|
370
|
+
pub(crate) pending_images: HashMap<String, Vec<(NodeId, ImageType)>>,
|
|
371
|
+
|
|
372
|
+
/// Nodes whose `background-image`/`mask-image` layers need flushing to
|
|
373
|
+
/// dedicated storage on the node because their style changed (populated by
|
|
374
|
+
/// the style traversal and by pseudo-element box construction).
|
|
375
|
+
pub(crate) pending_style_image_nodes: Vec<NodeId>,
|
|
376
|
+
|
|
377
|
+
// Tracks in-flight "critical" resources (e.g. stylesheets linked from the `<head>`),
|
|
378
|
+
// keyed by request id
|
|
379
|
+
pub(crate) pending_critical_resources: HashSet<usize>,
|
|
380
|
+
|
|
381
|
+
// Service providers
|
|
382
|
+
/// Network provider. Can be used to fetch assets.
|
|
383
|
+
pub net_provider: Arc<dyn NetProvider>,
|
|
384
|
+
/// Navigation provider. Can be used to navigate to a new page (bubbles up the event
|
|
385
|
+
/// on e.g. clicking a Link)
|
|
386
|
+
pub navigation_provider: Arc<dyn NavigationProvider>,
|
|
387
|
+
/// Shell provider. Can be used to request a redraw or set the cursor icon
|
|
388
|
+
pub shell_provider: Arc<dyn ShellProvider>,
|
|
389
|
+
/// HTML parser provider. Used to parse HTML for setInnerHTML
|
|
390
|
+
pub html_parser_provider: Arc<dyn HtmlParserProvider>,
|
|
391
|
+
/// Carried on every sub-resource `Request` this document issues; aborting
|
|
392
|
+
/// it cancels all in-flight fetches tied to this document. Set via
|
|
393
|
+
/// [`DocumentConfig::abort_signal`].
|
|
394
|
+
pub(crate) abort_signal: Option<AbortSignal>,
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
impl BaseDocument {
|
|
398
|
+
/// Create a new (empty) [`BaseDocument`] with the specified configuration
|
|
399
|
+
pub fn new(config: DocumentConfig) -> Self {
|
|
400
|
+
static ID_GENERATOR: AtomicUsize = AtomicUsize::new(1);
|
|
401
|
+
|
|
402
|
+
let id = ID_GENERATOR.fetch_add(1, Ordering::SeqCst);
|
|
403
|
+
|
|
404
|
+
let font_ctx = config
|
|
405
|
+
.font_ctx
|
|
406
|
+
.map(|mut font_ctx| {
|
|
407
|
+
font_ctx.source_cache.make_shared();
|
|
408
|
+
// font_ctx.collection.make_shared();
|
|
409
|
+
font_ctx
|
|
410
|
+
})
|
|
411
|
+
.unwrap_or_else(|| {
|
|
412
|
+
use parley::fontique::{Collection, CollectionOptions, SourceCache};
|
|
413
|
+
let mut font_ctx = FontContext {
|
|
414
|
+
source_cache: SourceCache::new_shared(),
|
|
415
|
+
collection: Collection::new(CollectionOptions {
|
|
416
|
+
shared: false,
|
|
417
|
+
system_fonts: cfg!(all(
|
|
418
|
+
feature = "system-fonts",
|
|
419
|
+
not(target_arch = "wasm32")
|
|
420
|
+
)),
|
|
421
|
+
}),
|
|
422
|
+
};
|
|
423
|
+
font_ctx
|
|
424
|
+
.collection
|
|
425
|
+
.register_fonts(Blob::new(Arc::new(crate::BULLET_FONT) as _), None);
|
|
426
|
+
font_ctx
|
|
427
|
+
});
|
|
428
|
+
let font_ctx = Arc::new(Mutex::new(font_ctx));
|
|
429
|
+
|
|
430
|
+
// Make sure we turn on stylo features *before* creating the Stylist
|
|
431
|
+
style_config::set_pref!("layout.grid.enabled", true);
|
|
432
|
+
style_config::set_pref!("layout.flexbox.balance", true);
|
|
433
|
+
style_config::set_pref!("layout.unimplemented", true);
|
|
434
|
+
style_config::set_pref!("layout.columns.enabled", true);
|
|
435
|
+
style_config::set_pref!("layout.css.basic-shape-shape.enabled", true);
|
|
436
|
+
style_config::set_pref!("layout.css.tree-counting-functions.enabled", true);
|
|
437
|
+
style_config::set_pref!("layout.css.progress-function.enabled", true);
|
|
438
|
+
style_config::set_pref!("layout.variable_fonts.enabled", true);
|
|
439
|
+
#[cfg(feature = "writing-mode")]
|
|
440
|
+
style_config::set_pref!("layout.writing-mode.enabled", true);
|
|
441
|
+
style_config::set_pref!("layout.threads", -1);
|
|
442
|
+
|
|
443
|
+
let viewport = config.viewport.unwrap_or_default();
|
|
444
|
+
let media_type = config.media_type.unwrap_or_else(MediaType::screen);
|
|
445
|
+
let device = make_device(
|
|
446
|
+
&viewport,
|
|
447
|
+
config.layout_viewport,
|
|
448
|
+
media_type.clone(),
|
|
449
|
+
font_ctx.clone(),
|
|
450
|
+
);
|
|
451
|
+
let stylist = Stylist::new(device, QuirksMode::NoQuirks);
|
|
452
|
+
let snapshots = SnapshotMap::new();
|
|
453
|
+
let nodes = Box::new(NodeTree::new());
|
|
454
|
+
let guard = SharedRwLock::new();
|
|
455
|
+
let nodes_to_id = HashMap::new();
|
|
456
|
+
|
|
457
|
+
let base_url = config
|
|
458
|
+
.base_url
|
|
459
|
+
.and_then(|url| DocumentUrl::from_str(&url).ok())
|
|
460
|
+
.unwrap_or_default();
|
|
461
|
+
|
|
462
|
+
let net_provider = config
|
|
463
|
+
.net_provider
|
|
464
|
+
.unwrap_or_else(|| Arc::new(DummyNetProvider));
|
|
465
|
+
let navigation_provider = config
|
|
466
|
+
.navigation_provider
|
|
467
|
+
.unwrap_or_else(|| Arc::new(DummyNavigationProvider));
|
|
468
|
+
let shell_provider = config
|
|
469
|
+
.shell_provider
|
|
470
|
+
.unwrap_or_else(|| Arc::new(DummyShellProvider));
|
|
471
|
+
let html_parser_provider = config
|
|
472
|
+
.html_parser_provider
|
|
473
|
+
.unwrap_or_else(|| Arc::new(DummyHtmlParserProvider));
|
|
474
|
+
|
|
475
|
+
let (tx, rx) = channel();
|
|
476
|
+
|
|
477
|
+
let mut doc = Self {
|
|
478
|
+
id,
|
|
479
|
+
tx,
|
|
480
|
+
rx: Some(rx),
|
|
481
|
+
|
|
482
|
+
guard,
|
|
483
|
+
nodes,
|
|
484
|
+
root_node_id: NodeId::default(),
|
|
485
|
+
stylist,
|
|
486
|
+
animations: DocumentAnimationSet::default(),
|
|
487
|
+
snapshots,
|
|
488
|
+
nodes_to_id,
|
|
489
|
+
viewport,
|
|
490
|
+
layout_viewport: config.layout_viewport,
|
|
491
|
+
round_layout: config.round_layout,
|
|
492
|
+
media_type,
|
|
493
|
+
pending_device_changes: DeviceChanges::empty(),
|
|
494
|
+
style_threading: config.style_threading,
|
|
495
|
+
incremental_layout: config.incremental.unwrap_or(true),
|
|
496
|
+
subdocument_depth: config.subdocument_depth,
|
|
497
|
+
devtool_settings: DevtoolSettings::default(),
|
|
498
|
+
url: base_url,
|
|
499
|
+
nodes_to_stylesheet: BTreeMap::new(),
|
|
500
|
+
stylesheet_generation: 0,
|
|
501
|
+
font_ctx,
|
|
502
|
+
#[cfg(feature = "parallel-construct")]
|
|
503
|
+
thread_font_contexts: ThreadLocal::new(),
|
|
504
|
+
layout_ctx: parley::LayoutContext::new(),
|
|
505
|
+
|
|
506
|
+
hover_node_id: None,
|
|
507
|
+
hover_hit_node_id: None,
|
|
508
|
+
hover_node_is_text: false,
|
|
509
|
+
last_client_pointer_position: None,
|
|
510
|
+
focus_node_id: None,
|
|
511
|
+
active_node_id: None,
|
|
512
|
+
mousedown_node_id: None,
|
|
513
|
+
has_active_animations: false,
|
|
514
|
+
subdoc_is_animating: false,
|
|
515
|
+
has_canvas: false,
|
|
516
|
+
sub_document_nodes: HashSet::new(),
|
|
517
|
+
iframe_loads: HashMap::new(),
|
|
518
|
+
|
|
519
|
+
#[cfg(feature = "custom-widget")]
|
|
520
|
+
custom_widget_nodes: HashSet::new(),
|
|
521
|
+
#[cfg(feature = "custom-widget")]
|
|
522
|
+
pending_resource_deallocations: Vec::new(),
|
|
523
|
+
|
|
524
|
+
changed_nodes: HashSet::new(),
|
|
525
|
+
deferred_construction_nodes: Vec::new(),
|
|
526
|
+
image_cache: HashMap::new(),
|
|
527
|
+
pending_images: HashMap::new(),
|
|
528
|
+
pending_style_image_nodes: Vec::new(),
|
|
529
|
+
pending_critical_resources: HashSet::new(),
|
|
530
|
+
controls_to_form: HashMap::new(),
|
|
531
|
+
net_provider,
|
|
532
|
+
navigation_provider,
|
|
533
|
+
shell_provider,
|
|
534
|
+
html_parser_provider,
|
|
535
|
+
abort_signal: config.abort_signal,
|
|
536
|
+
last_mousedown_time: None,
|
|
537
|
+
mousedown_position: taffy::Point::ZERO,
|
|
538
|
+
click_count: 0,
|
|
539
|
+
drag_mode: DragMode::None,
|
|
540
|
+
hovered_scrollbar: None,
|
|
541
|
+
scrollbar_activity: HashMap::new(),
|
|
542
|
+
scroll_animation: ScrollAnimationState::None,
|
|
543
|
+
text_selection: TextSelection::default(),
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
// Initialise document with root Document node
|
|
547
|
+
doc.root_node_id = doc.create_node(NodeData::Document(Box::default()));
|
|
548
|
+
doc.root_node_mut().flags.insert(NodeFlags::IS_IN_DOCUMENT);
|
|
549
|
+
|
|
550
|
+
for sheet in config.ua_stylesheets {
|
|
551
|
+
doc.add_user_agent_stylesheet(sheet);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Stylo data on the root node container is needed to render the node
|
|
555
|
+
let stylo_element_data = StyloElementData {
|
|
556
|
+
styles: ElementStyles {
|
|
557
|
+
primary: Some(
|
|
558
|
+
ComputedValues::initial_values_with_font_override(Font::initial_values())
|
|
559
|
+
.to_arc(),
|
|
560
|
+
),
|
|
561
|
+
..Default::default()
|
|
562
|
+
},
|
|
563
|
+
..Default::default()
|
|
564
|
+
};
|
|
565
|
+
let stylo_data = doc.root_node_mut().stylo_element_data_mut();
|
|
566
|
+
*stylo_data.ensure_init_mut() = stylo_element_data;
|
|
567
|
+
|
|
568
|
+
doc
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/// Set the Document's networking provider
|
|
572
|
+
pub fn set_net_provider(&mut self, net_provider: Arc<dyn NetProvider>) {
|
|
573
|
+
self.net_provider = net_provider;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/// Set the Document's navigation provider
|
|
577
|
+
pub fn set_navigation_provider(&mut self, navigation_provider: Arc<dyn NavigationProvider>) {
|
|
578
|
+
self.navigation_provider = navigation_provider;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/// Set the Document's shell provider
|
|
582
|
+
pub fn set_shell_provider(&mut self, shell_provider: Arc<dyn ShellProvider>) {
|
|
583
|
+
self.shell_provider = shell_provider;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/// Set the Document's html parser provider
|
|
587
|
+
pub fn set_html_parser_provider(&mut self, html_parser_provider: Arc<dyn HtmlParserProvider>) {
|
|
588
|
+
self.html_parser_provider = html_parser_provider;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/// Set base url for resolving linked resources (stylesheets, images, fonts, etc)
|
|
592
|
+
pub fn set_base_url(&mut self, url: &str) {
|
|
593
|
+
self.url = DocumentUrl::from(Url::parse(url).unwrap());
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/// The base url used for resolving linked resources (stylesheets, images, fonts, etc)
|
|
597
|
+
pub fn base_url(&self) -> &Url {
|
|
598
|
+
&self.url
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
pub fn guard(&self) -> &SharedRwLock {
|
|
602
|
+
&self.guard
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
pub fn tree(&self) -> &NodeTree {
|
|
606
|
+
&self.nodes
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
pub fn id(&self) -> usize {
|
|
610
|
+
self.id
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/// Wrapper around [`crate::net::stamped_request`]. Use the free function
|
|
614
|
+
/// when `&self` would conflict with a held `&mut` borrow on a field.
|
|
615
|
+
pub(crate) fn build_request(&self, url: url::Url) -> Request {
|
|
616
|
+
crate::net::stamped_request(url, self.abort_signal.as_ref())
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
pub fn favicon_url(&self) -> Option<String> {
|
|
620
|
+
self.tree().iter().find_map(|(_, node)| {
|
|
621
|
+
let data = &node.data;
|
|
622
|
+
if !data.is_element_with_tag_name(&local_name!("link")) {
|
|
623
|
+
return None;
|
|
624
|
+
}
|
|
625
|
+
let rel = data.attr(local_name!("rel"))?;
|
|
626
|
+
if !rel
|
|
627
|
+
.split_ascii_whitespace()
|
|
628
|
+
.any(|v| v.eq_ignore_ascii_case("icon"))
|
|
629
|
+
{
|
|
630
|
+
return None;
|
|
631
|
+
}
|
|
632
|
+
data.attr(local_name!("href")).map(|s| s.to_string())
|
|
633
|
+
})
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
|
|
637
|
+
self.nodes.get(node_id)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
|
|
641
|
+
self.nodes.get_mut(node_id)
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
pub fn get_focussed_node_id(&self) -> Option<NodeId> {
|
|
645
|
+
self.focus_node_id
|
|
646
|
+
.or(self.try_root_element().map(|el| el.id))
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
pub fn mutate<'doc>(&'doc mut self) -> DocumentMutator<'doc> {
|
|
650
|
+
DocumentMutator::new(self)
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
pub fn handle_dom_event<F: FnMut(DomEvent)>(
|
|
654
|
+
&mut self,
|
|
655
|
+
event: &mut DomEvent,
|
|
656
|
+
dispatch_event: F,
|
|
657
|
+
) {
|
|
658
|
+
handle_dom_event(self, event, dispatch_event)
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
pub fn as_any_mut(&mut self) -> &mut dyn Any {
|
|
662
|
+
self
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/// Find the label's bound input elements:
|
|
666
|
+
/// the element id referenced by the "for" attribute of a given label element
|
|
667
|
+
/// or the first input element which is nested in the label
|
|
668
|
+
/// Note that although there should only be one bound element,
|
|
669
|
+
/// we return all possibilities instead of just the first
|
|
670
|
+
/// in order to allow the caller to decide which one is correct
|
|
671
|
+
pub fn label_bound_input_element(&self, label_node_id: NodeId) -> Option<&Node> {
|
|
672
|
+
let label_element = self.nodes[label_node_id].element_data()?;
|
|
673
|
+
if let Some(target_element_dom_id) = label_element.attr(local_name!("for")) {
|
|
674
|
+
TreeTraverser::new(self)
|
|
675
|
+
.filter_map(|id| {
|
|
676
|
+
let node = self.get_node(id)?;
|
|
677
|
+
let element_data = node.element_data()?;
|
|
678
|
+
if element_data.name.local != local_name!("input") {
|
|
679
|
+
return None;
|
|
680
|
+
}
|
|
681
|
+
let id = element_data.id.as_ref()?;
|
|
682
|
+
if *id == *target_element_dom_id {
|
|
683
|
+
Some(node)
|
|
684
|
+
} else {
|
|
685
|
+
None
|
|
686
|
+
}
|
|
687
|
+
})
|
|
688
|
+
.next()
|
|
689
|
+
} else {
|
|
690
|
+
TreeTraverser::new_with_root(self, label_node_id)
|
|
691
|
+
.filter_map(|child_id| {
|
|
692
|
+
let node = self.get_node(child_id)?;
|
|
693
|
+
let element_data = node.element_data()?;
|
|
694
|
+
if element_data.name.local == local_name!("input") {
|
|
695
|
+
Some(node)
|
|
696
|
+
} else {
|
|
697
|
+
None
|
|
698
|
+
}
|
|
699
|
+
})
|
|
700
|
+
.next()
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
pub fn toggle_checkbox(el: &mut ElementData) -> bool {
|
|
705
|
+
let Some(is_checked) = el.checkbox_input_checked() else {
|
|
706
|
+
return false;
|
|
707
|
+
};
|
|
708
|
+
let checked = !is_checked;
|
|
709
|
+
el.set_checkbox_input_checked(checked);
|
|
710
|
+
|
|
711
|
+
checked
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
pub fn toggle_radio(&mut self, radio_set_name: String, target_radio_id: NodeId) {
|
|
715
|
+
let radio_ids: Vec<NodeId> = self
|
|
716
|
+
.nodes
|
|
717
|
+
.iter()
|
|
718
|
+
.filter_map(|(i, node)| {
|
|
719
|
+
let el = node.data.downcast_element()?;
|
|
720
|
+
(el.attr(local_name!("name")) == Some(&radio_set_name)
|
|
721
|
+
&& el.checkbox_input_checked().is_some())
|
|
722
|
+
.then_some(i)
|
|
723
|
+
})
|
|
724
|
+
.collect();
|
|
725
|
+
|
|
726
|
+
for i in radio_ids {
|
|
727
|
+
let checked = i == target_radio_id;
|
|
728
|
+
self.snapshot_node_and(i, ElementState::CHECKED, |node| {
|
|
729
|
+
if let Some(el) = node.element_data_mut() {
|
|
730
|
+
el.set_checkbox_input_checked(checked);
|
|
731
|
+
}
|
|
732
|
+
node.mark_ancestors_dirty();
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/// Toggle the `open` attribute of a `<details>` element, expanding or
|
|
738
|
+
/// collapsing it. This is the default action triggered when the element's
|
|
739
|
+
/// first `<summary>` child is activated.
|
|
740
|
+
pub fn toggle_details_open(&mut self, details_id: NodeId) {
|
|
741
|
+
use crate::qual_name;
|
|
742
|
+
|
|
743
|
+
let node = &self.nodes[details_id];
|
|
744
|
+
if !node.data.is_element_with_tag_name(&local_name!("details")) {
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
let is_open = node.data.has_attr(local_name!("open"));
|
|
748
|
+
|
|
749
|
+
// Note: HTML attributes are in the empty (null) namespace, so the
|
|
750
|
+
// QualName must not use the html namespace here, else it won't match
|
|
751
|
+
// an `open` attribute created by the HTML parser.
|
|
752
|
+
let mut mutator = self.mutate();
|
|
753
|
+
if is_open {
|
|
754
|
+
mutator.clear_attribute(details_id, qual_name!("open"));
|
|
755
|
+
} else {
|
|
756
|
+
mutator.set_attribute(details_id, qual_name!("open"), "");
|
|
757
|
+
}
|
|
758
|
+
drop(mutator);
|
|
759
|
+
|
|
760
|
+
self.shell_provider.request_redraw();
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
pub fn set_style_property(&mut self, node_id: NodeId, name: &str, value: &str) {
|
|
764
|
+
let node = &mut self.nodes[node_id];
|
|
765
|
+
let did_change = node.element_data_mut().unwrap().set_style_property(
|
|
766
|
+
name,
|
|
767
|
+
value,
|
|
768
|
+
&self.guard,
|
|
769
|
+
self.url.url_extra_data(),
|
|
770
|
+
);
|
|
771
|
+
if did_change {
|
|
772
|
+
node.set_restyle_hint(RestyleHint::RESTYLE_STYLE_ATTRIBUTE);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
pub fn remove_style_property(&mut self, node_id: NodeId, name: &str) {
|
|
777
|
+
let node = &mut self.nodes[node_id];
|
|
778
|
+
let did_change = node.element_data_mut().unwrap().remove_style_property(
|
|
779
|
+
name,
|
|
780
|
+
&self.guard,
|
|
781
|
+
self.url.url_extra_data(),
|
|
782
|
+
);
|
|
783
|
+
if did_change {
|
|
784
|
+
node.set_restyle_hint(RestyleHint::RESTYLE_STYLE_ATTRIBUTE);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
pub fn sub_document_node_ids(&self) -> Vec<NodeId> {
|
|
789
|
+
self.sub_document_nodes.iter().copied().collect()
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
pub fn set_sub_document(&mut self, node_id: NodeId, sub_document: Box<dyn Document>) {
|
|
793
|
+
self.nodes[node_id]
|
|
794
|
+
.element_data_mut()
|
|
795
|
+
.unwrap()
|
|
796
|
+
.set_sub_document(sub_document);
|
|
797
|
+
self.sub_document_nodes.insert(node_id);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
pub fn remove_sub_document(&mut self, node_id: NodeId) {
|
|
801
|
+
self.nodes[node_id]
|
|
802
|
+
.element_data_mut()
|
|
803
|
+
.unwrap()
|
|
804
|
+
.remove_sub_document();
|
|
805
|
+
self.sub_document_nodes.remove(&node_id);
|
|
806
|
+
if let Some(load) = self.iframe_loads.remove(&node_id) {
|
|
807
|
+
load.abort_controller.abort();
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/// Poll all sub-documents (see [`Document::poll`]), allowing them to make progress
|
|
812
|
+
/// on any pending async operations (e.g. JavaScript timers). Hosts which poll a
|
|
813
|
+
/// wrapper around a [`BaseDocument`] should call this from their `poll` implementation.
|
|
814
|
+
///
|
|
815
|
+
/// Returns `true` if any sub-document reported changes.
|
|
816
|
+
pub fn poll_subdocuments(&mut self, waker: Option<&Waker>) -> bool {
|
|
817
|
+
let mut has_changes = false;
|
|
818
|
+
for node_id in self.sub_document_nodes.iter().copied() {
|
|
819
|
+
let Some(sub_doc) = self
|
|
820
|
+
.nodes
|
|
821
|
+
.get_mut(node_id)
|
|
822
|
+
.and_then(|node| node.subdoc_mut())
|
|
823
|
+
else {
|
|
824
|
+
continue;
|
|
825
|
+
};
|
|
826
|
+
let task_context = waker.map(TaskContext::from_waker);
|
|
827
|
+
has_changes |= sub_doc.poll(task_context);
|
|
828
|
+
}
|
|
829
|
+
has_changes
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
#[cfg(feature = "custom-widget")]
|
|
833
|
+
pub fn custom_widget_node_ids(&self) -> Vec<NodeId> {
|
|
834
|
+
self.custom_widget_nodes.iter().copied().collect()
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
#[cfg(feature = "custom-widget")]
|
|
838
|
+
pub fn take_pending_resource_deallocations(&mut self) -> Vec<anyrender::ResourceId> {
|
|
839
|
+
std::mem::take(&mut self.pending_resource_deallocations)
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
#[cfg(feature = "custom-widget")]
|
|
843
|
+
pub fn set_custom_widget(&mut self, node_id: NodeId, widget: Box<dyn crate::Widget>) {
|
|
844
|
+
self.nodes[node_id]
|
|
845
|
+
.element_data_mut()
|
|
846
|
+
.unwrap()
|
|
847
|
+
.set_custom_widget(widget);
|
|
848
|
+
self.custom_widget_nodes.insert(node_id);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
#[cfg(feature = "custom-widget")]
|
|
852
|
+
pub fn remove_custom_widget(&mut self, node_id: NodeId) {
|
|
853
|
+
let resources_to_deallocate = self.nodes[node_id]
|
|
854
|
+
.element_data_mut()
|
|
855
|
+
.unwrap()
|
|
856
|
+
.remove_custom_widget();
|
|
857
|
+
self.pending_resource_deallocations
|
|
858
|
+
.extend_from_slice(&resources_to_deallocate);
|
|
859
|
+
self.custom_widget_nodes.remove(&node_id);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
pub fn root_node(&self) -> &Node {
|
|
863
|
+
&self.nodes[self.root_node_id]
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
pub fn root_node_mut(&mut self) -> &mut Node {
|
|
867
|
+
&mut self.nodes[self.root_node_id]
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
pub fn try_root_element(&self) -> Option<&Node> {
|
|
871
|
+
TDocument::as_node(&self.root_node()).first_element_child()
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
pub fn root_element(&self) -> &Node {
|
|
875
|
+
TDocument::as_node(&self.root_node())
|
|
876
|
+
.first_element_child()
|
|
877
|
+
.unwrap()
|
|
878
|
+
.as_element()
|
|
879
|
+
.unwrap()
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
pub fn create_node(&mut self, node_data: NodeData) -> NodeId {
|
|
883
|
+
let tree_ptr = self.nodes.as_mut() as *mut NodeTree;
|
|
884
|
+
let guard = self.guard.clone();
|
|
885
|
+
|
|
886
|
+
let id = self
|
|
887
|
+
.nodes
|
|
888
|
+
.insert_with_key(|id| Node::new(tree_ptr, id, guard, node_data));
|
|
889
|
+
|
|
890
|
+
// Mark the new node as changed.
|
|
891
|
+
self.changed_nodes.insert(id);
|
|
892
|
+
id
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/// Remove a node from the node tree, clearing any interaction state
|
|
896
|
+
/// (hover/active/focus/mousedown/selection/drag/scrollbar) that references
|
|
897
|
+
/// it so that stale NodeIds are never dereferenced after the slot is freed.
|
|
898
|
+
pub(crate) fn remove_node_from_tree(&mut self, node_id: NodeId) -> Option<Node> {
|
|
899
|
+
self.clear_interaction_state_for_removed_node(node_id);
|
|
900
|
+
self.nodes.remove(node_id)
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/// The nearest element ancestor of `node_id` that is still in the
|
|
904
|
+
/// document. Used to retarget hover/active state when the node they
|
|
905
|
+
/// reference is removed. Tolerates already-removed ancestors (subtree
|
|
906
|
+
/// teardown proceeds root-first) by giving up and returning `None`.
|
|
907
|
+
fn nearest_surviving_element_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
|
|
908
|
+
let mut current = self.get_node(node_id)?.parent;
|
|
909
|
+
while let Some(id) = current {
|
|
910
|
+
let node = self.get_node(id)?;
|
|
911
|
+
if node.is_element() && node.flags.is_in_document() {
|
|
912
|
+
return Some(id);
|
|
913
|
+
}
|
|
914
|
+
current = node.parent;
|
|
915
|
+
}
|
|
916
|
+
None
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/// Clear any interaction state (hover/active/focus/mousedown/selection/
|
|
920
|
+
/// drag/scrollbar) that references `node_id`, which is being removed from
|
|
921
|
+
/// the document, running the usual teardown steps. `node_id` must still be
|
|
922
|
+
/// present in the slab.
|
|
923
|
+
///
|
|
924
|
+
/// This matches browser semantics (WebKit `hoveredElementDidDetach` /
|
|
925
|
+
/// `elementInActiveChainDidDetach`, Blink `HoveredElementDetached` /
|
|
926
|
+
/// `ActiveChainNodeDetached`):
|
|
927
|
+
/// - Hover and active retarget to the nearest surviving element ancestor
|
|
928
|
+
/// as a *transient bridge*: the HOVER/ACTIVE element-state bits along
|
|
929
|
+
/// the surviving chain stay lit (no one-frame gap in `:hover`/`:active`
|
|
930
|
+
/// styling), and the subsequent hover diff can unset exactly the right
|
|
931
|
+
/// bits. Hover is then re-resolved against the pointer position by
|
|
932
|
+
/// [`Self::refresh_hover`] at the end of the next resolve pass (the
|
|
933
|
+
/// analogue of WebKit's "fake mouse move"), which corrects the bridge
|
|
934
|
+
/// value — including cases where the removed node overflowed its
|
|
935
|
+
/// ancestor's box, so the ancestor was never truly under the pointer.
|
|
936
|
+
/// - Focus resets to the body (encoded as `None`), running blur
|
|
937
|
+
/// side-effects (clearing focus element state and disabling IME for
|
|
938
|
+
/// text inputs).
|
|
939
|
+
pub(crate) fn clear_interaction_state_for_removed_node(&mut self, node_id: NodeId) {
|
|
940
|
+
if !self.nodes.contains_key(node_id) {
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
if self.hover_node_id == Some(node_id) {
|
|
945
|
+
self.hover_node_id = self.nearest_surviving_element_ancestor(node_id);
|
|
946
|
+
self.hover_node_is_text = false;
|
|
947
|
+
}
|
|
948
|
+
if self.hover_hit_node_id == Some(node_id) {
|
|
949
|
+
self.hover_hit_node_id = None;
|
|
950
|
+
}
|
|
951
|
+
if self.active_node_id == Some(node_id) {
|
|
952
|
+
self.active_node_id = self.nearest_surviving_element_ancestor(node_id);
|
|
953
|
+
}
|
|
954
|
+
if self.focus_node_id == Some(node_id) {
|
|
955
|
+
let shell_provider = self.shell_provider.clone();
|
|
956
|
+
self.nodes[node_id].blur(shell_provider);
|
|
957
|
+
self.focus_node_id = None;
|
|
958
|
+
}
|
|
959
|
+
if self.mousedown_node_id == Some(node_id) {
|
|
960
|
+
self.mousedown_node_id = None;
|
|
961
|
+
}
|
|
962
|
+
if self.text_selection.anchor.node_or_parent == Some(node_id)
|
|
963
|
+
|| self.text_selection.focus.node_or_parent == Some(node_id)
|
|
964
|
+
{
|
|
965
|
+
self.text_selection.clear();
|
|
966
|
+
}
|
|
967
|
+
if self
|
|
968
|
+
.hovered_scrollbar
|
|
969
|
+
.is_some_and(|scrollbar| scrollbar.node_id == node_id)
|
|
970
|
+
{
|
|
971
|
+
self.hovered_scrollbar = None;
|
|
972
|
+
}
|
|
973
|
+
let drag_references_node = match &self.drag_mode {
|
|
974
|
+
DragMode::Panning(state) => state.target == node_id,
|
|
975
|
+
DragMode::ScrollbarDrag(state) => state.scrollbar.node_id == node_id,
|
|
976
|
+
DragMode::Selecting | DragMode::None => false,
|
|
977
|
+
};
|
|
978
|
+
if drag_references_node {
|
|
979
|
+
self.drag_mode = DragMode::None;
|
|
980
|
+
}
|
|
981
|
+
self.scrollbar_activity.remove(&node_id);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
pub(crate) fn drop_node_ignoring_parent(&mut self, node_id: NodeId) -> Option<Node> {
|
|
985
|
+
self.drop_node_ignoring_parent_with(node_id, &mut |_| {})
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/// Like [`Self::drop_node_ignoring_parent`], but calls `on_drop` with the id of
|
|
989
|
+
/// every dropped node (the node itself and all of its descendants).
|
|
990
|
+
pub(crate) fn drop_node_ignoring_parent_with(
|
|
991
|
+
&mut self,
|
|
992
|
+
node_id: NodeId,
|
|
993
|
+
on_drop: &mut dyn FnMut(NodeId),
|
|
994
|
+
) -> Option<Node> {
|
|
995
|
+
let mut node = self.remove_node_from_tree(node_id);
|
|
996
|
+
if let Some(node) = &mut node {
|
|
997
|
+
on_drop(node_id);
|
|
998
|
+
if let Some(before) = node.before() {
|
|
999
|
+
self.drop_node_ignoring_parent_with(before, on_drop);
|
|
1000
|
+
}
|
|
1001
|
+
if let Some(after) = node.after() {
|
|
1002
|
+
self.drop_node_ignoring_parent_with(after, on_drop);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
for &child in &node.children {
|
|
1006
|
+
self.drop_node_ignoring_parent_with(child, on_drop);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// Anonymous blocks live only in the slab, so deallocate the ones this
|
|
1010
|
+
// node owns rather than leaking them.
|
|
1011
|
+
for &anon_id in &node.anonymous_blocks {
|
|
1012
|
+
self.deallocate_anonymous_block(anon_id);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
node
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/// Deallocate an anonymous block created in a previous construction
|
|
1019
|
+
/// round, along with any anonymous blocks nested within it.
|
|
1020
|
+
pub(crate) fn deallocate_anonymous_block(&mut self, anon_id: NodeId) {
|
|
1021
|
+
// The block may already have been removed from the slab (e.g. a
|
|
1022
|
+
// whitespace-only anonymous block dropped during construction).
|
|
1023
|
+
if !self.nodes.contains_key(anon_id) {
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Free any anonymous blocks that this block owns before removing it.
|
|
1028
|
+
let nested = std::mem::take(&mut self.nodes[anon_id].anonymous_blocks);
|
|
1029
|
+
for nested_id in nested {
|
|
1030
|
+
self.deallocate_anonymous_block(nested_id);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
self.remove_node_from_tree(anon_id);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/// Whether the document has been mutated
|
|
1037
|
+
pub fn has_changes(&self) -> bool {
|
|
1038
|
+
self.changed_nodes.is_empty()
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
pub fn create_text_node(&mut self, text: &str) -> NodeId {
|
|
1042
|
+
let content = text.to_string();
|
|
1043
|
+
let data = NodeData::Text(TextNodeData::new(content));
|
|
1044
|
+
self.create_node(data)
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
pub fn deep_clone_node(&mut self, node_id: NodeId) -> NodeId {
|
|
1048
|
+
// Load existing node
|
|
1049
|
+
let node = &self.nodes[node_id];
|
|
1050
|
+
let mut data = node.data.clone();
|
|
1051
|
+
|
|
1052
|
+
match &mut data {
|
|
1053
|
+
NodeData::Element(elem) | NodeData::AnonymousBlock(elem) => {
|
|
1054
|
+
if let Some(arc) = elem.style_attribute.as_mut() {
|
|
1055
|
+
let read_guard = self.guard().read();
|
|
1056
|
+
let block = arc.read_with(&read_guard);
|
|
1057
|
+
*arc = ServoArc::new(self.guard().wrap(block.clone()));
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
_ => {}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
let children = node.children.clone();
|
|
1064
|
+
|
|
1065
|
+
// Create new node
|
|
1066
|
+
let new_node_id = self.create_node(data);
|
|
1067
|
+
|
|
1068
|
+
// Recursively clone children
|
|
1069
|
+
let new_children: ThinVec<NodeId> = children
|
|
1070
|
+
.into_iter()
|
|
1071
|
+
.map(|child_id| self.deep_clone_node(child_id))
|
|
1072
|
+
.collect();
|
|
1073
|
+
for &child_id in &new_children {
|
|
1074
|
+
self.nodes[child_id].parent = Some(new_node_id);
|
|
1075
|
+
}
|
|
1076
|
+
self.nodes[new_node_id].children = new_children;
|
|
1077
|
+
|
|
1078
|
+
new_node_id
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
pub(crate) fn remove_and_drop_pe(&mut self, node_id: NodeId) -> Option<Node> {
|
|
1082
|
+
fn remove_pe_ignoring_parent(doc: &mut BaseDocument, node_id: NodeId) -> Option<Node> {
|
|
1083
|
+
let mut node = doc.remove_node_from_tree(node_id);
|
|
1084
|
+
if let Some(node) = &mut node {
|
|
1085
|
+
for &child in &node.children {
|
|
1086
|
+
remove_pe_ignoring_parent(doc, child);
|
|
1087
|
+
}
|
|
1088
|
+
for &anon_id in &node.anonymous_blocks {
|
|
1089
|
+
doc.deallocate_anonymous_block(anon_id);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
node
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
let node = remove_pe_ignoring_parent(self, node_id);
|
|
1096
|
+
|
|
1097
|
+
// Update child_idx values
|
|
1098
|
+
if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
|
|
1099
|
+
let parent = &mut self.nodes[parent_id];
|
|
1100
|
+
parent.children.retain(|id| *id != node_id);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
node
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
pub(crate) fn resolve_url(&self, raw: &str) -> url::Url {
|
|
1107
|
+
self.url.resolve_relative(raw).unwrap_or_else(|| {
|
|
1108
|
+
panic!(
|
|
1109
|
+
"to be able to resolve {raw} with the base_url: {:?}",
|
|
1110
|
+
*self.url
|
|
1111
|
+
)
|
|
1112
|
+
})
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
pub fn print_tree(&self) {
|
|
1116
|
+
crate::util::walk_tree(0, self.root_node());
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
pub fn print_subtree(&self, node_id: NodeId) {
|
|
1120
|
+
crate::util::walk_tree(0, &self.nodes[node_id]);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
pub fn reload_resource_by_href(&mut self, href_to_reload: &str) {
|
|
1124
|
+
for &node_id in self.nodes_to_stylesheet.keys() {
|
|
1125
|
+
let node = &self.nodes[node_id];
|
|
1126
|
+
let Some(element) = node.element_data() else {
|
|
1127
|
+
continue;
|
|
1128
|
+
};
|
|
1129
|
+
|
|
1130
|
+
if element.name.local == local_name!("link") {
|
|
1131
|
+
if let Some(href) = element.attr(local_name!("href")) {
|
|
1132
|
+
// println!("Node {node_id} {href} {href_to_reload} {} {}", resolved_href.as_str(), resolved_href.as_str() == url_to_reload);
|
|
1133
|
+
if href == href_to_reload {
|
|
1134
|
+
let resolved_href = self.resolve_url(href);
|
|
1135
|
+
self.net_provider.fetch(
|
|
1136
|
+
self.id(),
|
|
1137
|
+
self.build_request(resolved_href.clone()),
|
|
1138
|
+
ResourceHandler::boxed(
|
|
1139
|
+
self.tx.clone(),
|
|
1140
|
+
self.id,
|
|
1141
|
+
Some(node_id),
|
|
1142
|
+
self.shell_provider.clone(),
|
|
1143
|
+
StylesheetHandler {
|
|
1144
|
+
source_url: resolved_href,
|
|
1145
|
+
guard: self.guard.clone(),
|
|
1146
|
+
net_provider: self.net_provider.clone(),
|
|
1147
|
+
abort_signal: self.abort_signal.clone(),
|
|
1148
|
+
},
|
|
1149
|
+
),
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
pub fn process_style_element(&mut self, target_id: NodeId) {
|
|
1158
|
+
let css = self.nodes[target_id].text_content();
|
|
1159
|
+
let css = html_escape::decode_html_entities(&css);
|
|
1160
|
+
let sheet = self.make_stylesheet(&css, Origin::Author);
|
|
1161
|
+
self.add_stylesheet_for_node(sheet, target_id);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
pub fn remove_user_agent_stylesheet(&mut self, sheet: &UserAgentStylesheet) {
|
|
1165
|
+
if self.useragent_stylesheets().any(|s| *s == sheet.0) {
|
|
1166
|
+
self.stylist
|
|
1167
|
+
.remove_stylesheet(sheet.0.clone(), &self.guard.read());
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/// The document's base URL
|
|
1172
|
+
pub fn url(&self) -> &url::Url {
|
|
1173
|
+
&self.url
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/// Iterate over the author stylesheets (from `<style>` and `<link>` nodes)
|
|
1177
|
+
/// currently associated with this document
|
|
1178
|
+
pub fn author_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
|
|
1179
|
+
self.nodes_to_stylesheet.values()
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/// Iterate over the user-agent stylesheets currently associated with this document
|
|
1183
|
+
pub fn useragent_stylesheets(&self) -> impl Iterator<Item = &DocumentStyleSheet> {
|
|
1184
|
+
(0..self.stylist.sheet_count(Origin::UserAgent))
|
|
1185
|
+
.filter_map(|i| self.stylist.sheet_at(Origin::UserAgent, i))
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/// Append a user-agent stylesheet. Adding a sheet that is already present is a no-op.
|
|
1189
|
+
pub fn add_user_agent_stylesheet(&mut self, sheet: UserAgentStylesheet) {
|
|
1190
|
+
if self.useragent_stylesheets().any(|s| *s == sheet.0) {
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
self.stylist.append_stylesheet(sheet.0, &self.guard.read());
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
pub fn make_stylesheet(&self, css: impl AsRef<str>, origin: Origin) -> DocumentStyleSheet {
|
|
1197
|
+
let data = Stylesheet::from_str(
|
|
1198
|
+
css.as_ref(),
|
|
1199
|
+
self.url.url_extra_data(),
|
|
1200
|
+
origin,
|
|
1201
|
+
ServoArc::new(self.guard.wrap(MediaList::empty())),
|
|
1202
|
+
self.guard.clone(),
|
|
1203
|
+
Some(&StylesheetLoader {
|
|
1204
|
+
tx: self.tx.clone(),
|
|
1205
|
+
doc_id: self.id,
|
|
1206
|
+
net_provider: self.net_provider.clone(),
|
|
1207
|
+
shell_provider: self.shell_provider.clone(),
|
|
1208
|
+
abort_signal: self.abort_signal.clone(),
|
|
1209
|
+
import_depth: 0,
|
|
1210
|
+
}),
|
|
1211
|
+
None,
|
|
1212
|
+
QuirksMode::NoQuirks,
|
|
1213
|
+
AllowImportRules::Yes,
|
|
1214
|
+
);
|
|
1215
|
+
|
|
1216
|
+
DocumentStyleSheet(ServoArc::new(data))
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
pub fn upsert_stylesheet_for_node(&mut self, node_id: NodeId) {
|
|
1220
|
+
let raw_styles = self.nodes[node_id].text_content();
|
|
1221
|
+
let sheet = self.make_stylesheet(raw_styles, Origin::Author);
|
|
1222
|
+
self.add_stylesheet_for_node(sheet, node_id);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
pub fn add_stylesheet_for_node(&mut self, stylesheet: DocumentStyleSheet, node_id: NodeId) {
|
|
1226
|
+
let old = self.nodes_to_stylesheet.insert(node_id, stylesheet.clone());
|
|
1227
|
+
self.stylesheet_generation += 1;
|
|
1228
|
+
|
|
1229
|
+
if let Some(old) = old {
|
|
1230
|
+
self.stylist.remove_stylesheet(old, &self.guard.read())
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Fetch @font-face fonts
|
|
1234
|
+
crate::net::fetch_font_face(
|
|
1235
|
+
self.tx.clone(),
|
|
1236
|
+
self.id,
|
|
1237
|
+
Some(node_id),
|
|
1238
|
+
&stylesheet.0,
|
|
1239
|
+
&self.net_provider,
|
|
1240
|
+
&self.shell_provider,
|
|
1241
|
+
&self.guard.read(),
|
|
1242
|
+
self.abort_signal.as_ref(),
|
|
1243
|
+
);
|
|
1244
|
+
|
|
1245
|
+
// Store data on element
|
|
1246
|
+
let element = &mut self.nodes[node_id].element_data_mut().unwrap();
|
|
1247
|
+
element.special_data = SpecialElementData::Stylesheet(stylesheet.clone());
|
|
1248
|
+
|
|
1249
|
+
// TODO: Nodes could potentially get reused so ordering by node_id might be wrong.
|
|
1250
|
+
let insertion_point = self
|
|
1251
|
+
.nodes_to_stylesheet
|
|
1252
|
+
.range((Bound::Excluded(node_id), Bound::Unbounded))
|
|
1253
|
+
.next()
|
|
1254
|
+
.map(|(_, sheet)| sheet);
|
|
1255
|
+
|
|
1256
|
+
if let Some(insertion_point) = insertion_point {
|
|
1257
|
+
self.stylist.insert_stylesheet_before(
|
|
1258
|
+
stylesheet,
|
|
1259
|
+
insertion_point.clone(),
|
|
1260
|
+
&self.guard.read(),
|
|
1261
|
+
)
|
|
1262
|
+
} else {
|
|
1263
|
+
self.stylist
|
|
1264
|
+
.append_stylesheet(stylesheet, &self.guard.read())
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
pub fn handle_messages(&mut self) {
|
|
1269
|
+
// Remove event Reciever from the Document so that we can process events
|
|
1270
|
+
// without holding a borrow to the Document
|
|
1271
|
+
let rx = self.rx.take().unwrap();
|
|
1272
|
+
|
|
1273
|
+
while let Ok(msg) = rx.try_recv() {
|
|
1274
|
+
self.handle_message(msg);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// Put Reciever back
|
|
1278
|
+
self.rx = Some(rx);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
pub fn handle_message(&mut self, msg: DocumentEvent) {
|
|
1282
|
+
match msg {
|
|
1283
|
+
DocumentEvent::ResourceLoad(resource) => self.load_resource(resource),
|
|
1284
|
+
DocumentEvent::NavigateIframe { node_id, url } => self.navigate_iframe(node_id, url),
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/// Whether the Document has pending requests for "critical" resources (that should block rendering)
|
|
1289
|
+
pub fn has_pending_critical_resources(&self) -> bool {
|
|
1290
|
+
!self.pending_critical_resources.is_empty()
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
pub fn load_resource(&mut self, res: ResourceLoadResponse) {
|
|
1294
|
+
self.pending_critical_resources.remove(&res.request_id);
|
|
1295
|
+
|
|
1296
|
+
let resource = match res.result {
|
|
1297
|
+
Ok(resource) => resource,
|
|
1298
|
+
Err(err) => {
|
|
1299
|
+
if let Some(url) = res.resolved_url.as_ref() {
|
|
1300
|
+
let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
|
|
1301
|
+
#[cfg(feature = "tracing")]
|
|
1302
|
+
tracing::warn!(
|
|
1303
|
+
url = url.as_str(),
|
|
1304
|
+
waiting_nodes = waiting_nodes.len(),
|
|
1305
|
+
error = err.as_str(),
|
|
1306
|
+
"Resource load failed"
|
|
1307
|
+
);
|
|
1308
|
+
#[cfg(not(feature = "tracing"))]
|
|
1309
|
+
let _ = (waiting_nodes, err);
|
|
1310
|
+
} else {
|
|
1311
|
+
#[cfg(feature = "tracing")]
|
|
1312
|
+
tracing::warn!(error = err.as_str(), "Resource load failed (no url)");
|
|
1313
|
+
#[cfg(not(feature = "tracing"))]
|
|
1314
|
+
let _ = err;
|
|
1315
|
+
}
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1319
|
+
|
|
1320
|
+
match resource {
|
|
1321
|
+
Resource::Css(css) => {
|
|
1322
|
+
let node_id = res.node_id.unwrap();
|
|
1323
|
+
self.add_stylesheet_for_node(css, node_id);
|
|
1324
|
+
}
|
|
1325
|
+
Resource::ImportedCss(import_rule, sheet) => {
|
|
1326
|
+
let mut guard = self.guard.write();
|
|
1327
|
+
import_rule.write_with(&mut guard).stylesheet =
|
|
1328
|
+
style::stylesheets::import_rule::ImportSheet::Sheet(sheet);
|
|
1329
|
+
drop(guard);
|
|
1330
|
+
self.stylist
|
|
1331
|
+
.force_stylesheet_origins_dirty(style::stylesheets::OriginSet::all());
|
|
1332
|
+
}
|
|
1333
|
+
Resource::Image(_kind, width, height, image_data) => {
|
|
1334
|
+
// Create the ImageData and cache it
|
|
1335
|
+
let image = ImageData::Raster(RasterImageData::new(width, height, image_data));
|
|
1336
|
+
|
|
1337
|
+
let Some(url) = res.resolved_url.as_ref() else {
|
|
1338
|
+
return;
|
|
1339
|
+
};
|
|
1340
|
+
|
|
1341
|
+
self.apply_loaded_image(url, image);
|
|
1342
|
+
}
|
|
1343
|
+
#[cfg(feature = "svg")]
|
|
1344
|
+
Resource::Svg(_kind, svg) => {
|
|
1345
|
+
// Create the ImageData and cache it
|
|
1346
|
+
let image = ImageData::Svg(svg);
|
|
1347
|
+
|
|
1348
|
+
let Some(url) = res.resolved_url.as_ref() else {
|
|
1349
|
+
return;
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1352
|
+
self.apply_loaded_image(url, image);
|
|
1353
|
+
}
|
|
1354
|
+
Resource::DocumentSrc(html) => {
|
|
1355
|
+
let Some(node_id) = res.node_id else {
|
|
1356
|
+
return;
|
|
1357
|
+
};
|
|
1358
|
+
self.apply_iframe_html(node_id, res.request_id, res.resolved_url, &html);
|
|
1359
|
+
}
|
|
1360
|
+
Resource::Font(bytes, overrides) => {
|
|
1361
|
+
let font = Blob::new(Arc::new(bytes));
|
|
1362
|
+
|
|
1363
|
+
// Build a `FontInfoOverride` from the `@font-face` descriptors
|
|
1364
|
+
// captured during stylesheet parsing. Without this, parley
|
|
1365
|
+
// reads the family name from the TTF's own metadata, which
|
|
1366
|
+
// means CSS `font-family: 'Avenir Book'` won't match a font
|
|
1367
|
+
// file that internally identifies as `Avenir 45 Book`.
|
|
1368
|
+
let weight_override = overrides.weight.map(parley::fontique::FontWeight::new);
|
|
1369
|
+
let info_override = parley::fontique::FontInfoOverride {
|
|
1370
|
+
family_name: overrides.family_name.as_deref(),
|
|
1371
|
+
weight: weight_override,
|
|
1372
|
+
style: overrides.style,
|
|
1373
|
+
..Default::default()
|
|
1374
|
+
};
|
|
1375
|
+
|
|
1376
|
+
// TODO: Investigate eliminating double-box
|
|
1377
|
+
let mut global_font_ctx = self.font_ctx.lock().unwrap();
|
|
1378
|
+
global_font_ctx
|
|
1379
|
+
.collection
|
|
1380
|
+
.register_fonts(font.clone(), Some(info_override));
|
|
1381
|
+
|
|
1382
|
+
#[cfg(feature = "parallel-construct")]
|
|
1383
|
+
{
|
|
1384
|
+
rayon::broadcast(|_ctx| {
|
|
1385
|
+
let mut font_ctx = self
|
|
1386
|
+
.thread_font_contexts
|
|
1387
|
+
.get_or(|| RefCell::new(Box::new(global_font_ctx.clone())))
|
|
1388
|
+
.borrow_mut();
|
|
1389
|
+
font_ctx
|
|
1390
|
+
.collection
|
|
1391
|
+
.register_fonts(font.clone(), Some(info_override));
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
drop(global_font_ctx);
|
|
1395
|
+
|
|
1396
|
+
// TODO: see if we can only invalidate if resolved fonts may have changed
|
|
1397
|
+
self.invalidate_inline_contexts();
|
|
1398
|
+
}
|
|
1399
|
+
Resource::None => {
|
|
1400
|
+
// Do nothing
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
/// Cache a loaded image and apply it to all nodes waiting on it
|
|
1406
|
+
/// (`<img>` elements, `background-image` layers and `mask-image` layers).
|
|
1407
|
+
fn apply_loaded_image(&mut self, url: &str, image: ImageData) {
|
|
1408
|
+
// Get all nodes waiting for this image
|
|
1409
|
+
let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
|
|
1410
|
+
|
|
1411
|
+
#[cfg(feature = "tracing")]
|
|
1412
|
+
tracing::info!(
|
|
1413
|
+
"Image {url} loaded, applying to {} nodes",
|
|
1414
|
+
waiting_nodes.len()
|
|
1415
|
+
);
|
|
1416
|
+
|
|
1417
|
+
// Cache the image
|
|
1418
|
+
self.image_cache.insert(url.to_string(), image.clone());
|
|
1419
|
+
|
|
1420
|
+
// Apply to all waiting nodes
|
|
1421
|
+
for (node_id, image_type) in waiting_nodes {
|
|
1422
|
+
let Some(node) = self.get_node_mut(node_id) else {
|
|
1423
|
+
continue;
|
|
1424
|
+
};
|
|
1425
|
+
|
|
1426
|
+
match image_type {
|
|
1427
|
+
ImageType::Image => {
|
|
1428
|
+
let element = node.element_data_mut().unwrap();
|
|
1429
|
+
#[cfg(feature = "custom-widget")]
|
|
1430
|
+
if element.custom_widget_data().is_some() {
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1433
|
+
element.special_data = SpecialElementData::Image(Box::new(image.clone()));
|
|
1434
|
+
|
|
1435
|
+
// Clear layout cache
|
|
1436
|
+
node.clear_layout_cache();
|
|
1437
|
+
node.insert_damage(ALL_DAMAGE);
|
|
1438
|
+
}
|
|
1439
|
+
ImageType::Background(idx) | ImageType::Mask(idx) => {
|
|
1440
|
+
let layer_image = node.element_data_mut().and_then(|el| {
|
|
1441
|
+
let images = match image_type {
|
|
1442
|
+
ImageType::Background(_) => &mut el.background_images,
|
|
1443
|
+
ImageType::Mask(_) => &mut el.mask_images,
|
|
1444
|
+
ImageType::Image => unreachable!(),
|
|
1445
|
+
};
|
|
1446
|
+
images.get_mut(idx)
|
|
1447
|
+
});
|
|
1448
|
+
if let Some(Some(layer_image)) = layer_image {
|
|
1449
|
+
layer_image.status = Status::Ok;
|
|
1450
|
+
layer_image.image = image.clone();
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/// Snapshot the node's pre-mutation state (element state and attributes) ahead of
|
|
1458
|
+
/// an attribute mutation, so that the next style traversal can diff selector matches
|
|
1459
|
+
/// then-vs-now and invalidate the affected elements.
|
|
1460
|
+
pub fn snapshot_node(&mut self, node_id: NodeId) {
|
|
1461
|
+
self.snapshot_node_impl(node_id, true)
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/// Snapshot only the node's pre-change [`ElementState`] ahead of a state change
|
|
1465
|
+
/// (hover/focus/active/etc). Cheaper than [`Self::snapshot_node`] as it does not
|
|
1466
|
+
/// copy attributes or trigger attribute/class/id invalidation work.
|
|
1467
|
+
pub fn snapshot_node_state_only(&mut self, node_id: NodeId) {
|
|
1468
|
+
self.snapshot_node_impl(node_id, false)
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
fn snapshot_node_impl(&mut self, node_id: NodeId, capture_attrs: bool) {
|
|
1472
|
+
let node = &mut self.nodes[node_id];
|
|
1473
|
+
|
|
1474
|
+
// Do not snapshot nodes that have never been styled. A snapshot records an element's
|
|
1475
|
+
// pre-mutation state so a restyle can diff selector matches then-vs-now. An element
|
|
1476
|
+
// that has never been styled has no "then" to diff against. Snapshotting it anyway
|
|
1477
|
+
// makes Stylo's invalidation unwrap its (absent) primary style and panic.
|
|
1478
|
+
let has_been_styled = node.primary_styles().is_some();
|
|
1479
|
+
if !has_been_styled {
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
let opaque_node_id = TNode::opaque(&&*node);
|
|
1484
|
+
node.set_has_snapshot(true);
|
|
1485
|
+
node.snapshot_handled()
|
|
1486
|
+
.store(false, std::sync::atomic::Ordering::SeqCst);
|
|
1487
|
+
|
|
1488
|
+
// A snapshot records the element's state/attributes as they were *before the first
|
|
1489
|
+
// mutation* since the last style flush (matching Gecko's ServoElementSnapshot
|
|
1490
|
+
// semantics): state is captured at most once, and attributes are captured at most
|
|
1491
|
+
// once, but a state-only snapshot is upgraded to also capture attributes if an
|
|
1492
|
+
// attribute mutation follows.
|
|
1493
|
+
let needs_attrs = capture_attrs
|
|
1494
|
+
&& self
|
|
1495
|
+
.snapshots
|
|
1496
|
+
.get_mut(&opaque_node_id)
|
|
1497
|
+
.is_none_or(|snapshot| snapshot.attrs.is_none());
|
|
1498
|
+
|
|
1499
|
+
let (attrs, changed_attrs) = if needs_attrs {
|
|
1500
|
+
let node = &self.nodes[node_id];
|
|
1501
|
+
let attrs: Option<Vec<_>> = node.attrs().map(|attrs| {
|
|
1502
|
+
attrs
|
|
1503
|
+
.iter()
|
|
1504
|
+
.map(|attr| {
|
|
1505
|
+
let ident = AttrIdentifier {
|
|
1506
|
+
local_name: GenericAtomIdent(attr.name.local.clone()),
|
|
1507
|
+
name: GenericAtomIdent(attr.name.local.clone()),
|
|
1508
|
+
namespace: GenericAtomIdent(attr.name.ns.clone()),
|
|
1509
|
+
prefix: None,
|
|
1510
|
+
};
|
|
1511
|
+
|
|
1512
|
+
let value = if attr.name.local == local_name!("id") {
|
|
1513
|
+
AttrValue::Atom(Atom::from(&*attr.value))
|
|
1514
|
+
} else if attr.name.local == local_name!("class") {
|
|
1515
|
+
let classes = attr
|
|
1516
|
+
.value
|
|
1517
|
+
.split_ascii_whitespace()
|
|
1518
|
+
.map(Atom::from)
|
|
1519
|
+
.collect();
|
|
1520
|
+
AttrValue::TokenList(OnceLock::from(attr.value.clone()), classes)
|
|
1521
|
+
} else {
|
|
1522
|
+
AttrValue::String(attr.value.clone())
|
|
1523
|
+
};
|
|
1524
|
+
|
|
1525
|
+
(ident, value)
|
|
1526
|
+
})
|
|
1527
|
+
.collect()
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
let changed_attrs: Vec<_> = attrs
|
|
1531
|
+
.as_ref()
|
|
1532
|
+
.map(|attrs| attrs.iter().map(|attr| attr.0.name.clone()).collect())
|
|
1533
|
+
.unwrap_or_default();
|
|
1534
|
+
|
|
1535
|
+
(attrs, changed_attrs)
|
|
1536
|
+
} else {
|
|
1537
|
+
(None, Vec::new())
|
|
1538
|
+
};
|
|
1539
|
+
|
|
1540
|
+
if let Some(snapshot) = self.snapshots.get_mut(&opaque_node_id) {
|
|
1541
|
+
// The existing snapshot's state is preserved: it records the state before
|
|
1542
|
+
// the *first* change since the last style flush.
|
|
1543
|
+
if needs_attrs {
|
|
1544
|
+
snapshot.attrs = attrs;
|
|
1545
|
+
snapshot.changed_attrs = changed_attrs;
|
|
1546
|
+
snapshot.class_changed = true;
|
|
1547
|
+
snapshot.id_changed = true;
|
|
1548
|
+
snapshot.other_attributes_changed = true;
|
|
1549
|
+
}
|
|
1550
|
+
} else {
|
|
1551
|
+
self.snapshots.insert(
|
|
1552
|
+
opaque_node_id,
|
|
1553
|
+
ServoElementSnapshot {
|
|
1554
|
+
state: Some(*self.nodes[node_id].element_state()),
|
|
1555
|
+
attrs,
|
|
1556
|
+
changed_attrs,
|
|
1557
|
+
class_changed: needs_attrs,
|
|
1558
|
+
id_changed: needs_attrs,
|
|
1559
|
+
other_attributes_changed: needs_attrs,
|
|
1560
|
+
},
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
/// Returns whether any style rule depends on any of the given [`ElementState`] bits.
|
|
1566
|
+
/// If not, changing those bits cannot affect styling and snapshotting can be skipped.
|
|
1567
|
+
pub fn style_depends_on_state(&self, state: ElementState) -> bool {
|
|
1568
|
+
self.stylist.iter_origins().any(|(data, _)| {
|
|
1569
|
+
data.has_state_dependency(state) || data.has_nth_of_state_dependency(state)
|
|
1570
|
+
})
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
/// Apply a state change (hover/focus/active/etc affecting the given [`ElementState`]
|
|
1574
|
+
/// bits) to a node, taking a state-only snapshot beforehand if any style rule
|
|
1575
|
+
/// depends on those bits.
|
|
1576
|
+
pub fn snapshot_node_and(
|
|
1577
|
+
&mut self,
|
|
1578
|
+
node_id: NodeId,
|
|
1579
|
+
state: ElementState,
|
|
1580
|
+
cb: impl FnOnce(&mut Node),
|
|
1581
|
+
) {
|
|
1582
|
+
if self.style_depends_on_state(state) {
|
|
1583
|
+
self.snapshot_node_state_only(node_id);
|
|
1584
|
+
}
|
|
1585
|
+
cb(&mut self.nodes[node_id]);
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// Takes (x, y) co-ordinates (relative to the )
|
|
1589
|
+
pub fn hit(&self, x: f32, y: f32) -> Option<HitResult> {
|
|
1590
|
+
self.hit_with_scrollbar(x, y).0
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
/// The topmost element at viewport coordinates (x, y), or `None` if the point
|
|
1594
|
+
/// is outside the viewport. Anonymous boxes and text nodes are resolved to
|
|
1595
|
+
/// their nearest element; a point over the background hits the root element.
|
|
1596
|
+
///
|
|
1597
|
+
/// This implements the hit-testing semantics of `document.elementFromPoint()`.
|
|
1598
|
+
/// Hit testing consults layout, so [`resolve`](Self::resolve) should be called
|
|
1599
|
+
/// before this method to ensure layout is up to date.
|
|
1600
|
+
pub fn element_from_point(&self, x: f32, y: f32) -> Option<NodeId> {
|
|
1601
|
+
let viewport = self.viewport();
|
|
1602
|
+
let scale = viewport.scale();
|
|
1603
|
+
let (viewport_width, viewport_height) = (
|
|
1604
|
+
viewport.window_size.0 as f32 / scale,
|
|
1605
|
+
viewport.window_size.1 as f32 / scale,
|
|
1606
|
+
);
|
|
1607
|
+
if x < 0.0 || y < 0.0 || x > viewport_width || y > viewport_height {
|
|
1608
|
+
return None;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
let Some(hit) = self.hit(x, y) else {
|
|
1612
|
+
// The point is within the viewport but over no box: the root element
|
|
1613
|
+
// (which covers the viewport in an HTML document) is the hit target
|
|
1614
|
+
return self.try_root_element().map(|root| root.id);
|
|
1615
|
+
};
|
|
1616
|
+
|
|
1617
|
+
// Resolve anonymous boxes / pseudo-elements, then text nodes, to elements
|
|
1618
|
+
let mut node_id = self.nearest_non_anonymous_ancestor(hit.node_id)?;
|
|
1619
|
+
loop {
|
|
1620
|
+
let node = self.get_node(node_id)?;
|
|
1621
|
+
if node.is_element() {
|
|
1622
|
+
return Some(node_id);
|
|
1623
|
+
}
|
|
1624
|
+
node_id = node.parent?;
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
/// All elements at viewport coordinates (x, y), from topmost to bottommost,
|
|
1629
|
+
/// as for `document.elementsFromPoint()`.
|
|
1630
|
+
///
|
|
1631
|
+
/// The spec's paint-order list is approximated with the hit element followed
|
|
1632
|
+
/// by its ancestor elements (which is correct for non-overlapping content).
|
|
1633
|
+
/// As with [`element_from_point`](Self::element_from_point), layout should be
|
|
1634
|
+
/// resolved before calling this method.
|
|
1635
|
+
pub fn elements_from_point(&self, x: f32, y: f32) -> Vec<NodeId> {
|
|
1636
|
+
let mut element_ids = Vec::new();
|
|
1637
|
+
let mut current = self.element_from_point(x, y);
|
|
1638
|
+
while let Some(node_id) = current {
|
|
1639
|
+
element_ids.push(node_id);
|
|
1640
|
+
current = self
|
|
1641
|
+
.get_node(node_id)
|
|
1642
|
+
.and_then(|node| node.parent)
|
|
1643
|
+
.filter(|parent_id| {
|
|
1644
|
+
self.get_node(*parent_id)
|
|
1645
|
+
.is_some_and(|node| node.is_element())
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
element_ids
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
/// Walk up the tree to the nearest DOM node whose id is stable across
|
|
1652
|
+
/// box-tree reconstruction, so canonicalized interaction state never goes
|
|
1653
|
+
/// stale.
|
|
1654
|
+
///
|
|
1655
|
+
/// Layout-generated nodes (anonymous blocks and `::before`/`::after`
|
|
1656
|
+
/// pseudo-elements, both stored as anonymous blocks) get new ids on every
|
|
1657
|
+
/// reconstruction, so we skip any anonymous node *and* a non-anonymous node
|
|
1658
|
+
/// whose parent is anonymous (the pseudo's text content). The first
|
|
1659
|
+
/// non-anonymous node with a non-anonymous parent is a real DOM node; the
|
|
1660
|
+
/// root element's `Document` parent guarantees termination.
|
|
1661
|
+
///
|
|
1662
|
+
/// Returns `None` if `node_id` (or an ancestor) no longer exists.
|
|
1663
|
+
pub fn nearest_non_anonymous_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
|
|
1664
|
+
// Recurse up the tree keeping a window of the current node and its
|
|
1665
|
+
// parent, advancing one step per iteration so each node is looked up
|
|
1666
|
+
// exactly once.
|
|
1667
|
+
let mut node = self.get_node(node_id)?;
|
|
1668
|
+
loop {
|
|
1669
|
+
let parent = match node.parent {
|
|
1670
|
+
Some(parent_id) => self.get_node(parent_id)?,
|
|
1671
|
+
None => return Some(node.id),
|
|
1672
|
+
};
|
|
1673
|
+
if !node.is_anonymous() && !parent.is_anonymous() {
|
|
1674
|
+
return Some(node.id);
|
|
1675
|
+
}
|
|
1676
|
+
node = parent;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
pub fn focus_next_node(&mut self) -> Option<NodeId> {
|
|
1681
|
+
let focussed_node_id = self.get_focussed_node_id()?;
|
|
1682
|
+
let id = self.next_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
|
|
1683
|
+
self.set_focus_to(id);
|
|
1684
|
+
Some(id)
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
/// Move focus to the previous focussable node in the document
|
|
1688
|
+
pub fn focus_prev_node(&mut self) -> Option<NodeId> {
|
|
1689
|
+
let focussed_node_id = self.get_focussed_node_id()?;
|
|
1690
|
+
let id = self.prev_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
|
|
1691
|
+
self.set_focus_to(id);
|
|
1692
|
+
Some(id)
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
/// Clear the focussed node
|
|
1696
|
+
pub fn clear_focus(&mut self) {
|
|
1697
|
+
if let Some(id) = self.focus_node_id {
|
|
1698
|
+
let shell_provider = self.shell_provider.clone();
|
|
1699
|
+
self.snapshot_node_and(id, ElementState::FOCUS | ElementState::FOCUSRING, |node| {
|
|
1700
|
+
node.blur(shell_provider)
|
|
1701
|
+
});
|
|
1702
|
+
self.focus_node_id = None;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
pub fn set_mousedown_node_id(&mut self, node_id: Option<NodeId>) {
|
|
1707
|
+
self.mousedown_node_id = node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
|
|
1708
|
+
}
|
|
1709
|
+
pub fn set_focus_to(&mut self, focus_node_id: NodeId) -> bool {
|
|
1710
|
+
let Some(focus_node_id) = self.nearest_non_anonymous_ancestor(focus_node_id) else {
|
|
1711
|
+
return false;
|
|
1712
|
+
};
|
|
1713
|
+
if Some(focus_node_id) == self.focus_node_id {
|
|
1714
|
+
return false;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
#[cfg(feature = "tracing")]
|
|
1718
|
+
tracing::info!("Focussed node {focus_node_id}");
|
|
1719
|
+
|
|
1720
|
+
let shell_provider = self.shell_provider.clone();
|
|
1721
|
+
|
|
1722
|
+
// Remove focus from the old node
|
|
1723
|
+
if let Some(id) = self.focus_node_id {
|
|
1724
|
+
self.snapshot_node_and(id, ElementState::FOCUS | ElementState::FOCUSRING, |node| {
|
|
1725
|
+
node.blur(shell_provider.clone())
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// Focus the new node
|
|
1730
|
+
self.snapshot_node_and(
|
|
1731
|
+
focus_node_id,
|
|
1732
|
+
ElementState::FOCUS | ElementState::FOCUSRING,
|
|
1733
|
+
|node| node.focus(shell_provider),
|
|
1734
|
+
);
|
|
1735
|
+
|
|
1736
|
+
self.focus_node_id = Some(focus_node_id);
|
|
1737
|
+
|
|
1738
|
+
true
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
pub fn active_node(&mut self) -> bool {
|
|
1742
|
+
let Some(hover_node_id) = self.get_hover_node_id() else {
|
|
1743
|
+
return false;
|
|
1744
|
+
};
|
|
1745
|
+
|
|
1746
|
+
if let Some(active_node_id) = self.active_node_id {
|
|
1747
|
+
if active_node_id == hover_node_id {
|
|
1748
|
+
return true;
|
|
1749
|
+
}
|
|
1750
|
+
self.unactive_node();
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// hover_node_id is canonicalized when stored, so this always holds.
|
|
1754
|
+
debug_assert!(
|
|
1755
|
+
self.get_node(hover_node_id)
|
|
1756
|
+
.is_some_and(|node| !node.is_anonymous()),
|
|
1757
|
+
"interaction state must reference DOM nodes, not layout-generated nodes"
|
|
1758
|
+
);
|
|
1759
|
+
let active_node_id = Some(hover_node_id);
|
|
1760
|
+
|
|
1761
|
+
let node_path = self.maybe_node_element_ancestors(active_node_id);
|
|
1762
|
+
for &id in node_path.iter() {
|
|
1763
|
+
self.snapshot_node_and(id, ElementState::ACTIVE, |node| node.active());
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
self.active_node_id = active_node_id;
|
|
1767
|
+
|
|
1768
|
+
true
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
pub fn unactive_node(&mut self) -> bool {
|
|
1772
|
+
let Some(active_node_id) = self.active_node_id.take() else {
|
|
1773
|
+
return false;
|
|
1774
|
+
};
|
|
1775
|
+
|
|
1776
|
+
let node_path = self.maybe_node_element_ancestors(Some(active_node_id));
|
|
1777
|
+
for &id in node_path.iter() {
|
|
1778
|
+
self.snapshot_node_and(id, ElementState::ACTIVE, |node| node.unactive());
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
true
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
/// The scrollbar thumb currently under the pointer, if any.
|
|
1785
|
+
pub fn hovered_scrollbar(&self) -> Option<crate::node::ScrollbarRef> {
|
|
1786
|
+
self.hovered_scrollbar
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
/// The scrollbar thumb currently being dragged, if any.
|
|
1790
|
+
pub fn scrollbar_drag_target(&self) -> Option<crate::node::ScrollbarRef> {
|
|
1791
|
+
match &self.drag_mode {
|
|
1792
|
+
DragMode::ScrollbarDrag(state) => Some(state.scrollbar),
|
|
1793
|
+
_ => None,
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
/// The current opacity of `node_id`'s overlay scrollbars. They show at
|
|
1798
|
+
/// full opacity on scroll and fade out after a delay (Chromium's overlay
|
|
1799
|
+
/// timings); the pointer resting on a thumb, or dragging it, holds them
|
|
1800
|
+
/// visible.
|
|
1801
|
+
pub fn scrollbar_opacity(&self, node_id: NodeId) -> f32 {
|
|
1802
|
+
let interacting = |scrollbar: &crate::node::ScrollbarRef| scrollbar.node_id == node_id;
|
|
1803
|
+
if self.hovered_scrollbar.as_ref().is_some_and(interacting)
|
|
1804
|
+
|| self
|
|
1805
|
+
.scrollbar_drag_target()
|
|
1806
|
+
.as_ref()
|
|
1807
|
+
.is_some_and(interacting)
|
|
1808
|
+
{
|
|
1809
|
+
return 1.0;
|
|
1810
|
+
}
|
|
1811
|
+
self.scrollbar_activity.get(&node_id).map_or(0.0, |last| {
|
|
1812
|
+
crate::node::scrollbar::opacity_at(last.elapsed())
|
|
1813
|
+
})
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
/// Show `node_id`'s overlay scrollbars at full opacity and restart their
|
|
1817
|
+
/// fade-out delay.
|
|
1818
|
+
pub(crate) fn show_scrollbars(&mut self, node_id: NodeId) {
|
|
1819
|
+
if cfg!(feature = "scrollbars") {
|
|
1820
|
+
self.scrollbar_activity.insert(node_id, Instant::now());
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
/// Whether any overlay scrollbars are awaiting or animating their
|
|
1825
|
+
/// fade-out (so frames must keep rendering until they finish).
|
|
1826
|
+
fn scrollbars_animating(&self) -> bool {
|
|
1827
|
+
use crate::node::scrollbar::{FADE_DELAY, FADE_DURATION};
|
|
1828
|
+
self.scrollbar_activity
|
|
1829
|
+
.values()
|
|
1830
|
+
.any(|last| last.elapsed() < FADE_DELAY + FADE_DURATION)
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
/// [`hit`](Self::hit), also resolving the innermost overlay scrollbar
|
|
1834
|
+
/// thumb under the point (shares the traversal, so it costs nothing
|
|
1835
|
+
/// extra).
|
|
1836
|
+
pub(crate) fn hit_with_scrollbar(
|
|
1837
|
+
&self,
|
|
1838
|
+
x: f32,
|
|
1839
|
+
y: f32,
|
|
1840
|
+
) -> (Option<HitResult>, Option<crate::node::ScrollbarRef>) {
|
|
1841
|
+
if TDocument::as_node(&self.root_node())
|
|
1842
|
+
.first_element_child()
|
|
1843
|
+
.is_none()
|
|
1844
|
+
{
|
|
1845
|
+
#[cfg(feature = "tracing")]
|
|
1846
|
+
tracing::warn!("No DOM - not resolving hit test");
|
|
1847
|
+
return (None, None);
|
|
1848
|
+
}
|
|
1849
|
+
let mut scrollbar = None;
|
|
1850
|
+
let hit = self
|
|
1851
|
+
.root_element()
|
|
1852
|
+
.hit_inner(x, y, self.viewport().scale_f64(), &mut scrollbar);
|
|
1853
|
+
(hit, scrollbar)
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
pub fn set_hover_to(&mut self, x: f32, y: f32) -> bool {
|
|
1857
|
+
// Record the pointer position in client (unscrolled) coordinates so
|
|
1858
|
+
// that `refresh_hover` can re-resolve hover state after layout or
|
|
1859
|
+
// scroll changes.
|
|
1860
|
+
self.last_client_pointer_position = Some(taffy::Point {
|
|
1861
|
+
x: x - self.viewport_scroll().x as f32,
|
|
1862
|
+
y: y - self.viewport_scroll().y as f32,
|
|
1863
|
+
});
|
|
1864
|
+
|
|
1865
|
+
let (hit, hovered_scrollbar) = self.hit_with_scrollbar(x, y);
|
|
1866
|
+
// A faded-out thumb is not interactive: pointer moves never fade
|
|
1867
|
+
// overlay scrollbars back in (only scrolling shows them).
|
|
1868
|
+
let hovered_scrollbar =
|
|
1869
|
+
hovered_scrollbar.filter(|scrollbar| self.scrollbar_opacity(scrollbar.node_id) > 0.0);
|
|
1870
|
+
// Scrollbar-thumb hover is part of hover state: track it here so a
|
|
1871
|
+
// pointer crossing a thumb restyles it even when the hit node (the
|
|
1872
|
+
// content under the overlay thumb) is unchanged.
|
|
1873
|
+
let scrollbar_changed = hovered_scrollbar != self.hovered_scrollbar;
|
|
1874
|
+
if scrollbar_changed {
|
|
1875
|
+
// Entering a thumb restores full opacity mid-fade; leaving one
|
|
1876
|
+
// restarts the fade-out delay.
|
|
1877
|
+
for scrollbar in [self.hovered_scrollbar, hovered_scrollbar]
|
|
1878
|
+
.into_iter()
|
|
1879
|
+
.flatten()
|
|
1880
|
+
{
|
|
1881
|
+
self.show_scrollbars(scrollbar.node_id);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
self.hovered_scrollbar = hovered_scrollbar;
|
|
1885
|
+
|
|
1886
|
+
// Store both the precise layout node that was hit (transient: used for
|
|
1887
|
+
// cursor/style queries) and its canonical DOM target (persistent: must
|
|
1888
|
+
// not reference layout-generated nodes, whose ids die on box-tree
|
|
1889
|
+
// reconstruction).
|
|
1890
|
+
let hit_node_id = hit.map(|hit| hit.node_id);
|
|
1891
|
+
let hover_node_id = hit_node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
|
|
1892
|
+
let new_is_text = hit.map(|hit| hit.is_text).unwrap_or(false);
|
|
1893
|
+
|
|
1894
|
+
self.hover_hit_node_id = hit_node_id;
|
|
1895
|
+
self.hover_node_is_text = new_is_text;
|
|
1896
|
+
|
|
1897
|
+
// Return early if the new node is the same as the already-hovered node
|
|
1898
|
+
if hover_node_id == self.hover_node_id {
|
|
1899
|
+
return scrollbar_changed;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
let old_node_path = self.maybe_node_element_ancestors(self.hover_node_id);
|
|
1903
|
+
let new_node_path = self.maybe_node_element_ancestors(hover_node_id);
|
|
1904
|
+
let same_count = old_node_path
|
|
1905
|
+
.iter()
|
|
1906
|
+
.zip(&new_node_path)
|
|
1907
|
+
.take_while(|(o, n)| o == n)
|
|
1908
|
+
.count();
|
|
1909
|
+
for &id in old_node_path.iter().skip(same_count) {
|
|
1910
|
+
self.snapshot_node_and(id, ElementState::HOVER, |node| node.unhover());
|
|
1911
|
+
}
|
|
1912
|
+
for &id in new_node_path.iter().skip(same_count) {
|
|
1913
|
+
self.snapshot_node_and(id, ElementState::HOVER, |node| node.hover());
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
self.hover_node_id = hover_node_id;
|
|
1917
|
+
|
|
1918
|
+
// Request redraw
|
|
1919
|
+
self.shell_provider.request_redraw();
|
|
1920
|
+
|
|
1921
|
+
true
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
pub fn clear_hover(&mut self) -> bool {
|
|
1925
|
+
// The pointer is no longer over the document, so stop re-resolving
|
|
1926
|
+
// hover state against it.
|
|
1927
|
+
self.last_client_pointer_position = None;
|
|
1928
|
+
self.hover_hit_node_id = None;
|
|
1929
|
+
|
|
1930
|
+
let Some(hover_node_id) = self.hover_node_id else {
|
|
1931
|
+
return false;
|
|
1932
|
+
};
|
|
1933
|
+
|
|
1934
|
+
let old_node_path = self.maybe_node_element_ancestors(Some(hover_node_id));
|
|
1935
|
+
for &id in old_node_path.iter() {
|
|
1936
|
+
self.snapshot_node_and(id, ElementState::HOVER, |node| node.unhover());
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
self.hover_node_id = None;
|
|
1940
|
+
self.hover_node_is_text = false;
|
|
1941
|
+
|
|
1942
|
+
// Request redraw
|
|
1943
|
+
self.shell_provider.request_redraw();
|
|
1944
|
+
|
|
1945
|
+
true
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
/// Re-resolve hover state against the current layout using the last known
|
|
1949
|
+
/// pointer position.
|
|
1950
|
+
///
|
|
1951
|
+
/// TODO: synthesizing pointerenter/pointerleave DOM events for
|
|
1952
|
+
/// hover changes caused by layout shifts.
|
|
1953
|
+
pub fn refresh_hover(&mut self) -> bool {
|
|
1954
|
+
let Some(pos) = self.last_client_pointer_position else {
|
|
1955
|
+
return false;
|
|
1956
|
+
};
|
|
1957
|
+
let x = pos.x + self.viewport_scroll().x as f32;
|
|
1958
|
+
let y = pos.y + self.viewport_scroll().y as f32;
|
|
1959
|
+
let prev_hover = self.hover_state();
|
|
1960
|
+
let changed = self.set_hover_to(x, y);
|
|
1961
|
+
if self.hover_state() != prev_hover {
|
|
1962
|
+
self.update_cursor();
|
|
1963
|
+
}
|
|
1964
|
+
changed
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
/// The hover state that determines the cursor: the hovered node, the precise hit
|
|
1968
|
+
/// node and whether the hit is text
|
|
1969
|
+
pub(crate) fn hover_state(&self) -> (Option<NodeId>, Option<NodeId>, bool) {
|
|
1970
|
+
(
|
|
1971
|
+
self.hover_node_id,
|
|
1972
|
+
self.hover_hit_node_id,
|
|
1973
|
+
self.hover_node_is_text,
|
|
1974
|
+
)
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
/// Send the current cursor to the shell.
|
|
1978
|
+
///
|
|
1979
|
+
/// Hover updates don't send the cursor themselves: the cursor of a node hosting a
|
|
1980
|
+
/// sub-document is only known once the sub-document has handled the event.
|
|
1981
|
+
pub(crate) fn update_cursor(&self) {
|
|
1982
|
+
self.shell_provider.set_cursor(self.get_cursor());
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
pub fn get_hover_node_id(&self) -> Option<NodeId> {
|
|
1986
|
+
self.hover_node_id
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
pub fn get_mousedown_node_id(&self) -> Option<NodeId> {
|
|
1990
|
+
self.mousedown_node_id
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
pub fn set_viewport(&mut self, viewport: Viewport) {
|
|
1994
|
+
let changes = DeviceChanges::from_viewports(&self.viewport, &viewport);
|
|
1995
|
+
self.viewport = viewport;
|
|
1996
|
+
self.queue_device_changes(changes);
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
/// Set exact CSS dimensions for a vector/paged layout without changing its scale.
|
|
2000
|
+
pub fn set_layout_viewport(&mut self, size: Option<(f32, f32)>) {
|
|
2001
|
+
if self.layout_viewport != size {
|
|
2002
|
+
self.layout_viewport = size;
|
|
2003
|
+
self.queue_device_changes(DeviceChanges::VIEWPORT_SIZE);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
/// Returns the current CSS media type used to evaluate `@media` rules.
|
|
2008
|
+
pub fn media_type(&self) -> &MediaType {
|
|
2009
|
+
&self.media_type
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
/// Sets the CSS media type used to evaluate `@media` rules (e.g. `screen` or `print`)
|
|
2013
|
+
/// and queues a stylist device rebuild so updated rules apply on the next restyle.
|
|
2014
|
+
pub fn set_media_type(&mut self, media_type: MediaType) {
|
|
2015
|
+
if self.media_type == media_type {
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
self.media_type = media_type;
|
|
2019
|
+
self.queue_device_changes(DeviceChanges::MEDIA_TYPE);
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
pub fn viewport(&self) -> &Viewport {
|
|
2023
|
+
&self.viewport
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
pub fn viewport_mut(&mut self) -> ViewportMut<'_> {
|
|
2027
|
+
ViewportMut::new(self)
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
pub fn zoom_by(&mut self, increment: f32) {
|
|
2031
|
+
*self.viewport_mut().zoom_mut() += increment;
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
pub fn zoom_to(&mut self, zoom: f32) {
|
|
2035
|
+
*self.viewport_mut().zoom_mut() = zoom;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
pub fn get_viewport(&self) -> Viewport {
|
|
2039
|
+
self.viewport.clone()
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
/// Returns whether incremental layout is currently enabled for this document.
|
|
2043
|
+
///
|
|
2044
|
+
/// Both modes run the same style → damage → box construction → layout
|
|
2045
|
+
/// pipeline. In incremental mode only nodes carrying `RestyleDamage` (from
|
|
2046
|
+
/// mutations or restyles) are reconstructed and have their layout caches
|
|
2047
|
+
/// cleared; in non-incremental mode every node is marked damaged at the
|
|
2048
|
+
/// start of each `resolve`, so everything is rebuilt each frame.
|
|
2049
|
+
pub fn incremental_layout(&self) -> bool {
|
|
2050
|
+
self.incremental_layout
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
/// Enables or disables incremental layout for this document.
|
|
2054
|
+
///
|
|
2055
|
+
/// Disabling it does not switch to a separate code path: all nodes are
|
|
2056
|
+
/// simply treated as damaged on every `resolve` (see [`Self::incremental_layout`]).
|
|
2057
|
+
pub fn set_incremental_layout(&mut self, enabled: bool) {
|
|
2058
|
+
self.incremental_layout = enabled;
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
pub fn devtools(&self) -> &DevtoolSettings {
|
|
2062
|
+
&self.devtool_settings
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
pub fn devtools_mut(&mut self) -> &mut DevtoolSettings {
|
|
2066
|
+
&mut self.devtool_settings
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
pub fn subdoc(&self, node_id: NodeId) -> Option<&dyn Document> {
|
|
2070
|
+
self.get_node(node_id)
|
|
2071
|
+
.and_then(|node| node.element_data())
|
|
2072
|
+
.and_then(|el| el.sub_doc_data())
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
pub fn subdoc_mut(&mut self, node_id: NodeId) -> Option<&mut dyn Document> {
|
|
2076
|
+
self.get_node_mut(node_id)
|
|
2077
|
+
.and_then(|node| node.element_data_mut())
|
|
2078
|
+
.and_then(|el| el.sub_doc_data_mut())
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
pub fn is_animating(&self) -> bool {
|
|
2082
|
+
#[cfg(feature = "custom-widget")]
|
|
2083
|
+
let custom_widget_is_animating = self.custom_widget_nodes.iter().any(|&node_id| {
|
|
2084
|
+
self.nodes[node_id]
|
|
2085
|
+
.element_data()
|
|
2086
|
+
.and_then(|el| el.custom_widget_data())
|
|
2087
|
+
.is_some_and(|data| data.widget.requires_redraw())
|
|
2088
|
+
});
|
|
2089
|
+
#[cfg(not(feature = "custom-widget"))]
|
|
2090
|
+
let custom_widget_is_animating = false;
|
|
2091
|
+
|
|
2092
|
+
self.has_canvas
|
|
2093
|
+
| self.has_active_animations
|
|
2094
|
+
| self.subdoc_is_animating
|
|
2095
|
+
| custom_widget_is_animating
|
|
2096
|
+
| (self.scroll_animation != ScrollAnimationState::None)
|
|
2097
|
+
| self.scrollbars_animating()
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
/// Record pending [`DeviceChanges`] and request a redraw so that they are
|
|
2101
|
+
/// flushed (via [`Self::flush_pending_device_changes`]) on the next resolve.
|
|
2102
|
+
pub(crate) fn queue_device_changes(&mut self, changes: DeviceChanges) {
|
|
2103
|
+
if changes.is_empty() {
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
2106
|
+
self.pending_device_changes |= changes;
|
|
2107
|
+
self.shell_provider.request_redraw();
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
/// Apply any pending device changes to the stylist, coalescing all changes
|
|
2111
|
+
/// since the last flush into a single device rebuild.
|
|
2112
|
+
pub(crate) fn flush_pending_device_changes(&mut self) {
|
|
2113
|
+
let changes = std::mem::take(&mut self.pending_device_changes);
|
|
2114
|
+
if changes.is_empty() {
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
self.set_stylist_device(make_device(
|
|
2119
|
+
&self.viewport,
|
|
2120
|
+
self.layout_viewport,
|
|
2121
|
+
self.media_type.clone(),
|
|
2122
|
+
self.font_ctx.clone(),
|
|
2123
|
+
));
|
|
2124
|
+
self.scroll_viewport_by(0.0, 0.0); // Clamp scroll offset
|
|
2125
|
+
|
|
2126
|
+
// Text is shaped at a specific scale factor, so cached inline layouts
|
|
2127
|
+
// must be invalidated when the scale changes.
|
|
2128
|
+
if changes.contains(DeviceChanges::SCALE) {
|
|
2129
|
+
self.invalidate_inline_contexts();
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
// Color-scheme changes affect values that are resolved at cascade time
|
|
2133
|
+
// (`light-dark()`, system colors) without necessarily flipping any
|
|
2134
|
+
// media query result, so conservatively recascade the whole tree.
|
|
2135
|
+
if changes.contains(DeviceChanges::COLOR_SCHEME) {
|
|
2136
|
+
if let Some(root_id) = self.try_root_element().map(|el| el.id) {
|
|
2137
|
+
self.nodes[root_id].set_restyle_hint(RestyleHint::recascade_subtree());
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
/// Update the device and reset the stylist to process the new size
|
|
2143
|
+
pub fn set_stylist_device(&mut self, device: Device) {
|
|
2144
|
+
// Seed the new device with the root element's current style and font-relative
|
|
2145
|
+
// unit state (used to resolve rem/rlh/rex/rch/rcap/ric units). Stylo only
|
|
2146
|
+
// updates this state when the root element's style *changes* during a restyle,
|
|
2147
|
+
// so a freshly-built device would otherwise resolve these units against the
|
|
2148
|
+
// default font-size (16px) until the root's font-size next changes.
|
|
2149
|
+
let root_styles = self
|
|
2150
|
+
.try_root_element()
|
|
2151
|
+
.and_then(|root| root.primary_styles());
|
|
2152
|
+
if let Some(root_style) = root_styles.as_deref() {
|
|
2153
|
+
device.set_root_style(root_style);
|
|
2154
|
+
|
|
2155
|
+
let font = root_style.get_font();
|
|
2156
|
+
let font_size = font.clone_font_size().computed_size();
|
|
2157
|
+
device.set_root_font_size(root_style.effective_zoom.unzoom(font_size.px()));
|
|
2158
|
+
|
|
2159
|
+
let line_height = device
|
|
2160
|
+
.calc_line_height(font, root_style.writing_mode, None)
|
|
2161
|
+
.0;
|
|
2162
|
+
device.set_root_line_height(root_style.effective_zoom.unzoom(line_height.px()));
|
|
2163
|
+
}
|
|
2164
|
+
drop(root_styles);
|
|
2165
|
+
|
|
2166
|
+
let old_device = self.stylist.device();
|
|
2167
|
+
let viewport_size_changed = old_device.au_viewport_size() != device.au_viewport_size();
|
|
2168
|
+
let styles_use_viewport_units = old_device.used_viewport_size();
|
|
2169
|
+
|
|
2170
|
+
let origins = {
|
|
2171
|
+
let guard = &self.guard;
|
|
2172
|
+
let guards = StylesheetGuards {
|
|
2173
|
+
author: &guard.read(),
|
|
2174
|
+
ua_or_user: &guard.read(),
|
|
2175
|
+
};
|
|
2176
|
+
self.stylist.set_device(device, &guards)
|
|
2177
|
+
};
|
|
2178
|
+
|
|
2179
|
+
// Only fully invalidate element styles when the media query results of
|
|
2180
|
+
// some origin actually changed. `force_stylesheet_origins_dirty` fully
|
|
2181
|
+
// invalidates styles even when `origins` is empty, which would force a
|
|
2182
|
+
// full restyle on every viewport resize.
|
|
2183
|
+
if !origins.is_empty() {
|
|
2184
|
+
self.stylist.force_stylesheet_origins_dirty(origins);
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
// Styles that resolve viewport units (vw/vh/etc) still need recomputing
|
|
2188
|
+
// when the viewport size changes, even if no media query results
|
|
2189
|
+
// changed. Invalidate just the elements whose styles use viewport
|
|
2190
|
+
// units (tracked via per-element computed value flags).
|
|
2191
|
+
if viewport_size_changed && styles_use_viewport_units {
|
|
2192
|
+
if self
|
|
2193
|
+
.stylist
|
|
2194
|
+
.get_custom_property_initial_values_flags()
|
|
2195
|
+
.intersects(ComputedValueFlags::USES_VIEWPORT_UNITS)
|
|
2196
|
+
{
|
|
2197
|
+
self.stylist.rebuild_initial_values_for_custom_properties();
|
|
2198
|
+
}
|
|
2199
|
+
if let Some(root) = self.try_root_element() {
|
|
2200
|
+
style::invalidation::viewport_units::invalidate(root);
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
pub fn stylist_device(&mut self) -> &Device {
|
|
2206
|
+
self.flush_pending_device_changes();
|
|
2207
|
+
self.stylist.device()
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
pub fn get_cursor(&self) -> Option<CursorIcon> {
|
|
2211
|
+
// Prefer the precise hit node: `cursor` and `user-select` may be set on
|
|
2212
|
+
// a pseudo-element or resolved on an anonymous box, and text hits carry
|
|
2213
|
+
// is_text via the hit node. Fall back to the canonical hover node if
|
|
2214
|
+
// the hit node has been removed (it is transient across resolves).
|
|
2215
|
+
let node_id = self
|
|
2216
|
+
.hover_hit_node_id
|
|
2217
|
+
.filter(|&id| self.nodes.contains_key(id))
|
|
2218
|
+
.or(self.get_hover_node_id())?;
|
|
2219
|
+
let node = &self.nodes[node_id];
|
|
2220
|
+
|
|
2221
|
+
if let Some(subdoc) = node.subdoc().map(|doc| doc.inner())
|
|
2222
|
+
&& subdoc.get_hover_node_id().is_some()
|
|
2223
|
+
{
|
|
2224
|
+
return subdoc.get_cursor();
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
let style = node.primary_styles()?;
|
|
2228
|
+
let user_select = style.clone_user_select();
|
|
2229
|
+
let keyword = style.clone_cursor().keyword;
|
|
2230
|
+
|
|
2231
|
+
// Return cursor from style if it is non-auto
|
|
2232
|
+
if keyword != CursorKind::Auto {
|
|
2233
|
+
return stylo_to_cursor_icon(keyword);
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
// Return text cursor for text inputs
|
|
2237
|
+
if node
|
|
2238
|
+
.element_data()
|
|
2239
|
+
.is_some_and(|e| e.text_input_data().is_some())
|
|
2240
|
+
{
|
|
2241
|
+
return Some(CursorIcon::Text);
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
// Use "pointer" cursor if any ancestor is a link
|
|
2245
|
+
let mut maybe_node = Some(node);
|
|
2246
|
+
while let Some(node) = maybe_node {
|
|
2247
|
+
if node.is_link() {
|
|
2248
|
+
return Some(CursorIcon::Pointer);
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
maybe_node = node.layout_parent.get().map(|node_id| node.with(node_id));
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
// Return text cursor for text nodes
|
|
2255
|
+
if self.hover_node_is_text {
|
|
2256
|
+
return Some(match user_select {
|
|
2257
|
+
UserSelect::Text | UserSelect::All | UserSelect::Auto | UserSelect::Contain => {
|
|
2258
|
+
CursorIcon::Text
|
|
2259
|
+
}
|
|
2260
|
+
UserSelect::None => CursorIcon::Default,
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
// Else fallback to default cursor
|
|
2265
|
+
Some(CursorIcon::Default)
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
pub fn viewport_scroll(&self) -> crate::Point<f64> {
|
|
2269
|
+
self.nodes.viewport_scroll()
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
pub fn set_viewport_scroll(&mut self, scroll: crate::Point<f64>) {
|
|
2273
|
+
self.nodes.set_viewport_scroll(scroll);
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
/// Find the node targeted by a URL fragment (the `#...` part of a URL).
|
|
2277
|
+
///
|
|
2278
|
+
/// Per the HTML spec, this is the element whose `id` matches the fragment, falling
|
|
2279
|
+
/// back to the first `<a>` element whose `name` attribute matches.
|
|
2280
|
+
pub fn get_fragment_target(&self, fragment: &str) -> Option<NodeId> {
|
|
2281
|
+
if let Some(node_id) = self.get_element_by_id(fragment) {
|
|
2282
|
+
return Some(node_id);
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
// Fall back to a named anchor: `<a name="...">`
|
|
2286
|
+
self.nodes.iter().find_map(|(id, node)| {
|
|
2287
|
+
let el = node.element_data()?;
|
|
2288
|
+
(el.name.local == local_name!("a") && el.attr(local_name!("name")) == Some(fragment))
|
|
2289
|
+
.then_some(id)
|
|
2290
|
+
})
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
/// `node_id`'s document-relative position and unrounded (sub-pixel) layout in physical
|
|
2294
|
+
/// coordinates, for CSSOM geometry APIs such as `getBoundingClientRect`.
|
|
2295
|
+
#[cfg(not(feature = "writing-mode"))]
|
|
2296
|
+
pub(crate) fn physical_unrounded_geometry(
|
|
2297
|
+
&self,
|
|
2298
|
+
node_id: NodeId,
|
|
2299
|
+
) -> (crate::util::Point<f32>, taffy::Layout) {
|
|
2300
|
+
let mut pos = crate::util::Point { x: 0.0, y: 0.0 };
|
|
2301
|
+
let mut current = Some(node_id);
|
|
2302
|
+
while let Some(id) = current {
|
|
2303
|
+
let node = &self.nodes[id];
|
|
2304
|
+
pos.x += node.unrounded_layout().location.x - node.scroll_offset().x as f32;
|
|
2305
|
+
pos.y += node.unrounded_layout().location.y - node.scroll_offset().y as f32;
|
|
2306
|
+
current = node.containing_block();
|
|
2307
|
+
}
|
|
2308
|
+
(pos, *self.nodes[node_id].unrounded_layout())
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
/// Computes the size and position of the `Node` relative to the viewport
|
|
2312
|
+
pub fn get_client_bounding_rect(&self, node_id: NodeId) -> Option<BoundingRect> {
|
|
2313
|
+
// Non-atomic inline elements have no layout box of their own: return
|
|
2314
|
+
// the union of their per-line-box fragment rects.
|
|
2315
|
+
if let Some(rects) = self.inline_fragment_rects(node_id) {
|
|
2316
|
+
return rects.reduce(BoundingRect::union);
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
self.get_node(node_id)?;
|
|
2320
|
+
let (pos, layout) = self.physical_unrounded_geometry(node_id);
|
|
2321
|
+
let size = layout.size;
|
|
2322
|
+
|
|
2323
|
+
Some(BoundingRect {
|
|
2324
|
+
x: snap_to_layout_unit(pos.x as f64 - self.viewport_scroll().x),
|
|
2325
|
+
y: snap_to_layout_unit(pos.y as f64 - self.viewport_scroll().y),
|
|
2326
|
+
width: snap_to_layout_unit(size.width as f64),
|
|
2327
|
+
height: snap_to_layout_unit(size.height as f64),
|
|
2328
|
+
})
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
/// CSSOM View's `offsetLeft`/`offsetTop`/`offsetWidth`/`offsetHeight` as a single rect:
|
|
2332
|
+
/// the node's border box, positioned relative to the padding edge of its `offsetParent`
|
|
2333
|
+
/// (or to the initial containing block when the `offsetParent` is a non-positioned
|
|
2334
|
+
/// `body`, or there is none).
|
|
2335
|
+
///
|
|
2336
|
+
/// Nodes with their own layout box use that box directly. Non-atomic inline elements
|
|
2337
|
+
/// have no layout box: they use the bounding box of their per-line-box fragments, and
|
|
2338
|
+
/// their `offsetParent` is resolved from the containing inline root.
|
|
2339
|
+
pub fn offset_rect(&self, node_id: NodeId) -> Option<BoundingRect> {
|
|
2340
|
+
let node = self.get_node(node_id)?;
|
|
2341
|
+
|
|
2342
|
+
// Nodes with their own layout box: use it directly
|
|
2343
|
+
let Some(rects) = self.inline_fragment_rects(node_id) else {
|
|
2344
|
+
let pos = node.offset_top_left();
|
|
2345
|
+
let size = node.unrounded_layout().size;
|
|
2346
|
+
#[cfg(feature = "writing-mode")]
|
|
2347
|
+
let size = match node.containing_block() {
|
|
2348
|
+
Some(parent) if self.nodes[parent].layout_data().writing_mode.is_vertical() => {
|
|
2349
|
+
size.transpose()
|
|
2350
|
+
}
|
|
2351
|
+
_ => size,
|
|
2352
|
+
};
|
|
2353
|
+
return Some(BoundingRect {
|
|
2354
|
+
x: pos.x as f64,
|
|
2355
|
+
y: pos.y as f64,
|
|
2356
|
+
width: size.width as f64,
|
|
2357
|
+
height: size.height as f64,
|
|
2358
|
+
});
|
|
2359
|
+
};
|
|
2360
|
+
|
|
2361
|
+
// Union the per-line-box fragments into a single bounding box
|
|
2362
|
+
let bounds = rects.reduce(BoundingRect::union)?;
|
|
2363
|
+
|
|
2364
|
+
// Fragment rects are viewport-relative; convert to document-relative
|
|
2365
|
+
let scroll = self.viewport_scroll();
|
|
2366
|
+
let mut x = bounds.x + scroll.x;
|
|
2367
|
+
let mut y = bounds.y + scroll.y;
|
|
2368
|
+
|
|
2369
|
+
// Resolve the offsetParent from the inline root (the root itself if it is positioned)
|
|
2370
|
+
let inline_root = node.inline_root_ancestor()?;
|
|
2371
|
+
let offset_parent = if inline_root.is_offset_parent() {
|
|
2372
|
+
Some(inline_root)
|
|
2373
|
+
} else {
|
|
2374
|
+
inline_root.offset_parent()
|
|
2375
|
+
};
|
|
2376
|
+
// Make the position relative to the offsetParent's padding edge
|
|
2377
|
+
if let Some(parent) = offset_parent.filter(|parent| !parent.is_static_body()) {
|
|
2378
|
+
let (parent_pos, parent_layout) = self.physical_unrounded_geometry(parent.id);
|
|
2379
|
+
let border = parent_layout.border;
|
|
2380
|
+
x -= (parent_pos.x + border.left) as f64;
|
|
2381
|
+
y -= (parent_pos.y + border.top) as f64;
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
Some(BoundingRect {
|
|
2385
|
+
x,
|
|
2386
|
+
y,
|
|
2387
|
+
width: bounds.width,
|
|
2388
|
+
height: bounds.height,
|
|
2389
|
+
})
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
/// Computes the sizes and positions of the `Node`'s box fragments relative to the
|
|
2393
|
+
/// viewport (CSSOM `getClientRects()` semantics). Nodes with their own layout box
|
|
2394
|
+
/// return a single rect. Non-atomic inline elements (which are laid out as style
|
|
2395
|
+
/// spans within an inline root's text layout) return one rect per line box.
|
|
2396
|
+
pub fn node_client_rects(&self, node_id: NodeId) -> Vec<BoundingRect> {
|
|
2397
|
+
match self.inline_fragment_rects(node_id) {
|
|
2398
|
+
Some(rects) => rects.collect(),
|
|
2399
|
+
None => self.get_client_bounding_rect(node_id).into_iter().collect(),
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
/// Computes per-line-box fragment rects for a non-atomic inline element by walking
|
|
2404
|
+
/// the containing inline root's text layout. Returns `None` for nodes that have
|
|
2405
|
+
/// their own layout box (which should use `get_client_bounding_rect` instead).
|
|
2406
|
+
pub fn inline_fragment_rects(
|
|
2407
|
+
&self,
|
|
2408
|
+
node_id: NodeId,
|
|
2409
|
+
) -> Option<impl Iterator<Item = BoundingRect> + '_> {
|
|
2410
|
+
let node = self.get_node(node_id)?;
|
|
2411
|
+
|
|
2412
|
+
// Only non-atomic inline elements lack their own layout box: they are
|
|
2413
|
+
// flattened into the containing inline root's text layout as style spans.
|
|
2414
|
+
let fragments = node.inline_fragment_boxes()?;
|
|
2415
|
+
let inline_root = node.inline_root_ancestor()?;
|
|
2416
|
+
|
|
2417
|
+
// Fragment boxes are relative to the inline root's border box.
|
|
2418
|
+
let (root_pos, _) = self.physical_unrounded_geometry(inline_root.id);
|
|
2419
|
+
let origin_x = root_pos.x as f64 - self.viewport_scroll().x;
|
|
2420
|
+
let origin_y = root_pos.y as f64 - self.viewport_scroll().y;
|
|
2421
|
+
|
|
2422
|
+
Some(fragments.map(move |rect| BoundingRect {
|
|
2423
|
+
x: snap_to_layout_unit(origin_x + rect.left as f64),
|
|
2424
|
+
y: snap_to_layout_unit(origin_y + rect.top as f64),
|
|
2425
|
+
width: snap_to_layout_unit((rect.right - rect.left) as f64),
|
|
2426
|
+
height: snap_to_layout_unit((rect.bottom - rect.top) as f64),
|
|
2427
|
+
}))
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
/// The first element in tree order with the given tag name. The root element and
|
|
2431
|
+
/// its children are checked first as a fast path before a full tree search, making
|
|
2432
|
+
/// this suitable for the `documentElement`/`head`/`body` document accessors.
|
|
2433
|
+
pub fn find_element_by_tag_name(&self, tag: &LocalName) -> Option<&Node> {
|
|
2434
|
+
let root = self.try_root_element()?;
|
|
2435
|
+
if root.data.is_element_with_tag_name(tag) {
|
|
2436
|
+
return Some(root);
|
|
2437
|
+
}
|
|
2438
|
+
root.children
|
|
2439
|
+
.iter()
|
|
2440
|
+
.copied()
|
|
2441
|
+
.find(|child_id| {
|
|
2442
|
+
self.get_node(*child_id)
|
|
2443
|
+
.is_some_and(|child| child.data.is_element_with_tag_name(tag))
|
|
2444
|
+
})
|
|
2445
|
+
.or_else(|| {
|
|
2446
|
+
TreeTraverser::new(self)
|
|
2447
|
+
.find(|node_id| self.nodes[*node_id].data.is_element_with_tag_name(tag))
|
|
2448
|
+
})
|
|
2449
|
+
.map(|node_id| &self.nodes[node_id])
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
/// The document's `body` element, as for `document.body`
|
|
2453
|
+
pub fn find_body_node(&self) -> Option<&Node> {
|
|
2454
|
+
self.find_element_by_tag_name(&local_name!("body"))
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
/// The document's `head` element, as for `document.head`
|
|
2458
|
+
pub fn find_head_node(&self) -> Option<&Node> {
|
|
2459
|
+
self.find_element_by_tag_name(&local_name!("head"))
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
/// The document's `title` element
|
|
2463
|
+
pub fn find_title_node(&self) -> Option<&Node> {
|
|
2464
|
+
self.find_element_by_tag_name(&local_name!("title"))
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
pub fn with_text_input(
|
|
2468
|
+
&mut self,
|
|
2469
|
+
node_id: NodeId,
|
|
2470
|
+
cb: impl FnOnce(PlainEditorDriver<TextBrush>),
|
|
2471
|
+
) {
|
|
2472
|
+
let Some(node) = self.nodes.get_mut(node_id) else {
|
|
2473
|
+
return;
|
|
2474
|
+
};
|
|
2475
|
+
|
|
2476
|
+
if let Some(text_input) = node
|
|
2477
|
+
.element_data_mut()
|
|
2478
|
+
.and_then(|el| el.text_input_data_mut())
|
|
2479
|
+
{
|
|
2480
|
+
let mut font_ctx = self.font_ctx.lock().unwrap();
|
|
2481
|
+
let layout_ctx = &mut self.layout_ctx;
|
|
2482
|
+
let driver = text_input.editor.driver(&mut font_ctx, layout_ctx);
|
|
2483
|
+
cb(driver)
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
/// Recompute the scroll offset of the text input at `node_id` (if any) so that its caret
|
|
2488
|
+
/// remains visible within the input's content box.
|
|
2489
|
+
pub(crate) fn clamp_text_input_scroll(&mut self, node_id: NodeId) {
|
|
2490
|
+
let Some(node) = self.nodes.get_mut(node_id) else {
|
|
2491
|
+
return;
|
|
2492
|
+
};
|
|
2493
|
+
|
|
2494
|
+
let content_box_width = node.final_layout().content_box_width();
|
|
2495
|
+
let content_box_height = node.final_layout().content_box_height();
|
|
2496
|
+
|
|
2497
|
+
if let Some(text_input) = node
|
|
2498
|
+
.element_data_mut()
|
|
2499
|
+
.and_then(|el| el.text_input_data_mut())
|
|
2500
|
+
{
|
|
2501
|
+
text_input.clamp_scroll_offset(content_box_width, content_box_height);
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
pub(crate) fn compute_has_canvas(&self) -> bool {
|
|
2506
|
+
TreeTraverser::new(self).any(|node_id| {
|
|
2507
|
+
let node = &self.nodes[node_id];
|
|
2508
|
+
let Some(element) = node.element_data() else {
|
|
2509
|
+
return false;
|
|
2510
|
+
};
|
|
2511
|
+
if element.name.local == local_name!("canvas") && element.has_attr(local_name!("src")) {
|
|
2512
|
+
return true;
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
false
|
|
2516
|
+
})
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
// Text selection methods
|
|
2520
|
+
|
|
2521
|
+
/// Find the text position (inline_root_id, byte_offset) at a given point.
|
|
2522
|
+
/// Uses hit() for proper coordinate transformation, then finds the inline root
|
|
2523
|
+
/// and byte offset.
|
|
2524
|
+
pub fn find_text_position(&self, x: f32, y: f32) -> Option<(NodeId, usize)> {
|
|
2525
|
+
let hit = self.hit(x, y)?;
|
|
2526
|
+
let hit_node = self.get_node(hit.node_id)?;
|
|
2527
|
+
let inline_root = hit_node.inline_root_ancestor()?;
|
|
2528
|
+
let byte_offset = inline_root.text_offset_at_point(hit.x, hit.y)?;
|
|
2529
|
+
Some((inline_root.id, byte_offset))
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
/// Set the text selection range (creates a new selection from anchor to focus)
|
|
2533
|
+
pub fn set_text_selection(
|
|
2534
|
+
&mut self,
|
|
2535
|
+
anchor_node: NodeId,
|
|
2536
|
+
anchor_offset: usize,
|
|
2537
|
+
focus_node: NodeId,
|
|
2538
|
+
focus_offset: usize,
|
|
2539
|
+
) {
|
|
2540
|
+
self.text_selection =
|
|
2541
|
+
TextSelection::new(anchor_node, anchor_offset, focus_node, focus_offset);
|
|
2542
|
+
|
|
2543
|
+
// For anonymous blocks, switch to storing parent+sibling_index (stable reference)
|
|
2544
|
+
if let (Some(parent), Some(idx)) = self.anonymous_block_location(anchor_node) {
|
|
2545
|
+
self.text_selection
|
|
2546
|
+
.anchor
|
|
2547
|
+
.set_anonymous(parent, idx, anchor_offset);
|
|
2548
|
+
}
|
|
2549
|
+
if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
|
|
2550
|
+
self.text_selection
|
|
2551
|
+
.focus
|
|
2552
|
+
.set_anonymous(parent, idx, focus_offset);
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
/// Get the parent ID and sibling index for a node if it's an anonymous block.
|
|
2557
|
+
/// Returns (None, None) for non-anonymous blocks.
|
|
2558
|
+
fn anonymous_block_location(&self, node_id: NodeId) -> (Option<NodeId>, Option<usize>) {
|
|
2559
|
+
let Some(node) = self.get_node(node_id) else {
|
|
2560
|
+
return (None, None);
|
|
2561
|
+
};
|
|
2562
|
+
|
|
2563
|
+
if !node.is_anonymous() {
|
|
2564
|
+
return (None, None);
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
let Some(parent_id) = node.parent else {
|
|
2568
|
+
return (None, None);
|
|
2569
|
+
};
|
|
2570
|
+
|
|
2571
|
+
let Some(parent) = self.get_node(parent_id) else {
|
|
2572
|
+
return (Some(parent_id), None);
|
|
2573
|
+
};
|
|
2574
|
+
|
|
2575
|
+
let layout_children = parent.layout_children.borrow();
|
|
2576
|
+
let Some(children) = layout_children.as_ref() else {
|
|
2577
|
+
return (Some(parent_id), None);
|
|
2578
|
+
};
|
|
2579
|
+
|
|
2580
|
+
// Find the index of this anonymous block among siblings
|
|
2581
|
+
let mut anon_index = 0;
|
|
2582
|
+
for &child_id in children.iter() {
|
|
2583
|
+
if child_id == node_id {
|
|
2584
|
+
return (Some(parent_id), Some(anon_index));
|
|
2585
|
+
}
|
|
2586
|
+
if self.get_node(child_id).is_some_and(|n| n.is_anonymous()) {
|
|
2587
|
+
anon_index += 1;
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
(Some(parent_id), None)
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
/// Clear the text selection
|
|
2595
|
+
pub fn clear_text_selection(&mut self) {
|
|
2596
|
+
self.text_selection.clear();
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
/// Update the selection focus point (used during mouse drag to extend selection).
|
|
2600
|
+
pub fn update_selection_focus(&mut self, focus_node: NodeId, focus_offset: usize) {
|
|
2601
|
+
// For anonymous blocks, store parent+sibling_index; otherwise store node directly
|
|
2602
|
+
if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
|
|
2603
|
+
self.text_selection
|
|
2604
|
+
.focus
|
|
2605
|
+
.set_anonymous(parent, idx, focus_offset);
|
|
2606
|
+
} else {
|
|
2607
|
+
self.text_selection.set_focus(focus_node, focus_offset);
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
/// Extend text selection to the given point. Returns true if selection was updated.
|
|
2612
|
+
/// This is a convenience method that combines find_text_position and update_selection_focus.
|
|
2613
|
+
pub fn extend_text_selection_to_point(&mut self, x: f32, y: f32) -> bool {
|
|
2614
|
+
if !self.text_selection.anchor.is_some() {
|
|
2615
|
+
return false;
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
if let Some((node, offset)) = self.find_text_position(x, y) {
|
|
2619
|
+
self.update_selection_focus(node, offset);
|
|
2620
|
+
self.shell_provider.request_redraw();
|
|
2621
|
+
true
|
|
2622
|
+
} else {
|
|
2623
|
+
false
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
/// Find the Nth anonymous block under a parent.
|
|
2628
|
+
fn find_anonymous_block_by_index(
|
|
2629
|
+
&self,
|
|
2630
|
+
parent_id: NodeId,
|
|
2631
|
+
target_index: usize,
|
|
2632
|
+
) -> Option<NodeId> {
|
|
2633
|
+
let parent = self.get_node(parent_id)?;
|
|
2634
|
+
let layout_children = parent.layout_children.borrow();
|
|
2635
|
+
let children = layout_children.as_ref()?;
|
|
2636
|
+
|
|
2637
|
+
children
|
|
2638
|
+
.iter()
|
|
2639
|
+
.filter(|&&child_id| self.get_node(child_id).is_some_and(|n| n.is_anonymous()))
|
|
2640
|
+
.nth(target_index)
|
|
2641
|
+
.copied()
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
/// Check if there is an active (non-empty) text selection
|
|
2645
|
+
pub fn has_text_selection(&self) -> bool {
|
|
2646
|
+
self.text_selection.is_active()
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
/// Get the selected text content, supporting selection across multiple inline roots.
|
|
2650
|
+
pub fn get_selected_text(&self) -> Option<String> {
|
|
2651
|
+
let ranges = self.get_text_selection_ranges();
|
|
2652
|
+
if ranges.is_empty() {
|
|
2653
|
+
return None;
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
let mut result = String::new();
|
|
2657
|
+
for (node_id, start, end) in &ranges {
|
|
2658
|
+
let node = self.get_node(*node_id)?;
|
|
2659
|
+
let element_data = node.element_data()?;
|
|
2660
|
+
let inline_layout = element_data.inline_layout_data.as_ref()?;
|
|
2661
|
+
|
|
2662
|
+
if *end > inline_layout.text.len() {
|
|
2663
|
+
continue;
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
if !result.is_empty() {
|
|
2667
|
+
result.push(' ');
|
|
2668
|
+
}
|
|
2669
|
+
result.push_str(&inline_layout.text[*start..*end]);
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
if result.is_empty() {
|
|
2673
|
+
None
|
|
2674
|
+
} else {
|
|
2675
|
+
Some(result)
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
/// Get all selection ranges as Vec<(node_id, start_offset, end_offset)>.
|
|
2680
|
+
/// Returns empty vec if no selection.
|
|
2681
|
+
pub fn get_text_selection_ranges(&self) -> Vec<(NodeId, usize, usize)> {
|
|
2682
|
+
let lookup = |parent_id, idx| self.find_anonymous_block_by_index(parent_id, idx);
|
|
2683
|
+
|
|
2684
|
+
let anchor_node = match self.text_selection.anchor.resolve_node_id(lookup) {
|
|
2685
|
+
Some(id) => id,
|
|
2686
|
+
None => return Vec::new(),
|
|
2687
|
+
};
|
|
2688
|
+
let focus_node = match self.text_selection.focus.resolve_node_id(lookup) {
|
|
2689
|
+
Some(id) => id,
|
|
2690
|
+
None => return Vec::new(),
|
|
2691
|
+
};
|
|
2692
|
+
|
|
2693
|
+
// Guard against stale selection endpoints: nodes may have been removed from
|
|
2694
|
+
// the document (e.g. by script) since the selection was made.
|
|
2695
|
+
let node_is_in_doc = |node_id: NodeId| {
|
|
2696
|
+
self.nodes
|
|
2697
|
+
.get(node_id)
|
|
2698
|
+
.is_some_and(|node| node.flags.is_in_document())
|
|
2699
|
+
};
|
|
2700
|
+
if !node_is_in_doc(anchor_node) || !node_is_in_doc(focus_node) {
|
|
2701
|
+
return Vec::new();
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
// Single node selection
|
|
2705
|
+
if anchor_node == focus_node {
|
|
2706
|
+
let start = self
|
|
2707
|
+
.text_selection
|
|
2708
|
+
.anchor
|
|
2709
|
+
.offset
|
|
2710
|
+
.min(self.text_selection.focus.offset);
|
|
2711
|
+
let end = self
|
|
2712
|
+
.text_selection
|
|
2713
|
+
.anchor
|
|
2714
|
+
.offset
|
|
2715
|
+
.max(self.text_selection.focus.offset);
|
|
2716
|
+
|
|
2717
|
+
if start == end {
|
|
2718
|
+
return Vec::new();
|
|
2719
|
+
}
|
|
2720
|
+
return vec![(anchor_node, start, end)];
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
// Multi-node selection: collect all inline roots between anchor and focus
|
|
2724
|
+
let inline_roots = self.collect_inline_roots_in_range(anchor_node, focus_node);
|
|
2725
|
+
if inline_roots.is_empty() {
|
|
2726
|
+
return Vec::new();
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
// Determine document order using the collected inline_roots order
|
|
2730
|
+
// (inline_roots is already in document order from first to last)
|
|
2731
|
+
let first_in_roots = inline_roots[0];
|
|
2732
|
+
|
|
2733
|
+
let (first_node, first_offset, last_node, last_offset) =
|
|
2734
|
+
if first_in_roots == anchor_node || (first_in_roots != focus_node) {
|
|
2735
|
+
// anchor is first (or neither endpoint is in roots, which shouldn't happen)
|
|
2736
|
+
(
|
|
2737
|
+
anchor_node,
|
|
2738
|
+
self.text_selection.anchor.offset,
|
|
2739
|
+
focus_node,
|
|
2740
|
+
self.text_selection.focus.offset,
|
|
2741
|
+
)
|
|
2742
|
+
} else {
|
|
2743
|
+
// focus is first
|
|
2744
|
+
(
|
|
2745
|
+
focus_node,
|
|
2746
|
+
self.text_selection.focus.offset,
|
|
2747
|
+
anchor_node,
|
|
2748
|
+
self.text_selection.anchor.offset,
|
|
2749
|
+
)
|
|
2750
|
+
};
|
|
2751
|
+
|
|
2752
|
+
let mut ranges = Vec::with_capacity(inline_roots.len());
|
|
2753
|
+
|
|
2754
|
+
for &node_id in &inline_roots {
|
|
2755
|
+
let Some(node) = self.get_node(node_id) else {
|
|
2756
|
+
continue;
|
|
2757
|
+
};
|
|
2758
|
+
let Some(element_data) = node.element_data() else {
|
|
2759
|
+
continue;
|
|
2760
|
+
};
|
|
2761
|
+
let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
|
|
2762
|
+
continue;
|
|
2763
|
+
};
|
|
2764
|
+
|
|
2765
|
+
let text_len = inline_layout.text.len();
|
|
2766
|
+
|
|
2767
|
+
if node_id == first_node && node_id == last_node {
|
|
2768
|
+
let start = first_offset.min(last_offset);
|
|
2769
|
+
let end = first_offset.max(last_offset);
|
|
2770
|
+
if start < end && end <= text_len {
|
|
2771
|
+
ranges.push((node_id, start, end));
|
|
2772
|
+
}
|
|
2773
|
+
} else if node_id == first_node {
|
|
2774
|
+
if first_offset < text_len {
|
|
2775
|
+
ranges.push((node_id, first_offset, text_len));
|
|
2776
|
+
}
|
|
2777
|
+
} else if node_id == last_node {
|
|
2778
|
+
if last_offset > 0 && last_offset <= text_len {
|
|
2779
|
+
ranges.push((node_id, 0, last_offset));
|
|
2780
|
+
}
|
|
2781
|
+
} else if text_len > 0 {
|
|
2782
|
+
ranges.push((node_id, 0, text_len));
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
ranges
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
2791
|
+
pub struct BoundingRect {
|
|
2792
|
+
pub x: f64,
|
|
2793
|
+
pub y: f64,
|
|
2794
|
+
pub width: f64,
|
|
2795
|
+
pub height: f64,
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
impl BoundingRect {
|
|
2799
|
+
/// The smallest rect containing both `self` and `other`.
|
|
2800
|
+
pub fn union(self, other: BoundingRect) -> BoundingRect {
|
|
2801
|
+
let x = self.x.min(other.x);
|
|
2802
|
+
let y = self.y.min(other.y);
|
|
2803
|
+
BoundingRect {
|
|
2804
|
+
x,
|
|
2805
|
+
y,
|
|
2806
|
+
width: (self.x + self.width).max(other.x + other.width) - x,
|
|
2807
|
+
height: (self.y + self.height).max(other.y + other.height) - y,
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
/// Snap a CSSOM geometry value to a 1/64px grid (the precision of Blink's `LayoutUnit`).
|
|
2813
|
+
/// Layout values are accumulated in `f32`, so e.g. seven `55/7`-wide flex items would
|
|
2814
|
+
/// otherwise end at `55.0000005` and appear to overflow their 55px container.
|
|
2815
|
+
fn snap_to_layout_unit(value: f64) -> f64 {
|
|
2816
|
+
(value * 64.0).round() / 64.0
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
impl AsRef<BaseDocument> for BaseDocument {
|
|
2820
|
+
fn as_ref(&self) -> &BaseDocument {
|
|
2821
|
+
self
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
impl AsMut<BaseDocument> for BaseDocument {
|
|
2826
|
+
fn as_mut(&mut self) -> &mut BaseDocument {
|
|
2827
|
+
self
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
#[cfg(test)]
|
|
2832
|
+
mod zoom_tests {
|
|
2833
|
+
use super::*;
|
|
2834
|
+
use blitz_traits::shell::ColorScheme;
|
|
2835
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
2836
|
+
|
|
2837
|
+
#[derive(Default)]
|
|
2838
|
+
struct CountingShellProvider {
|
|
2839
|
+
redraw_count: AtomicUsize,
|
|
2840
|
+
}
|
|
2841
|
+
impl ShellProvider for CountingShellProvider {
|
|
2842
|
+
fn request_redraw(&self) {
|
|
2843
|
+
self.redraw_count.fetch_add(1, Ordering::SeqCst);
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
#[test]
|
|
2848
|
+
fn zoom_requests_redraw() {
|
|
2849
|
+
let shell_provider = Arc::new(CountingShellProvider::default());
|
|
2850
|
+
let mut doc = BaseDocument::new(DocumentConfig {
|
|
2851
|
+
viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
|
|
2852
|
+
shell_provider: Some(shell_provider.clone() as _),
|
|
2853
|
+
..Default::default()
|
|
2854
|
+
});
|
|
2855
|
+
doc.resolve(0.0);
|
|
2856
|
+
|
|
2857
|
+
let base = shell_provider.redraw_count.load(Ordering::SeqCst);
|
|
2858
|
+
doc.zoom_by(0.5);
|
|
2859
|
+
assert!(shell_provider.redraw_count.load(Ordering::SeqCst) > base);
|
|
2860
|
+
|
|
2861
|
+
let base = shell_provider.redraw_count.load(Ordering::SeqCst);
|
|
2862
|
+
doc.zoom_to(1.0);
|
|
2863
|
+
assert!(shell_provider.redraw_count.load(Ordering::SeqCst) > base);
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
#[cfg(test)]
|
|
2868
|
+
mod hover_state_tests {
|
|
2869
|
+
use super::*;
|
|
2870
|
+
use crate::{Attribute, qual_name};
|
|
2871
|
+
use blitz_traits::shell::ColorScheme;
|
|
2872
|
+
|
|
2873
|
+
/// Build `<html><body style="margin:0"><div style="width:300px">some text
|
|
2874
|
+
/// <div style="height:50px"></div></div></body></html>` manually (the HTML
|
|
2875
|
+
/// parser lives in blitz-html, which would be a circular dev-dependency).
|
|
2876
|
+
/// The bare text next to a block sibling gets wrapped in an anonymous
|
|
2877
|
+
/// block, which becomes the inline root: text hits report the anonymous
|
|
2878
|
+
/// block as the hit node.
|
|
2879
|
+
fn make_doc() -> (BaseDocument, NodeId) {
|
|
2880
|
+
let mut doc = BaseDocument::new(DocumentConfig {
|
|
2881
|
+
viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
|
|
2882
|
+
..Default::default()
|
|
2883
|
+
});
|
|
2884
|
+
let root_id = doc.root_node().id;
|
|
2885
|
+
let style = |value: &str| Attribute {
|
|
2886
|
+
name: qual_name!("style"),
|
|
2887
|
+
value: value.to_string(),
|
|
2888
|
+
};
|
|
2889
|
+
|
|
2890
|
+
let mut mutator = doc.mutate();
|
|
2891
|
+
let html = mutator.create_element(qual_name!("html"), vec![]);
|
|
2892
|
+
let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
|
|
2893
|
+
let container = mutator.create_element(qual_name!("div"), vec![style("width:300px")]);
|
|
2894
|
+
let text = mutator.create_text_node("some text");
|
|
2895
|
+
let block = mutator.create_element(qual_name!("div"), vec![style("height:50px")]);
|
|
2896
|
+
mutator.append_children(container, &[text, block]);
|
|
2897
|
+
mutator.append_children(body, &[container]);
|
|
2898
|
+
mutator.append_children(html, &[body]);
|
|
2899
|
+
mutator.append_children(root_id, &[html]);
|
|
2900
|
+
drop(mutator);
|
|
2901
|
+
|
|
2902
|
+
doc.resolve(0.0);
|
|
2903
|
+
(doc, container)
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
/// Whether text laid out with a real (non-zero-metric) font. Without the
|
|
2907
|
+
/// `system-fonts` feature text measures 0x0 and text hits are impossible,
|
|
2908
|
+
/// making these tests vacuous.
|
|
2909
|
+
fn text_has_size(doc: &BaseDocument, container: NodeId) -> bool {
|
|
2910
|
+
doc.nodes[container].final_layout().size.height > 50.0
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
/// Regression test: hovering bare text wrapped in an anonymous block must
|
|
2914
|
+
/// report a text cursor. The hit node for such text is the anonymous
|
|
2915
|
+
/// inline root itself, while the *stored* hover target is canonicalized to
|
|
2916
|
+
/// the containing element — the cursor must be derived from the precise
|
|
2917
|
+
/// hit node, not the canonical target.
|
|
2918
|
+
#[test]
|
|
2919
|
+
fn hovering_text_in_anonymous_block_reports_text_cursor() {
|
|
2920
|
+
let (mut doc, container) = make_doc();
|
|
2921
|
+
if !text_has_size(&doc, container) {
|
|
2922
|
+
eprintln!("skipping: no usable font (text measures 0x0)");
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
doc.set_hover_to(5.0, 8.0);
|
|
2927
|
+
assert!(doc.hover_node_is_text, "expected a text hit");
|
|
2928
|
+
let hit_id = doc.hover_hit_node_id.expect("expected a hit node");
|
|
2929
|
+
assert!(
|
|
2930
|
+
doc.nodes[hit_id].is_anonymous(),
|
|
2931
|
+
"expected the hit node to be the anonymous inline root"
|
|
2932
|
+
);
|
|
2933
|
+
assert_eq!(
|
|
2934
|
+
doc.get_hover_node_id(),
|
|
2935
|
+
Some(container),
|
|
2936
|
+
"expected the stored hover target to be the containing element"
|
|
2937
|
+
);
|
|
2938
|
+
assert_eq!(doc.get_cursor(), Some(CursorIcon::Text));
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
/// Hovering the empty region of the anonymous block (right of the text) is
|
|
2942
|
+
/// not a text hit: default cursor, same canonical hover target.
|
|
2943
|
+
#[test]
|
|
2944
|
+
fn hovering_anonymous_block_whitespace_reports_default_cursor() {
|
|
2945
|
+
let (mut doc, container) = make_doc();
|
|
2946
|
+
if !text_has_size(&doc, container) {
|
|
2947
|
+
eprintln!("skipping: no usable font (text measures 0x0)");
|
|
2948
|
+
return;
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
doc.set_hover_to(250.0, 8.0);
|
|
2952
|
+
assert!(!doc.hover_node_is_text);
|
|
2953
|
+
assert_eq!(doc.get_hover_node_id(), Some(container));
|
|
2954
|
+
assert_eq!(doc.get_cursor(), Some(CursorIcon::Default));
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
#[cfg(test)]
|
|
2959
|
+
mod hover_invalidation_tests {
|
|
2960
|
+
use super::*;
|
|
2961
|
+
use crate::{Attribute, QualName, qual_name};
|
|
2962
|
+
use blitz_traits::shell::ColorScheme;
|
|
2963
|
+
|
|
2964
|
+
/// Build `<html><body style="margin:0"><div style="width:300px;height:100px">
|
|
2965
|
+
/// <span>text</span></div></body></html>` with a `div:hover` rule.
|
|
2966
|
+
fn make_doc() -> (BaseDocument, NodeId, NodeId) {
|
|
2967
|
+
let mut doc = BaseDocument::new(DocumentConfig {
|
|
2968
|
+
viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
|
|
2969
|
+
..Default::default()
|
|
2970
|
+
});
|
|
2971
|
+
let sheet = UserAgentStylesheet::parse(
|
|
2972
|
+
"div:hover { background-color: rgb(255, 0, 0); } div:hover span { color: rgb(0, 255, 0); }",
|
|
2973
|
+
);
|
|
2974
|
+
doc.add_user_agent_stylesheet(sheet);
|
|
2975
|
+
let root_id = doc.root_node().id;
|
|
2976
|
+
let style = |value: &str| Attribute {
|
|
2977
|
+
name: qual_name!("style"),
|
|
2978
|
+
value: value.to_string(),
|
|
2979
|
+
};
|
|
2980
|
+
|
|
2981
|
+
let mut mutator = doc.mutate();
|
|
2982
|
+
let html = mutator.create_element(qual_name!("html"), vec![]);
|
|
2983
|
+
let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
|
|
2984
|
+
let div =
|
|
2985
|
+
mutator.create_element(qual_name!("div"), vec![style("width:300px;height:100px")]);
|
|
2986
|
+
let span = mutator.create_element(qual_name!("span"), vec![]);
|
|
2987
|
+
let text = mutator.create_text_node("some text");
|
|
2988
|
+
mutator.append_children(span, &[text]);
|
|
2989
|
+
mutator.append_children(div, &[span]);
|
|
2990
|
+
mutator.append_children(body, &[div]);
|
|
2991
|
+
mutator.append_children(html, &[body]);
|
|
2992
|
+
mutator.append_children(root_id, &[html]);
|
|
2993
|
+
drop(mutator);
|
|
2994
|
+
|
|
2995
|
+
doc.resolve(0.0);
|
|
2996
|
+
(doc, div, span)
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
fn bg_color(doc: &BaseDocument, id: NodeId) -> String {
|
|
3000
|
+
format!(
|
|
3001
|
+
"{:?}",
|
|
3002
|
+
doc.nodes[id]
|
|
3003
|
+
.primary_styles()
|
|
3004
|
+
.unwrap()
|
|
3005
|
+
.get_background()
|
|
3006
|
+
.background_color
|
|
3007
|
+
)
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
fn text_color(doc: &BaseDocument, id: NodeId) -> String {
|
|
3011
|
+
format!(
|
|
3012
|
+
"{:?}",
|
|
3013
|
+
doc.nodes[id].primary_styles().unwrap().clone_color()
|
|
3014
|
+
)
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
#[test]
|
|
3018
|
+
fn hover_styles_apply_and_clear() {
|
|
3019
|
+
let (mut doc, div, span) = make_doc();
|
|
3020
|
+
let initial_bg = bg_color(&doc, div);
|
|
3021
|
+
let initial_color = text_color(&doc, span);
|
|
3022
|
+
|
|
3023
|
+
// Hover the div
|
|
3024
|
+
doc.set_hover_to(10.0, 10.0);
|
|
3025
|
+
assert!(doc.nodes[div].is_hovered());
|
|
3026
|
+
doc.resolve(0.0);
|
|
3027
|
+
let hovered_bg = bg_color(&doc, div);
|
|
3028
|
+
let hovered_color = text_color(&doc, span);
|
|
3029
|
+
assert_ne!(initial_bg, hovered_bg, "hover should change div background");
|
|
3030
|
+
assert_ne!(
|
|
3031
|
+
initial_color, hovered_color,
|
|
3032
|
+
"hover should change span color"
|
|
3033
|
+
);
|
|
3034
|
+
|
|
3035
|
+
// Move the pointer off the div (below it, over the body)
|
|
3036
|
+
doc.set_hover_to(10.0, 200.0);
|
|
3037
|
+
assert!(!doc.nodes[div].is_hovered());
|
|
3038
|
+
doc.resolve(0.0);
|
|
3039
|
+
assert_eq!(
|
|
3040
|
+
bg_color(&doc, div),
|
|
3041
|
+
initial_bg,
|
|
3042
|
+
"unhover should restore div background"
|
|
3043
|
+
);
|
|
3044
|
+
assert_eq!(
|
|
3045
|
+
text_color(&doc, span),
|
|
3046
|
+
initial_color,
|
|
3047
|
+
"unhover should restore span color"
|
|
3048
|
+
);
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
/// Mimics BBC's headline hover pattern: `a:link:hover .headline` with the
|
|
3052
|
+
/// headline several levels below the anchor.
|
|
3053
|
+
#[test]
|
|
3054
|
+
fn ancestor_hover_with_link_state_updates_descendant() {
|
|
3055
|
+
let mut doc = BaseDocument::new(DocumentConfig {
|
|
3056
|
+
viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
|
|
3057
|
+
..Default::default()
|
|
3058
|
+
});
|
|
3059
|
+
let sheet = UserAgentStylesheet::parse(
|
|
3060
|
+
".promo:link:hover .headline, .promo:visited:hover .headline { color: rgb(184, 0, 0); text-decoration-line: underline; }",
|
|
3061
|
+
);
|
|
3062
|
+
doc.add_user_agent_stylesheet(sheet);
|
|
3063
|
+
let root_id = doc.root_node().id;
|
|
3064
|
+
let attr = |name: QualName, value: &str| Attribute {
|
|
3065
|
+
name,
|
|
3066
|
+
value: value.to_string(),
|
|
3067
|
+
};
|
|
3068
|
+
|
|
3069
|
+
let mut mutator = doc.mutate();
|
|
3070
|
+
let html = mutator.create_element(qual_name!("html"), vec![]);
|
|
3071
|
+
let body = mutator.create_element(
|
|
3072
|
+
qual_name!("body"),
|
|
3073
|
+
vec![attr(qual_name!("style"), "margin:0")],
|
|
3074
|
+
);
|
|
3075
|
+
let a = mutator.create_element(
|
|
3076
|
+
qual_name!("a"),
|
|
3077
|
+
vec![
|
|
3078
|
+
attr(qual_name!("href"), "https://example.com"),
|
|
3079
|
+
attr(qual_name!("class"), "promo"),
|
|
3080
|
+
attr(
|
|
3081
|
+
qual_name!("style"),
|
|
3082
|
+
"display:block;width:300px;height:100px",
|
|
3083
|
+
),
|
|
3084
|
+
],
|
|
3085
|
+
);
|
|
3086
|
+
let p = mutator.create_element(qual_name!("p"), vec![]);
|
|
3087
|
+
let span = mutator.create_element(
|
|
3088
|
+
qual_name!("span"),
|
|
3089
|
+
vec![attr(qual_name!("class"), "headline")],
|
|
3090
|
+
);
|
|
3091
|
+
let text = mutator.create_text_node("Headline text");
|
|
3092
|
+
mutator.append_children(span, &[text]);
|
|
3093
|
+
mutator.append_children(p, &[span]);
|
|
3094
|
+
mutator.append_children(a, &[p]);
|
|
3095
|
+
mutator.append_children(body, &[a]);
|
|
3096
|
+
mutator.append_children(html, &[body]);
|
|
3097
|
+
mutator.append_children(root_id, &[html]);
|
|
3098
|
+
drop(mutator);
|
|
3099
|
+
|
|
3100
|
+
doc.resolve(0.0);
|
|
3101
|
+
let initial_color = text_color(&doc, span);
|
|
3102
|
+
|
|
3103
|
+
doc.set_hover_to(10.0, 10.0);
|
|
3104
|
+
assert!(doc.nodes[a].is_hovered());
|
|
3105
|
+
doc.resolve(0.0);
|
|
3106
|
+
let hovered_color = text_color(&doc, span);
|
|
3107
|
+
assert_ne!(
|
|
3108
|
+
initial_color, hovered_color,
|
|
3109
|
+
"hovering the anchor should change the headline color"
|
|
3110
|
+
);
|
|
3111
|
+
|
|
3112
|
+
doc.set_hover_to(10.0, 200.0);
|
|
3113
|
+
assert!(!doc.nodes[a].is_hovered());
|
|
3114
|
+
doc.resolve(0.0);
|
|
3115
|
+
assert_eq!(
|
|
3116
|
+
text_color(&doc, span),
|
|
3117
|
+
initial_color,
|
|
3118
|
+
"unhovering the anchor should restore the headline color"
|
|
3119
|
+
);
|
|
3120
|
+
}
|
|
3121
|
+
|
|
3122
|
+
/// Toggling a checkbox must invalidate `:checked`-dependent styles.
|
|
3123
|
+
#[test]
|
|
3124
|
+
fn checkbox_toggle_updates_checked_styles() {
|
|
3125
|
+
let mut doc = BaseDocument::new(DocumentConfig {
|
|
3126
|
+
viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
|
|
3127
|
+
..Default::default()
|
|
3128
|
+
});
|
|
3129
|
+
let sheet = UserAgentStylesheet::parse(
|
|
3130
|
+
"input:checked { color: rgb(184, 0, 0); } input:checked + label { color: rgb(0, 184, 0); }",
|
|
3131
|
+
);
|
|
3132
|
+
doc.add_user_agent_stylesheet(sheet);
|
|
3133
|
+
let root_id = doc.root_node().id;
|
|
3134
|
+
let attr = |name: QualName, value: &str| Attribute {
|
|
3135
|
+
name,
|
|
3136
|
+
value: value.to_string(),
|
|
3137
|
+
};
|
|
3138
|
+
|
|
3139
|
+
let mut mutator = doc.mutate();
|
|
3140
|
+
let html = mutator.create_element(qual_name!("html"), vec![]);
|
|
3141
|
+
let body = mutator.create_element(
|
|
3142
|
+
qual_name!("body"),
|
|
3143
|
+
vec![attr(qual_name!("style"), "margin:0")],
|
|
3144
|
+
);
|
|
3145
|
+
let input = mutator.create_element(
|
|
3146
|
+
qual_name!("input"),
|
|
3147
|
+
vec![attr(qual_name!("type"), "checkbox")],
|
|
3148
|
+
);
|
|
3149
|
+
let label = mutator.create_element(qual_name!("label"), vec![]);
|
|
3150
|
+
let text = mutator.create_text_node("label text");
|
|
3151
|
+
mutator.append_children(label, &[text]);
|
|
3152
|
+
mutator.append_children(body, &[input, label]);
|
|
3153
|
+
mutator.append_children(html, &[body]);
|
|
3154
|
+
mutator.append_children(root_id, &[html]);
|
|
3155
|
+
drop(mutator);
|
|
3156
|
+
|
|
3157
|
+
doc.resolve(0.0);
|
|
3158
|
+
let initial_input_color = text_color(&doc, input);
|
|
3159
|
+
let initial_label_color = text_color(&doc, label);
|
|
3160
|
+
|
|
3161
|
+
// Toggle the checkbox on (as the click handler does)
|
|
3162
|
+
doc.snapshot_node_and(input, ElementState::CHECKED, |node| {
|
|
3163
|
+
if let Some(el) = node.element_data_mut() {
|
|
3164
|
+
BaseDocument::toggle_checkbox(el);
|
|
3165
|
+
}
|
|
3166
|
+
node.mark_ancestors_dirty();
|
|
3167
|
+
});
|
|
3168
|
+
doc.resolve(0.0);
|
|
3169
|
+
assert_ne!(
|
|
3170
|
+
text_color(&doc, input),
|
|
3171
|
+
initial_input_color,
|
|
3172
|
+
"checking should change the input color"
|
|
3173
|
+
);
|
|
3174
|
+
assert_ne!(
|
|
3175
|
+
text_color(&doc, label),
|
|
3176
|
+
initial_label_color,
|
|
3177
|
+
"checking should change the sibling label color"
|
|
3178
|
+
);
|
|
3179
|
+
|
|
3180
|
+
// Toggle the checkbox back off
|
|
3181
|
+
doc.snapshot_node_and(input, ElementState::CHECKED, |node| {
|
|
3182
|
+
if let Some(el) = node.element_data_mut() {
|
|
3183
|
+
BaseDocument::toggle_checkbox(el);
|
|
3184
|
+
}
|
|
3185
|
+
node.mark_ancestors_dirty();
|
|
3186
|
+
});
|
|
3187
|
+
doc.resolve(0.0);
|
|
3188
|
+
assert_eq!(
|
|
3189
|
+
text_color(&doc, input),
|
|
3190
|
+
initial_input_color,
|
|
3191
|
+
"unchecking should restore the input color"
|
|
3192
|
+
);
|
|
3193
|
+
assert_eq!(
|
|
3194
|
+
text_color(&doc, label),
|
|
3195
|
+
initial_label_color,
|
|
3196
|
+
"unchecking should restore the sibling label color"
|
|
3197
|
+
);
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
/// A repaint-only style change to an existing `::before` pseudo-element
|
|
3201
|
+
/// (no box construction damage) must be flushed to the pseudo-element's
|
|
3202
|
+
/// anonymous node during the style traversal.
|
|
3203
|
+
#[test]
|
|
3204
|
+
fn hover_updates_existing_pseudo_element_style() {
|
|
3205
|
+
let mut doc = BaseDocument::new(DocumentConfig {
|
|
3206
|
+
viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
|
|
3207
|
+
..Default::default()
|
|
3208
|
+
});
|
|
3209
|
+
let sheet = UserAgentStylesheet::parse(
|
|
3210
|
+
"div::before { content: \"x\"; color: rgb(1, 2, 3); } \
|
|
3211
|
+
div:hover::before { color: rgb(0, 0, 255); }",
|
|
3212
|
+
);
|
|
3213
|
+
doc.add_user_agent_stylesheet(sheet);
|
|
3214
|
+
let root_id = doc.root_node().id;
|
|
3215
|
+
let style = |value: &str| Attribute {
|
|
3216
|
+
name: qual_name!("style"),
|
|
3217
|
+
value: value.to_string(),
|
|
3218
|
+
};
|
|
3219
|
+
|
|
3220
|
+
let mut mutator = doc.mutate();
|
|
3221
|
+
let html = mutator.create_element(qual_name!("html"), vec![]);
|
|
3222
|
+
let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
|
|
3223
|
+
let div = mutator.create_element(
|
|
3224
|
+
qual_name!("div"),
|
|
3225
|
+
vec![style("display:block;width:300px;height:100px")],
|
|
3226
|
+
);
|
|
3227
|
+
let text = mutator.create_text_node("some text");
|
|
3228
|
+
mutator.append_children(div, &[text]);
|
|
3229
|
+
mutator.append_children(body, &[div]);
|
|
3230
|
+
mutator.append_children(html, &[body]);
|
|
3231
|
+
mutator.append_children(root_id, &[html]);
|
|
3232
|
+
drop(mutator);
|
|
3233
|
+
|
|
3234
|
+
doc.resolve(0.0);
|
|
3235
|
+
let before = doc.nodes[div].before().expect("::before node should exist");
|
|
3236
|
+
let initial_color = text_color(&doc, before);
|
|
3237
|
+
|
|
3238
|
+
doc.set_hover_to(10.0, 10.0);
|
|
3239
|
+
assert!(doc.nodes[div].is_hovered());
|
|
3240
|
+
doc.resolve(0.0);
|
|
3241
|
+
assert_ne!(
|
|
3242
|
+
text_color(&doc, before),
|
|
3243
|
+
initial_color,
|
|
3244
|
+
"hover should change the ::before color"
|
|
3245
|
+
);
|
|
3246
|
+
|
|
3247
|
+
doc.set_hover_to(10.0, 200.0);
|
|
3248
|
+
assert!(!doc.nodes[div].is_hovered());
|
|
3249
|
+
doc.resolve(0.0);
|
|
3250
|
+
assert_eq!(
|
|
3251
|
+
text_color(&doc, before),
|
|
3252
|
+
initial_color,
|
|
3253
|
+
"unhover should restore the ::before color"
|
|
3254
|
+
);
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
/// Background-image layers must be flushed to the node's dedicated image
|
|
3258
|
+
/// storage when its style changes (queued during the style traversal),
|
|
3259
|
+
/// now that image flushing no longer runs on every node in the flush pass.
|
|
3260
|
+
#[test]
|
|
3261
|
+
fn hover_updates_background_image_layers() {
|
|
3262
|
+
let mut doc = BaseDocument::new(DocumentConfig {
|
|
3263
|
+
viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
|
|
3264
|
+
..Default::default()
|
|
3265
|
+
});
|
|
3266
|
+
let sheet = UserAgentStylesheet::parse(
|
|
3267
|
+
"div { background-image: url(\"https://example.com/a.png\"); } \
|
|
3268
|
+
div:hover { background-image: url(\"https://example.com/b.png\"); }",
|
|
3269
|
+
);
|
|
3270
|
+
doc.add_user_agent_stylesheet(sheet);
|
|
3271
|
+
let root_id = doc.root_node().id;
|
|
3272
|
+
let style = |value: &str| Attribute {
|
|
3273
|
+
name: qual_name!("style"),
|
|
3274
|
+
value: value.to_string(),
|
|
3275
|
+
};
|
|
3276
|
+
|
|
3277
|
+
let mut mutator = doc.mutate();
|
|
3278
|
+
let html = mutator.create_element(qual_name!("html"), vec![]);
|
|
3279
|
+
let body = mutator.create_element(qual_name!("body"), vec![style("margin:0")]);
|
|
3280
|
+
let div = mutator.create_element(
|
|
3281
|
+
qual_name!("div"),
|
|
3282
|
+
vec![style("display:block;width:300px;height:100px")],
|
|
3283
|
+
);
|
|
3284
|
+
mutator.append_children(body, &[div]);
|
|
3285
|
+
mutator.append_children(html, &[body]);
|
|
3286
|
+
mutator.append_children(root_id, &[html]);
|
|
3287
|
+
drop(mutator);
|
|
3288
|
+
|
|
3289
|
+
let background_image_url = |doc: &BaseDocument, id: NodeId| -> Option<String> {
|
|
3290
|
+
let elem = doc.nodes[id].data.downcast_element().unwrap();
|
|
3291
|
+
elem.background_images
|
|
3292
|
+
.first()
|
|
3293
|
+
.and_then(|img| img.as_ref())
|
|
3294
|
+
.map(|img| img.url.as_str().to_string())
|
|
3295
|
+
};
|
|
3296
|
+
|
|
3297
|
+
doc.resolve(0.0);
|
|
3298
|
+
assert_eq!(
|
|
3299
|
+
background_image_url(&doc, div).as_deref(),
|
|
3300
|
+
Some("https://example.com/a.png"),
|
|
3301
|
+
"initial resolve should flush the background image"
|
|
3302
|
+
);
|
|
3303
|
+
|
|
3304
|
+
doc.set_hover_to(10.0, 10.0);
|
|
3305
|
+
assert!(doc.nodes[div].is_hovered());
|
|
3306
|
+
doc.resolve(0.0);
|
|
3307
|
+
assert_eq!(
|
|
3308
|
+
background_image_url(&doc, div).as_deref(),
|
|
3309
|
+
Some("https://example.com/b.png"),
|
|
3310
|
+
"hover should flush the changed background image"
|
|
3311
|
+
);
|
|
3312
|
+
|
|
3313
|
+
doc.set_hover_to(10.0, 200.0);
|
|
3314
|
+
assert!(!doc.nodes[div].is_hovered());
|
|
3315
|
+
doc.resolve(0.0);
|
|
3316
|
+
assert_eq!(
|
|
3317
|
+
background_image_url(&doc, div).as_deref(),
|
|
3318
|
+
Some("https://example.com/a.png"),
|
|
3319
|
+
"unhover should flush the restored background image"
|
|
3320
|
+
);
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
#[cfg(test)]
|
|
3325
|
+
mod font_face_override_tests {
|
|
3326
|
+
use super::*;
|
|
3327
|
+
use crate::net::{FontFaceOverrides, Resource, ResourceLoadResponse};
|
|
3328
|
+
|
|
3329
|
+
/// Regression-pin for the `@font-face` descriptor-honouring fix.
|
|
3330
|
+
///
|
|
3331
|
+
/// The bug was that `Resource::Font` carried only the raw font bytes,
|
|
3332
|
+
/// so `load_resource` registered fonts with `info_override = None` and
|
|
3333
|
+
/// parley fell back to the TTF's internal `name` table. After the fix,
|
|
3334
|
+
/// `Resource::Font` carries `FontFaceOverrides` and `load_resource`
|
|
3335
|
+
/// builds a `FontInfoOverride` from them — meaning a CSS-declared
|
|
3336
|
+
/// `font-family` alias wins over the file's own metadata.
|
|
3337
|
+
///
|
|
3338
|
+
/// We drive `load_resource` directly with a fabricated response rather
|
|
3339
|
+
/// than go through HTML parsing → `fetch_font_face`, because the
|
|
3340
|
+
/// downstream HTML parser lives in `blitz-html` (would be a circular
|
|
3341
|
+
/// crate dependency). The mapping from `@font-face` descriptors into
|
|
3342
|
+
/// `FontFaceOverrides` is covered by the unit tests in `net.rs`; this
|
|
3343
|
+
/// test pins the load-side of the pipeline.
|
|
3344
|
+
#[test]
|
|
3345
|
+
fn font_face_overrides_alias_family_name() {
|
|
3346
|
+
const ALIAS: &str = "AliasedFamily";
|
|
3347
|
+
|
|
3348
|
+
let mut document = BaseDocument::new(DocumentConfig::default());
|
|
3349
|
+
|
|
3350
|
+
// Sanity: the alias name is not registered before we feed the font.
|
|
3351
|
+
{
|
|
3352
|
+
let mut ctx = document.font_ctx.lock().unwrap();
|
|
3353
|
+
assert!(
|
|
3354
|
+
ctx.collection.family_id(ALIAS).is_none(),
|
|
3355
|
+
"alias must not exist before registration",
|
|
3356
|
+
);
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
// Drive `load_resource` with a `Resource::Font` whose overrides
|
|
3360
|
+
// assert the CSS-side family name. We use the bullet font as a
|
|
3361
|
+
// valid font payload — its internal `name` table is irrelevant to
|
|
3362
|
+
// the assertion; what matters is whether the override wins.
|
|
3363
|
+
let response = ResourceLoadResponse {
|
|
3364
|
+
request_id: 0,
|
|
3365
|
+
node_id: None,
|
|
3366
|
+
resolved_url: Some(String::from("test://aliased-family")),
|
|
3367
|
+
result: Ok(Resource::Font(
|
|
3368
|
+
blitz_traits::net::Bytes::from_static(crate::BULLET_FONT),
|
|
3369
|
+
FontFaceOverrides {
|
|
3370
|
+
family_name: Some(String::from(ALIAS)),
|
|
3371
|
+
weight: Some(800.0),
|
|
3372
|
+
style: Some(parley::fontique::FontStyle::Italic),
|
|
3373
|
+
},
|
|
3374
|
+
)),
|
|
3375
|
+
};
|
|
3376
|
+
document.load_resource(response);
|
|
3377
|
+
|
|
3378
|
+
// The override must have taken effect: parley's `Collection` now
|
|
3379
|
+
// resolves the CSS-declared alias to a registered family.
|
|
3380
|
+
let mut ctx = document.font_ctx.lock().unwrap();
|
|
3381
|
+
let family_id = ctx
|
|
3382
|
+
.collection
|
|
3383
|
+
.family_id(ALIAS)
|
|
3384
|
+
.expect("CSS-declared family name should be registered as a family alias");
|
|
3385
|
+
let resolved_name = ctx
|
|
3386
|
+
.collection
|
|
3387
|
+
.family_name(family_id)
|
|
3388
|
+
.expect("family id should resolve back to a name");
|
|
3389
|
+
assert_eq!(
|
|
3390
|
+
resolved_name, ALIAS,
|
|
3391
|
+
"registered family should report the CSS-declared name, \
|
|
3392
|
+
not the font file's internal `name` table entry",
|
|
3393
|
+
);
|
|
3394
|
+
}
|
|
3395
|
+
}
|