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