Skip to main content

tessera_ui/
nested_scroll.rs

1//! Nested scrolling through attached layout ancestry.
2//!
3//! ## Usage
4//!
5//! Coordinate scroll and fling consumption between nested containers.
6
7use crate::{CallbackWith, LocalToWindow, PxPosition, ScrollEventSource, State};
8
9#[derive(Clone, Copy, Debug, Default, PartialEq)]
10/// Scroll displacement along each axis in the connection coordinate space.
11pub struct ScrollDelta {
12    /// Horizontal component.
13    pub x: f32,
14    /// Vertical component.
15    pub y: f32,
16}
17
18impl ScrollDelta {
19    /// Zero motion on both axes.
20    pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
21
22    /// Creates an empty connection or motion value.
23    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    /// Returns whether both axes have negligible motion.
35    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)]
57/// Scroll velocity along each axis in units per second.
58pub struct ScrollVelocity {
59    /// Horizontal component.
60    pub x: f32,
61    /// Vertical component.
62    pub y: f32,
63}
64
65impl ScrollVelocity {
66    /// Zero motion on both axes.
67    pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
68
69    /// Creates an empty connection or motion value.
70    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    /// Returns whether both axes have negligible motion.
82    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)]
104/// Available displacement before a descendant consumes a scroll sample.
105pub struct PreScrollInput {
106    /// Motion available for this connection to consume.
107    pub available: ScrollDelta,
108    /// The input source that produced this sample.
109    pub source: ScrollEventSource,
110}
111
112#[derive(Clone, Copy)]
113/// Consumed and remaining displacement after a descendant scrolls.
114pub struct PostScrollInput {
115    /// Motion already consumed by the descendant.
116    pub consumed_by_child: ScrollDelta,
117    /// Motion available for this connection to consume.
118    pub available: ScrollDelta,
119    /// The input source that produced this sample.
120    pub source: ScrollEventSource,
121}
122
123#[derive(Clone, Copy)]
124/// Available velocity before a descendant begins its fling.
125pub struct PreFlingInput {
126    /// Motion available for this connection to consume.
127    pub available: ScrollVelocity,
128}
129
130#[derive(Clone, PartialEq, Eq)]
131/// Scroll and fling consumption coordinated with an ancestor connection.
132pub 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    /// Creates an empty connection or motion value.
143    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    /// Uses the observed local-to-window scale for coordinate conversion.
155    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    /// Views this connection in the supplied caller coordinate space.
169    pub fn in_space(&self, transform: LocalToWindow) -> NestedScrollSpace<'_> {
170        NestedScrollSpace {
171            connection: self,
172            transform,
173        }
174    }
175
176    /// Sets the ancestor that receives motion remaining after local consumption.
177    pub fn with_parent(mut self, parent: Option<Self>) -> Self {
178        self.parent = parent.map(Box::new);
179        self
180    }
181
182    /// Sets the callback that consumes displacement before the child scrolls.
183    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    /// Sets the callback that consumes remaining displacement after the child scrolls.
192    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    /// Sets the callback that consumes velocity before the child flings.
201    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    /// Dispatches displacement before child consumption and returns the consumed amount.
210    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    /// Sets the callback that consumes velocity after the child flings.
228    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    /// Dispatches velocity after the child flings and returns the consumed amount.
237    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    /// Dispatches remaining displacement after child consumption and returns the consumed amount.
252    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    /// Dispatches velocity before the child flings and returns the consumed amount.
276    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
326/// A connection viewed from a caller coordinate space.
327pub struct NestedScrollSpace<'a> {
328    connection: &'a NestedScrollConnection,
329    transform: LocalToWindow,
330}
331impl NestedScrollSpace<'_> {
332    /// Dispatches displacement before child consumption and returns the consumed amount.
333    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    /// Dispatches remaining displacement after child consumption and returns the consumed amount.
341    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    /// Dispatches velocity before the child flings and returns the consumed amount.
357    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    /// Dispatches velocity after the child flings and returns the consumed amount.
365    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    /// Returns the nearest connection attached above the current build node.
409    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}