Skip to main content

ui/components/
scrollbar.rs

1use std::{
2    any::Any,
3    fmt::Debug,
4    ops::Not,
5    time::{Duration, Instant},
6};
7
8use gpui::{
9    Along, Anchor, App, AppContext as _, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask,
10    Context, Corners, CursorStyle, DispatchPhase, Div, Edges, Element, ElementId, Entity, EntityId,
11    GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
12    LayoutId, ListState, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement,
13    Pixels, Point, Position, Render, ScrollHandle, ScrollWheelEvent, Size, Stateful,
14    StatefulInteractiveElement, Style, Styled, Task, UniformListDecoration,
15    UniformListScrollHandle, Window, ease_in_out, prelude::FluentBuilder as _, px, quad, relative,
16    size,
17};
18use gpui_util::ResultExt;
19use smallvec::SmallVec;
20use theme::ActiveTheme as _;
21
22use std::ops::Range;
23
24use crate::scrollbars::{ScrollbarAutoHide, ScrollbarVisibility, ShowScrollbar};
25
26const SCROLLBAR_HIDE_DELAY_INTERVAL: Duration = Duration::from_secs(1);
27const SCROLLBAR_HIDE_DURATION: Duration = Duration::from_millis(400);
28const SCROLLBAR_SHOW_DURATION: Duration = Duration::from_millis(50);
29
30pub const EDITOR_SCROLLBAR_WIDTH: Pixels = ScrollbarStyle::Editor.to_pixels();
31const SCROLLBAR_PADDING: Pixels = px(4.);
32const BORDER_WIDTH: Pixels = px(1.);
33
34pub mod scrollbars {
35    use gpui::{App, Global};
36    use schemars::JsonSchema;
37    use serde::{Deserialize, Serialize};
38
39    /// When to show the scrollbar in the editor.
40    ///
41    /// Default: auto
42    #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
43    #[serde(rename_all = "snake_case")]
44    pub enum ShowScrollbar {
45        /// Show the scrollbar if there's important information or
46        /// follow the system's configured behavior.
47        #[default]
48        Auto,
49        /// Match the system's configured behavior.
50        System,
51        /// Always show the scrollbar.
52        Always,
53        /// Never show the scrollbar.
54        Never,
55    }
56
57    pub trait ScrollbarVisibility: 'static {
58        fn visibility(&self, cx: &App) -> ShowScrollbar;
59    }
60
61    #[derive(Default)]
62    pub struct ScrollbarAutoHide(pub bool);
63
64    impl ScrollbarAutoHide {
65        pub fn should_hide(&self) -> bool {
66            self.0
67        }
68    }
69
70    impl Global for ScrollbarAutoHide {}
71}
72
73fn get_scrollbar_state<T>(
74    mut config: Scrollbars<T>,
75    caller_location: &'static std::panic::Location,
76    window: &mut Window,
77    cx: &mut App,
78) -> Entity<ScrollbarStateWrapper<T>>
79where
80    T: ScrollableHandle,
81{
82    let element_id = config.id.take().unwrap_or_else(|| caller_location.into());
83    let track_color = config.track_color;
84    let has_border = config.border;
85
86    let state = window.use_keyed_state(element_id, cx, |_, cx| {
87        let parent_id = cx.entity_id();
88        ScrollbarStateWrapper(cx.new(|cx| ScrollbarState::new_from_config(config, parent_id, cx)))
89    });
90
91    state.update(cx, |state, cx| {
92        state.0.update(cx, |state, _cx| {
93            state.update_colors(track_color, has_border)
94        })
95    });
96    state
97}
98
99pub trait WithScrollbar: Sized {
100    type Output;
101
102    fn custom_scrollbars<T>(
103        self,
104        config: Scrollbars<T>,
105        window: &mut Window,
106        cx: &mut App,
107    ) -> Self::Output
108    where
109        T: ScrollableHandle;
110
111    // TODO: account for these cases properly
112    // #[track_caller]
113    // fn horizontal_scrollbar(self, window: &mut Window, cx: &mut App) -> Self::Output {
114    //     self.custom_scrollbars(
115    //         Scrollbars::new(ScrollAxes::Horizontal).ensure_id(core::panic::Location::caller()),
116    //         window,
117    //         cx,
118    //     )
119    // }
120
121    // #[track_caller]
122    // fn vertical_scrollbar(self, window: &mut Window, cx: &mut App) -> Self::Output {
123    //     self.custom_scrollbars(
124    //         Scrollbars::new(ScrollAxes::Vertical).ensure_id(core::panic::Location::caller()),
125    //         window,
126    //         cx,
127    //     )
128    // }
129
130    #[track_caller]
131    fn vertical_scrollbar_for<ScrollHandle: ScrollableHandle + Clone>(
132        self,
133        scroll_handle: &ScrollHandle,
134        window: &mut Window,
135        cx: &mut App,
136    ) -> Self::Output {
137        self.custom_scrollbars(
138            Scrollbars::new(ScrollAxes::Vertical)
139                .tracked_scroll_handle(scroll_handle)
140                .ensure_id(core::panic::Location::caller()),
141            window,
142            cx,
143        )
144    }
145}
146
147impl WithScrollbar for Stateful<Div> {
148    type Output = Self;
149
150    #[track_caller]
151    fn custom_scrollbars<T>(
152        self,
153        config: Scrollbars<T>,
154        window: &mut Window,
155        cx: &mut App,
156    ) -> Self::Output
157    where
158        T: ScrollableHandle,
159    {
160        render_scrollbar(
161            get_scrollbar_state(config, std::panic::Location::caller(), window, cx),
162            self,
163            cx,
164        )
165    }
166}
167
168impl WithScrollbar for Div {
169    type Output = Stateful<Div>;
170
171    #[track_caller]
172    fn custom_scrollbars<T>(
173        self,
174        config: Scrollbars<T>,
175        window: &mut Window,
176        cx: &mut App,
177    ) -> Self::Output
178    where
179        T: ScrollableHandle,
180    {
181        let scrollbar = get_scrollbar_state(config, std::panic::Location::caller(), window, cx);
182        // We know this ID stays consistent as long as the element is rendered for
183        // consecutive frames, which is sufficient for our use case here
184        let scrollbar_entity_id = scrollbar.entity_id();
185
186        render_scrollbar(
187            scrollbar,
188            self.id(("track-scroll", scrollbar_entity_id)),
189            cx,
190        )
191    }
192}
193
194fn render_scrollbar<T>(
195    scrollbar: Entity<ScrollbarStateWrapper<T>>,
196    div: Stateful<Div>,
197    cx: &App,
198) -> Stateful<Div>
199where
200    T: ScrollableHandle,
201{
202    let state = &scrollbar.read(cx).0;
203
204    div.when_some(state.read(cx).handle_to_track(), |this, handle| {
205        this.track_scroll(handle).when_some(
206            state.read(cx).visible_axes(),
207            |this, axes| match axes {
208                ScrollAxes::Horizontal => this.overflow_x_scroll(),
209                ScrollAxes::Vertical => this.overflow_y_scroll(),
210                ScrollAxes::Both => this.overflow_scroll(),
211            },
212        )
213    })
214    .when_some(
215        state
216            .read(cx)
217            .space_to_reserve_for(ScrollbarAxis::Horizontal),
218        |this, space| this.pb(space),
219    )
220    .when_some(
221        state.read(cx).space_to_reserve_for(ScrollbarAxis::Vertical),
222        |this, space| this.pr(space),
223    )
224    .child(state.clone())
225}
226
227impl<T: ScrollableHandle> UniformListDecoration for ScrollbarStateWrapper<T> {
228    fn compute(
229        &self,
230        _visible_range: Range<usize>,
231        _bounds: Bounds<Pixels>,
232        scroll_offset: Point<Pixels>,
233        _item_height: Pixels,
234        _item_count: usize,
235        _window: &mut Window,
236        _cx: &mut App,
237    ) -> gpui::AnyElement {
238        ScrollbarElement {
239            origin: -scroll_offset,
240            state: self.0.clone(),
241        }
242        .into_any()
243    }
244}
245
246// impl WithScrollbar for UniformList {
247//     type Output = Self;
248
249//     #[track_caller]
250//     fn custom_scrollbars<S, T>(
251//         self,
252//         config: Scrollbars<S, T>,
253//         window: &mut Window,
254//         cx: &mut App,
255//     ) -> Self::Output
256//     where
257//         S: ScrollbarVisibilitySetting,
258//         T: ScrollableHandle,
259//     {
260//         let scrollbar = get_scrollbar_state(config, std::panic::Location::caller(), window, cx);
261//         self.when_some(
262//             scrollbar.read_with(cx, |wrapper, cx| {
263//                 wrapper
264//                     .0
265//                     .read(cx)
266//                     .handle_to_track::<UniformListScrollHandle>()
267//                     .cloned()
268//             }),
269//             |this, handle| this.track_scroll(handle),
270//         )
271//         .with_decoration(scrollbar)
272//     }
273// }
274
275#[derive(Copy, Clone, PartialEq, Eq)]
276enum ShowBehavior {
277    Always,
278    Autohide,
279    Never,
280}
281
282impl ShowBehavior {
283    fn from_setting(setting: ShowScrollbar, cx: &mut App) -> Self {
284        match setting {
285            ShowScrollbar::Never => Self::Never,
286            ShowScrollbar::Auto => Self::Autohide,
287            ShowScrollbar::System => {
288                if cx.default_global::<ScrollbarAutoHide>().should_hide() {
289                    Self::Autohide
290                } else {
291                    Self::Always
292                }
293            }
294            ShowScrollbar::Always => Self::Always,
295        }
296    }
297}
298
299pub enum ScrollAxes {
300    Horizontal,
301    Vertical,
302    Both,
303}
304
305impl ScrollAxes {
306    fn apply_to<T>(self, point: Point<T>, value: T) -> Point<T>
307    where
308        T: Debug + Default + PartialEq + Clone,
309    {
310        match self {
311            Self::Horizontal => point.apply_along(ScrollbarAxis::Horizontal, |_| value),
312            Self::Vertical => point.apply_along(ScrollbarAxis::Vertical, |_| value),
313            Self::Both => Point::new(value.clone(), value),
314        }
315    }
316}
317
318#[derive(Clone, Debug, Default, PartialEq)]
319enum ReservedSpace {
320    #[default]
321    None,
322    Thumb,
323    Track,
324    StableTrack,
325}
326
327impl ReservedSpace {
328    fn is_visible(&self) -> bool {
329        *self != ReservedSpace::None
330    }
331
332    fn needs_scroll_track(&self) -> bool {
333        matches!(self, Self::Track | Self::StableTrack)
334    }
335
336    fn needs_space_reserved(&self, max_offset: Pixels) -> bool {
337        match self {
338            Self::StableTrack => true,
339            Self::Track => !max_offset.is_zero(),
340            _ => false,
341        }
342    }
343}
344
345#[derive(Clone)]
346enum Handle<T: ScrollableHandle> {
347    Tracked(T),
348    Untracked(fn() -> T),
349}
350
351#[derive(Clone, Copy, Default, PartialEq)]
352pub enum ScrollbarStyle {
353    #[default]
354    Regular,
355    Editor,
356}
357
358impl ScrollbarStyle {
359    pub const fn to_pixels(&self) -> Pixels {
360        match self {
361            ScrollbarStyle::Regular => px(6.),
362            ScrollbarStyle::Editor => px(15.),
363        }
364    }
365}
366
367#[derive(Clone)]
368pub struct Scrollbars<T: ScrollableHandle = ScrollHandle> {
369    id: Option<ElementId>,
370    get_visibility: fn(&App) -> ShowScrollbar,
371    tracked_entity: Option<Option<EntityId>>,
372    scrollable_handle: Handle<T>,
373    visibility: Point<ReservedSpace>,
374    style: Option<ScrollbarStyle>,
375    track_color: Option<Hsla>,
376    border: bool,
377}
378
379impl Scrollbars {
380    pub fn new(show_along: ScrollAxes) -> Self {
381        Self::new_with_setting(show_along, |_| ShowScrollbar::default())
382    }
383
384    pub fn always_visible(show_along: ScrollAxes) -> Self {
385        Self::new_with_setting(show_along, |_| ShowScrollbar::Always)
386    }
387
388    pub fn for_settings<S: ScrollbarVisibility + Default>() -> Scrollbars {
389        Scrollbars::new_with_setting(ScrollAxes::Both, |cx| S::default().visibility(cx))
390    }
391}
392
393impl Scrollbars {
394    fn new_with_setting(show_along: ScrollAxes, get_visibility: fn(&App) -> ShowScrollbar) -> Self {
395        Self {
396            id: None,
397            get_visibility,
398            scrollable_handle: Handle::Untracked(ScrollHandle::new),
399            tracked_entity: None,
400            visibility: show_along.apply_to(Default::default(), ReservedSpace::Thumb),
401            style: None,
402            track_color: None,
403            border: false,
404        }
405    }
406}
407
408impl<ScrollHandle: ScrollableHandle> Scrollbars<ScrollHandle> {
409    pub fn id(mut self, id: impl Into<ElementId>) -> Self {
410        self.id = Some(id.into());
411        self
412    }
413
414    fn ensure_id(mut self, id: impl Into<ElementId>) -> Self {
415        if self.id.is_none() {
416            self.id = Some(id.into());
417        }
418        self
419    }
420
421    /// Notify the current context whenever this scrollbar gets a scroll event
422    pub fn notify_content(mut self) -> Self {
423        self.tracked_entity = Some(None);
424        self
425    }
426
427    /// Set a parent model which should be notified whenever this scrollbar gets a scroll event.
428    pub fn tracked_entity(mut self, entity_id: EntityId) -> Self {
429        self.tracked_entity = Some(Some(entity_id));
430        self
431    }
432
433    pub fn tracked_scroll_handle<TrackedHandle: ScrollableHandle>(
434        self,
435        tracked_scroll_handle: &TrackedHandle,
436    ) -> Scrollbars<TrackedHandle> {
437        let Self {
438            id,
439            tracked_entity: tracked_entity_id,
440            visibility,
441            get_visibility,
442            track_color,
443            border,
444            style,
445            ..
446        } = self;
447
448        Scrollbars {
449            scrollable_handle: Handle::Tracked(tracked_scroll_handle.clone()),
450            id,
451            tracked_entity: tracked_entity_id,
452            visibility,
453            track_color,
454            border,
455            get_visibility,
456            style,
457        }
458    }
459
460    pub fn show_along(mut self, along: ScrollAxes) -> Self {
461        self.visibility = along.apply_to(self.visibility, ReservedSpace::Thumb);
462        self
463    }
464
465    pub fn style(mut self, style: ScrollbarStyle) -> Self {
466        self.style = Some(style);
467        self
468    }
469
470    pub fn with_track_along(mut self, along: ScrollAxes, background_color: Hsla) -> Self {
471        self.visibility = along.apply_to(self.visibility, ReservedSpace::Track);
472        self.track_color = Some(background_color);
473        self
474    }
475
476    pub fn with_stable_track_along(mut self, along: ScrollAxes, background_color: Hsla) -> Self {
477        self.visibility = along.apply_to(self.visibility, ReservedSpace::StableTrack);
478        self.track_color = Some(background_color);
479        self.border = true;
480        self
481    }
482}
483
484#[derive(PartialEq, Clone, Debug)]
485enum VisibilityState {
486    Visible,
487    Animating { showing: bool, delta: f32 },
488    ThumbHidden,
489    Hidden,
490    Disabled,
491}
492
493enum AnimationState {
494    InProgress {
495        current_delta: f32,
496        animation_duration: Duration,
497        showing: bool,
498    },
499    Stale,
500}
501
502const DELTA_MAX: f32 = 1.0;
503
504impl VisibilityState {
505    fn from_behavior(behavior: ShowBehavior) -> Self {
506        match behavior {
507            ShowBehavior::Always => Self::Visible,
508            ShowBehavior::Never => Self::Disabled,
509            ShowBehavior::Autohide => Self::for_show(),
510        }
511    }
512
513    fn for_show() -> Self {
514        Self::Animating {
515            showing: true,
516            delta: Default::default(),
517        }
518    }
519
520    fn for_autohide() -> Self {
521        Self::Animating {
522            showing: Default::default(),
523            delta: Default::default(),
524        }
525    }
526
527    fn is_visible(&self) -> bool {
528        matches!(
529            self,
530            Self::Visible | Self::Animating { .. } | Self::ThumbHidden
531        )
532    }
533
534    #[inline]
535    fn is_disabled(&self) -> bool {
536        *self == VisibilityState::Disabled
537    }
538
539    fn animation_state(&self) -> Option<AnimationState> {
540        match self {
541            Self::ThumbHidden => Some(AnimationState::Stale),
542            Self::Animating { showing, delta } => Some(AnimationState::InProgress {
543                current_delta: *delta,
544                animation_duration: if *showing {
545                    SCROLLBAR_SHOW_DURATION
546                } else {
547                    SCROLLBAR_HIDE_DURATION
548                },
549                showing: *showing,
550            }),
551            _ => None,
552        }
553    }
554
555    fn set_delta(&mut self, new_delta: f32, keep_track_visible: bool) {
556        match self {
557            Self::Animating { showing, delta } if new_delta >= DELTA_MAX => {
558                if *showing {
559                    *self = Self::Visible;
560                } else if keep_track_visible {
561                    *self = Self::ThumbHidden;
562                } else {
563                    *self = Self::Hidden;
564                }
565            }
566            Self::Animating { delta, .. } => *delta = new_delta,
567            _ => {}
568        }
569    }
570
571    fn toggle_visible(&self, show_behavior: ShowBehavior) -> Self {
572        match self {
573            Self::Hidden | Self::ThumbHidden => {
574                if show_behavior == ShowBehavior::Autohide {
575                    Self::for_show()
576                } else {
577                    Self::Visible
578                }
579            }
580            Self::Animating {
581                showing: false,
582                delta: progress,
583            } => Self::Animating {
584                showing: true,
585                delta: DELTA_MAX - progress,
586            },
587            _ => self.clone(),
588        }
589    }
590}
591
592enum ParentHoverEvent {
593    Within,
594    Entered,
595    Exited,
596    Outside,
597}
598
599#[derive(Clone)]
600struct TrackColors {
601    background: Hsla,
602    has_border: bool,
603}
604
605pub fn on_new_scrollbars<T: gpui::Global>(cx: &mut App) {
606    cx.observe_new::<ScrollbarState>(|_, window, cx| {
607        if let Some(window) = window {
608            cx.observe_global_in::<T>(window, ScrollbarState::settings_changed)
609                .detach();
610        }
611    })
612    .detach();
613}
614
615/// This is used to ensure notifies within the state do not notify the parent
616/// unintentionally.
617struct ScrollbarStateWrapper<T: ScrollableHandle>(Entity<ScrollbarState<T>>);
618
619/// A scrollbar state that should be persisted across frames.
620struct ScrollbarState<T: ScrollableHandle = ScrollHandle> {
621    thumb_state: ThumbState,
622    notify_id: Option<EntityId>,
623    manually_added: bool,
624    scroll_handle: T,
625    show_behavior: ShowBehavior,
626    get_visibility: fn(&App) -> ShowScrollbar,
627    visibility: Point<ReservedSpace>,
628    track_color: Option<TrackColors>,
629    show_state: VisibilityState,
630    style: ScrollbarStyle,
631    mouse_in_parent: bool,
632    last_prepaint_state: Option<ScrollbarPrepaintState>,
633    _auto_hide_task: Option<Task<()>>,
634}
635
636impl<T: ScrollableHandle> ScrollbarState<T> {
637    fn new_from_config(config: Scrollbars<T>, parent_id: EntityId, cx: &mut Context<Self>) -> Self {
638        let (manually_added, scroll_handle) = match config.scrollable_handle {
639            Handle::Tracked(handle) => (true, handle),
640            Handle::Untracked(func) => (false, func()),
641        };
642
643        let show_behavior = ShowBehavior::from_setting((config.get_visibility)(cx), cx);
644        ScrollbarState {
645            thumb_state: Default::default(),
646            notify_id: config.tracked_entity.map(|id| id.unwrap_or(parent_id)),
647            manually_added,
648            scroll_handle,
649            visibility: config.visibility,
650            track_color: config.track_color.map(|color| TrackColors {
651                background: color,
652                has_border: config.border,
653            }),
654            show_behavior,
655            get_visibility: config.get_visibility,
656            style: config.style.unwrap_or_default(),
657            show_state: VisibilityState::from_behavior(show_behavior),
658            mouse_in_parent: true,
659            last_prepaint_state: None,
660            _auto_hide_task: None,
661        }
662    }
663
664    fn settings_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
665        self.set_show_behavior(
666            ShowBehavior::from_setting((self.get_visibility)(cx), cx),
667            window,
668            cx,
669        );
670    }
671
672    /// Schedules a scrollbar auto hide if no auto hide is currently in progress yet.
673    fn schedule_auto_hide(&mut self, window: &mut Window, cx: &mut Context<Self>) {
674        if self._auto_hide_task.is_none() {
675            self._auto_hide_task = (self.visible() && self.show_behavior == ShowBehavior::Autohide)
676                .then(|| {
677                    cx.spawn_in(window, async move |scrollbar_state, cx| {
678                        cx.background_executor()
679                            .timer(SCROLLBAR_HIDE_DELAY_INTERVAL)
680                            .await;
681                        scrollbar_state
682                            .update(cx, |state, cx| {
683                                if state.thumb_state == ThumbState::Inactive {
684                                    state.set_visibility(VisibilityState::for_autohide(), cx);
685                                }
686                                state._auto_hide_task.take();
687                            })
688                            .log_err();
689                    })
690                });
691        }
692    }
693
694    fn show_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Self>) {
695        let visibility = self.show_state.toggle_visible(self.show_behavior);
696        self.set_visibility(visibility, cx);
697        self._auto_hide_task.take();
698        self.schedule_auto_hide(window, cx);
699    }
700
701    fn set_show_behavior(
702        &mut self,
703        behavior: ShowBehavior,
704        window: &mut Window,
705        cx: &mut Context<Self>,
706    ) {
707        if self.show_behavior != behavior {
708            self.show_behavior = behavior;
709            self.set_visibility(VisibilityState::from_behavior(behavior), cx);
710            self.schedule_auto_hide(window, cx);
711            cx.notify();
712        }
713    }
714
715    fn set_visibility(&mut self, visibility: VisibilityState, cx: &mut Context<Self>) {
716        if self.show_state != visibility {
717            self.show_state = visibility;
718            cx.notify();
719        }
720    }
721
722    #[inline]
723    fn visible_axes(&self) -> Option<ScrollAxes> {
724        match (&self.visibility.x, &self.visibility.y) {
725            (ReservedSpace::None, ReservedSpace::None) => None,
726            (ReservedSpace::None, _) => Some(ScrollAxes::Vertical),
727            (_, ReservedSpace::None) => Some(ScrollAxes::Horizontal),
728            _ => Some(ScrollAxes::Both),
729        }
730    }
731
732    fn space_to_reserve_for(&self, axis: ScrollbarAxis) -> Option<Pixels> {
733        (self.show_state.is_disabled().not()
734            && self
735                .visibility
736                .along(axis)
737                .needs_space_reserved(self.scroll_handle().max_offset().along(axis)))
738        .then(|| self.space_to_reserve())
739    }
740
741    fn space_to_reserve(&self) -> Pixels {
742        self.style.to_pixels() + 2 * SCROLLBAR_PADDING
743    }
744
745    fn handle_to_track<Handle: ScrollableHandle>(&self) -> Option<&Handle> {
746        (!self.manually_added)
747            .then(|| (self.scroll_handle() as &dyn Any).downcast_ref::<Handle>())
748            .flatten()
749    }
750
751    fn scroll_handle(&self) -> &T {
752        &self.scroll_handle
753    }
754
755    fn set_offset(&mut self, offset: Point<Pixels>, cx: &mut Context<Self>) {
756        self.scroll_handle.set_offset(offset);
757        self.notify_parent(cx);
758        cx.notify();
759    }
760
761    fn is_dragging(&self) -> bool {
762        self.thumb_state.is_dragging()
763    }
764
765    fn set_dragging(
766        &mut self,
767        axis: ScrollbarAxis,
768        drag_offset: Pixels,
769        window: &mut Window,
770        cx: &mut Context<Self>,
771    ) {
772        self.set_thumb_state(ThumbState::Dragging(axis, drag_offset), window, cx);
773        self.scroll_handle().drag_started();
774    }
775
776    fn update_hovered_thumb(
777        &mut self,
778        position: &Point<Pixels>,
779        window: &mut Window,
780        cx: &mut Context<Self>,
781    ) {
782        self.set_thumb_state(
783            if let Some(&ScrollbarLayout { axis, .. }) =
784                self.last_prepaint_state.as_ref().and_then(|state| {
785                    state
786                        .thumb_for_position(position)
787                        .filter(|thumb| thumb.cursor_hitbox.is_hovered(window))
788                })
789            {
790                ThumbState::Hover(axis)
791            } else {
792                ThumbState::Inactive
793            },
794            window,
795            cx,
796        );
797    }
798
799    fn set_thumb_state(&mut self, state: ThumbState, window: &mut Window, cx: &mut Context<Self>) {
800        if self.thumb_state != state {
801            if state == ThumbState::Inactive {
802                self.schedule_auto_hide(window, cx);
803            } else {
804                self.set_visibility(self.show_state.toggle_visible(self.show_behavior), cx);
805                self._auto_hide_task.take();
806            }
807            self.thumb_state = state;
808            cx.notify();
809        }
810    }
811
812    fn update_parent_hovered(&mut self, window: &Window) -> ParentHoverEvent {
813        let last_parent_hovered = self.mouse_in_parent;
814        self.mouse_in_parent = self.parent_hovered(window);
815        let state_changed = self.mouse_in_parent != last_parent_hovered;
816        match (self.mouse_in_parent, state_changed) {
817            (true, true) => ParentHoverEvent::Entered,
818            (true, false) => ParentHoverEvent::Within,
819            (false, true) => ParentHoverEvent::Exited,
820            (false, false) => ParentHoverEvent::Outside,
821        }
822    }
823
824    fn update_colors(&mut self, track_color: Option<Hsla>, has_border: bool) {
825        self.track_color = track_color.map(|color| TrackColors {
826            background: color,
827            has_border,
828        });
829    }
830
831    fn parent_hovered(&self, window: &Window) -> bool {
832        self.last_prepaint_state
833            .as_ref()
834            .is_some_and(|state| state.parent_bounds_hitbox.is_hovered(window))
835    }
836
837    fn hit_for_position(&self, position: &Point<Pixels>) -> Option<&ScrollbarLayout> {
838        self.last_prepaint_state
839            .as_ref()
840            .and_then(|state| state.hit_for_position(position))
841    }
842
843    fn thumb_for_axis(&self, axis: ScrollbarAxis) -> Option<&ScrollbarLayout> {
844        self.last_prepaint_state
845            .as_ref()
846            .and_then(|state| state.thumbs.iter().find(|thumb| thumb.axis == axis))
847    }
848
849    fn thumb_ranges(
850        &self,
851    ) -> impl Iterator<Item = (ScrollbarAxis, Range<f32>, ReservedSpace)> + '_ {
852        const MINIMUM_THUMB_SIZE: Pixels = px(25.);
853        let max_offset = self.scroll_handle().max_offset();
854        let viewport_size = self.scroll_handle().viewport().size;
855        let current_offset = self.scroll_handle().offset();
856
857        [ScrollbarAxis::Horizontal, ScrollbarAxis::Vertical]
858            .into_iter()
859            .filter(|&axis| self.visibility.along(axis).is_visible())
860            .flat_map(move |axis| {
861                let max_offset = max_offset.along(axis);
862                let viewport_size = viewport_size.along(axis);
863                if max_offset.is_zero() || viewport_size.is_zero() {
864                    return None;
865                }
866                let content_size = viewport_size + max_offset;
867                let visible_percentage = viewport_size / content_size;
868                let thumb_size = MINIMUM_THUMB_SIZE.max(viewport_size * visible_percentage);
869                if thumb_size > viewport_size {
870                    return None;
871                }
872                let current_offset = current_offset
873                    .along(axis)
874                    .clamp(-max_offset, Pixels::ZERO)
875                    .abs();
876                let start_offset = (current_offset / max_offset) * (viewport_size - thumb_size);
877                let thumb_percentage_start = start_offset / viewport_size;
878                let thumb_percentage_end = (start_offset + thumb_size) / viewport_size;
879                Some((
880                    axis,
881                    thumb_percentage_start..thumb_percentage_end,
882                    self.visibility.along(axis),
883                ))
884            })
885    }
886
887    fn visible(&self) -> bool {
888        self.show_state.is_visible()
889    }
890
891    #[inline]
892    fn disabled(&self) -> bool {
893        self.show_state.is_disabled()
894    }
895
896    fn notify_parent(&self, cx: &mut App) {
897        if let Some(entity_id) = self.notify_id {
898            cx.notify(entity_id);
899        }
900    }
901}
902
903impl<T: ScrollableHandle> Render for ScrollbarState<T> {
904    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
905        ScrollbarElement {
906            state: cx.entity(),
907            origin: Default::default(),
908        }
909    }
910}
911
912struct ScrollbarElement<T: ScrollableHandle> {
913    origin: Point<Pixels>,
914    state: Entity<ScrollbarState<T>>,
915}
916
917#[derive(Default, Debug, PartialEq, Eq)]
918enum ThumbState {
919    #[default]
920    Inactive,
921    Hover(ScrollbarAxis),
922    Dragging(ScrollbarAxis, Pixels),
923}
924
925impl ThumbState {
926    fn is_dragging(&self) -> bool {
927        matches!(*self, ThumbState::Dragging(..))
928    }
929}
930
931impl ScrollableHandle for UniformListScrollHandle {
932    fn max_offset(&self) -> Point<Pixels> {
933        self.0.borrow().base_handle.max_offset()
934    }
935
936    fn set_offset(&self, point: Point<Pixels>) {
937        self.0.borrow().base_handle.set_offset(point);
938    }
939
940    fn offset(&self) -> Point<Pixels> {
941        self.0.borrow().base_handle.offset()
942    }
943
944    fn viewport(&self) -> Bounds<Pixels> {
945        self.0.borrow().base_handle.bounds()
946    }
947}
948
949impl ScrollableHandle for ListState {
950    fn max_offset(&self) -> Point<Pixels> {
951        self.max_offset_for_scrollbar()
952    }
953
954    fn set_offset(&self, point: Point<Pixels>) {
955        self.set_offset_from_scrollbar(point);
956    }
957
958    fn offset(&self) -> Point<Pixels> {
959        self.scroll_px_offset_for_scrollbar()
960    }
961
962    fn drag_started(&self) {
963        self.scrollbar_drag_started();
964    }
965
966    fn drag_ended(&self) {
967        self.scrollbar_drag_ended();
968    }
969
970    fn viewport(&self) -> Bounds<Pixels> {
971        self.viewport_bounds()
972    }
973}
974
975impl ScrollableHandle for ScrollHandle {
976    fn max_offset(&self) -> Point<Pixels> {
977        self.max_offset()
978    }
979
980    fn set_offset(&self, point: Point<Pixels>) {
981        self.set_offset(point);
982    }
983
984    fn offset(&self) -> Point<Pixels> {
985        self.offset()
986    }
987
988    fn viewport(&self) -> Bounds<Pixels> {
989        self.bounds()
990    }
991}
992
993pub trait ScrollableHandle: 'static + Any + Sized + Clone {
994    fn max_offset(&self) -> Point<Pixels>;
995    fn set_offset(&self, point: Point<Pixels>);
996    fn offset(&self) -> Point<Pixels>;
997    fn viewport(&self) -> Bounds<Pixels>;
998    fn drag_started(&self) {}
999    fn drag_ended(&self) {}
1000
1001    fn scrollable_along(&self, axis: ScrollbarAxis) -> bool {
1002        self.max_offset().along(axis) > Pixels::ZERO
1003    }
1004    fn content_size(&self) -> Size<Pixels> {
1005        self.viewport().size + self.max_offset().into()
1006    }
1007}
1008
1009enum ScrollbarMouseEvent {
1010    TrackClick,
1011    ThumbDrag(Pixels),
1012}
1013
1014struct ScrollbarLayout {
1015    thumb_bounds: Bounds<Pixels>,
1016    track_bounds: Bounds<Pixels>,
1017    cursor_hitbox: Hitbox,
1018    reserved_space: ReservedSpace,
1019    track_config: Option<(Bounds<Pixels>, TrackColors)>,
1020    axis: ScrollbarAxis,
1021}
1022
1023impl ScrollbarLayout {
1024    fn compute_click_offset(
1025        &self,
1026        event_position: Point<Pixels>,
1027        max_offset: Point<Pixels>,
1028        event_type: ScrollbarMouseEvent,
1029    ) -> Pixels {
1030        let Self {
1031            track_bounds,
1032            thumb_bounds,
1033            axis,
1034            ..
1035        } = self;
1036        let axis = *axis;
1037
1038        let viewport_size = track_bounds.size.along(axis);
1039        let thumb_size = thumb_bounds.size.along(axis);
1040        let thumb_offset = match event_type {
1041            ScrollbarMouseEvent::TrackClick => thumb_size / 2.,
1042            ScrollbarMouseEvent::ThumbDrag(thumb_offset) => thumb_offset,
1043        };
1044
1045        let thumb_start =
1046            (event_position.along(axis) - track_bounds.origin.along(axis) - thumb_offset)
1047                .clamp(px(0.), viewport_size - thumb_size);
1048
1049        let max_offset = max_offset.along(axis);
1050        let percentage = if viewport_size > thumb_size {
1051            thumb_start / (viewport_size - thumb_size)
1052        } else {
1053            0.
1054        };
1055
1056        -max_offset * percentage
1057    }
1058}
1059
1060impl PartialEq for ScrollbarLayout {
1061    fn eq(&self, other: &Self) -> bool {
1062        if self.axis != other.axis {
1063            return false;
1064        }
1065
1066        let axis = self.axis;
1067        let thumb_offset =
1068            self.thumb_bounds.origin.along(axis) - self.track_bounds.origin.along(axis);
1069        let other_thumb_offset =
1070            other.thumb_bounds.origin.along(axis) - other.track_bounds.origin.along(axis);
1071
1072        thumb_offset == other_thumb_offset
1073            && self.thumb_bounds.size.along(axis) == other.thumb_bounds.size.along(axis)
1074    }
1075}
1076
1077pub struct ScrollbarPrepaintState {
1078    parent_bounds_hitbox: Hitbox,
1079    thumbs: SmallVec<[ScrollbarLayout; 2]>,
1080}
1081
1082impl ScrollbarPrepaintState {
1083    fn thumb_for_position(&self, position: &Point<Pixels>) -> Option<&ScrollbarLayout> {
1084        self.thumbs
1085            .iter()
1086            .find(|info| info.thumb_bounds.contains(position))
1087    }
1088
1089    fn hit_for_position(&self, position: &Point<Pixels>) -> Option<&ScrollbarLayout> {
1090        self.thumbs.iter().find(|info| {
1091            if info.reserved_space.needs_scroll_track() {
1092                info.track_bounds.contains(position)
1093            } else {
1094                info.thumb_bounds.contains(position)
1095            }
1096        })
1097    }
1098}
1099
1100impl PartialEq for ScrollbarPrepaintState {
1101    fn eq(&self, other: &Self) -> bool {
1102        self.thumbs == other.thumbs
1103    }
1104}
1105
1106impl<T: ScrollableHandle> Element for ScrollbarElement<T> {
1107    type RequestLayoutState = ();
1108    type PrepaintState = Option<(ScrollbarPrepaintState, Option<f32>)>;
1109
1110    fn id(&self) -> Option<ElementId> {
1111        Some(("scrollbar_animation", self.state.entity_id()).into())
1112    }
1113
1114    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
1115        None
1116    }
1117
1118    fn request_layout(
1119        &mut self,
1120        _id: Option<&GlobalElementId>,
1121        _inspector_id: Option<&gpui::InspectorElementId>,
1122        window: &mut Window,
1123        cx: &mut App,
1124    ) -> (LayoutId, Self::RequestLayoutState) {
1125        let scrollbar_style = Style {
1126            position: Position::Absolute,
1127            inset: Edges::default(),
1128            size: size(relative(1.), relative(1.)).map(Into::into),
1129            ..Default::default()
1130        };
1131
1132        (window.request_layout(scrollbar_style, None, cx), ())
1133    }
1134
1135    fn prepaint(
1136        &mut self,
1137        id: Option<&GlobalElementId>,
1138        _inspector_id: Option<&gpui::InspectorElementId>,
1139        bounds: Bounds<Pixels>,
1140        _request_layout: &mut Self::RequestLayoutState,
1141        window: &mut Window,
1142        cx: &mut App,
1143    ) -> Self::PrepaintState {
1144        let prepaint_state =
1145            self.state
1146                .read(cx)
1147                .disabled()
1148                .not()
1149                .then(|| ScrollbarPrepaintState {
1150                    thumbs: {
1151                        let state = self.state.read(cx);
1152                        let thumb_ranges = state.thumb_ranges().collect::<SmallVec<[_; 2]>>();
1153                        let width = state.style.to_pixels();
1154                        let track_color = state.track_color.as_ref();
1155
1156                        let additional_padding = if thumb_ranges.len() == 2 {
1157                            width
1158                        } else {
1159                            Pixels::ZERO
1160                        };
1161
1162                        thumb_ranges
1163                            .into_iter()
1164                            .map(|(axis, thumb_range, reserved_space)| {
1165                                let track_anchor = match axis {
1166                                    ScrollbarAxis::Horizontal => Anchor::BottomLeft,
1167                                    ScrollbarAxis::Vertical => Anchor::TopRight,
1168                                };
1169
1170                                let scroll_track_bounds = Bounds::from_anchor_and_size(
1171                                    track_anchor,
1172                                    self.origin + bounds.corner(track_anchor),
1173                                    bounds.size.apply_along(axis.invert(), |_| {
1174                                        width
1175                                            + match state.style {
1176                                                ScrollbarStyle::Regular => 2 * SCROLLBAR_PADDING,
1177                                                ScrollbarStyle::Editor => Pixels::ZERO,
1178                                            }
1179                                    }),
1180                                );
1181
1182                                let has_border =
1183                                    track_color.is_some_and(|track_colors| track_colors.has_border);
1184
1185                                // Rounded style needs a bit of padding, whereas for editor scrollbars,
1186                                // we want the full length of the track
1187                                let thumb_container_bounds = match state.style {
1188                                    ScrollbarStyle::Regular => {
1189                                        scroll_track_bounds.dilate(-SCROLLBAR_PADDING)
1190                                    }
1191                                    ScrollbarStyle::Editor if has_border => scroll_track_bounds
1192                                        .extend(match axis {
1193                                            ScrollbarAxis::Horizontal => Edges {
1194                                                top: -BORDER_WIDTH,
1195                                                ..Default::default()
1196                                            },
1197
1198                                            ScrollbarAxis::Vertical => Edges {
1199                                                left: -BORDER_WIDTH,
1200                                                ..Default::default()
1201                                            },
1202                                        }),
1203                                    ScrollbarStyle::Editor => scroll_track_bounds,
1204                                };
1205
1206                                let available_space =
1207                                    thumb_container_bounds.size.along(axis) - additional_padding;
1208
1209                                let thumb_offset = thumb_range.start * available_space;
1210                                let thumb_end = thumb_range.end * available_space;
1211                                let thumb_bounds = Bounds::new(
1212                                    thumb_container_bounds
1213                                        .origin
1214                                        .apply_along(axis, |origin| origin + thumb_offset),
1215                                    thumb_container_bounds
1216                                        .size
1217                                        .apply_along(axis, |_| thumb_end - thumb_offset),
1218                                );
1219
1220                                let needs_scroll_track = reserved_space.needs_scroll_track();
1221
1222                                ScrollbarLayout {
1223                                    thumb_bounds,
1224                                    track_bounds: thumb_container_bounds,
1225                                    axis,
1226                                    cursor_hitbox: window.insert_hitbox(
1227                                        if needs_scroll_track {
1228                                            if has_border && state.style == ScrollbarStyle::Editor {
1229                                                scroll_track_bounds
1230                                            } else {
1231                                                thumb_container_bounds
1232                                            }
1233                                        } else {
1234                                            thumb_bounds
1235                                        },
1236                                        HitboxBehavior::BlockMouseExceptScroll,
1237                                    ),
1238                                    track_config: track_color
1239                                        .filter(|_| needs_scroll_track)
1240                                        .map(|color| (scroll_track_bounds, color.clone())),
1241                                    reserved_space,
1242                                }
1243                            })
1244                            .collect()
1245                    },
1246                    parent_bounds_hitbox: window.insert_hitbox(bounds, HitboxBehavior::Normal),
1247                });
1248        if prepaint_state
1249            .as_ref()
1250            .is_some_and(|state| Some(state) != self.state.read(cx).last_prepaint_state.as_ref())
1251        {
1252            self.state
1253                .update(cx, |state, cx| state.show_scrollbars(window, cx));
1254        }
1255
1256        prepaint_state.map(|state| {
1257            let autohide_delta = self
1258                .state
1259                .read(cx)
1260                .show_state
1261                .animation_state()
1262                .map(|state| match state {
1263                    AnimationState::InProgress {
1264                        current_delta,
1265                        animation_duration: delta_duration,
1266                        showing: should_invert,
1267                    } => window.with_element_state(id.unwrap(), |state, window| {
1268                        let state = state.unwrap_or_else(|| Instant::now());
1269                        let current = Instant::now();
1270
1271                        let new_delta = DELTA_MAX.min(
1272                            current_delta + (current - state).div_duration_f32(delta_duration),
1273                        );
1274                        self.state.update(cx, |state, _| {
1275                            let has_border = state
1276                                .track_color
1277                                .as_ref()
1278                                .is_some_and(|track_colors| track_colors.has_border);
1279                            state.show_state.set_delta(new_delta, has_border)
1280                        });
1281
1282                        window.request_animation_frame();
1283                        let delta = if should_invert {
1284                            DELTA_MAX - current_delta
1285                        } else {
1286                            current_delta
1287                        };
1288                        (ease_in_out(delta), current)
1289                    }),
1290                    AnimationState::Stale => 1.0,
1291                });
1292
1293            (state, autohide_delta)
1294        })
1295    }
1296
1297    fn paint(
1298        &mut self,
1299        _id: Option<&GlobalElementId>,
1300        _inspector_id: Option<&gpui::InspectorElementId>,
1301        Bounds { origin, size }: Bounds<Pixels>,
1302        _request_layout: &mut Self::RequestLayoutState,
1303        prepaint_state: &mut Self::PrepaintState,
1304        window: &mut Window,
1305        cx: &mut App,
1306    ) {
1307        let Some((prepaint_state, autohide_fade)) = prepaint_state.take() else {
1308            return;
1309        };
1310
1311        let bounds = Bounds::new(self.origin + origin, size);
1312        window.with_content_mask(Some(ContentMask { bounds }), |window| {
1313            let colors = cx.theme().colors();
1314
1315            let capture_phase;
1316
1317            if self.state.read(cx).visible() {
1318                let state = self.state.read(cx);
1319                let thumb_state = &state.thumb_state;
1320                let style = state.style;
1321
1322                if thumb_state.is_dragging() {
1323                    capture_phase = DispatchPhase::Capture;
1324                } else {
1325                    capture_phase = DispatchPhase::Bubble;
1326                }
1327
1328                for ScrollbarLayout {
1329                    thumb_bounds,
1330                    cursor_hitbox,
1331                    axis,
1332                    reserved_space,
1333                    track_config,
1334                    ..
1335                } in &prepaint_state.thumbs
1336                {
1337                    const MAXIMUM_OPACITY: f32 = 0.7;
1338                    let (thumb_base_color, hovered) = match thumb_state {
1339                        ThumbState::Dragging(dragged_axis, _) if dragged_axis == axis => {
1340                            (colors.scrollbar_thumb_active_background, false)
1341                        }
1342                        ThumbState::Hover(hovered_axis) if hovered_axis == axis => {
1343                            (colors.scrollbar_thumb_hover_background, true)
1344                        }
1345                        _ => (colors.scrollbar_thumb_background, false),
1346                    };
1347
1348                    let blend_color = track_config
1349                        .as_ref()
1350                        .map(|(_, colors)| colors.background)
1351                        .unwrap_or(colors.surface_background);
1352
1353                    let blending_color = if hovered || reserved_space.needs_scroll_track() {
1354                        blend_color
1355                    } else {
1356                        blend_color.min(blend_color.alpha(MAXIMUM_OPACITY))
1357                    };
1358
1359                    let mut thumb_color = blending_color.blend(thumb_base_color);
1360
1361                    if !hovered && let Some(fade) = autohide_fade {
1362                        thumb_color.fade_out(fade);
1363                    }
1364
1365                    if let Some((track_bounds, colors)) = track_config {
1366                        let has_border = colors.has_border;
1367
1368                        let mut track_color = colors.background;
1369                        if let Some(fade) = autohide_fade
1370                            && !has_border
1371                        {
1372                            track_color.fade_out(fade);
1373                        }
1374
1375                        let border_edges = has_border
1376                            .then(|| match axis {
1377                                ScrollbarAxis::Horizontal => Edges {
1378                                    top: BORDER_WIDTH,
1379                                    ..Default::default()
1380                                },
1381                                ScrollbarAxis::Vertical => Edges {
1382                                    left: BORDER_WIDTH,
1383                                    ..Default::default()
1384                                },
1385                            })
1386                            .unwrap_or_default();
1387
1388                        let border_color = if has_border {
1389                            cx.theme().colors().border_variant.opacity(0.6)
1390                        } else {
1391                            Hsla::transparent_black()
1392                        };
1393
1394                        window.paint_quad(quad(
1395                            *track_bounds,
1396                            Corners::default(),
1397                            track_color,
1398                            border_edges,
1399                            border_color,
1400                            BorderStyle::Solid,
1401                        ));
1402                    }
1403
1404                    window.paint_quad(quad(
1405                        *thumb_bounds,
1406                        match style {
1407                            ScrollbarStyle::Regular => Corners::all(Pixels::MAX)
1408                                .clamp_radii_for_quad_size(thumb_bounds.size),
1409                            ScrollbarStyle::Editor => Corners::default(),
1410                        },
1411                        thumb_color,
1412                        Edges::default(),
1413                        Hsla::transparent_black(),
1414                        BorderStyle::default(),
1415                    ));
1416
1417                    if thumb_state.is_dragging() {
1418                        window.set_window_cursor_style(CursorStyle::Arrow);
1419                    } else {
1420                        window.set_cursor_style(CursorStyle::Arrow, cursor_hitbox);
1421                    }
1422                }
1423            } else {
1424                capture_phase = DispatchPhase::Bubble;
1425            }
1426
1427            self.state.update(cx, |state, _| {
1428                state.last_prepaint_state = Some(prepaint_state)
1429            });
1430
1431            window.on_mouse_event({
1432                let state = self.state.clone();
1433
1434                move |event: &MouseDownEvent, phase, window, cx| {
1435                    state.update(cx, |state, cx| {
1436                        let Some(scrollbar_layout) = (phase == capture_phase
1437                            && event.button == MouseButton::Left)
1438                            .then(|| state.hit_for_position(&event.position))
1439                            .flatten()
1440                        else {
1441                            return;
1442                        };
1443
1444                        let ScrollbarLayout {
1445                            thumb_bounds, axis, ..
1446                        } = scrollbar_layout;
1447
1448                        if thumb_bounds.contains(&event.position) {
1449                            let offset =
1450                                event.position.along(*axis) - thumb_bounds.origin.along(*axis);
1451                            state.set_dragging(*axis, offset, window, cx);
1452                        } else {
1453                            let scroll_handle = state.scroll_handle();
1454                            let click_offset = scrollbar_layout.compute_click_offset(
1455                                event.position,
1456                                scroll_handle.max_offset(),
1457                                ScrollbarMouseEvent::TrackClick,
1458                            );
1459                            state.set_offset(
1460                                scroll_handle.offset().apply_along(*axis, |_| click_offset),
1461                                cx,
1462                            );
1463                        };
1464
1465                        cx.stop_propagation();
1466                    });
1467                }
1468            });
1469
1470            window.on_mouse_event({
1471                let state = self.state.clone();
1472
1473                move |event: &ScrollWheelEvent, phase, window, cx| {
1474                    state.update(cx, |state, cx| {
1475                        if phase.capture() && state.parent_hovered(window) {
1476                            state.update_hovered_thumb(&event.position, window, cx)
1477                        }
1478                    });
1479                }
1480            });
1481
1482            window.on_mouse_event({
1483                let state = self.state.clone();
1484
1485                move |event: &MouseMoveEvent, phase, window, cx| {
1486                    if phase != capture_phase {
1487                        return;
1488                    }
1489
1490                    match state.read(cx).thumb_state {
1491                        ThumbState::Dragging(axis, drag_state) if event.dragging() => {
1492                            if let Some(scrollbar_layout) = state.read(cx).thumb_for_axis(axis) {
1493                                let scroll_handle = state.read(cx).scroll_handle();
1494                                let drag_offset = scrollbar_layout.compute_click_offset(
1495                                    event.position,
1496                                    scroll_handle.max_offset(),
1497                                    ScrollbarMouseEvent::ThumbDrag(drag_state),
1498                                );
1499                                let new_offset =
1500                                    scroll_handle.offset().apply_along(axis, |_| drag_offset);
1501
1502                                state.update(cx, |state, cx| state.set_offset(new_offset, cx));
1503                                cx.stop_propagation();
1504                            }
1505                        }
1506                        _ => state.update(cx, |state, cx| {
1507                            match state.update_parent_hovered(window) {
1508                                hover @ ParentHoverEvent::Entered
1509                                | hover @ ParentHoverEvent::Within
1510                                    if event.pressed_button.is_none() =>
1511                                {
1512                                    if matches!(hover, ParentHoverEvent::Entered) {
1513                                        state.show_scrollbars(window, cx);
1514                                    }
1515                                    state.update_hovered_thumb(&event.position, window, cx);
1516                                    if state.thumb_state != ThumbState::Inactive {
1517                                        cx.stop_propagation();
1518                                    }
1519                                }
1520                                ParentHoverEvent::Exited => {
1521                                    state.set_thumb_state(ThumbState::Inactive, window, cx);
1522                                }
1523                                _ => {}
1524                            }
1525                        }),
1526                    }
1527                }
1528            });
1529
1530            window.on_mouse_event({
1531                let state = self.state.clone();
1532                move |event: &MouseUpEvent, phase, window, cx| {
1533                    if phase != capture_phase {
1534                        return;
1535                    }
1536
1537                    state.update(cx, |state, cx| {
1538                        if state.is_dragging() {
1539                            state.scroll_handle().drag_ended();
1540                        }
1541
1542                        if !state.parent_hovered(window) {
1543                            state.schedule_auto_hide(window, cx);
1544                            return;
1545                        }
1546
1547                        state.update_hovered_thumb(&event.position, window, cx);
1548                    });
1549                }
1550            });
1551        })
1552    }
1553}
1554
1555impl<T: ScrollableHandle> IntoElement for ScrollbarElement<T> {
1556    type Element = Self;
1557
1558    fn into_element(self) -> Self::Element {
1559        self
1560    }
1561}