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