1use std::cell::Cell;
21use std::hash::{Hash, Hasher};
22
23use frus_core::{TargetPlatform, VelocityStrategy, VelocityTracker};
24
25use crate::physics::{OverscrollIndicator, ScrollDecelerationRate, ScrollPhysics, Scrollbars};
26use crate::theme::Theme;
27use crate::widget::Widget;
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31enum Base {
32 Plain,
34 Material,
36}
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
57pub struct ScrollBehavior {
58 base: Base,
59 scrollbars: bool,
60 overscroll: bool,
61 physics: Option<ScrollPhysics>,
62 platform: Option<TargetPlatform>,
63}
64
65impl Default for ScrollBehavior {
66 fn default() -> Self {
68 Self::new()
69 }
70}
71
72impl ScrollBehavior {
73 pub const fn new() -> Self {
78 Self {
79 base: Base::Plain,
80 scrollbars: true,
81 overscroll: true,
82 physics: None,
83 platform: None,
84 }
85 }
86
87 pub const fn material() -> Self {
92 Self {
93 base: Base::Material,
94 ..Self::new()
95 }
96 }
97
98 #[must_use]
101 pub const fn with_scrollbars(mut self, scrollbars: bool) -> Self {
102 self.scrollbars = scrollbars;
103 self
104 }
105
106 #[must_use]
109 pub const fn with_overscroll(mut self, overscroll: bool) -> Self {
110 self.overscroll = overscroll;
111 self
112 }
113
114 #[must_use]
116 pub const fn with_physics(mut self, physics: ScrollPhysics) -> Self {
117 self.physics = Some(physics);
118 self
119 }
120
121 #[must_use]
123 pub const fn with_platform(mut self, platform: TargetPlatform) -> Self {
124 self.platform = Some(platform);
125 self
126 }
127
128 pub fn platform(&self, theme: &Theme) -> TargetPlatform {
131 self.platform.unwrap_or_else(|| match self.base {
132 Base::Material => theme.platform,
133 Base::Plain => frus_core::default_target_platform(),
134 })
135 }
136
137 pub fn scroll_physics(&self, theme: &Theme) -> ScrollPhysics {
141 self.physics.unwrap_or(match self.platform(theme) {
142 TargetPlatform::Ios => ScrollPhysics::bouncing(ScrollDecelerationRate::Normal),
143 TargetPlatform::MacOs => ScrollPhysics::bouncing(ScrollDecelerationRate::Fast),
144 TargetPlatform::Android
145 | TargetPlatform::Fuchsia
146 | TargetPlatform::Linux
147 | TargetPlatform::Windows => ScrollPhysics::CLAMPING,
148 })
149 }
150
151 pub fn scrollbars(&self, theme: &Theme) -> Scrollbars {
155 if !self.scrollbars {
156 return Scrollbars::Never;
157 }
158 match self.platform(theme) {
159 TargetPlatform::Linux | TargetPlatform::MacOs | TargetPlatform::Windows => {
160 Scrollbars::Always
161 }
162 TargetPlatform::Android | TargetPlatform::Fuchsia | TargetPlatform::Ios => {
163 Scrollbars::Never
164 }
165 }
166 }
167
168 pub fn overscroll_indicator(&self, theme: &Theme) -> Option<OverscrollIndicator> {
173 if !self.overscroll {
174 return None;
175 }
176 match (self.platform(theme), self.base) {
177 (TargetPlatform::Android, Base::Material) => Some(OverscrollIndicator::Stretch),
178 (TargetPlatform::Android, Base::Plain) | (TargetPlatform::Fuchsia, _) => {
179 Some(OverscrollIndicator::Glow)
180 }
181 (
182 TargetPlatform::Ios
183 | TargetPlatform::Linux
184 | TargetPlatform::MacOs
185 | TargetPlatform::Windows,
186 _,
187 ) => None,
188 }
189 }
190
191 pub fn velocity_tracker(&self, theme: &Theme) -> VelocityTracker {
195 VelocityTracker::new(match self.platform(theme) {
196 TargetPlatform::Ios => {
197 VelocityStrategy::RecentAverage(frus_core::BOUNCING_FLING_WEIGHTS)
198 }
199 TargetPlatform::MacOs => {
200 VelocityStrategy::RecentAverage(frus_core::DESKTOP_FLING_WEIGHTS)
201 }
202 TargetPlatform::Android
203 | TargetPlatform::Fuchsia
204 | TargetPlatform::Linux
205 | TargetPlatform::Windows => VelocityStrategy::Regression,
206 })
207 }
208
209 pub(crate) fn shape_hash<H: Hasher>(&self, hasher: &mut H) {
212 self.hash(hasher);
213 }
214}
215
216thread_local! {
217 static AMBIENT: Cell<ScrollBehavior> = const { Cell::new(ScrollBehavior::new()) };
220}
221
222pub struct ScrollConfiguration<Msg = crate::callback::Callback> {
224 behavior: ScrollBehavior,
225 inner: Box<dyn Widget<Msg>>,
226}
227
228impl ScrollConfiguration {
229 pub fn of() -> ScrollBehavior {
232 AMBIENT.with(Cell::get)
233 }
234
235 pub fn install(behavior: ScrollBehavior) -> ScrollBehaviorGuard {
239 ScrollBehaviorGuard(AMBIENT.with(|a| a.replace(behavior)))
240 }
241
242 pub fn set_root(behavior: ScrollBehavior) {
246 AMBIENT.with(|a| a.set(behavior));
247 }
248}
249
250impl<Msg> ScrollConfiguration<Msg> {
251 pub fn new(behavior: ScrollBehavior, child: impl Widget<Msg> + 'static) -> Self {
253 Self {
254 behavior,
255 inner: Box::new(child),
256 }
257 }
258
259 fn restyle(&self, base: frus_layout::Style) -> frus_layout::Style {
260 base
261 }
262}
263
264pub struct ScrollBehaviorGuard(ScrollBehavior);
266
267impl Drop for ScrollBehaviorGuard {
268 fn drop(&mut self) {
269 AMBIENT.with(|a| a.set(self.0));
270 }
271}
272
273crate::transparent::forward_transparent!(ScrollConfiguration {
274 fn key(&self) -> Option<u64> {
275 self.inner.key()
276 }
277 fn positioned(&self) -> Option<crate::positioned::Positioning> {
278 self.inner.positioned()
279 }
280 fn theme_override(&self, inherited: &crate::theme::Theme) -> Option<Box<crate::theme::Theme>> {
281 self.inner.theme_override(inherited)
282 }
283 fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
284 self.inner.media_override(inherited)
285 }
286 fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
287 self.inner.scaffold_override()
288 }
289 fn scroll_behavior_override(&self) -> Option<ScrollBehavior> {
290 Some(self.inner.scroll_behavior_override().unwrap_or(self.behavior))
291 }
292 fn autofill_group(&self) -> bool {
293 self.inner.autofill_group()
294 }
295 fn area_toolbar(
296 &self,
297 context: crate::ToolbarContext,
298 ) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
299 self.inner.area_toolbar(context)
300 }
301});
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306 use frus_core::TargetPlatform as P;
307
308 fn under(platform: P) -> Theme {
309 Theme::default().with_platform(platform)
310 }
311
312 #[test]
314 fn the_application_s_behaviour_follows_the_theme_s_platform() {
315 let behavior = ScrollBehavior::material();
316 for platform in P::ALL {
317 let theme = under(platform);
318 assert_eq!(behavior.platform(&theme), platform);
319 let physics = match platform {
320 P::Ios => ScrollPhysics::bouncing(ScrollDecelerationRate::Normal),
321 P::MacOs => ScrollPhysics::bouncing(ScrollDecelerationRate::Fast),
322 _ => ScrollPhysics::CLAMPING,
323 };
324 assert_eq!(behavior.scroll_physics(&theme), physics, "{platform}");
325 let bars = if platform.is_desktop() {
326 Scrollbars::Always
327 } else {
328 Scrollbars::Never
329 };
330 assert_eq!(behavior.scrollbars(&theme), bars, "{platform}");
331 let past = match platform {
332 P::Android => Some(OverscrollIndicator::Stretch),
333 P::Fuchsia => Some(OverscrollIndicator::Glow),
334 _ => None,
335 };
336 assert_eq!(behavior.overscroll_indicator(&theme), past, "{platform}");
337 let fling = match platform {
338 P::Ios => VelocityStrategy::RecentAverage(frus_core::BOUNCING_FLING_WEIGHTS),
339 P::MacOs => VelocityStrategy::RecentAverage(frus_core::DESKTOP_FLING_WEIGHTS),
340 _ => VelocityStrategy::Regression,
341 };
342 assert_eq!(
343 behavior.velocity_tracker(&theme).strategy(),
344 fling,
345 "{platform}"
346 );
347 }
348 }
349
350 #[test]
353 fn the_base_behaviour_follows_the_system() {
354 let base = ScrollBehavior::new();
355 let system = frus_core::default_target_platform();
356 for platform in P::ALL {
357 assert_eq!(base.platform(&under(platform)), system);
358 }
359 let android = base.with_platform(P::Android);
360 assert_eq!(
361 android.overscroll_indicator(&under(P::Ios)),
362 Some(OverscrollIndicator::Glow)
363 );
364 assert_eq!(
365 ScrollConfiguration::of(),
366 ScrollBehavior::new(),
367 "nothing installed"
368 );
369 }
370
371 #[test]
374 fn a_changed_behaviour_keeps_what_it_did_not_change() {
375 let desktop = under(P::Windows);
376 let android = under(P::Android);
377 let quiet = ScrollBehavior::material()
378 .with_scrollbars(false)
379 .with_overscroll(false);
380 assert_eq!(quiet.scrollbars(&desktop), Scrollbars::Never);
381 assert_eq!(quiet.overscroll_indicator(&android), None);
382 assert_eq!(quiet.scroll_physics(&android), ScrollPhysics::CLAMPING);
383 let bouncy = ScrollBehavior::material().with_physics(ScrollPhysics::BOUNCING);
384 assert_eq!(bouncy.scroll_physics(&android), ScrollPhysics::BOUNCING);
385 assert_eq!(bouncy.scrollbars(&desktop), Scrollbars::Always);
386 let ios = ScrollBehavior::material().with_platform(P::Ios);
387 assert_eq!(ios.platform(&android), P::Ios);
388 assert_eq!(ios.overscroll_indicator(&android), None);
389 }
390
391 #[test]
393 fn an_installed_behaviour_is_put_back() {
394 let outer = ScrollBehavior::material();
395 let inner = outer.with_scrollbars(false);
396 let _a = ScrollConfiguration::install(outer);
397 {
398 let _b = ScrollConfiguration::install(inner);
399 assert_eq!(ScrollConfiguration::of(), inner);
400 }
401 assert_eq!(ScrollConfiguration::of(), outer);
402 }
403}