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