impulse_thaw/scrollbar/
mod.rs

1use leptos::{ev, html, leptos_dom::helpers::WindowListenerHandle, prelude::*};
2use thaw_utils::{class_list, mount_style, ComponentRef};
3
4#[component]
5pub fn Scrollbar(
6    #[prop(optional, into)] class: MaybeProp<String>,
7    #[prop(optional, into)] style: MaybeProp<String>,
8    /// Class name of content div.
9    #[prop(optional, into)]
10    content_class: MaybeProp<String>,
11    /// Style of content div.
12    #[prop(optional, into)]
13    content_style: MaybeProp<String>,
14    /// Size of scrollbar.
15    #[prop(default = 8)]
16    size: u8,
17    #[prop(optional)] comp_ref: Option<ComponentRef<ScrollbarRef>>,
18    children: Children,
19) -> impl IntoView {
20    mount_style("scrollbar", include_str!("./scrollbar.css"));
21    let container_ref = NodeRef::<html::Div>::new();
22    let content_ref = NodeRef::<html::Div>::new();
23    let x_track_ref = NodeRef::<html::Div>::new();
24    let y_track_ref = NodeRef::<html::Div>::new();
25    let is_show_x_thumb = RwSignal::new(false);
26    let is_show_y_thumb = RwSignal::new(false);
27    let x_track_width = RwSignal::new(0);
28    let y_track_height = RwSignal::new(0);
29    let container_width = RwSignal::new(0);
30    let container_height = RwSignal::new(0);
31    let container_scroll_top = RwSignal::new(0);
32    let container_scroll_left = RwSignal::new(0);
33    let content_width = RwSignal::new(0);
34    let content_height = RwSignal::new(0);
35    let thumb_status = StoredValue::new(None::<ThumbStatus>);
36
37    if let Some(comp_ref) = comp_ref {
38        comp_ref.load(ScrollbarRef {
39            container_scroll_top,
40            container_ref,
41            content_ref,
42        });
43    }
44
45    let x_thumb_width = Memo::new(move |_| {
46        let content_width = f64::from(content_width.get());
47        let x_track_width = f64::from(x_track_width.get());
48        let container_width = f64::from(container_width.get());
49        if content_width <= 0.0 {
50            return 0.0;
51        }
52        if content_width <= container_width {
53            return 0.0;
54        }
55        x_track_width * container_width / content_width
56    });
57    let x_thumb_left = Memo::new(move |_| {
58        let x_track_width = f64::from(x_track_width.get());
59        let x_thumb_width = f64::from(x_thumb_width.get());
60        if x_track_width == x_thumb_width {
61            is_show_x_thumb.set(false);
62            return 0.0;
63        }
64
65        let container_width = f64::from(container_width.get());
66        let container_scroll_left = f64::from(container_scroll_left.get());
67        let content_width = f64::from(content_width.get());
68
69        let diff = content_width - container_width;
70        if diff <= 0.0 {
71            0.0
72        } else {
73            (container_scroll_left / diff) * (x_track_width - x_thumb_width)
74        }
75    });
76
77    let y_thumb_height = Memo::new(move |_| {
78        let content_height = f64::from(content_height.get());
79        let y_track_height = f64::from(y_track_height.get());
80        let container_height = f64::from(container_height.get());
81        if content_height <= 0.0 {
82            return 0.0;
83        }
84        if content_height <= container_height {
85            return 0.0;
86        }
87
88        y_track_height * container_height / content_height
89    });
90    let y_thumb_top = Memo::new(move |_| {
91        let y_track_height = f64::from(y_track_height.get());
92        let y_thumb_height = f64::from(y_thumb_height.get());
93        if y_track_height == y_thumb_height {
94            is_show_y_thumb.set(false);
95            return 0.0;
96        }
97
98        let container_height = f64::from(container_height.get());
99        let container_scroll_top = f64::from(container_scroll_top.get());
100        let content_height = f64::from(content_height.get());
101
102        let diff = content_height - container_height;
103        if diff <= 0.0 {
104            0.0
105        } else {
106            (container_scroll_top / diff) * (y_track_height - y_thumb_height)
107        }
108    });
109
110    let sync_scroll_state = move || {
111        if let Some(el) = container_ref.get_untracked() {
112            container_scroll_top.set(el.scroll_top());
113            container_scroll_left.set(el.scroll_left());
114        }
115    };
116    let sync_position_state = move || {
117        if let Some(el) = container_ref.get_untracked() {
118            container_width.set(el.offset_width());
119            container_height.set(el.offset_height());
120        }
121        if let Some(el) = content_ref.get_untracked() {
122            content_width.set(el.offset_width());
123            content_height.set(el.offset_height());
124        }
125        if let Some(el) = x_track_ref.get() {
126            x_track_width.set(el.offset_width());
127        }
128        if let Some(el) = y_track_ref.get() {
129            y_track_height.set(el.offset_height());
130        }
131    };
132    let on_mouseenter = move |_| {
133        is_show_x_thumb.set(true);
134        is_show_y_thumb.set(true);
135        thumb_status.update_value(|thumb_status| {
136            if thumb_status.is_some() {
137                *thumb_status = Some(ThumbStatus::Enter);
138            }
139        });
140        sync_position_state();
141        sync_scroll_state();
142    };
143    let on_mouseleave = move |_| {
144        if Some(true)
145            == thumb_status.try_update_value(|thumb_status| {
146                if thumb_status.is_some() {
147                    *thumb_status = Some(ThumbStatus::DelayLeave);
148                    false
149                } else {
150                    true
151                }
152            })
153        {
154            is_show_y_thumb.set(false);
155            is_show_x_thumb.set(false);
156        }
157    };
158
159    let on_scroll = move |_| {
160        sync_scroll_state();
161    };
162
163    let x_trumb_mousemove_handle = StoredValue::new(None::<WindowListenerHandle>);
164    let x_trumb_mouseup_handle = StoredValue::new(None::<WindowListenerHandle>);
165    let memo_x_left = StoredValue::new(0);
166    let memo_mouse_x = StoredValue::new(0);
167    let on_x_thumb_mousedown = move |e: ev::MouseEvent| {
168        e.prevent_default();
169        e.stop_propagation();
170        let handle = window_event_listener(ev::mousemove, move |e| {
171            let container_width = container_width.get();
172            let content_width = content_width.get();
173            let x_track_width = x_track_width.get();
174            let x_thumb_width = x_thumb_width.get() as i32;
175
176            let x_diff = e.client_x() - memo_mouse_x.get_value();
177            let to_scroll_left_upper_bound = content_width - container_width;
178            let scroll_left =
179                (x_diff * to_scroll_left_upper_bound) / (x_track_width - x_thumb_width);
180
181            let mut to_scroll_left = memo_x_left.get_value() + scroll_left;
182            to_scroll_left = to_scroll_left.min(to_scroll_left_upper_bound);
183            to_scroll_left = to_scroll_left.max(0);
184
185            if let Some(el) = container_ref.get_untracked() {
186                el.set_scroll_left(to_scroll_left);
187            }
188        });
189        x_trumb_mousemove_handle.set_value(Some(handle));
190        let handle = window_event_listener(ev::mouseup, move |_| {
191            x_trumb_mousemove_handle.update_value(|handle| {
192                if let Some(handle) = handle.take() {
193                    handle.remove();
194                }
195            });
196            x_trumb_mouseup_handle.update_value(|handle| {
197                if let Some(handle) = handle.take() {
198                    handle.remove();
199                }
200            });
201            if let Some(Some(status)) =
202                thumb_status.try_update_value(|thumb_status| thumb_status.take())
203            {
204                if status == ThumbStatus::DelayLeave {
205                    is_show_x_thumb.set(false);
206                    is_show_y_thumb.set(false);
207                }
208            }
209        });
210        x_trumb_mouseup_handle.set_value(Some(handle));
211        memo_x_left.set_value(container_scroll_left.get());
212        memo_mouse_x.set_value(e.client_x());
213        thumb_status.set_value(Some(ThumbStatus::Enter));
214    };
215
216    let y_trumb_mousemove_handle = StoredValue::new(None::<WindowListenerHandle>);
217    let y_trumb_mouseup_handle = StoredValue::new(None::<WindowListenerHandle>);
218    let memo_y_top = StoredValue::new(0);
219    let memo_mouse_y = StoredValue::new(0);
220    let on_y_thumb_mousedown = move |e: ev::MouseEvent| {
221        e.prevent_default();
222        e.stop_propagation();
223        let handle = window_event_listener(ev::mousemove, move |e| {
224            let container_height = container_height.get();
225            let content_height = content_height.get();
226            let y_track_height = y_track_height.get();
227            let y_thumb_height = y_thumb_height.get() as i32;
228
229            let y_diff = e.client_y() - memo_mouse_y.get_value();
230            let to_scroll_top_upper_bound = content_height - container_height;
231            let scroll_top =
232                (y_diff * to_scroll_top_upper_bound) / (y_track_height - y_thumb_height);
233
234            let mut to_scroll_top = memo_y_top.get_value() + scroll_top;
235            to_scroll_top = to_scroll_top.min(to_scroll_top_upper_bound);
236            to_scroll_top = to_scroll_top.max(0);
237
238            if let Some(el) = container_ref.get_untracked() {
239                el.set_scroll_top(to_scroll_top);
240            }
241        });
242        y_trumb_mousemove_handle.set_value(Some(handle));
243        let handle = window_event_listener(ev::mouseup, move |_| {
244            y_trumb_mousemove_handle.update_value(|handle| {
245                if let Some(handle) = handle.take() {
246                    handle.remove();
247                }
248            });
249            y_trumb_mouseup_handle.update_value(|handle| {
250                if let Some(handle) = handle.take() {
251                    handle.remove();
252                }
253            });
254            if let Some(Some(status)) =
255                thumb_status.try_update_value(|thumb_status| thumb_status.take())
256            {
257                if status == ThumbStatus::DelayLeave {
258                    is_show_x_thumb.set(false);
259                    is_show_y_thumb.set(false);
260                }
261            }
262        });
263        y_trumb_mouseup_handle.set_value(Some(handle));
264        memo_y_top.set_value(container_scroll_top.get());
265        memo_mouse_y.set_value(e.client_y());
266        thumb_status.set_value(Some(ThumbStatus::Enter));
267    };
268
269    on_cleanup(move || {
270        x_trumb_mousemove_handle.update_value(|handle| {
271            if let Some(handle) = handle.take() {
272                handle.remove();
273            }
274        });
275        x_trumb_mouseup_handle.update_value(|handle| {
276            if let Some(handle) = handle.take() {
277                handle.remove();
278            }
279        });
280        y_trumb_mousemove_handle.update_value(|handle| {
281            if let Some(handle) = handle.take() {
282                handle.remove();
283            }
284        });
285        y_trumb_mouseup_handle.update_value(|handle| {
286            if let Some(handle) = handle.take() {
287                handle.remove();
288            }
289        });
290    });
291
292    view! {
293        <div
294            class=class_list!["thaw-scrollbar", class]
295            style=move || {
296                format!("--thaw-scrollbar-size: {}px;{}", size, style.get().unwrap_or_default())
297            }
298
299            on:mouseenter=on_mouseenter
300            on:mouseleave=on_mouseleave
301        >
302
303            <div class="thaw-scrollbar__container" node_ref=container_ref on:scroll=on_scroll>
304                <div
305                    class=class_list!["thaw-scrollbar__content", content_class]
306
307                    style=move || {
308                        format!("width: fit-content; {}", content_style.get().unwrap_or_default())
309                    }
310
311                    node_ref=content_ref
312                >
313                    {children()}
314                </div>
315            </div>
316            <div class="thaw-scrollbar__track--vertical" node_ref=y_track_ref>
317                <div
318                    class="thaw-scrollabr__thumb"
319                    style:display=move || {
320                        (!is_show_y_thumb.get()).then_some("none").unwrap_or_default()
321                    }
322                    style:height=move || format!("{}px", y_thumb_height.get())
323                    style:top=move || format!("{}px", y_thumb_top.get())
324                    on:mousedown=on_y_thumb_mousedown
325                ></div>
326            </div>
327            <div class="thaw-scrollbar__track--horizontal" node_ref=x_track_ref>
328                <div
329                    class="thaw-scrollabr__thumb"
330                    style:display=move || {
331                        (!is_show_x_thumb.get()).then_some("none").unwrap_or_default()
332                    }
333                    style:width=move || format!("{}px", x_thumb_width.get())
334                    style:left=move || format!("{}px", x_thumb_left.get())
335                    on:mousedown=on_x_thumb_mousedown
336                ></div>
337            </div>
338        </div>
339    }
340}
341
342#[derive(Clone, PartialEq)]
343enum ThumbStatus {
344    Enter,
345    DelayLeave,
346}
347
348#[derive(Clone)]
349pub struct ScrollbarRef {
350    container_scroll_top: RwSignal<i32>,
351    container_ref: NodeRef<html::Div>,
352    pub content_ref: NodeRef<html::Div>,
353}
354
355impl ScrollbarRef {
356    pub fn container_scroll_top(&self) -> i32 {
357        self.container_scroll_top.get_untracked()
358    }
359
360    /// Scroll content.
361    pub fn scroll_to_with_scroll_to_options(&self, options: &web_sys::ScrollToOptions) {
362        if let Some(el) = self.container_ref.get_untracked() {
363            el.scroll_to_with_scroll_to_options(options);
364        }
365    }
366}