impulse_thaw/scrollbar/
mod.rs1use 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 #[prop(optional, into)]
10 content_class: MaybeProp<String>,
11 #[prop(optional, into)]
13 content_style: MaybeProp<String>,
14 #[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 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}