gpui_component/scroll/
scrollbar.rs

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