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 #[inline(never)]
232 pub fn of() -> ScrollBehavior {
233 AMBIENT.with(Cell::get)
234 }
235
236 #[inline(never)]
240 pub fn install(behavior: ScrollBehavior) -> ScrollBehaviorGuard {
241 ScrollBehaviorGuard(AMBIENT.with(|a| a.replace(behavior)))
242 }
243
244 #[inline(never)]
248 pub fn set_root(behavior: ScrollBehavior) {
249 AMBIENT.with(|a| a.set(behavior));
250 }
251}
252
253impl<Msg> ScrollConfiguration<Msg> {
254 pub fn new(behavior: ScrollBehavior, child: impl Widget<Msg> + 'static) -> Self {
256 Self {
257 behavior,
258 inner: Box::new(child),
259 }
260 }
261
262 fn restyle(&self, base: frus_layout::Style) -> frus_layout::Style {
263 base
264 }
265}
266
267pub struct ScrollBehaviorGuard(ScrollBehavior);
269
270impl Drop for ScrollBehaviorGuard {
271 #[inline(never)]
272 fn drop(&mut self) {
273 AMBIENT.with(|a| a.set(self.0));
274 }
275}
276
277crate::transparent::forward_transparent!(ScrollConfiguration {
278 fn key(&self) -> Option<u64> {
279 self.inner.key()
280 }
281 fn positioned(&self) -> Option<crate::positioned::Positioning> {
282 self.inner.positioned()
283 }
284 fn theme_override(&self, inherited: &crate::theme::Theme) -> Option<Box<crate::theme::Theme>> {
285 self.inner.theme_override(inherited)
286 }
287 fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
288 self.inner.media_override(inherited)
289 }
290 fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
291 self.inner.scaffold_override()
292 }
293 fn scroll_behavior_override(&self) -> Option<ScrollBehavior> {
294 Some(self.inner.scroll_behavior_override().unwrap_or(self.behavior))
295 }
296 fn autofill_group(&self) -> bool {
297 self.inner.autofill_group()
298 }
299 fn area_toolbar(
300 &self,
301 context: crate::ToolbarContext,
302 ) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
303 self.inner.area_toolbar(context)
304 }
305});
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use frus_core::TargetPlatform as P;
311
312 fn under(platform: P) -> Theme {
313 Theme::default().with_platform(platform)
314 }
315
316 #[test]
318 fn the_application_s_behaviour_follows_the_theme_s_platform() {
319 let behavior = ScrollBehavior::material();
320 for platform in P::ALL {
321 let theme = under(platform);
322 assert_eq!(behavior.platform(&theme), platform);
323 let physics = match platform {
324 P::Ios => ScrollPhysics::bouncing(ScrollDecelerationRate::Normal),
325 P::MacOs => ScrollPhysics::bouncing(ScrollDecelerationRate::Fast),
326 _ => ScrollPhysics::CLAMPING,
327 };
328 assert_eq!(behavior.scroll_physics(&theme), physics, "{platform}");
329 let bars = if platform.is_desktop() {
330 Scrollbars::Always
331 } else {
332 Scrollbars::Never
333 };
334 assert_eq!(behavior.scrollbars(&theme), bars, "{platform}");
335 let past = match platform {
336 P::Android => Some(OverscrollIndicator::Stretch),
337 P::Fuchsia => Some(OverscrollIndicator::Glow),
338 _ => None,
339 };
340 assert_eq!(behavior.overscroll_indicator(&theme), past, "{platform}");
341 let fling = match platform {
342 P::Ios => VelocityStrategy::RecentAverage(frus_core::BOUNCING_FLING_WEIGHTS),
343 P::MacOs => VelocityStrategy::RecentAverage(frus_core::DESKTOP_FLING_WEIGHTS),
344 _ => VelocityStrategy::Regression,
345 };
346 assert_eq!(
347 behavior.velocity_tracker(&theme).strategy(),
348 fling,
349 "{platform}"
350 );
351 }
352 }
353
354 #[test]
357 fn the_base_behaviour_follows_the_system() {
358 let base = ScrollBehavior::new();
359 let system = frus_core::default_target_platform();
360 for platform in P::ALL {
361 assert_eq!(base.platform(&under(platform)), system);
362 }
363 let android = base.with_platform(P::Android);
364 assert_eq!(
365 android.overscroll_indicator(&under(P::Ios)),
366 Some(OverscrollIndicator::Glow)
367 );
368 assert_eq!(
369 ScrollConfiguration::of(),
370 ScrollBehavior::new(),
371 "nothing installed"
372 );
373 }
374
375 #[test]
378 fn a_changed_behaviour_keeps_what_it_did_not_change() {
379 let desktop = under(P::Windows);
380 let android = under(P::Android);
381 let quiet = ScrollBehavior::material()
382 .with_scrollbars(false)
383 .with_overscroll(false);
384 assert_eq!(quiet.scrollbars(&desktop), Scrollbars::Never);
385 assert_eq!(quiet.overscroll_indicator(&android), None);
386 assert_eq!(quiet.scroll_physics(&android), ScrollPhysics::CLAMPING);
387 let bouncy = ScrollBehavior::material().with_physics(ScrollPhysics::BOUNCING);
388 assert_eq!(bouncy.scroll_physics(&android), ScrollPhysics::BOUNCING);
389 assert_eq!(bouncy.scrollbars(&desktop), Scrollbars::Always);
390 let ios = ScrollBehavior::material().with_platform(P::Ios);
391 assert_eq!(ios.platform(&android), P::Ios);
392 assert_eq!(ios.overscroll_indicator(&android), None);
393 }
394
395 #[test]
397 fn an_installed_behaviour_is_put_back() {
398 let outer = ScrollBehavior::material();
399 let inner = outer.with_scrollbars(false);
400 let _a = ScrollConfiguration::install(outer);
401 {
402 let _b = ScrollConfiguration::install(inner);
403 assert_eq!(ScrollConfiguration::of(), inner);
404 }
405 assert_eq!(ScrollConfiguration::of(), outer);
406 }
407}