Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:36:15.499Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

scrollbar.rs

1576 lines · 53.3 KB · rust
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                    } => {
1268                        if cx.reduce_motion() {
1269                            self.state.update(cx, |state, _| {
1270                                let has_border = state
1271                                    .track_color
1272                                    .as_ref()
1273                                    .is_some_and(|track_colors| track_colors.has_border);
1274                                state.show_state.set_delta(DELTA_MAX, has_border)
1275                            });
1276                            if should_invert { 0.0 } else { DELTA_MAX }
1277                        } else {
1278                            window.with_element_state(id.unwrap(), |state, window| {
1279                                let state = state.unwrap_or_else(|| Instant::now());
1280                                let current = Instant::now();
1281
1282                                let new_delta = DELTA_MAX.min(
1283                                    current_delta
1284                                        + (current - state).div_duration_f32(delta_duration),
1285                                );
1286                                self.state.update(cx, |state, _| {
1287                                    let has_border = state
1288                                        .track_color
1289                                        .as_ref()
1290                                        .is_some_and(|track_colors| track_colors.has_border);
1291                                    state.show_state.set_delta(new_delta, has_border)
1292                                });
1293
1294                                window.request_animation_frame();
1295                                let delta = if should_invert {
1296                                    DELTA_MAX - current_delta
1297                                } else {
1298                                    current_delta
1299                                };
1300                                (ease_in_out(delta), current)
1301                            })
1302                        }
1303                    }
1304                    AnimationState::Stale => 1.0,
1305                });
1306
1307            (state, autohide_delta)
1308        })
1309    }
1310
1311    fn paint(
1312        &mut self,
1313        _id: Option<&GlobalElementId>,
1314        _inspector_id: Option<&gpui::InspectorElementId>,
1315        Bounds { origin, size }: Bounds<Pixels>,
1316        _request_layout: &mut Self::RequestLayoutState,
1317        prepaint_state: &mut Self::PrepaintState,
1318        window: &mut Window,
1319        cx: &mut App,
1320    ) {
1321        let Some((prepaint_state, autohide_fade)) = prepaint_state.take() else {
1322            return;
1323        };
1324
1325        let bounds = Bounds::new(self.origin + origin, size);
1326        window.with_content_mask(Some(ContentMask { bounds }), |window| {
1327            let colors = cx.theme().colors();
1328
1329            let capture_phase;
1330
1331            if self.state.read(cx).visible() {
1332                let state = self.state.read(cx);
1333                let thumb_state = &state.thumb_state;
1334                let style = state.style;
1335
1336                if thumb_state.is_dragging() {
1337                    capture_phase = DispatchPhase::Capture;
1338                } else {
1339                    capture_phase = DispatchPhase::Bubble;
1340                }
1341
1342                for ScrollbarLayout {
1343                    thumb_bounds,
1344                    cursor_hitbox,
1345                    axis,
1346                    reserved_space,
1347                    track_config,
1348                    ..
1349                } in &prepaint_state.thumbs
1350                {
1351                    const MAXIMUM_OPACITY: f32 = 0.7;
1352                    let (thumb_base_color, hovered) = match thumb_state {
1353                        ThumbState::Dragging(dragged_axis, _) if dragged_axis == axis => {
1354                            (colors.scrollbar_thumb_active_background, false)
1355                        }
1356                        ThumbState::Hover(hovered_axis) if hovered_axis == axis => {
1357                            (colors.scrollbar_thumb_hover_background, true)
1358                        }
1359                        _ => (colors.scrollbar_thumb_background, false),
1360                    };
1361
1362                    let blend_color = track_config
1363                        .as_ref()
1364                        .map(|(_, colors)| colors.background)
1365                        .unwrap_or(colors.surface_background);
1366
1367                    let blending_color = if hovered || reserved_space.needs_scroll_track() {
1368                        blend_color
1369                    } else {
1370                        blend_color.min(blend_color.alpha(MAXIMUM_OPACITY))
1371                    };
1372
1373                    let mut thumb_color = blending_color.blend(thumb_base_color);
1374
1375                    if !hovered && let Some(fade) = autohide_fade {
1376                        thumb_color.fade_out(fade);
1377                    }
1378
1379                    if let Some((track_bounds, colors)) = track_config {
1380                        let has_border = colors.has_border;
1381
1382                        let mut track_color = colors.background;
1383                        if let Some(fade) = autohide_fade
1384                            && !has_border
1385                        {
1386                            track_color.fade_out(fade);
1387                        }
1388
1389                        let border_edges = has_border
1390                            .then(|| match axis {
1391                                ScrollbarAxis::Horizontal => Edges {
1392                                    top: BORDER_WIDTH,
1393                                    ..Default::default()
1394                                },
1395                                ScrollbarAxis::Vertical => Edges {
1396                                    left: BORDER_WIDTH,
1397                                    ..Default::default()
1398                                },
1399                            })
1400                            .unwrap_or_default();
1401
1402                        let border_color = if has_border {
1403                            cx.theme().colors().border_variant.opacity(0.6)
1404                        } else {
1405                            Hsla::transparent_black()
1406                        };
1407
1408                        window.paint_quad(quad(
1409                            *track_bounds,
1410                            Corners::default(),
1411                            track_color,
1412                            border_edges,
1413                            border_color,
1414                            BorderStyle::Solid,
1415                        ));
1416                    }
1417
1418                    window.paint_quad(quad(
1419                        *thumb_bounds,
1420                        match style {
1421                            ScrollbarStyle::Regular => Corners::all(Pixels::MAX)
1422                                .clamp_radii_for_quad_size(thumb_bounds.size),
1423                            ScrollbarStyle::Editor => Corners::default(),
1424                        },
1425                        thumb_color,
1426                        Edges::default(),
1427                        Hsla::transparent_black(),
1428                        BorderStyle::default(),
1429                    ));
1430
1431                    if thumb_state.is_dragging() {
1432                        window.set_window_cursor_style(CursorStyle::Arrow);
1433                    } else {
1434                        window.set_cursor_style(CursorStyle::Arrow, cursor_hitbox);
1435                    }
1436                }
1437            } else {
1438                capture_phase = DispatchPhase::Bubble;
1439            }
1440
1441            self.state.update(cx, |state, _| {
1442                state.last_prepaint_state = Some(prepaint_state)
1443            });
1444
1445            window.on_mouse_event({
1446                let state = self.state.clone();
1447
1448                move |event: &MouseDownEvent, phase, window, cx| {
1449                    state.update(cx, |state, cx| {
1450                        let Some(scrollbar_layout) = (phase == capture_phase
1451                            && event.button == MouseButton::Left)
1452                            .then(|| state.hit_for_position(&event.position))
1453                            .flatten()
1454                        else {
1455                            return;
1456                        };
1457
1458                        let ScrollbarLayout {
1459                            thumb_bounds, axis, ..
1460                        } = scrollbar_layout;
1461
1462                        if thumb_bounds.contains(&event.position) {
1463                            let offset =
1464                                event.position.along(*axis) - thumb_bounds.origin.along(*axis);
1465                            state.set_dragging(*axis, offset, window, cx);
1466                        } else {
1467                            let scroll_handle = state.scroll_handle();
1468                            let click_offset = scrollbar_layout.compute_click_offset(
1469                                event.position,
1470                                scroll_handle.max_offset(),
1471                                ScrollbarMouseEvent::TrackClick,
1472                            );
1473                            state.set_offset(
1474                                scroll_handle.offset().apply_along(*axis, |_| click_offset),
1475                                cx,
1476                            );
1477                        };
1478
1479                        cx.stop_propagation();
1480                    });
1481                }
1482            });
1483
1484            window.on_mouse_event({
1485                let state = self.state.clone();
1486
1487                move |event: &ScrollWheelEvent, phase, window, cx| {
1488                    state.update(cx, |state, cx| {
1489                        if phase.capture() && state.parent_hovered(window) {
1490                            state.update_hovered_thumb(&event.position, window, cx)
1491                        }
1492                    });
1493                }
1494            });
1495
1496            window.on_mouse_event({
1497                let state = self.state.clone();
1498
1499                move |event: &MouseMoveEvent, phase, window, cx| {
1500                    if phase != capture_phase {
1501                        return;
1502                    }
1503
1504                    match state.read(cx).thumb_state {
1505                        ThumbState::Dragging(axis, drag_state) if event.dragging() => {
1506                            if let Some(scrollbar_layout) = state.read(cx).thumb_for_axis(axis) {
1507                                let scroll_handle = state.read(cx).scroll_handle();
1508                                let drag_offset = scrollbar_layout.compute_click_offset(
1509                                    event.position,
1510                                    scroll_handle.max_offset(),
1511                                    ScrollbarMouseEvent::ThumbDrag(drag_state),
1512                                );
1513                                let new_offset =
1514                                    scroll_handle.offset().apply_along(axis, |_| drag_offset);
1515
1516                                state.update(cx, |state, cx| state.set_offset(new_offset, cx));
1517                                cx.stop_propagation();
1518                            }
1519                        }
1520                        _ => state.update(cx, |state, cx| {
1521                            match state.update_parent_hovered(window) {
1522                                hover @ ParentHoverEvent::Entered
1523                                | hover @ ParentHoverEvent::Within
1524                                    if event.pressed_button.is_none() =>
1525                                {
1526                                    if matches!(hover, ParentHoverEvent::Entered) {
1527                                        state.show_scrollbars(window, cx);
1528                                    }
1529                                    state.update_hovered_thumb(&event.position, window, cx);
1530                                    if state.thumb_state != ThumbState::Inactive {
1531                                        cx.stop_propagation();
1532                                    }
1533                                }
1534                                ParentHoverEvent::Exited => {
1535                                    state.set_thumb_state(ThumbState::Inactive, window, cx);
1536                                }
1537                                _ => {}
1538                            }
1539                        }),
1540                    }
1541                }
1542            });
1543
1544            window.on_mouse_event({
1545                let state = self.state.clone();
1546                move |event: &MouseUpEvent, phase, window, cx| {
1547                    if phase != capture_phase {
1548                        return;
1549                    }
1550
1551                    state.update(cx, |state, cx| {
1552                        if state.is_dragging() {
1553                            state.scroll_handle().drag_ended();
1554                        }
1555
1556                        if !state.parent_hovered(window) {
1557                            state.schedule_auto_hide(window, cx);
1558                            return;
1559                        }
1560
1561                        state.update_hovered_thumb(&event.position, window, cx);
1562                    });
1563                }
1564            });
1565        })
1566    }
1567}
1568
1569impl<T: ScrollableHandle> IntoElement for ScrollbarElement<T> {
1570    type Element = Self;
1571
1572    fn into_element(self) -> Self::Element {
1573        self
1574    }
1575}
1576
Served at tenant.openagents/omega Member data and write actions are omitted.