Skip to main content

freya_components/scrollviews/
scrollbar.rs

1use freya_animation::prelude::{
2    AnimNum,
3    AnimatedValue,
4    Ease,
5    Function,
6    OnChange,
7    OnCreation,
8    ReadAnimatedValue,
9    use_animation_with_dependencies,
10};
11use freya_core::prelude::*;
12use freya_sdk::timeout::Timeout;
13use torin::{
14    prelude::{
15        Alignment,
16        Direction,
17        Gaps,
18        Point2D,
19        Position,
20        Size2D,
21    },
22    size::Size,
23};
24
25use crate::{
26    define_theme,
27    get_theme,
28    scrollviews::{
29        ScrollController,
30        ScrollThumb,
31        shared::{
32            Axis,
33            SCROLLBAR_MARGIN,
34        },
35    },
36};
37
38define_theme! {
39    %[component]
40    pub ScrollBar {
41        %[fields]
42        background: Color,
43        thumb_background: Color,
44        hover_thumb_background: Color,
45        active_thumb_background: Color,
46        thumb_cross_size: f32,
47        expanded_thumb_cross_size: f32,
48        opacity: f32,
49        expanded_opacity: f32,
50        cross_gap: f32,
51        expanded_cross_gap: f32,
52    }
53}
54
55/// Data passed to a scrollbar renderer.
56#[derive(Clone, PartialEq)]
57pub struct ScrollBarContext {
58    pub axis: Axis,
59    pub scroll_position: Point2D,
60    pub viewport_size: Size2D,
61    pub content_size: Size2D,
62    pub scroll_controller: ScrollController,
63    pub timeout: Timeout,
64    /// Whether idle scrollbars should hide when the timeout elapses.
65    pub auto_hide_scrollbars: bool,
66    pub clicking_scrollbar: State<Option<(Axis, f64)>>,
67    pub thumb_events: ScrollBarThumbEvents,
68    pub thumb_offset: f32,
69    pub track_size: Size,
70    pub thumb_length: f32,
71}
72
73/// Pointer event handlers for a custom scrollbar thumb.
74#[derive(Clone, PartialEq)]
75pub struct ScrollBarThumbEvents {
76    pub on_pointer_down: EventHandler<Event<PointerEventData>>,
77    pub on_pointer_press: EventHandler<Event<PointerEventData>>,
78}
79
80impl ScrollBarThumbEvents {
81    pub(crate) fn new(axis: Axis, mut clicking_scrollbar: State<Option<(Axis, f64)>>) -> Self {
82        let on_pointer_down = move |event: Event<PointerEventData>| {
83            if !event.data().is_primary() {
84                return;
85            }
86            let location = event.element_location();
87            let pointer = if axis == Axis::X {
88                location.x
89            } else {
90                location.y
91            };
92            clicking_scrollbar.set(Some((axis, pointer)));
93        };
94        let on_pointer_press = move |event: Event<PointerEventData>| {
95            event.prevent_default();
96            event.stop_propagation();
97            clicking_scrollbar.set(None);
98        };
99
100        Self {
101            on_pointer_down: on_pointer_down.into(),
102            on_pointer_press: on_pointer_press.into(),
103        }
104    }
105}
106
107#[derive(Clone, PartialEq)]
108pub struct ScrollBar {
109    context: ScrollBarContext,
110    theme: Option<ScrollBarThemePartial>,
111    key: DiffKey,
112}
113
114impl ScrollBar {
115    /// Creates the default scrollbar renderer.
116    pub fn default_renderer() -> Callback<ScrollBarContext, Element> {
117        (|context| Self::new(context).into()).into()
118    }
119
120    /// Creates a built-in scrollbar from renderer context data.
121    pub fn new(context: ScrollBarContext) -> Self {
122        Self {
123            context,
124            theme: None,
125            key: DiffKey::None,
126        }
127    }
128
129    /// Overrides built-in scrollbar theme values for this instance.
130    pub fn theme(mut self, theme: ScrollBarThemePartial) -> Self {
131        self.theme = Some(theme);
132        self
133    }
134}
135
136impl KeyExt for ScrollBar {
137    fn key_mut(&mut self) -> &mut DiffKey {
138        &mut self.key
139    }
140}
141
142impl ComponentOwned for ScrollBar {
143    fn render(self) -> impl IntoElement {
144        let ScrollBar { context, theme, .. } = self;
145        let scrollbar_theme = get_theme!(&theme, ScrollBarThemePreference, "scrollbar");
146        let ScrollBarContext {
147            axis,
148            timeout,
149            auto_hide_scrollbars,
150            clicking_scrollbar,
151            thumb_events,
152            thumb_offset,
153            track_size,
154            thumb_length,
155            ..
156        } = context;
157        let mut hovering = use_state(|| false);
158        let is_expanded = clicking_scrollbar.read().is_some() || *hovering.read();
159        let is_hidden = auto_hide_scrollbars && timeout.elapsed() && !is_expanded;
160
161        let animation = use_animation_with_dependencies(&is_expanded, move |conf, is_expanded| {
162            conf.on_creation(OnCreation::Finish);
163            conf.on_change(OnChange::Rerun);
164
165            let expand = |from: f32, to: f32| {
166                AnimNum::new(from, to)
167                    .time(207)
168                    .function(Function::Expo)
169                    .ease(Ease::Out)
170            };
171            let value = (
172                expand(
173                    scrollbar_theme.thumb_cross_size,
174                    scrollbar_theme.expanded_thumb_cross_size,
175                ),
176                expand(scrollbar_theme.opacity, scrollbar_theme.expanded_opacity),
177                expand(
178                    scrollbar_theme.cross_gap,
179                    scrollbar_theme.expanded_cross_gap,
180                ),
181            );
182
183            if *is_expanded {
184                value
185            } else {
186                value.into_reversed()
187            }
188        });
189        let (cross_size, opacity, cross_gap) = animation.get().value();
190
191        let (
192            width,
193            height,
194            offset_x,
195            offset_y,
196            bar_width,
197            bar_height,
198            bar_margin,
199            thumb_width,
200            thumb_height,
201            thumb_offset_x,
202            thumb_offset_y,
203        ) = match axis {
204            Axis::X => (
205                track_size,
206                Size::px(20.),
207                0.,
208                -20.,
209                Size::fill(),
210                Size::px(cross_size),
211                Gaps::new(0., SCROLLBAR_MARGIN, cross_gap, SCROLLBAR_MARGIN),
212                Size::Inner,
213                Size::px(20.),
214                thumb_offset + SCROLLBAR_MARGIN,
215                0.,
216            ),
217            Axis::Y => (
218                Size::px(20.),
219                track_size,
220                -20.,
221                0.,
222                Size::px(cross_size),
223                Size::fill(),
224                Gaps::new(SCROLLBAR_MARGIN, cross_gap, SCROLLBAR_MARGIN, 0.),
225                Size::px(20.),
226                Size::Inner,
227                0.,
228                thumb_offset + SCROLLBAR_MARGIN,
229            ),
230        };
231
232        let on_pointer_over = move |_| {
233            if !cfg!(target_os = "android") {
234                hovering.set_if_modified(true);
235            }
236        };
237        let on_pointer_out = move |_| {
238            if !cfg!(target_os = "android") {
239                hovering.set_if_modified(false);
240            }
241        };
242
243        rect()
244            .position(Position::new_absolute())
245            .width(if is_hidden { Size::px(0.) } else { width })
246            .height(if is_hidden { Size::px(0.) } else { height })
247            .offset_x(offset_x)
248            .offset_y(offset_y)
249            .layer(999)
250            .cursor(CursorIcon::Default)
251            .on_pointer_down(|e: Event<PointerEventData>| {
252                e.stop_propagation();
253            })
254            .maybe_child((!is_hidden).then(|| {
255                rect()
256                    .width(Size::fill())
257                    .height(Size::fill())
258                    .direction(if axis == Axis::Y {
259                        Direction::vertical()
260                    } else {
261                        Direction::horizontal()
262                    })
263                    .cross_align(Alignment::end())
264                    .on_pointer_over(on_pointer_over)
265                    .on_pointer_out(on_pointer_out)
266                    .child(
267                        rect()
268                            .width(bar_width)
269                            .height(bar_height)
270                            .margin(bar_margin)
271                            .background(scrollbar_theme.background.with_a(opacity as u8))
272                            .corner_radius(8.)
273                            .shadow(Shadow::new().blur(6.).color((
274                                0,
275                                0,
276                                0,
277                                (opacity * 0.28) as u8,
278                            ))),
279                    )
280                    .child(
281                        rect()
282                            .position(Position::new_absolute())
283                            .width(thumb_width)
284                            .height(thumb_height)
285                            .offset_x(thumb_offset_x)
286                            .offset_y(thumb_offset_y)
287                            .child(ScrollThumb {
288                                theme,
289                                clicking_scrollbar,
290                                thumb_events,
291                                axis,
292                                size: thumb_length,
293                                cross_size,
294                                cross_gap,
295                            }),
296                    )
297            }))
298    }
299
300    fn render_key(&self) -> DiffKey {
301        self.key.clone().or(self.default_key())
302    }
303}