1use std::{
2 cell::Cell,
3 ops::Deref,
4 panic::Location,
5 rc::Rc,
6 time::{Duration, Instant},
7};
8
9use crate::{ActiveTheme, AxisExt};
10use gpui::{
11 App, Axis, BorderStyle, Bounds, ContentMask, Corner, CursorStyle, Edges, Element, ElementId,
12 GlobalElementId, Hitbox, HitboxBehavior, Hsla, InspectorElementId, IntoElement, IsZero,
13 LayoutId, ListState, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point,
14 Position, ScrollHandle, ScrollWheelEvent, Size, Style, Timer, UniformListScrollHandle, Window,
15 fill, point, px, relative, size,
16};
17use schemars::JsonSchema;
18use serde::{Deserialize, Serialize};
19
20const WIDTH: Pixels = px(4. * 2. + 8.);
22const MIN_THUMB_SIZE: f32 = 48.;
23
24const THUMB_WIDTH: Pixels = px(6.);
25const THUMB_RADIUS: Pixels = px(6. / 2.);
26const THUMB_INSET: Pixels = px(4.);
27
28const THUMB_ACTIVE_WIDTH: Pixels = px(8.);
29const THUMB_ACTIVE_RADIUS: Pixels = px(8. / 2.);
30const THUMB_ACTIVE_INSET: Pixels = px(4.);
31
32const FADE_OUT_DURATION: f32 = 3.0;
33const FADE_OUT_DELAY: f32 = 2.0;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default, JsonSchema)]
37pub enum ScrollbarShow {
38 #[default]
40 Scrolling,
41 Hover,
43 Always,
45}
46
47impl ScrollbarShow {
48 fn is_hover(&self) -> bool {
49 matches!(self, Self::Hover)
50 }
51
52 fn is_always(&self) -> bool {
53 matches!(self, Self::Always)
54 }
55}
56
57pub trait ScrollbarHandle: 'static {
59 fn offset(&self) -> Point<Pixels>;
61 fn set_offset(&self, offset: Point<Pixels>);
63 fn content_size(&self) -> Size<Pixels>;
65 fn start_drag(&self) {}
67 fn end_drag(&self) {}
69}
70
71impl ScrollbarHandle for ScrollHandle {
72 fn offset(&self) -> Point<Pixels> {
73 self.offset()
74 }
75
76 fn set_offset(&self, offset: Point<Pixels>) {
77 self.set_offset(offset);
78 }
79
80 fn content_size(&self) -> Size<Pixels> {
81 self.max_offset() + self.bounds().size
82 }
83}
84
85impl ScrollbarHandle for UniformListScrollHandle {
86 fn offset(&self) -> Point<Pixels> {
87 self.0.borrow().base_handle.offset()
88 }
89
90 fn set_offset(&self, offset: Point<Pixels>) {
91 self.0.borrow_mut().base_handle.set_offset(offset)
92 }
93
94 fn content_size(&self) -> Size<Pixels> {
95 let base_handle = &self.0.borrow().base_handle;
96 base_handle.max_offset() + base_handle.bounds().size
97 }
98}
99
100impl ScrollbarHandle for ListState {
101 fn offset(&self) -> Point<Pixels> {
102 self.scroll_px_offset_for_scrollbar()
103 }
104
105 fn set_offset(&self, offset: Point<Pixels>) {
106 self.set_offset_from_scrollbar(offset);
107 }
108
109 fn content_size(&self) -> Size<Pixels> {
110 self.viewport_bounds().size + self.max_offset_for_scrollbar()
111 }
112
113 fn start_drag(&self) {
114 self.scrollbar_drag_started();
115 }
116
117 fn end_drag(&self) {
118 self.scrollbar_drag_ended();
119 }
120}
121
122#[doc(hidden)]
123#[derive(Debug, Clone)]
124struct ScrollbarState(Rc<Cell<ScrollbarStateInner>>);
125
126#[doc(hidden)]
127#[derive(Debug, Clone, Copy)]
128struct ScrollbarStateInner {
129 hovered_axis: Option<Axis>,
130 hovered_on_thumb: Option<Axis>,
131 dragged_axis: Option<Axis>,
132 drag_pos: Point<Pixels>,
133 last_scroll_offset: Point<Pixels>,
134 last_scroll_time: Option<Instant>,
135 last_update: Instant,
137 idle_timer_scheduled: bool,
138}
139
140impl Default for ScrollbarState {
141 fn default() -> Self {
142 Self(Rc::new(Cell::new(ScrollbarStateInner {
143 hovered_axis: None,
144 hovered_on_thumb: None,
145 dragged_axis: None,
146 drag_pos: point(px(0.), px(0.)),
147 last_scroll_offset: point(px(0.), px(0.)),
148 last_scroll_time: None,
149 last_update: Instant::now(),
150 idle_timer_scheduled: false,
151 })))
152 }
153}
154
155impl Deref for ScrollbarState {
156 type Target = Rc<Cell<ScrollbarStateInner>>;
157
158 fn deref(&self) -> &Self::Target {
159 &self.0
160 }
161}
162
163impl ScrollbarStateInner {
164 fn with_drag_pos(&self, axis: Axis, pos: Point<Pixels>) -> Self {
165 let mut state = *self;
166 if axis.is_vertical() {
167 state.drag_pos.y = pos.y;
168 } else {
169 state.drag_pos.x = pos.x;
170 }
171
172 state.dragged_axis = Some(axis);
173 state
174 }
175
176 fn with_unset_drag_pos(&self) -> Self {
177 let mut state = *self;
178 state.dragged_axis = None;
179 state
180 }
181
182 fn with_hovered(&self, axis: Option<Axis>) -> Self {
183 let mut state = *self;
184 state.hovered_axis = axis;
185 if axis.is_some() {
186 state.last_scroll_time = Some(std::time::Instant::now());
187 }
188 state
189 }
190
191 fn with_hovered_on_thumb(&self, axis: Option<Axis>) -> Self {
192 let mut state = *self;
193 state.hovered_on_thumb = axis;
194 if self.is_scrollbar_visible() {
195 if axis.is_some() {
196 state.last_scroll_time = Some(std::time::Instant::now());
197 }
198 }
199 state
200 }
201
202 fn with_last_scroll(
203 &self,
204 last_scroll_offset: Point<Pixels>,
205 last_scroll_time: Option<Instant>,
206 ) -> Self {
207 let mut state = *self;
208 state.last_scroll_offset = last_scroll_offset;
209 state.last_scroll_time = last_scroll_time;
210 state
211 }
212
213 fn with_last_scroll_time(&self, t: Option<Instant>) -> Self {
214 let mut state = *self;
215 state.last_scroll_time = t;
216 state
217 }
218
219 fn with_last_update(&self, t: Instant) -> Self {
220 let mut state = *self;
221 state.last_update = t;
222 state
223 }
224
225 fn with_idle_timer_scheduled(&self, scheduled: bool) -> Self {
226 let mut state = *self;
227 state.idle_timer_scheduled = scheduled;
228 state
229 }
230
231 fn is_scrollbar_visible(&self) -> bool {
232 if self.dragged_axis.is_some() {
234 return true;
235 }
236
237 if let Some(last_time) = self.last_scroll_time {
238 let elapsed = Instant::now().duration_since(last_time).as_secs_f32();
239 elapsed < FADE_OUT_DURATION
240 } else {
241 false
242 }
243 }
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248pub enum ScrollbarAxis {
249 Vertical,
251 Horizontal,
253 Both,
255}
256
257impl From<Axis> for ScrollbarAxis {
258 fn from(axis: Axis) -> Self {
259 match axis {
260 Axis::Vertical => Self::Vertical,
261 Axis::Horizontal => Self::Horizontal,
262 }
263 }
264}
265
266impl ScrollbarAxis {
267 #[inline]
269 pub fn is_vertical(&self) -> bool {
270 matches!(self, Self::Vertical)
271 }
272
273 #[inline]
275 pub fn is_horizontal(&self) -> bool {
276 matches!(self, Self::Horizontal)
277 }
278
279 #[inline]
281 pub fn is_both(&self) -> bool {
282 matches!(self, Self::Both)
283 }
284
285 #[inline]
287 pub fn has_vertical(&self) -> bool {
288 matches!(self, Self::Vertical | Self::Both)
289 }
290
291 #[inline]
293 pub fn has_horizontal(&self) -> bool {
294 matches!(self, Self::Horizontal | Self::Both)
295 }
296
297 #[inline]
298 fn all(&self) -> Vec<Axis> {
299 match self {
300 Self::Vertical => vec![Axis::Vertical],
301 Self::Horizontal => vec![Axis::Horizontal],
302 Self::Both => vec![Axis::Horizontal, Axis::Vertical],
305 }
306 }
307}
308
309pub struct Scrollbar {
311 pub(crate) id: ElementId,
312 axis: ScrollbarAxis,
313 scrollbar_show: Option<ScrollbarShow>,
314 scroll_handle: Rc<dyn ScrollbarHandle>,
315 scroll_size: Option<Size<Pixels>>,
316 max_fps: usize,
321}
322
323impl Scrollbar {
324 #[track_caller]
328 pub fn new<H: ScrollbarHandle + Clone>(scroll_handle: &H) -> Self {
329 let caller = Location::caller();
330 Self {
331 id: ElementId::CodeLocation(*caller),
332 axis: ScrollbarAxis::Both,
333 scrollbar_show: None,
334 scroll_handle: Rc::new(scroll_handle.clone()),
335 max_fps: 120,
336 scroll_size: None,
337 }
338 }
339
340 #[track_caller]
342 pub fn horizontal<H: ScrollbarHandle + Clone>(scroll_handle: &H) -> Self {
343 Self::new(scroll_handle).axis(ScrollbarAxis::Horizontal)
344 }
345
346 #[track_caller]
348 pub fn vertical<H: ScrollbarHandle + Clone>(scroll_handle: &H) -> Self {
349 Self::new(scroll_handle).axis(ScrollbarAxis::Vertical)
350 }
351
352 pub fn id(mut self, id: impl Into<ElementId>) -> Self {
356 self.id = id.into();
357 self
358 }
359
360 pub fn scrollbar_show(mut self, scrollbar_show: ScrollbarShow) -> Self {
362 self.scrollbar_show = Some(scrollbar_show);
363 self
364 }
365
366 pub fn scroll_size(mut self, scroll_size: Size<Pixels>) -> Self {
370 self.scroll_size = Some(scroll_size);
371 self
372 }
373
374 pub fn axis(mut self, axis: impl Into<ScrollbarAxis>) -> Self {
376 self.axis = axis.into();
377 self
378 }
379
380 pub(crate) fn max_fps(mut self, max_fps: usize) -> Self {
386 self.max_fps = max_fps.clamp(30, 120);
387 self
388 }
389
390 pub(crate) const fn width() -> Pixels {
392 WIDTH
393 }
394
395 fn style_for_active(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
396 (
397 cx.theme().scrollbar_thumb_hover,
398 cx.theme().scrollbar,
399 cx.theme().border,
400 THUMB_ACTIVE_WIDTH,
401 THUMB_ACTIVE_INSET,
402 THUMB_ACTIVE_RADIUS,
403 )
404 }
405
406 fn style_for_hovered_thumb(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
407 (
408 cx.theme().scrollbar_thumb_hover,
409 cx.theme().scrollbar,
410 cx.theme().border,
411 THUMB_ACTIVE_WIDTH,
412 THUMB_ACTIVE_INSET,
413 THUMB_ACTIVE_RADIUS,
414 )
415 }
416
417 fn style_for_hovered_bar(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
418 (
419 cx.theme().scrollbar_thumb,
420 cx.theme().scrollbar,
421 gpui::transparent_black(),
422 THUMB_ACTIVE_WIDTH,
423 THUMB_ACTIVE_INSET,
424 THUMB_ACTIVE_RADIUS,
425 )
426 }
427
428 fn style_for_normal(&self, cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
429 let scrollbar_show = self.scrollbar_show.unwrap_or(cx.theme().scrollbar_show);
430 let (width, inset, radius) = match scrollbar_show {
431 ScrollbarShow::Scrolling => (THUMB_WIDTH, THUMB_INSET, THUMB_RADIUS),
432 _ => (THUMB_ACTIVE_WIDTH, THUMB_ACTIVE_INSET, THUMB_ACTIVE_RADIUS),
433 };
434
435 (
436 cx.theme().scrollbar_thumb,
437 cx.theme().scrollbar,
438 gpui::transparent_black(),
439 width,
440 inset,
441 radius,
442 )
443 }
444
445 fn style_for_idle(&self, cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
446 let scrollbar_show = self.scrollbar_show.unwrap_or(cx.theme().scrollbar_show);
447 let (width, inset, radius) = match scrollbar_show {
448 ScrollbarShow::Scrolling => (THUMB_WIDTH, THUMB_INSET, THUMB_RADIUS),
449 _ => (THUMB_ACTIVE_WIDTH, THUMB_ACTIVE_INSET, THUMB_ACTIVE_RADIUS),
450 };
451
452 (
453 gpui::transparent_black(),
454 gpui::transparent_black(),
455 gpui::transparent_black(),
456 width,
457 inset,
458 radius,
459 )
460 }
461}
462
463impl IntoElement for Scrollbar {
464 type Element = Self;
465
466 fn into_element(self) -> Self::Element {
467 self
468 }
469}
470
471#[doc(hidden)]
472pub struct PrepaintState {
473 hitbox: Hitbox,
474 scrollbar_state: ScrollbarState,
475 states: Vec<AxisPrepaintState>,
476}
477
478#[doc(hidden)]
479pub struct AxisPrepaintState {
480 axis: Axis,
481 bar_hitbox: Hitbox,
482 bounds: Bounds<Pixels>,
483 radius: Pixels,
484 bg: Hsla,
485 border: Hsla,
486 thumb_bounds: Bounds<Pixels>,
487 thumb_fill_bounds: Bounds<Pixels>,
489 thumb_bg: Hsla,
490 scroll_size: Pixels,
491 container_size: Pixels,
492 thumb_size: Pixels,
493 margin_end: Pixels,
494}
495
496impl Element for Scrollbar {
497 type RequestLayoutState = ();
498 type PrepaintState = PrepaintState;
499
500 fn id(&self) -> Option<gpui::ElementId> {
501 Some(self.id.clone())
502 }
503
504 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
505 None
506 }
507
508 fn request_layout(
509 &mut self,
510 _: Option<&GlobalElementId>,
511 _: Option<&InspectorElementId>,
512 window: &mut Window,
513 cx: &mut App,
514 ) -> (LayoutId, Self::RequestLayoutState) {
515 let mut style = Style::default();
516 style.position = Position::Absolute;
517 style.flex_grow = 1.0;
518 style.flex_shrink = 1.0;
519 style.size.width = relative(1.).into();
520 style.size.height = relative(1.).into();
521
522 (window.request_layout(style, None, cx), ())
523 }
524
525 fn prepaint(
526 &mut self,
527 _: Option<&GlobalElementId>,
528 _: Option<&InspectorElementId>,
529 bounds: Bounds<Pixels>,
530 _: &mut Self::RequestLayoutState,
531 window: &mut Window,
532 cx: &mut App,
533 ) -> Self::PrepaintState {
534 let hitbox = window.with_content_mask(Some(ContentMask { bounds }), |window| {
535 window.insert_hitbox(bounds, HitboxBehavior::Normal)
536 });
537
538 let state = window
539 .use_state(cx, |_, _| ScrollbarState::default())
540 .read(cx)
541 .clone();
542
543 let mut states = vec![];
544 let mut has_both = self.axis.is_both();
545 let scroll_size = self
546 .scroll_size
547 .unwrap_or(self.scroll_handle.content_size());
548
549 for axis in self.axis.all().into_iter() {
550 let is_vertical = axis.is_vertical();
551 let (scroll_area_size, container_size, scroll_position) = if is_vertical {
552 (
553 scroll_size.height,
554 hitbox.size.height,
555 self.scroll_handle.offset().y,
556 )
557 } else {
558 (
559 scroll_size.width,
560 hitbox.size.width,
561 self.scroll_handle.offset().x,
562 )
563 };
564
565 let margin_end = if has_both && !is_vertical {
567 WIDTH
568 } else {
569 px(0.)
570 };
571
572 if scroll_area_size <= container_size {
574 has_both = false;
575 continue;
576 }
577
578 let thumb_length =
579 (container_size / scroll_area_size * container_size).max(px(MIN_THUMB_SIZE));
580 let thumb_start = -(scroll_position / (scroll_area_size - container_size)
581 * (container_size - margin_end - thumb_length));
582 let thumb_end = (thumb_start + thumb_length).min(container_size - margin_end);
583
584 let bounds = Bounds {
585 origin: if is_vertical {
586 point(hitbox.origin.x + hitbox.size.width - WIDTH, hitbox.origin.y)
587 } else {
588 point(
589 hitbox.origin.x,
590 hitbox.origin.y + hitbox.size.height - WIDTH,
591 )
592 },
593 size: gpui::Size {
594 width: if is_vertical {
595 WIDTH
596 } else {
597 hitbox.size.width
598 },
599 height: if is_vertical {
600 hitbox.size.height
601 } else {
602 WIDTH
603 },
604 },
605 };
606
607 let scrollbar_show = self.scrollbar_show.unwrap_or(cx.theme().scrollbar_show);
608 let is_always_to_show = scrollbar_show.is_always();
609 let is_hover_to_show = scrollbar_show.is_hover();
610 let is_hovered_on_bar = state.get().hovered_axis == Some(axis);
611 let is_hovered_on_thumb = state.get().hovered_on_thumb == Some(axis);
612 let is_offset_changed = state.get().last_scroll_offset != self.scroll_handle.offset();
613
614 let (thumb_bg, bar_bg, bar_border, thumb_width, inset, radius) =
615 if state.get().dragged_axis == Some(axis) {
616 Self::style_for_active(cx)
617 } else if is_hover_to_show && (is_hovered_on_bar || is_hovered_on_thumb) {
618 if is_hovered_on_thumb {
619 Self::style_for_hovered_thumb(cx)
620 } else {
621 Self::style_for_hovered_bar(cx)
622 }
623 } else if is_offset_changed {
624 self.style_for_normal(cx)
625 } else if is_always_to_show {
626 if is_hovered_on_thumb {
627 Self::style_for_hovered_thumb(cx)
628 } else {
629 Self::style_for_hovered_bar(cx)
630 }
631 } else {
632 let mut idle_state = self.style_for_idle(cx);
633 if let Some(last_time) = state.get().last_scroll_time {
635 let elapsed = Instant::now().duration_since(last_time).as_secs_f32();
636 if is_hovered_on_bar {
637 state.set(state.get().with_last_scroll_time(Some(Instant::now())));
638 idle_state = if is_hovered_on_thumb {
639 Self::style_for_hovered_thumb(cx)
640 } else {
641 Self::style_for_hovered_bar(cx)
642 };
643 } else if elapsed < FADE_OUT_DELAY {
644 idle_state.0 = cx.theme().scrollbar_thumb;
645
646 if !state.get().idle_timer_scheduled {
647 let state = state.clone();
648 state.set(state.get().with_idle_timer_scheduled(true));
649 let current_view = window.current_view();
650 let next_delay = Duration::from_secs_f32(FADE_OUT_DELAY - elapsed);
651 window
652 .spawn(cx, async move |cx| {
653 Timer::after(next_delay).await;
654 state.set(state.get().with_idle_timer_scheduled(false));
655 cx.update(|_, cx| cx.notify(current_view)).ok();
656 })
657 .detach();
658 }
659 } else if elapsed < FADE_OUT_DURATION {
660 let opacity = 1.0 - (elapsed - FADE_OUT_DELAY).powi(10);
661 idle_state.0 = cx.theme().scrollbar_thumb.opacity(opacity);
662
663 window.request_animation_frame();
664 }
665 }
666
667 idle_state
668 };
669
670 let thumb_length = thumb_end - thumb_start - inset * 2;
672 let thumb_bounds = if is_vertical {
673 Bounds::from_corner_and_size(
674 Corner::TopRight,
675 bounds.top_right() + point(-inset, inset + thumb_start),
676 size(WIDTH, thumb_length),
677 )
678 } else {
679 Bounds::from_corner_and_size(
680 Corner::BottomLeft,
681 bounds.bottom_left() + point(inset + thumb_start, -inset),
682 size(thumb_length, WIDTH),
683 )
684 };
685
686 let thumb_fill_bounds = if is_vertical {
688 Bounds::from_corner_and_size(
689 Corner::TopRight,
690 bounds.top_right() + point(-inset, inset + thumb_start),
691 size(thumb_width, thumb_length),
692 )
693 } else {
694 Bounds::from_corner_and_size(
695 Corner::BottomLeft,
696 bounds.bottom_left() + point(inset + thumb_start, -inset),
697 size(thumb_length, thumb_width),
698 )
699 };
700
701 let bar_hitbox = window.with_content_mask(Some(ContentMask { bounds }), |window| {
702 window.insert_hitbox(bounds, gpui::HitboxBehavior::Normal)
703 });
704
705 states.push(AxisPrepaintState {
706 axis,
707 bar_hitbox,
708 bounds,
709 radius,
710 bg: bar_bg,
711 border: bar_border,
712 thumb_bounds,
713 thumb_fill_bounds,
714 thumb_bg,
715 scroll_size: scroll_area_size,
716 container_size,
717 thumb_size: thumb_length,
718 margin_end,
719 })
720 }
721
722 PrepaintState {
723 hitbox,
724 states,
725 scrollbar_state: state,
726 }
727 }
728
729 fn paint(
730 &mut self,
731 _: Option<&GlobalElementId>,
732 _: Option<&InspectorElementId>,
733 _: Bounds<Pixels>,
734 _: &mut Self::RequestLayoutState,
735 prepaint: &mut Self::PrepaintState,
736 window: &mut Window,
737 cx: &mut App,
738 ) {
739 let scrollbar_state = &prepaint.scrollbar_state;
740 let scrollbar_show = self.scrollbar_show.unwrap_or(cx.theme().scrollbar_show);
741 let view_id = window.current_view();
742 let hitbox_bounds = prepaint.hitbox.bounds;
743 let is_visible = scrollbar_state.get().is_scrollbar_visible() || scrollbar_show.is_always();
744 let is_hover_to_show = scrollbar_show.is_hover();
745
746 if self.scroll_handle.offset() != scrollbar_state.get().last_scroll_offset {
748 scrollbar_state.set(
749 scrollbar_state
750 .get()
751 .with_last_scroll(self.scroll_handle.offset(), Some(Instant::now())),
752 );
753 cx.notify(view_id);
754 }
755
756 window.with_content_mask(
757 Some(ContentMask {
758 bounds: hitbox_bounds,
759 }),
760 |window| {
761 for state in prepaint.states.iter() {
762 let axis = state.axis;
763 let mut radius = state.radius;
764 if cx.theme().radius.is_zero() {
765 radius = px(0.);
766 }
767 let bounds = state.bounds;
768 let thumb_bounds = state.thumb_bounds;
769 let scroll_area_size = state.scroll_size;
770 let container_size = state.container_size;
771 let thumb_size = state.thumb_size;
772 let margin_end = state.margin_end;
773 let is_vertical = axis.is_vertical();
774
775 window.set_cursor_style(CursorStyle::default(), &state.bar_hitbox);
776
777 window.paint_layer(hitbox_bounds, |cx| {
778 cx.paint_quad(fill(state.bounds, state.bg));
779
780 cx.paint_quad(PaintQuad {
781 bounds,
782 corner_radii: (0.).into(),
783 background: gpui::transparent_black().into(),
784 border_widths: if is_vertical {
785 Edges {
786 top: px(0.),
787 right: px(0.),
788 bottom: px(0.),
789 left: px(0.),
790 }
791 } else {
792 Edges {
793 top: px(0.),
794 right: px(0.),
795 bottom: px(0.),
796 left: px(0.),
797 }
798 },
799 border_color: state.border,
800 border_style: BorderStyle::default(),
801 });
802
803 cx.paint_quad(
804 fill(state.thumb_fill_bounds, state.thumb_bg).corner_radii(radius),
805 );
806 });
807
808 window.on_mouse_event({
809 let state = scrollbar_state.clone();
810 let scroll_handle = self.scroll_handle.clone();
811
812 move |event: &ScrollWheelEvent, phase, _, cx| {
813 if phase.bubble() && hitbox_bounds.contains(&event.position) {
814 if scroll_handle.offset() != state.get().last_scroll_offset {
815 state.set(state.get().with_last_scroll(
816 scroll_handle.offset(),
817 Some(Instant::now()),
818 ));
819 cx.notify(view_id);
820 }
821 }
822 }
823 });
824
825 let safe_range = (-scroll_area_size + container_size)..px(0.);
826
827 if is_hover_to_show || is_visible {
828 window.on_mouse_event({
829 let state = scrollbar_state.clone();
830 let scroll_handle = self.scroll_handle.clone();
831
832 move |event: &MouseDownEvent, phase, _, cx| {
833 if phase.bubble() && bounds.contains(&event.position) {
834 cx.stop_propagation();
835
836 if thumb_bounds.contains(&event.position) {
837 let pos = event.position - thumb_bounds.origin;
839
840 scroll_handle.start_drag();
841 state.set(state.get().with_drag_pos(axis, pos));
842
843 cx.notify(view_id);
844 } else {
845 let offset = scroll_handle.offset();
848 let percentage = if is_vertical {
849 (event.position.y - thumb_size / 2. - bounds.origin.y)
850 / (bounds.size.height - thumb_size)
851 } else {
852 (event.position.x - thumb_size / 2. - bounds.origin.x)
853 / (bounds.size.width - thumb_size)
854 }
855 .min(1.);
856
857 if is_vertical {
858 scroll_handle.set_offset(point(
859 offset.x,
860 (-scroll_area_size * percentage)
861 .clamp(safe_range.start, safe_range.end),
862 ));
863 } else {
864 scroll_handle.set_offset(point(
865 (-scroll_area_size * percentage)
866 .clamp(safe_range.start, safe_range.end),
867 offset.y,
868 ));
869 }
870 }
871 }
872 }
873 });
874 }
875
876 window.on_mouse_event({
877 let scroll_handle = self.scroll_handle.clone();
878 let state = scrollbar_state.clone();
879 let max_fps_duration = Duration::from_millis((1000 / self.max_fps) as u64);
880
881 move |event: &MouseMoveEvent, _, _, cx| {
882 let mut notify = false;
883 let need_hover_to_update = is_hover_to_show || is_visible;
886 if bounds.contains(&event.position) && need_hover_to_update {
888 state.set(state.get().with_hovered(Some(axis)));
889
890 if state.get().hovered_axis != Some(axis) {
891 notify = true;
892 }
893 } else {
894 if state.get().hovered_axis == Some(axis) {
895 if state.get().hovered_axis.is_some() {
896 state.set(state.get().with_hovered(None));
897 notify = true;
898 }
899 }
900 }
901
902 if thumb_bounds.contains(&event.position) {
904 if state.get().hovered_on_thumb != Some(axis) {
905 state.set(state.get().with_hovered_on_thumb(Some(axis)));
906 notify = true;
907 }
908 } else {
909 if state.get().hovered_on_thumb == Some(axis) {
910 state.set(state.get().with_hovered_on_thumb(None));
911 notify = true;
912 }
913 }
914
915 if state.get().dragged_axis == Some(axis) && event.dragging() {
917 cx.stop_propagation();
919
920 let drag_pos = state.get().drag_pos;
923
924 let percentage = (if is_vertical {
925 (event.position.y - drag_pos.y - bounds.origin.y)
926 / (bounds.size.height - thumb_size)
927 } else {
928 (event.position.x - drag_pos.x - bounds.origin.x)
929 / (bounds.size.width - thumb_size - margin_end)
930 })
931 .clamp(0., 1.);
932
933 let offset = if is_vertical {
934 point(
935 scroll_handle.offset().x,
936 (-(scroll_area_size - container_size) * percentage)
937 .clamp(safe_range.start, safe_range.end),
938 )
939 } else {
940 point(
941 (-(scroll_area_size - container_size) * percentage)
942 .clamp(safe_range.start, safe_range.end),
943 scroll_handle.offset().y,
944 )
945 };
946
947 if (scroll_handle.offset().y - offset.y).abs() > px(1.)
948 || (scroll_handle.offset().x - offset.x).abs() > px(1.)
949 {
950 if state.get().last_update.elapsed() > max_fps_duration {
952 scroll_handle.set_offset(offset);
953 state.set(state.get().with_last_update(Instant::now()));
954 notify = true;
955 }
956 }
957 }
958
959 if notify {
960 cx.notify(view_id);
961 }
962 }
963 });
964
965 window.on_mouse_event({
966 let state = scrollbar_state.clone();
967 let scroll_handle = self.scroll_handle.clone();
968
969 move |_event: &MouseUpEvent, phase, _, cx| {
970 if phase.bubble() {
971 scroll_handle.end_drag();
972 state.set(state.get().with_unset_drag_pos());
973 cx.notify(view_id);
974 }
975 }
976 });
977 }
978 },
979 );
980 }
981}