Skip to main content

cranpose_ui/
nested_scroll.rs

1//! Nested scrolling: a container takes part in the scrolls of the scrollers
2//! inside it, as Compose's `Modifier.nestedScroll` and
3//! `NestedScrollConnection` do. Pull to refresh and collapsing headers are
4//! built on it.
5//!
6//! A [`NestedScroll`] container provides its connection to the scrollers
7//! composed inside it: `vertical_scroll`, `horizontal_scroll` and the lazy
8//! lists. Before a scroller moves, the containers around it may take part of
9//! the movement, outermost first; after it moved, they may take what it left,
10//! innermost first; and when a drag ends, they may take part of its velocity
11//! before the scroller flings.
12
13use std::rc::Rc;
14
15use cranpose_core::{CompositionLocal, CompositionLocalProvider, compositionLocalOf};
16use cranpose_macros::composable;
17
18/// What moved a scroll.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum NestedScrollSource {
21    /// A finger or a held mouse button.
22    Drag,
23    /// The motion that continues after a drag.
24    Fling,
25    /// A mouse wheel or a trackpad.
26    Wheel,
27}
28
29/// The axis a scroller moves along.
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum ScrollAxis {
32    /// Content moves up and down.
33    Vertical,
34    /// Content moves left and right.
35    Horizontal,
36}
37
38/// A container's part in the scrolls inside it.
39///
40/// Amounts follow the content: a positive amount moves the content toward
41/// the end of the axis, as a finger dragging down or right does, so a list
42/// at its top that is dragged further down has a positive amount left over.
43/// Each method returns the part it consumed, of the same sign.
44pub trait NestedScrollConnection {
45    /// Takes part of `available` before the scroller moves.
46    fn on_pre_scroll(
47        &self,
48        _axis: ScrollAxis,
49        _available: f32,
50        _source: NestedScrollSource,
51    ) -> f32 {
52        0.0
53    }
54
55    /// Takes part of `available`, what the scroller left after it consumed
56    /// `consumed`.
57    fn on_post_scroll(
58        &self,
59        _axis: ScrollAxis,
60        _consumed: f32,
61        _available: f32,
62        _source: NestedScrollSource,
63    ) -> f32 {
64        0.0
65    }
66
67    /// Takes part of the `velocity` a drag ended with, in content units per
68    /// second, before the scroller flings with the rest.
69    fn on_pre_fling(&self, _axis: ScrollAxis, _velocity: f32) -> f32 {
70        0.0
71    }
72}
73
74/// The containers around a scroller, innermost first.
75#[derive(Clone)]
76pub(crate) struct NestedScrollParent {
77    connection: Rc<dyn NestedScrollConnection>,
78    outer: Option<Rc<NestedScrollParent>>,
79}
80
81/// The same connections in the same order: a recomposed container that
82/// keeps its connection changes nothing under it.
83impl PartialEq for NestedScrollParent {
84    fn eq(&self, other: &Self) -> bool {
85        Rc::ptr_eq(&self.connection, &other.connection) && self.outer == other.outer
86    }
87}
88
89impl NestedScrollParent {
90    /// An identity for gesture keys: a scroller restarts its gesture when it
91    /// moves under another container.
92    pub(crate) fn identity(&self) -> usize {
93        Rc::as_ptr(&self.connection).cast::<()>() as usize
94    }
95
96    /// The outer containers first, then this one.
97    pub(crate) fn pre_scroll(
98        &self,
99        axis: ScrollAxis,
100        available: f32,
101        source: NestedScrollSource,
102    ) -> f32 {
103        let outer = self
104            .outer
105            .as_ref()
106            .map_or(0.0, |outer| outer.pre_scroll(axis, available, source));
107        outer
108            + self
109                .connection
110                .on_pre_scroll(axis, available - outer, source)
111    }
112
113    /// This container first, then the outer ones.
114    pub(crate) fn post_scroll(
115        &self,
116        axis: ScrollAxis,
117        consumed: f32,
118        available: f32,
119        source: NestedScrollSource,
120    ) -> f32 {
121        let own = self
122            .connection
123            .on_post_scroll(axis, consumed, available, source);
124        own + self.outer.as_ref().map_or(0.0, |outer| {
125            outer.post_scroll(axis, consumed + own, available - own, source)
126        })
127    }
128
129    /// The outer containers first, then this one.
130    pub(crate) fn pre_fling(&self, axis: ScrollAxis, velocity: f32) -> f32 {
131        let outer = self
132            .outer
133            .as_ref()
134            .map_or(0.0, |outer| outer.pre_fling(axis, velocity));
135        outer + self.connection.on_pre_fling(axis, velocity - outer)
136    }
137
138    /// `apply` moves the scroller by what the containers leave of `available`
139    /// and returns what it consumed; the result is what the containers and
140    /// the scroller consumed together.
141    pub(crate) fn dispatch(
142        &self,
143        axis: ScrollAxis,
144        available: f32,
145        source: NestedScrollSource,
146        apply: impl FnOnce(f32) -> f32,
147    ) -> f32 {
148        let before = self.pre_scroll(axis, available, source);
149        let consumed = apply(available - before);
150        let after = self.post_scroll(axis, consumed, available - before - consumed, source);
151        before + consumed + after
152    }
153}
154
155fn local_nested_scroll() -> CompositionLocal<Option<NestedScrollParent>> {
156    crate::thread_state::with_environment(|locals| {
157        locals
158            .nested_scroll
159            .get_or_init(|| compositionLocalOf(|| None))
160            .clone()
161    })
162}
163
164/// The containers around a scroller built in the running composition; none
165/// outside a composition.
166pub(crate) fn current_nested_scroll_parent() -> Option<NestedScrollParent> {
167    cranpose_core::composer_context::current_composer()?;
168    local_nested_scroll().current()
169}
170
171/// Gives the scrollers composed in `content` the container `connection`,
172/// inside the containers around this call.
173#[composable]
174pub fn NestedScroll(connection: Rc<dyn NestedScrollConnection>, content: impl FnOnce()) {
175    let local = local_nested_scroll();
176    let parent = NestedScrollParent {
177        connection,
178        outer: local.current().map(Rc::new),
179    };
180    CompositionLocalProvider(vec![local.provides(Some(parent))], content);
181}