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