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,780 @@
|
|
|
1
|
+
//! Scrolling: user-initiated (interactive) and programmatic scrolling of nodes and the
|
|
2
|
+
//! viewport, and the scroll animations (smooth scrolls and flings) which drive them.
|
|
3
|
+
|
|
4
|
+
use blitz_traits::events::{BlitzScrollEvent, DomEvent, DomEventData};
|
|
5
|
+
use blitz_traits::node_id::NodeId;
|
|
6
|
+
use style::values::computed::Overflow;
|
|
7
|
+
use web_time::{SystemTime, UNIX_EPOCH};
|
|
8
|
+
|
|
9
|
+
use crate::BaseDocument;
|
|
10
|
+
use crate::util::Point;
|
|
11
|
+
|
|
12
|
+
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
13
|
+
pub enum ScrollBehavior {
|
|
14
|
+
#[default]
|
|
15
|
+
Auto,
|
|
16
|
+
Instant,
|
|
17
|
+
Smooth,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
21
|
+
pub enum ScrollLogicalPosition {
|
|
22
|
+
Start,
|
|
23
|
+
Center,
|
|
24
|
+
End,
|
|
25
|
+
Nearest,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// What a scroll applies to.
|
|
29
|
+
///
|
|
30
|
+
/// Per the CSS overflow propagation rules the root element has no scrolling mechanism of its
|
|
31
|
+
/// own (its overflow is applied to the viewport), so [`ScrollTarget::Node`] holding the root
|
|
32
|
+
/// element is equivalent to [`ScrollTarget::Viewport`].
|
|
33
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
34
|
+
pub(crate) enum ScrollTarget {
|
|
35
|
+
Node(NodeId),
|
|
36
|
+
Viewport,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// How far to scroll.
|
|
40
|
+
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
41
|
+
pub(crate) enum ScrollAmount {
|
|
42
|
+
/// An absolute scroll offset.
|
|
43
|
+
To(Point<f64>),
|
|
44
|
+
/// A delta to apply to the current scroll offset.
|
|
45
|
+
By(Point<f64>),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// What to do with scroll which the target cannot consume.
|
|
49
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
50
|
+
pub(crate) enum ScrollOverflow {
|
|
51
|
+
/// Discard it: the scroll affects exactly one scroller.
|
|
52
|
+
Clamp,
|
|
53
|
+
/// Transfer it to the next scroller in the scroll chain (the parent node, and eventually
|
|
54
|
+
/// the viewport).
|
|
55
|
+
Chain,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Who initiated a scroll. Determines which overflow values count as scrollable:
|
|
59
|
+
/// `overflow: hidden` boxes have a scrolling box which programmatic scrolls apply to,
|
|
60
|
+
/// but no user scrolling mechanism.
|
|
61
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
62
|
+
pub(crate) enum ScrollSource {
|
|
63
|
+
/// A user-initiated scroll (wheel, touch, scrollbar drag, keyboard).
|
|
64
|
+
User,
|
|
65
|
+
/// A script-initiated scroll (`scrollTo` and friends, fragment navigation).
|
|
66
|
+
Programmatic,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
70
|
+
pub(crate) struct ScrollRequest {
|
|
71
|
+
pub(crate) target: ScrollTarget,
|
|
72
|
+
pub(crate) amount: ScrollAmount,
|
|
73
|
+
pub(crate) overflow: ScrollOverflow,
|
|
74
|
+
pub(crate) source: ScrollSource,
|
|
75
|
+
pub(crate) behavior: ScrollBehavior,
|
|
76
|
+
/// Whether to abort a smooth scroll in progress. Set for user-initiated scrolls, so that
|
|
77
|
+
/// an animation does not fight the user's input for the rest of its duration.
|
|
78
|
+
pub(crate) interrupt_animation: bool,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
82
|
+
pub(crate) struct FlingState {
|
|
83
|
+
pub(crate) target: NodeId,
|
|
84
|
+
pub(crate) last_seen_time: f64,
|
|
85
|
+
pub(crate) x_velocity: f64,
|
|
86
|
+
pub(crate) y_velocity: f64,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// State driving a smooth (animated) scroll towards a target offset. Used for
|
|
90
|
+
/// fragment navigation (`#anchor` links) and programmatic smooth scrolling.
|
|
91
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
92
|
+
pub(crate) struct ScrollToState {
|
|
93
|
+
/// What is being scrolled.
|
|
94
|
+
pub(crate) target: ScrollTarget,
|
|
95
|
+
/// The scroll offset at the start of the animation.
|
|
96
|
+
pub(crate) start: Point<f64>,
|
|
97
|
+
/// The scroll offset to animate towards.
|
|
98
|
+
pub(crate) end: Point<f64>,
|
|
99
|
+
/// Time (in milliseconds since the Unix epoch) at which the animation started.
|
|
100
|
+
pub(crate) start_time: f64,
|
|
101
|
+
/// Total duration of the animation in milliseconds.
|
|
102
|
+
pub(crate) duration: f64,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
106
|
+
pub(crate) enum ScrollAnimationState {
|
|
107
|
+
None,
|
|
108
|
+
Fling(FlingState),
|
|
109
|
+
/// A smooth scroll of the viewport towards a target offset.
|
|
110
|
+
ScrollTo(ScrollToState),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Cubic ease-in-out easing function, mapping a normalised time `t` in `[0, 1]`
|
|
114
|
+
/// to an eased progress value in `[0, 1]`. Used to give smooth scrolls a natural
|
|
115
|
+
/// acceleration/deceleration curve.
|
|
116
|
+
fn ease_in_out_cubic(t: f64) -> f64 {
|
|
117
|
+
if t < 0.5 {
|
|
118
|
+
4.0 * t * t * t
|
|
119
|
+
} else {
|
|
120
|
+
let f = 2.0 * t - 2.0;
|
|
121
|
+
1.0 + (f * f * f) / 2.0
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
impl BaseDocument {
|
|
126
|
+
/// Apply a scroll to the document, returning whether anything moved.
|
|
127
|
+
///
|
|
128
|
+
/// This is the single scrolling primitive: user-initiated scrolls
|
|
129
|
+
/// ([`BaseDocument::scroll_chain_by`]) and programmatic scrolls
|
|
130
|
+
/// ([`BaseDocument::scroll_to`]) differ only in the [`ScrollRequest`] they build.
|
|
131
|
+
pub(crate) fn scroll(
|
|
132
|
+
&mut self,
|
|
133
|
+
request: ScrollRequest,
|
|
134
|
+
dispatch_event: &mut dyn FnMut(DomEvent),
|
|
135
|
+
) -> bool {
|
|
136
|
+
if request.interrupt_animation
|
|
137
|
+
&& matches!(self.scroll_animation, ScrollAnimationState::ScrollTo(_))
|
|
138
|
+
{
|
|
139
|
+
self.scroll_animation = ScrollAnimationState::None;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let target = self.canonical_scroll_target(request.target);
|
|
143
|
+
|
|
144
|
+
// Text inputs and sub-documents scroll their own content rather than an overflow
|
|
145
|
+
// scrollport, so they only take part in the chained (user-initiated) path.
|
|
146
|
+
if let (ScrollTarget::Node(node_id), ScrollAmount::By(delta), ScrollOverflow::Chain) =
|
|
147
|
+
(target, request.amount, request.overflow)
|
|
148
|
+
{
|
|
149
|
+
if let Some(has_changed) =
|
|
150
|
+
self.scroll_inner_content_by(node_id, delta, request, dispatch_event)
|
|
151
|
+
{
|
|
152
|
+
return has_changed;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let include_hidden = request.source == ScrollSource::Programmatic;
|
|
157
|
+
let (current, max) = self.scroll_state(target, include_hidden);
|
|
158
|
+
let unclamped = match request.amount {
|
|
159
|
+
ScrollAmount::To(to) => to,
|
|
160
|
+
ScrollAmount::By(by) => Point {
|
|
161
|
+
x: current.x + by.x,
|
|
162
|
+
y: current.y + by.y,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
let end = Point {
|
|
166
|
+
x: unclamped.x.clamp(0.0, max.x),
|
|
167
|
+
y: unclamped.y.clamp(0.0, max.y),
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if self.should_scroll_smoothly(target, request.behavior) {
|
|
171
|
+
self.start_scroll_animation(target, end);
|
|
172
|
+
return end != current;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let has_changed = self.write_scroll_offset(target, end, dispatch_event);
|
|
176
|
+
|
|
177
|
+
// Transfer the delta the target could not consume to the next scroller in the chain.
|
|
178
|
+
if request.overflow == ScrollOverflow::Chain {
|
|
179
|
+
let remainder = Point {
|
|
180
|
+
x: unclamped.x - end.x,
|
|
181
|
+
y: unclamped.y - end.y,
|
|
182
|
+
};
|
|
183
|
+
if remainder != Point::ZERO {
|
|
184
|
+
if let Some(next) = self.next_scroller_in_chain(target) {
|
|
185
|
+
let request = ScrollRequest {
|
|
186
|
+
target: next,
|
|
187
|
+
amount: ScrollAmount::By(remainder),
|
|
188
|
+
..request
|
|
189
|
+
};
|
|
190
|
+
return has_changed | self.scroll(request, dispatch_event);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
has_changed
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/// Scroll a node which scrolls content of its own rather than an overflow scrollport
|
|
199
|
+
/// (a text input or a sub-document), returning `None` if the node is not such a node.
|
|
200
|
+
fn scroll_inner_content_by(
|
|
201
|
+
&mut self,
|
|
202
|
+
node_id: NodeId,
|
|
203
|
+
delta: Point<f64>,
|
|
204
|
+
request: ScrollRequest,
|
|
205
|
+
dispatch_event: &mut dyn FnMut(DomEvent),
|
|
206
|
+
) -> Option<bool> {
|
|
207
|
+
let node = self.nodes.get_mut(node_id)?;
|
|
208
|
+
|
|
209
|
+
if let Some(mut sub_doc) = node.subdoc_mut().map(|doc| doc.inner_mut()) {
|
|
210
|
+
let target = sub_doc
|
|
211
|
+
.get_hover_node_id()
|
|
212
|
+
.map_or(ScrollTarget::Viewport, ScrollTarget::Node);
|
|
213
|
+
// TODO: propagate the remaining scroll to the outer document
|
|
214
|
+
let request = ScrollRequest {
|
|
215
|
+
target,
|
|
216
|
+
amount: ScrollAmount::By(delta),
|
|
217
|
+
..request
|
|
218
|
+
};
|
|
219
|
+
return Some(sub_doc.scroll(request, dispatch_event));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Single-line inputs scroll their text horizontally, multi-line inputs vertically.
|
|
223
|
+
node.element_data()?.text_input_data()?;
|
|
224
|
+
let content_box_width = node.final_layout().content_box_width();
|
|
225
|
+
let content_box_height = node.final_layout().content_box_height();
|
|
226
|
+
let input = node
|
|
227
|
+
.element_data_mut()
|
|
228
|
+
.and_then(|el| el.text_input_data_mut())
|
|
229
|
+
.unwrap();
|
|
230
|
+
|
|
231
|
+
// `TextInputData::scroll_by` takes (and returns the unconsumed part of) a delta in
|
|
232
|
+
// the opposite direction to a scroll offset.
|
|
233
|
+
let mut remainder = delta;
|
|
234
|
+
if input.is_multiline {
|
|
235
|
+
remainder.y =
|
|
236
|
+
-(input.scroll_by(-delta.y as f32, content_box_width, content_box_height) as f64);
|
|
237
|
+
} else {
|
|
238
|
+
remainder.x =
|
|
239
|
+
-(input.scroll_by(-delta.x as f32, content_box_width, content_box_height) as f64);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let has_changed = remainder != delta;
|
|
243
|
+
|
|
244
|
+
if remainder != Point::ZERO {
|
|
245
|
+
if let Some(next) = self.next_scroller_in_chain(ScrollTarget::Node(node_id)) {
|
|
246
|
+
let request = ScrollRequest {
|
|
247
|
+
target: next,
|
|
248
|
+
amount: ScrollAmount::By(remainder),
|
|
249
|
+
..request
|
|
250
|
+
};
|
|
251
|
+
return Some(has_changed | self.scroll(request, dispatch_event));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
Some(has_changed)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/// The scroller which unconsumed scroll is transferred to: the node's parent, or the
|
|
259
|
+
/// viewport once the chain runs out of nodes.
|
|
260
|
+
fn next_scroller_in_chain(&self, target: ScrollTarget) -> Option<ScrollTarget> {
|
|
261
|
+
match target {
|
|
262
|
+
ScrollTarget::Viewport => None,
|
|
263
|
+
ScrollTarget::Node(node_id) => Some(
|
|
264
|
+
self.nodes
|
|
265
|
+
.get(node_id)
|
|
266
|
+
.and_then(|node| node.parent)
|
|
267
|
+
.map_or(ScrollTarget::Viewport, ScrollTarget::Node),
|
|
268
|
+
),
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/// Resolve a scroll target to the scroller which actually moves: the root element scrolls
|
|
273
|
+
/// the viewport, per the CSS overflow propagation rules.
|
|
274
|
+
fn canonical_scroll_target(&self, target: ScrollTarget) -> ScrollTarget {
|
|
275
|
+
match target {
|
|
276
|
+
ScrollTarget::Node(node_id)
|
|
277
|
+
if self.try_root_element().is_some_and(|el| el.id == node_id) =>
|
|
278
|
+
{
|
|
279
|
+
ScrollTarget::Viewport
|
|
280
|
+
}
|
|
281
|
+
target => target,
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/// Write a scroll target's offset (which must already be clamped to its scrollable
|
|
286
|
+
/// range), dispatching a `scroll` event and returning whether the offset changed.
|
|
287
|
+
pub(crate) fn write_scroll_offset(
|
|
288
|
+
&mut self,
|
|
289
|
+
target: ScrollTarget,
|
|
290
|
+
offset: Point<f64>,
|
|
291
|
+
dispatch_event: &mut dyn FnMut(DomEvent),
|
|
292
|
+
) -> bool {
|
|
293
|
+
match self.canonical_scroll_target(target) {
|
|
294
|
+
ScrollTarget::Viewport => {
|
|
295
|
+
let initial = self.viewport_scroll();
|
|
296
|
+
self.set_viewport_scroll(offset);
|
|
297
|
+
if offset == initial {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if let Some(root) = self.try_root_element() {
|
|
302
|
+
let root_id = root.id;
|
|
303
|
+
let layout = *root.final_layout();
|
|
304
|
+
let scale = self.viewport.scale() as f64;
|
|
305
|
+
let event = BlitzScrollEvent {
|
|
306
|
+
scroll_top: offset.y,
|
|
307
|
+
scroll_left: offset.x,
|
|
308
|
+
scroll_width: layout.size.width.max(layout.scrollable_overflow_rect.right)
|
|
309
|
+
as i32,
|
|
310
|
+
scroll_height: layout
|
|
311
|
+
.size
|
|
312
|
+
.height
|
|
313
|
+
.max(layout.scrollable_overflow_rect.bottom)
|
|
314
|
+
as i32,
|
|
315
|
+
client_width: (self.viewport.window_size.0 as f64 / scale) as i32,
|
|
316
|
+
client_height: (self.viewport.window_size.1 as f64 / scale) as i32,
|
|
317
|
+
};
|
|
318
|
+
dispatch_event(DomEvent::new(root_id, DomEventData::Scroll(event)));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
self.shell_provider.request_redraw();
|
|
322
|
+
true
|
|
323
|
+
}
|
|
324
|
+
ScrollTarget::Node(node_id) => {
|
|
325
|
+
let Some(node) = self.nodes.get_mut(node_id) else {
|
|
326
|
+
return false;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
let initial = *node.scroll_offset();
|
|
330
|
+
*node.scroll_offset_mut() = offset;
|
|
331
|
+
if offset == initial {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
let layout = *node.final_layout();
|
|
336
|
+
self.nodes.bump_geometry_generation();
|
|
337
|
+
let event = BlitzScrollEvent {
|
|
338
|
+
scroll_top: offset.y,
|
|
339
|
+
scroll_left: offset.x,
|
|
340
|
+
scroll_width: layout.scroll_width() as i32,
|
|
341
|
+
scroll_height: layout.scroll_height() as i32,
|
|
342
|
+
client_width: layout.size.width as i32,
|
|
343
|
+
client_height: layout.size.height as i32,
|
|
344
|
+
};
|
|
345
|
+
dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
|
|
346
|
+
|
|
347
|
+
self.show_scrollbars(node_id);
|
|
348
|
+
self.shell_provider.request_redraw();
|
|
349
|
+
true
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
pub fn scroll_node_by<F: FnMut(DomEvent)>(
|
|
355
|
+
&mut self,
|
|
356
|
+
node_id: NodeId,
|
|
357
|
+
x: f64,
|
|
358
|
+
y: f64,
|
|
359
|
+
dispatch_event: F,
|
|
360
|
+
) {
|
|
361
|
+
self.scroll_node_by_has_changed(node_id, x, y, dispatch_event);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/// Scroll a node by given x and y
|
|
365
|
+
/// Will bubble scrolling up to parent node once it can no longer scroll further
|
|
366
|
+
/// If we're already at the root node, bubbles scrolling up to the viewport
|
|
367
|
+
pub fn scroll_node_by_has_changed<F: FnMut(DomEvent)>(
|
|
368
|
+
&mut self,
|
|
369
|
+
node_id: NodeId,
|
|
370
|
+
x: f64,
|
|
371
|
+
y: f64,
|
|
372
|
+
mut dispatch_event: F,
|
|
373
|
+
) -> bool {
|
|
374
|
+
self.scroll(
|
|
375
|
+
ScrollRequest {
|
|
376
|
+
target: ScrollTarget::Node(node_id),
|
|
377
|
+
// A user-facing scroll delta moves the content, i.e. the opposite direction
|
|
378
|
+
// to the scroll offset.
|
|
379
|
+
amount: ScrollAmount::By(Point { x: -x, y: -y }),
|
|
380
|
+
overflow: ScrollOverflow::Chain,
|
|
381
|
+
source: ScrollSource::User,
|
|
382
|
+
behavior: ScrollBehavior::Instant,
|
|
383
|
+
interrupt_animation: false,
|
|
384
|
+
},
|
|
385
|
+
&mut dispatch_event,
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
pub fn scroll_viewport_by(&mut self, x: f64, y: f64) {
|
|
390
|
+
self.scroll_viewport_by_has_changed(x, y);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/// Scroll the viewport by the given values
|
|
394
|
+
pub fn scroll_viewport_by_has_changed(&mut self, x: f64, y: f64) -> bool {
|
|
395
|
+
self.scroll(
|
|
396
|
+
ScrollRequest {
|
|
397
|
+
target: ScrollTarget::Viewport,
|
|
398
|
+
amount: ScrollAmount::By(Point { x: -x, y: -y }),
|
|
399
|
+
overflow: ScrollOverflow::Clamp,
|
|
400
|
+
source: ScrollSource::User,
|
|
401
|
+
behavior: ScrollBehavior::Instant,
|
|
402
|
+
interrupt_animation: false,
|
|
403
|
+
},
|
|
404
|
+
&mut |_| {},
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
pub(crate) fn scroll_chain_by(
|
|
409
|
+
&mut self,
|
|
410
|
+
anchor_node_id: Option<NodeId>,
|
|
411
|
+
scroll_x: f64,
|
|
412
|
+
scroll_y: f64,
|
|
413
|
+
dispatch_event: &mut dyn FnMut(DomEvent),
|
|
414
|
+
) -> bool {
|
|
415
|
+
self.scroll(
|
|
416
|
+
ScrollRequest {
|
|
417
|
+
target: anchor_node_id.map_or(ScrollTarget::Viewport, ScrollTarget::Node),
|
|
418
|
+
amount: ScrollAmount::By(Point {
|
|
419
|
+
x: -scroll_x,
|
|
420
|
+
y: -scroll_y,
|
|
421
|
+
}),
|
|
422
|
+
overflow: ScrollOverflow::Chain,
|
|
423
|
+
source: ScrollSource::User,
|
|
424
|
+
behavior: ScrollBehavior::Instant,
|
|
425
|
+
// A user-initiated scroll aborts any smooth scroll in progress, so that the
|
|
426
|
+
// two do not fight over the scroll offset for the rest of the animation.
|
|
427
|
+
interrupt_animation: true,
|
|
428
|
+
},
|
|
429
|
+
dispatch_event,
|
|
430
|
+
)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/// Duration (in milliseconds) of an animated scroll.
|
|
434
|
+
const SMOOTH_SCROLL_DURATION_MS: f64 = 300.0;
|
|
435
|
+
|
|
436
|
+
/// Returns the current scroll offset and the maximum scroll offset (the minimum is
|
|
437
|
+
/// always `0`) for the given scroll target. `include_hidden` controls whether
|
|
438
|
+
/// `overflow: hidden` axes count as scrollable (they do for programmatic scrolls,
|
|
439
|
+
/// which apply to any scrolling box, but not for user scrolls).
|
|
440
|
+
pub(crate) fn scroll_state(
|
|
441
|
+
&self,
|
|
442
|
+
target: ScrollTarget,
|
|
443
|
+
include_hidden: bool,
|
|
444
|
+
) -> (Point<f64>, Point<f64>) {
|
|
445
|
+
match self.canonical_scroll_target(target) {
|
|
446
|
+
ScrollTarget::Viewport => {
|
|
447
|
+
// The viewport scrolls the root element's scrollable overflow, which includes
|
|
448
|
+
// both the root element itself and any content which overflows it (e.g. when
|
|
449
|
+
// the root element has a fixed height but its content is taller). A document
|
|
450
|
+
// without a root element has no scrollable content.
|
|
451
|
+
let (content_width, content_height) = match self.try_root_element() {
|
|
452
|
+
Some(root) => {
|
|
453
|
+
let layout = root.final_layout();
|
|
454
|
+
(
|
|
455
|
+
layout.size.width.max(layout.scrollable_overflow_rect.right) as f64,
|
|
456
|
+
layout
|
|
457
|
+
.size
|
|
458
|
+
.height
|
|
459
|
+
.max(layout.scrollable_overflow_rect.bottom)
|
|
460
|
+
as f64,
|
|
461
|
+
)
|
|
462
|
+
}
|
|
463
|
+
None => (0.0, 0.0),
|
|
464
|
+
};
|
|
465
|
+
let scale = self.viewport.scale() as f64;
|
|
466
|
+
let window_width = self.viewport.window_size.0 as f64 / scale;
|
|
467
|
+
let window_height = self.viewport.window_size.1 as f64 / scale;
|
|
468
|
+
let max = Point {
|
|
469
|
+
x: (content_width - window_width).max(0.0),
|
|
470
|
+
y: (content_height - window_height).max(0.0),
|
|
471
|
+
};
|
|
472
|
+
(self.viewport_scroll(), max)
|
|
473
|
+
}
|
|
474
|
+
ScrollTarget::Node(node_id) => {
|
|
475
|
+
let Some(node) = self.nodes.get(node_id) else {
|
|
476
|
+
return (Point::ZERO, Point::ZERO);
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
// An axis with a non-scrolling overflow value has no scrollable range, even
|
|
480
|
+
// when its content overflows.
|
|
481
|
+
let scrollable = |overflow| match overflow {
|
|
482
|
+
Overflow::Scroll | Overflow::Auto => true,
|
|
483
|
+
Overflow::Hidden => include_hidden,
|
|
484
|
+
_ => false,
|
|
485
|
+
};
|
|
486
|
+
let (can_x_scroll, can_y_scroll) = node
|
|
487
|
+
.primary_styles()
|
|
488
|
+
.map(|styles| {
|
|
489
|
+
(
|
|
490
|
+
scrollable(styles.clone_overflow_x()),
|
|
491
|
+
scrollable(styles.clone_overflow_y()),
|
|
492
|
+
)
|
|
493
|
+
})
|
|
494
|
+
.unwrap_or((false, false));
|
|
495
|
+
let max = Point {
|
|
496
|
+
x: match can_x_scroll {
|
|
497
|
+
true => node.final_layout().scroll_width() as f64,
|
|
498
|
+
false => 0.0,
|
|
499
|
+
},
|
|
500
|
+
y: match can_y_scroll {
|
|
501
|
+
true => node.final_layout().scroll_height() as f64,
|
|
502
|
+
false => 0.0,
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
(*node.scroll_offset(), max)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/// Start a smooth (animated) scroll towards the given absolute scroll offset. The
|
|
511
|
+
/// animation is advanced each frame in [`BaseDocument::resolve_scroll_animation`].
|
|
512
|
+
fn start_scroll_animation(&mut self, target: ScrollTarget, end: Point<f64>) {
|
|
513
|
+
let start = self.scroll_state(target, true).0;
|
|
514
|
+
|
|
515
|
+
let start_time = SystemTime::now()
|
|
516
|
+
.duration_since(UNIX_EPOCH)
|
|
517
|
+
.unwrap()
|
|
518
|
+
.as_millis() as u64 as f64;
|
|
519
|
+
|
|
520
|
+
self.scroll_animation = ScrollAnimationState::ScrollTo(ScrollToState {
|
|
521
|
+
target,
|
|
522
|
+
start,
|
|
523
|
+
end,
|
|
524
|
+
start_time,
|
|
525
|
+
duration: Self::SMOOTH_SCROLL_DURATION_MS,
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// Ensure the frame loop runs so the animation is driven to completion.
|
|
529
|
+
self.shell_provider.request_redraw();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
fn should_scroll_smoothly(&self, target: ScrollTarget, behavior: ScrollBehavior) -> bool {
|
|
533
|
+
match behavior {
|
|
534
|
+
ScrollBehavior::Auto => {
|
|
535
|
+
let styled_node = match target {
|
|
536
|
+
ScrollTarget::Node(node_id) => self.nodes.get(node_id),
|
|
537
|
+
ScrollTarget::Viewport => self.try_root_element(),
|
|
538
|
+
};
|
|
539
|
+
styled_node.is_some_and(|node| {
|
|
540
|
+
node.primary_styles().is_some_and(|style| {
|
|
541
|
+
style.clone_scroll_behavior()
|
|
542
|
+
== style::computed_values::scroll_behavior::T::Smooth
|
|
543
|
+
})
|
|
544
|
+
})
|
|
545
|
+
}
|
|
546
|
+
ScrollBehavior::Instant => false,
|
|
547
|
+
ScrollBehavior::Smooth => true,
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/// Scroll an element to the given absolute scroll offset in CSS pixels.
|
|
552
|
+
///
|
|
553
|
+
/// Unlike a user-initiated scroll, a programmatic scroll targets exactly one scroller:
|
|
554
|
+
/// scroll the element cannot consume is discarded rather than transferred to an ancestor.
|
|
555
|
+
pub fn scroll_to(&mut self, node_id: NodeId, x: f64, y: f64, behavior: ScrollBehavior) {
|
|
556
|
+
self.scroll_programmatically(node_id, ScrollAmount::To(Point { x, y }), behavior);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/// Scroll an element by the given relative offset in CSS pixels.
|
|
560
|
+
pub fn scroll_by(&mut self, node_id: NodeId, x: f64, y: f64, behavior: ScrollBehavior) {
|
|
561
|
+
self.scroll_programmatically(node_id, ScrollAmount::By(Point { x, y }), behavior);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
fn scroll_programmatically(
|
|
565
|
+
&mut self,
|
|
566
|
+
node_id: NodeId,
|
|
567
|
+
amount: ScrollAmount,
|
|
568
|
+
behavior: ScrollBehavior,
|
|
569
|
+
) {
|
|
570
|
+
if self.nodes.get(node_id).is_none() {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// TODO: dispatch `scroll` events for programmatic scrolls.
|
|
575
|
+
self.scroll(
|
|
576
|
+
ScrollRequest {
|
|
577
|
+
target: ScrollTarget::Node(node_id),
|
|
578
|
+
amount,
|
|
579
|
+
overflow: ScrollOverflow::Clamp,
|
|
580
|
+
source: ScrollSource::Programmatic,
|
|
581
|
+
behavior,
|
|
582
|
+
interrupt_animation: true,
|
|
583
|
+
},
|
|
584
|
+
&mut |_| {},
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
fn aligned_scroll_offset(
|
|
589
|
+
current: f64,
|
|
590
|
+
viewport_size: f64,
|
|
591
|
+
target_start: f64,
|
|
592
|
+
target_size: f64,
|
|
593
|
+
position: ScrollLogicalPosition,
|
|
594
|
+
) -> f64 {
|
|
595
|
+
let target_end = target_start + target_size;
|
|
596
|
+
match position {
|
|
597
|
+
ScrollLogicalPosition::Start => target_start,
|
|
598
|
+
ScrollLogicalPosition::Center => target_start - (viewport_size - target_size) / 2.0,
|
|
599
|
+
ScrollLogicalPosition::End => target_end - viewport_size,
|
|
600
|
+
ScrollLogicalPosition::Nearest => {
|
|
601
|
+
let viewport_end = current + viewport_size;
|
|
602
|
+
if (target_start >= current && target_end <= viewport_end)
|
|
603
|
+
|| (target_start <= current && target_end >= viewport_end)
|
|
604
|
+
{
|
|
605
|
+
current
|
|
606
|
+
} else {
|
|
607
|
+
let start_offset = target_start;
|
|
608
|
+
let end_offset = target_end - viewport_size;
|
|
609
|
+
if (start_offset - current).abs() < (end_offset - current).abs() {
|
|
610
|
+
start_offset
|
|
611
|
+
} else {
|
|
612
|
+
end_offset
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/// Scroll the viewport so that the given element has the requested alignment in each axis.
|
|
620
|
+
pub fn scroll_into_view(
|
|
621
|
+
&mut self,
|
|
622
|
+
node_id: NodeId,
|
|
623
|
+
behavior: ScrollBehavior,
|
|
624
|
+
vertical: ScrollLogicalPosition,
|
|
625
|
+
horizontal: ScrollLogicalPosition,
|
|
626
|
+
) {
|
|
627
|
+
let Some(node) = self.nodes.get(node_id) else {
|
|
628
|
+
return;
|
|
629
|
+
};
|
|
630
|
+
let target =
|
|
631
|
+
node.absolute_position(node.scroll_offset().x as f32, node.scroll_offset().y as f32);
|
|
632
|
+
let target_size = node.final_layout().size;
|
|
633
|
+
let Some(root_id) = self.try_root_element().map(|root| root.id) else {
|
|
634
|
+
return;
|
|
635
|
+
};
|
|
636
|
+
let scale = self.viewport.scale() as f64;
|
|
637
|
+
let viewport_width = self.viewport.window_size.0 as f64 / scale;
|
|
638
|
+
let viewport_height = self.viewport.window_size.1 as f64 / scale;
|
|
639
|
+
let x = Self::aligned_scroll_offset(
|
|
640
|
+
self.viewport_scroll().x,
|
|
641
|
+
viewport_width,
|
|
642
|
+
target.x as f64,
|
|
643
|
+
target_size.width as f64,
|
|
644
|
+
horizontal,
|
|
645
|
+
);
|
|
646
|
+
let y = Self::aligned_scroll_offset(
|
|
647
|
+
self.viewport_scroll().y,
|
|
648
|
+
viewport_height,
|
|
649
|
+
target.y as f64,
|
|
650
|
+
target_size.height as f64,
|
|
651
|
+
vertical,
|
|
652
|
+
);
|
|
653
|
+
self.scroll_to(root_id, x, y, behavior);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/// Resolve a URL fragment (the `#...` part of a URL) to a scroll target.
|
|
657
|
+
///
|
|
658
|
+
/// Returns `None` if the fragment matches no element and is not a top-of-document
|
|
659
|
+
/// fragment. Otherwise returns `Some(target)`, where `target` is `Some(node_id)` for
|
|
660
|
+
/// the element to scroll to, or `None` to scroll to the top of the document (matching
|
|
661
|
+
/// browser behaviour for empty and `top` fragments).
|
|
662
|
+
fn resolve_fragment_scroll_target(&self, fragment: &str) -> Option<Option<NodeId>> {
|
|
663
|
+
// Fragments are percent-encoded in URLs (e.g. `%20`); decode before matching.
|
|
664
|
+
let decoded = percent_encoding::percent_decode_str(fragment)
|
|
665
|
+
.decode_utf8_lossy()
|
|
666
|
+
.into_owned();
|
|
667
|
+
|
|
668
|
+
if !decoded.is_empty() {
|
|
669
|
+
if let Some(node_id) = self.get_fragment_target(&decoded) {
|
|
670
|
+
return Some(Some(node_id));
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// An empty fragment, or the special "top" fragment when no matching element exists,
|
|
675
|
+
// scrolls to the top of the document.
|
|
676
|
+
if decoded.is_empty() || decoded.eq_ignore_ascii_case("top") {
|
|
677
|
+
return Some(None);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
None
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
fn scroll_to_fragment_with_behavior(
|
|
684
|
+
&mut self,
|
|
685
|
+
fragment: &str,
|
|
686
|
+
behavior: ScrollBehavior,
|
|
687
|
+
) -> bool {
|
|
688
|
+
match self.resolve_fragment_scroll_target(fragment) {
|
|
689
|
+
Some(Some(node_id)) => {
|
|
690
|
+
self.scroll_into_view(
|
|
691
|
+
node_id,
|
|
692
|
+
behavior,
|
|
693
|
+
ScrollLogicalPosition::Start,
|
|
694
|
+
ScrollLogicalPosition::Nearest,
|
|
695
|
+
);
|
|
696
|
+
true
|
|
697
|
+
}
|
|
698
|
+
Some(None) => {
|
|
699
|
+
let root_id = self.root_element().id;
|
|
700
|
+
self.scroll_to(root_id, 0.0, 0.0, behavior);
|
|
701
|
+
true
|
|
702
|
+
}
|
|
703
|
+
None => false,
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/// Scroll to the element targeted by the given URL fragment (the `#...` part of a URL).
|
|
708
|
+
///
|
|
709
|
+
/// An empty fragment (or a `top` fragment that matches no element) scrolls to the top
|
|
710
|
+
/// of the document, matching browser behaviour. Returns `true` if a scroll target was
|
|
711
|
+
/// found.
|
|
712
|
+
pub fn scroll_to_fragment(&mut self, fragment: &str) -> bool {
|
|
713
|
+
self.scroll_to_fragment_with_behavior(fragment, ScrollBehavior::Auto)
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/// Like [`BaseDocument::scroll_to_fragment`], but animates the viewport towards the
|
|
717
|
+
/// target instead of jumping instantly. Returns `true` if a scroll target was found.
|
|
718
|
+
pub fn scroll_to_fragment_smooth(&mut self, fragment: &str) -> bool {
|
|
719
|
+
self.scroll_to_fragment_with_behavior(fragment, ScrollBehavior::Smooth)
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
pub fn resolve_scroll_animation(&mut self) {
|
|
723
|
+
match &mut self.scroll_animation {
|
|
724
|
+
ScrollAnimationState::Fling(fling_state) => {
|
|
725
|
+
let time_ms = SystemTime::now()
|
|
726
|
+
.duration_since(UNIX_EPOCH)
|
|
727
|
+
.unwrap()
|
|
728
|
+
.as_millis() as u64 as f64;
|
|
729
|
+
|
|
730
|
+
let time_diff_ms = time_ms - fling_state.last_seen_time;
|
|
731
|
+
|
|
732
|
+
// 0.95 @ 60fps normalized to actual frame times
|
|
733
|
+
let deceleration = 1.0 - ((0.05 / 16.66666) * time_diff_ms);
|
|
734
|
+
|
|
735
|
+
fling_state.x_velocity *= deceleration;
|
|
736
|
+
fling_state.y_velocity *= deceleration;
|
|
737
|
+
fling_state.last_seen_time = time_ms;
|
|
738
|
+
let fling_state = fling_state.clone();
|
|
739
|
+
|
|
740
|
+
let dx = fling_state.x_velocity * time_diff_ms;
|
|
741
|
+
let dy = fling_state.y_velocity * time_diff_ms;
|
|
742
|
+
|
|
743
|
+
self.scroll_chain_by(Some(fling_state.target), dx, dy, &mut |_| {});
|
|
744
|
+
if fling_state.x_velocity.abs() < 0.1 && fling_state.y_velocity.abs() < 0.1 {
|
|
745
|
+
self.scroll_animation = ScrollAnimationState::None;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
ScrollAnimationState::ScrollTo(scroll_to) => {
|
|
749
|
+
let scroll_to = scroll_to.clone();
|
|
750
|
+
let time_ms = SystemTime::now()
|
|
751
|
+
.duration_since(UNIX_EPOCH)
|
|
752
|
+
.unwrap()
|
|
753
|
+
.as_millis() as u64 as f64;
|
|
754
|
+
|
|
755
|
+
// Normalised progress through the animation, clamped to [0, 1].
|
|
756
|
+
let progress = if scroll_to.duration <= 0.0 {
|
|
757
|
+
1.0
|
|
758
|
+
} else {
|
|
759
|
+
((time_ms - scroll_to.start_time) / scroll_to.duration).clamp(0.0, 1.0)
|
|
760
|
+
};
|
|
761
|
+
let eased = ease_in_out_cubic(progress);
|
|
762
|
+
|
|
763
|
+
// Interpolate the target offset and move to it.
|
|
764
|
+
let target = Point {
|
|
765
|
+
x: scroll_to.start.x + (scroll_to.end.x - scroll_to.start.x) * eased,
|
|
766
|
+
y: scroll_to.start.y + (scroll_to.end.y - scroll_to.start.y) * eased,
|
|
767
|
+
};
|
|
768
|
+
// TODO: dispatch `scroll` events for programmatic scrolls.
|
|
769
|
+
self.write_scroll_offset(scroll_to.target, target, &mut |_| {});
|
|
770
|
+
|
|
771
|
+
if progress >= 1.0 {
|
|
772
|
+
self.scroll_animation = ScrollAnimationState::None;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
ScrollAnimationState::None => {
|
|
776
|
+
// Do nothing
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|