1use crate::{CallbackWith, LocalToWindow, PxPosition, ScrollEventSource, State};
8
9#[derive(Clone, Copy, Debug, Default, PartialEq)]
10pub struct ScrollDelta {
12 pub x: f32,
14 pub y: f32,
16}
17
18impl ScrollDelta {
19 pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
21
22 pub const fn new(x: f32, y: f32) -> Self {
24 Self { x, y }
25 }
26
27 fn clamp_consumed(self, consumed: Self) -> Self {
28 Self {
29 x: clamp_axis_consumed(self.x, consumed.x),
30 y: clamp_axis_consumed(self.y, consumed.y),
31 }
32 }
33
34 pub fn is_zero(self) -> bool {
36 self.x.abs() <= f32::EPSILON && self.y.abs() <= f32::EPSILON
37 }
38}
39
40impl std::ops::Add for ScrollDelta {
41 type Output = Self;
42
43 fn add(self, rhs: Self) -> Self::Output {
44 Self::new(self.x + rhs.x, self.y + rhs.y)
45 }
46}
47
48impl std::ops::Sub for ScrollDelta {
49 type Output = Self;
50
51 fn sub(self, rhs: Self) -> Self::Output {
52 Self::new(self.x - rhs.x, self.y - rhs.y)
53 }
54}
55
56#[derive(Clone, Copy, Debug, Default, PartialEq)]
57pub struct ScrollVelocity {
59 pub x: f32,
61 pub y: f32,
63}
64
65impl ScrollVelocity {
66 pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
68
69 pub const fn new(x: f32, y: f32) -> Self {
71 Self { x, y }
72 }
73
74 fn clamp_consumed(self, consumed: Self) -> Self {
75 Self {
76 x: clamp_axis_consumed(self.x, consumed.x),
77 y: clamp_axis_consumed(self.y, consumed.y),
78 }
79 }
80
81 pub fn is_zero(self) -> bool {
83 self.x.abs() <= f32::EPSILON && self.y.abs() <= f32::EPSILON
84 }
85}
86
87impl std::ops::Add for ScrollVelocity {
88 type Output = Self;
89
90 fn add(self, rhs: Self) -> Self::Output {
91 Self::new(self.x + rhs.x, self.y + rhs.y)
92 }
93}
94
95impl std::ops::Sub for ScrollVelocity {
96 type Output = Self;
97
98 fn sub(self, rhs: Self) -> Self::Output {
99 Self::new(self.x - rhs.x, self.y - rhs.y)
100 }
101}
102
103#[derive(Clone, Copy)]
104pub struct PreScrollInput {
106 pub available: ScrollDelta,
108 pub source: ScrollEventSource,
110}
111
112#[derive(Clone, Copy)]
113pub struct PostScrollInput {
115 pub consumed_by_child: ScrollDelta,
117 pub available: ScrollDelta,
119 pub source: ScrollEventSource,
121}
122
123#[derive(Clone, Copy)]
124pub struct PreFlingInput {
126 pub available: ScrollVelocity,
128}
129
130#[derive(Clone, PartialEq, Eq)]
131pub struct NestedScrollConnection {
133 coordinate_scale: Option<State<f32>>,
134 parent: Option<Box<NestedScrollConnection>>,
135 on_pre_scroll: CallbackWith<PreScrollInput, ScrollDelta>,
136 on_post_scroll: CallbackWith<PostScrollInput, ScrollDelta>,
137 on_pre_fling: CallbackWith<PreFlingInput, ScrollVelocity>,
138 on_post_fling: CallbackWith<ScrollVelocity, ScrollVelocity>,
139}
140
141impl NestedScrollConnection {
142 pub fn new() -> Self {
144 Self {
145 coordinate_scale: None,
146 parent: None,
147 on_pre_scroll: CallbackWith::default_value(),
148 on_post_scroll: CallbackWith::default_value(),
149 on_pre_fling: CallbackWith::default_value(),
150 on_post_fling: CallbackWith::default_value(),
151 }
152 }
153
154 pub fn with_coordinate_scale(mut self, scale: State<f32>) -> Self {
156 self.coordinate_scale = Some(scale);
157 self
158 }
159
160 fn transform(&self) -> LocalToWindow {
161 LocalToWindow::new(
162 self.coordinate_scale.map_or(1.0, |scale| scale.get()),
163 PxPosition::ZERO,
164 )
165 .expect("Nested scroll scale must be finite and positive")
166 }
167
168 pub fn in_space(&self, transform: LocalToWindow) -> NestedScrollSpace<'_> {
170 NestedScrollSpace {
171 connection: self,
172 transform,
173 }
174 }
175
176 pub fn with_parent(mut self, parent: Option<Self>) -> Self {
178 self.parent = parent.map(Box::new);
179 self
180 }
181
182 pub fn with_pre_scroll_handler(
184 mut self,
185 handler: CallbackWith<PreScrollInput, ScrollDelta>,
186 ) -> Self {
187 self.on_pre_scroll = handler;
188 self
189 }
190
191 pub fn with_post_scroll_handler(
193 mut self,
194 handler: CallbackWith<PostScrollInput, ScrollDelta>,
195 ) -> Self {
196 self.on_post_scroll = handler;
197 self
198 }
199
200 pub fn with_pre_fling_handler(
202 mut self,
203 handler: CallbackWith<PreFlingInput, ScrollVelocity>,
204 ) -> Self {
205 self.on_pre_fling = handler;
206 self
207 }
208
209 pub fn pre_scroll(&self, available: ScrollDelta, source: ScrollEventSource) -> ScrollDelta {
211 let local = available.clamp_consumed(map_delta(
212 self.transform(),
213 self.on_pre_scroll.call(PreScrollInput {
214 available: unmap_delta(self.transform(), available),
215 source,
216 }),
217 ));
218 let remaining = available - local;
219 let parent = self
220 .parent
221 .as_ref()
222 .map(|connection| connection.pre_scroll(remaining, source))
223 .unwrap_or(ScrollDelta::ZERO);
224 local + parent
225 }
226
227 pub fn with_post_fling_handler(
229 mut self,
230 handler: CallbackWith<ScrollVelocity, ScrollVelocity>,
231 ) -> Self {
232 self.on_post_fling = handler;
233 self
234 }
235
236 pub fn post_fling(&self, available: ScrollVelocity) -> ScrollVelocity {
238 let local = available.clamp_consumed(map_velocity(
239 self.transform(),
240 self.on_post_fling
241 .call(unmap_velocity(self.transform(), available)),
242 ));
243 let parent = self
244 .parent
245 .as_ref()
246 .map(|connection| connection.post_fling(available - local))
247 .unwrap_or(ScrollVelocity::ZERO);
248 local + parent
249 }
250
251 pub fn post_scroll(
253 &self,
254 consumed_by_child: ScrollDelta,
255 available: ScrollDelta,
256 source: ScrollEventSource,
257 ) -> ScrollDelta {
258 let local = available.clamp_consumed(map_delta(
259 self.transform(),
260 self.on_post_scroll.call(PostScrollInput {
261 consumed_by_child: unmap_delta(self.transform(), consumed_by_child),
262 available: unmap_delta(self.transform(), available),
263 source,
264 }),
265 ));
266 let remaining = available - local;
267 let parent = self
268 .parent
269 .as_ref()
270 .map(|connection| connection.post_scroll(consumed_by_child + local, remaining, source))
271 .unwrap_or(ScrollDelta::ZERO);
272 local + parent
273 }
274
275 pub fn pre_fling(&self, available: ScrollVelocity) -> ScrollVelocity {
277 let local = available.clamp_consumed(map_velocity(
278 self.transform(),
279 self.on_pre_fling.call(PreFlingInput {
280 available: unmap_velocity(self.transform(), available),
281 }),
282 ));
283 let remaining = available - local;
284 let parent = self
285 .parent
286 .as_ref()
287 .map(|connection| connection.pre_fling(remaining))
288 .unwrap_or(ScrollVelocity::ZERO);
289 local + parent
290 }
291}
292
293impl Default for NestedScrollConnection {
294 fn default() -> Self {
295 Self::new()
296 }
297}
298
299fn clamp_axis_consumed(available: f32, consumed: f32) -> f32 {
300 if available > 0.0 {
301 consumed.clamp(0.0, available)
302 } else if available < 0.0 {
303 consumed.clamp(available, 0.0)
304 } else {
305 0.0
306 }
307}
308
309fn map_delta(transform: LocalToWindow, value: ScrollDelta) -> ScrollDelta {
310 let value = transform.map_vector(PxPosition::from_f32_xy(value.x, value.y));
311 ScrollDelta::new(value.x, value.y)
312}
313fn unmap_delta(transform: LocalToWindow, value: ScrollDelta) -> ScrollDelta {
314 let value = transform.inverse_vector(PxPosition::from_f32_xy(value.x, value.y));
315 ScrollDelta::new(value.x, value.y)
316}
317fn map_velocity(transform: LocalToWindow, value: ScrollVelocity) -> ScrollVelocity {
318 let value = transform.map_vector(PxPosition::from_f32_xy(value.x, value.y));
319 ScrollVelocity::new(value.x, value.y)
320}
321fn unmap_velocity(transform: LocalToWindow, value: ScrollVelocity) -> ScrollVelocity {
322 let value = transform.inverse_vector(PxPosition::from_f32_xy(value.x, value.y));
323 ScrollVelocity::new(value.x, value.y)
324}
325
326pub struct NestedScrollSpace<'a> {
328 connection: &'a NestedScrollConnection,
329 transform: LocalToWindow,
330}
331impl NestedScrollSpace<'_> {
332 pub fn pre_scroll(&self, available: ScrollDelta, source: ScrollEventSource) -> ScrollDelta {
334 unmap_delta(
335 self.transform,
336 self.connection
337 .pre_scroll(map_delta(self.transform, available), source),
338 )
339 }
340 pub fn post_scroll(
342 &self,
343 consumed: ScrollDelta,
344 available: ScrollDelta,
345 source: ScrollEventSource,
346 ) -> ScrollDelta {
347 unmap_delta(
348 self.transform,
349 self.connection.post_scroll(
350 map_delta(self.transform, consumed),
351 map_delta(self.transform, available),
352 source,
353 ),
354 )
355 }
356 pub fn pre_fling(&self, available: ScrollVelocity) -> ScrollVelocity {
358 unmap_velocity(
359 self.transform,
360 self.connection
361 .pre_fling(map_velocity(self.transform, available)),
362 )
363 }
364 pub fn post_fling(&self, available: ScrollVelocity) -> ScrollVelocity {
366 unmap_velocity(
367 self.transform,
368 self.connection
369 .post_fling(map_velocity(self.transform, available)),
370 )
371 }
372}
373
374#[derive(Clone)]
375struct ScrollReader {
376 anchor_instance: u64,
377 provider: Option<u64>,
378 connection: Option<NestedScrollConnection>,
379}
380
381#[derive(Default)]
382struct ScrollDependencies {
383 readers: rustc_hash::FxHashMap<u64, ScrollReader>,
384 by_provider: rustc_hash::FxHashMap<Option<u64>, rustc_hash::FxHashSet<u64>>,
385 owners: rustc_hash::FxHashMap<u64, Option<NestedScrollConnection>>,
386 pending_readers: rustc_hash::FxHashSet<u64>,
387}
388
389#[derive(Default)]
390struct ScrollObserver(std::cell::RefCell<ScrollDependencies>);
391
392fn nearest_above(anchor: crate::NodeId) -> (Option<u64>, Option<NestedScrollConnection>) {
393 crate::runtime::TesseraRuntime::with(|runtime| {
394 let tree = runtime.component_tree.tree();
395 let mut parent = tree.get(anchor).and_then(|node| node.parent());
396 while let Some(id) = parent {
397 let Some(node) = tree.get(id) else { break };
398 if let Some(connection) = node.get().modifier.nested_scroll_connection() {
399 return (Some(node.get().instance_logic_id), Some(connection));
400 }
401 parent = node.parent();
402 }
403 (None, None)
404 })
405}
406
407impl NestedScrollConnection {
408 pub fn nearest() -> Option<Self> {
410 crate::runtime::ensure_build_phase();
411 let (anchor, anchor_instance) = crate::runtime::TesseraRuntime::with(|runtime| {
412 let anchor = runtime.component_tree.current_build_node_id()?;
413 let instance = runtime
414 .component_tree
415 .tree()
416 .get(anchor)?
417 .get()
418 .instance_key;
419 Some((anchor, instance))
420 })?;
421 let (provider, connection) = nearest_above(anchor);
422 if let Some(boundary) = crate::runtime::current_replay_boundary_instance_key_from_scope() {
423 let observer = crate::local_runtime_resource::<ScrollObserver>();
424 let previous = {
425 let mut dependencies = observer.0.borrow_mut();
426 if let Some(previous) = dependencies.readers.get(&boundary) {
427 let previous_provider = previous.provider;
428 if let Some(readers) = dependencies.by_provider.get_mut(&previous_provider) {
429 readers.remove(&boundary);
430 }
431 }
432 dependencies
433 .by_provider
434 .entry(provider)
435 .or_default()
436 .insert(boundary);
437 dependencies.readers.insert(
438 boundary,
439 ScrollReader {
440 anchor_instance,
441 provider,
442 connection: connection.clone(),
443 },
444 )
445 };
446 drop(previous);
447 }
448 connection
449 }
450}
451
452pub(crate) fn attachment_changed() {
453 let Some((anchor, owner, connection)) = crate::runtime::TesseraRuntime::with(|runtime| {
454 let anchor = runtime.component_tree.current_build_node_id()?;
455 let node = runtime.component_tree.current_node()?;
456 Some((
457 anchor,
458 node.instance_logic_id,
459 node.modifier.nested_scroll_connection(),
460 ))
461 }) else {
462 return;
463 };
464 let observer = crate::local_runtime_resource::<ScrollObserver>();
465 let previous = {
466 let mut dependencies = observer.0.borrow_mut();
467 if dependencies
468 .owners
469 .get(&owner)
470 .is_some_and(|previous| previous == &connection)
471 || (connection.is_none() && !dependencies.owners.contains_key(&owner))
472 {
473 return;
474 }
475 let previous = dependencies.owners.insert(owner, connection);
476 let parent_provider = nearest_above(anchor).0;
477 for provider in [Some(owner), parent_provider] {
478 if let Some(readers) = dependencies.by_provider.get(&provider) {
479 let readers = readers.iter().copied().collect::<Vec<_>>();
480 dependencies.pending_readers.extend(readers);
481 }
482 }
483 previous
484 };
485 drop(previous);
486}
487
488pub(crate) fn flush_attachment_changes() {
489 let observer = crate::local_runtime_resource::<ScrollObserver>();
490 let candidates = {
491 let mut dependencies = observer.0.borrow_mut();
492 std::mem::take(&mut dependencies.pending_readers)
493 .into_iter()
494 .filter_map(|reader| {
495 dependencies
496 .readers
497 .get(&reader)
498 .cloned()
499 .map(|dependency| (reader, dependency))
500 })
501 .collect::<Vec<_>>()
502 };
503 for (reader, dependency) in candidates {
504 let anchor = crate::runtime::TesseraRuntime::with(|runtime| {
505 runtime
506 .component_tree
507 .find_node_id_by_instance_key(dependency.anchor_instance)
508 });
509 let Some(anchor) = anchor else {
510 continue;
511 };
512 let current = nearest_above(anchor);
513 if current.0 != dependency.provider || current.1 != dependency.connection {
514 crate::runtime::record_replay_boundary_invalidation_for_instance_key(reader);
515 }
516 }
517}
518
519pub(crate) fn remove_readers(keys: &rustc_hash::FxHashSet<u64>) {
520 let observer = crate::local_runtime_resource::<ScrollObserver>();
521 let removed = {
522 let mut dependencies = observer.0.borrow_mut();
523 let mut removed = Vec::new();
524 for key in keys {
525 dependencies.pending_readers.remove(key);
526 if let Some(reader) = dependencies.readers.remove(key) {
527 if let Some(readers) = dependencies.by_provider.get_mut(&reader.provider) {
528 readers.remove(key);
529 if readers.is_empty() {
530 dependencies.by_provider.remove(&reader.provider);
531 }
532 }
533 removed.push(reader);
534 }
535 }
536 removed
537 };
538 drop(removed);
539}
540
541pub(crate) fn remove_owners(keys: &rustc_hash::FxHashSet<u64>) {
542 let observer = crate::local_runtime_resource::<ScrollObserver>();
543 let removed: Vec<_> = {
544 let mut dependencies = observer.0.borrow_mut();
545 keys.iter()
546 .filter_map(|key| dependencies.owners.remove(key))
547 .collect()
548 };
549 drop(removed);
550}
551
552impl crate::modifier::BuildModifierNode for NestedScrollConnection {
553 fn apply(&self, _runtime: &mut crate::runtime::TesseraRuntime) {}
554
555 fn nested_scroll_connection(&self) -> Option<Self> {
556 Some(self.clone())
557 }
558}