freya_components/scrollviews/
scrollbar.rs1use 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#[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 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#[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 pub fn default_renderer() -> Callback<ScrollBarContext, Element> {
117 (|context| Self::new(context).into()).into()
118 }
119
120 pub fn new(context: ScrollBarContext) -> Self {
122 Self {
123 context,
124 theme: None,
125 key: DiffKey::None,
126 }
127 }
128
129 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}