repose_material/material3/
pull_to_refresh.rs1#![allow(non_snake_case)]
2
3use std::cell::{Cell, RefCell};
4use std::rc::Rc;
5use web_time::Duration;
6
7use crate::{Icon, Symbol};
8use repose_core::animation::{AnimationSpec, Easing, RepeatableSpec};
9use repose_core::*;
10use repose_ui::{
11 Box, Column, TextStyle,
12 ViewExt,
13 anim::animate_f32_from,
14};
15
16use super::*;
17
18#[derive(Clone, Debug)]
20pub struct PullToRefreshConfig {
21 pub modifier: Modifier,
22 pub indicator_color: Color,
23 pub threshold: f32,
24 pub content_alignment: AlignItems,
25}
26
27impl Default for PullToRefreshConfig {
28 fn default() -> Self {
29 Self {
30 modifier: Modifier::new(),
31 indicator_color: PullToRefreshDefaults::indicator_color(),
32 threshold: PullToRefreshDefaults::THRESHOLD,
33 content_alignment: AlignItems::FLEX_START,
34 }
35 }
36}
37
38pub struct PullToRefreshState {
44 refreshing: Signal<bool>,
45 scroll_state: RefCell<Option<Rc<repose_ui::scroll::ScrollState>>>,
46 threshold: f32,
47 triggered: Cell<bool>,
48}
49
50impl Default for PullToRefreshState {
51 fn default() -> Self {
52 Self::new()
53 }
54}
55
56impl PullToRefreshState {
57 pub fn new() -> Self {
58 Self {
59 refreshing: signal(false),
60 scroll_state: RefCell::new(None),
61 threshold: 64.0,
62 triggered: Cell::new(false),
63 }
64 }
65
66 pub fn set_scroll_state(&self, state: Rc<repose_ui::scroll::ScrollState>) {
69 *self.scroll_state.borrow_mut() = Some(state);
70 }
71
72 pub fn set_threshold(&mut self, px: f32) {
74 self.threshold = px;
75 }
76
77 pub fn is_refreshing(&self) -> bool {
78 self.refreshing.get()
79 }
80
81 pub fn set_refreshing(&self, v: bool) {
82 self.refreshing.set(v);
83 if !v && let Some(sc) = self.scroll_state.borrow().as_ref() {
84 sc.set_overscroll(0.0);
85 }
86 }
87
88 pub fn pull_offset(&self) -> f32 {
90 if let Some(sc) = self.scroll_state.borrow().as_ref() {
91 let os = sc.overscroll_offset();
92 if os < 0.0 { -os } else { 0.0 }
93 } else {
94 0.0
95 }
96 }
97}
98
99pub fn PullToRefresh(
108 state: Rc<PullToRefreshState>,
109 modifier: Modifier,
110 on_refresh: Rc<dyn Fn()>,
111 content: View,
112 config: PullToRefreshConfig,
113) -> View {
114 let pull = state.pull_offset();
115 let refreshing = state.is_refreshing();
116 let threshold = config.threshold;
117
118 if state.triggered.get() && !refreshing && pull < threshold {
119 state.triggered.set(false);
120 }
121
122 if !refreshing && !state.triggered.get() && pull >= threshold {
123 state.triggered.set(true);
124 state.refreshing.set(true);
125 (on_refresh)();
126 }
127
128 let frac_key = format!("ptr_frac_{}", Rc::as_ptr(&state) as u64);
129 let raw_frac = if refreshing {
130 1.0
131 } else if pull > 0.0 {
132 pull / threshold
133 } else {
134 0.0
135 };
136 let distance_fraction = animate_f32_from(frac_key, 0.0, raw_frac, theme().motion.color);
137
138 let adjusted_percent = (distance_fraction.min(1.0) - 0.4).max(0.0) * 5.0 / 3.0;
139 let overshoot_percent = (distance_fraction - 1.0).max(0.0);
140 let linear_tension = overshoot_percent.min(2.0);
141 let tension_percent = linear_tension - linear_tension.powi(2) / 4.0;
142 let rotation_turns = (-0.25 + 0.4 * adjusted_percent + tension_percent) * 0.5;
143 let spinner_rotation_rad = rotation_turns * std::f32::consts::TAU;
145
146 let indicator_h = distance_fraction * threshold;
148 let comp_scale = adjusted_percent.min(1.0);
149 let icon_size = if refreshing {
150 24.0
151 } else {
152 (16.0 + comp_scale * 8.0).min(24.0)
153 };
154 let rotation = if refreshing {
155 animate_f32_from(
156 "ptr_spin",
157 0.0,
158 std::f32::consts::TAU,
159 AnimationSpec::tween(Duration::from_millis(1000), Easing::Linear)
160 .repeated(RepeatableSpec::infinite()),
161 )
162 } else {
163 spinner_rotation_rad
164 };
165 let alpha = if refreshing {
166 1.0
167 } else if distance_fraction >= 1.0 {
168 1.0
169 } else {
170 0.3
171 };
172 Column(modifier.align_items(config.content_alignment)).child((
173 if distance_fraction > 0.01 {
174 Box(Modifier::new()
175 .fill_max_width()
176 .height(indicator_h)
177 .align_items(AlignItems::CENTER)
178 .justify_content(JustifyContent::CENTER))
179 .child(
180 Box(Modifier::new()
181 .size(icon_size, icon_size)
182 .translate(icon_size * 0.5, icon_size * 0.5)
183 .rotate(rotation)
184 .translate(-icon_size * 0.5, -icon_size * 0.5))
185 .child(if refreshing {
186 Icon(Symbol::new("refresh", '\u{E5D5}'))
187 .size(24.0)
188 .color(config.indicator_color)
189 } else {
190 Icon(Symbol::new("arrow_downward", '\u{E5DB}'))
191 .size(icon_size)
192 .color(config.indicator_color.with_alpha_f32(alpha))
193 }),
194 )
195 } else {
196 Box(Modifier::new())
197 },
198 content,
199 ))
200}