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