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, 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/// Scrollbar show mode.
20#[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
38/// The width of the scrollbar (THUMB_ACTIVE_INSET * 2 + THUMB_ACTIVE_WIDTH)
39const 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    /// The full size of the content, including padding.
57    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 offset
125    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        // On drag
222        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    /// Return true if the scrollbar axis is vertical.
253    pub fn is_vertical(&self) -> bool {
254        matches!(self, Self::Vertical)
255    }
256
257    /// Return true if the scrollbar axis is horizontal.
258    pub fn is_horizontal(&self) -> bool {
259        matches!(self, Self::Horizontal)
260    }
261
262    /// Return true if the scrollbar axis is both vertical and horizontal.
263    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            // This should keep Horizontal first, Vertical is the primary axis
283            // if Vertical not need display, then Horizontal will not keep right margin.
284            Self::Both => vec![Axis::Horizontal, Axis::Vertical],
285        }
286    }
287}
288
289/// Scrollbar control for scroll-area or a uniform-list.
290pub struct Scrollbar {
291    axis: ScrollbarAxis,
292    scroll_handle: Rc<Box<dyn ScrollHandleOffsetable>>,
293    state: ScrollbarState,
294    scroll_size: Option<Size<Pixels>>,
295    /// Maximum frames per second for scrolling by drag. Default is 120 FPS.
296    ///
297    /// This is used to limit the update rate of the scrollbar when it is
298    /// being dragged for some complex interactions for reducing CPU usage.
299    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    // Get the width of the scrollbar.
318    pub(crate) const fn width() -> Pixels {
319        WIDTH
320    }
321
322    /// Create with vertical and horizontal scrollbar.
323    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    /// Create with horizontal scrollbar.
331    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    /// Create with vertical scrollbar.
339    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    /// Create vertical scrollbar for uniform list.
347    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    /// Set a special scroll size of the content area, default is None.
355    ///
356    /// Default will sync the `content_size` from `scroll_handle`.
357    pub fn scroll_size(mut self, scroll_size: Size<Pixels>) -> Self {
358        self.scroll_size = Some(scroll_size);
359        self
360    }
361
362    /// Set scrollbar axis.
363    pub fn axis(mut self, axis: impl Into<ScrollbarAxis>) -> Self {
364        self.axis = axis.into();
365        self
366    }
367
368    /// Set maximum frames per second for scrolling by drag. Default is 120 FPS.
369    ///
370    /// If you have very high CPU usage, consider reducing this value to improve performance.
371    ///
372    /// Available values: 30..120
373    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    // Bounds of thumb to be rendered.
466    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            // The horizontal scrollbar is set avoid overlapping with the vertical scrollbar, if the vertical scrollbar is visible.
539            let margin_end = if has_both && !is_vertical {
540                WIDTH
541            } else {
542                px(0.)
543            };
544
545            // Hide scrollbar, if the scroll area is smaller than the container.
546            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                    // Delay 2s to fade out the scrollbar thumb (in 1s)
607                    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            // The clickable area of the thumb
644            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            // The actual render area of the thumb
660            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        // Update last_scroll_time when offset is changed.
715        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                                        // click on the thumb bar, set the drag position
803                                        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                                        // click on the scrollbar, jump to the position
811                                        // Set the thumb bar center to the click position
812                                        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                            // When is hover to show mode or it was visible,
849                            // we need to update the hovered state and increase the last_scroll_time.
850                            let need_hover_to_update = is_hover_to_show || is_visible;
851                            // Update hovered state for scrollbar
852                            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                            // Update hovered state for scrollbar thumb
868                            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                            // Move thumb position on dragging
881                            if state.get().dragged_axis == Some(axis) && event.dragging() {
882                                // Stop the event propagation to avoid selecting text or other side effects.
883                                cx.stop_propagation();
884
885                                // drag_pos is the position of the mouse down event
886                                // We need to keep the thumb bar still at the origin down position
887                                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                                    // Limit update rate
916                                    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}