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, 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 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,
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) -> Point<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) -> Point<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) -> Point<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) -> Point<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().into()
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: Point<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 if self.axis != other.axis {
1045 return false;
1046 }
1047
1048 let axis = self.axis;
1049 let thumb_offset =
1050 self.thumb_bounds.origin.along(axis) - self.track_bounds.origin.along(axis);
1051 let other_thumb_offset =
1052 other.thumb_bounds.origin.along(axis) - other.track_bounds.origin.along(axis);
1053
1054 thumb_offset == other_thumb_offset
1055 && self.thumb_bounds.size.along(axis) == other.thumb_bounds.size.along(axis)
1056 }
1057}
1058
1059pub struct ScrollbarPrepaintState {
1060 parent_bounds_hitbox: Hitbox,
1061 thumbs: SmallVec<[ScrollbarLayout; 2]>,
1062}
1063
1064impl ScrollbarPrepaintState {
1065 fn thumb_for_position(&self, position: &Point<Pixels>) -> Option<&ScrollbarLayout> {
1066 self.thumbs
1067 .iter()
1068 .find(|info| info.thumb_bounds.contains(position))
1069 }
1070
1071 fn hit_for_position(&self, position: &Point<Pixels>) -> Option<&ScrollbarLayout> {
1072 self.thumbs.iter().find(|info| {
1073 if info.reserved_space.needs_scroll_track() {
1074 info.track_bounds.contains(position)
1075 } else {
1076 info.thumb_bounds.contains(position)
1077 }
1078 })
1079 }
1080}
1081
1082impl PartialEq for ScrollbarPrepaintState {
1083 fn eq(&self, other: &Self) -> bool {
1084 self.thumbs == other.thumbs
1085 }
1086}
1087
1088impl<T: ScrollableHandle> Element for ScrollbarElement<T> {
1089 type RequestLayoutState = ();
1090 type PrepaintState = Option<(ScrollbarPrepaintState, Option<f32>)>;
1091
1092 fn id(&self) -> Option<ElementId> {
1093 Some(("scrollbar_animation", self.state.entity_id()).into())
1094 }
1095
1096 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
1097 None
1098 }
1099
1100 fn request_layout(
1101 &mut self,
1102 _id: Option<&GlobalElementId>,
1103 _inspector_id: Option<&gpui::InspectorElementId>,
1104 window: &mut Window,
1105 cx: &mut App,
1106 ) -> (LayoutId, Self::RequestLayoutState) {
1107 let scrollbar_style = Style {
1108 position: Position::Absolute,
1109 inset: Edges::default(),
1110 size: size(relative(1.), relative(1.)).map(Into::into),
1111 ..Default::default()
1112 };
1113
1114 (window.request_layout(scrollbar_style, None, cx), ())
1115 }
1116
1117 fn prepaint(
1118 &mut self,
1119 id: Option<&GlobalElementId>,
1120 _inspector_id: Option<&gpui::InspectorElementId>,
1121 bounds: Bounds<Pixels>,
1122 _request_layout: &mut Self::RequestLayoutState,
1123 window: &mut Window,
1124 cx: &mut App,
1125 ) -> Self::PrepaintState {
1126 let prepaint_state = self
1127 .state
1128 .read(cx)
1129 .disabled()
1130 .not()
1131 .then(|| ScrollbarPrepaintState {
1132 thumbs: {
1133 let state = self.state.read(cx);
1134 let thumb_ranges = state.thumb_ranges().collect::<Vec<_>>();
1135 let width = state.width.to_pixels();
1136 let track_color = state.track_color;
1137
1138 let additional_padding = if thumb_ranges.len() == 2 {
1139 width
1140 } else {
1141 Pixels::ZERO
1142 };
1143
1144 thumb_ranges
1145 .into_iter()
1146 .map(|(axis, thumb_range, reserved_space)| {
1147 let track_anchor = match axis {
1148 ScrollbarAxis::Horizontal => Corner::BottomLeft,
1149 ScrollbarAxis::Vertical => Corner::TopRight,
1150 };
1151 let Bounds { origin, size } = Bounds::from_corner_and_size(
1152 track_anchor,
1153 bounds
1154 .corner(track_anchor)
1155 .apply_along(axis.invert(), |corner| {
1156 corner - SCROLLBAR_PADDING
1157 }),
1158 bounds.size.apply_along(axis.invert(), |_| width),
1159 );
1160 let scroll_track_bounds = Bounds::new(self.origin + origin, size);
1161
1162 let padded_bounds = scroll_track_bounds.extend(match axis {
1163 ScrollbarAxis::Horizontal => Edges {
1164 right: -SCROLLBAR_PADDING,
1165 left: -SCROLLBAR_PADDING,
1166 ..Default::default()
1167 },
1168 ScrollbarAxis::Vertical => Edges {
1169 top: -SCROLLBAR_PADDING,
1170 bottom: -SCROLLBAR_PADDING,
1171 ..Default::default()
1172 },
1173 });
1174
1175 let available_space =
1176 padded_bounds.size.along(axis) - additional_padding;
1177
1178 let thumb_offset = thumb_range.start * available_space;
1179 let thumb_end = thumb_range.end * available_space;
1180 let thumb_bounds = Bounds::new(
1181 padded_bounds
1182 .origin
1183 .apply_along(axis, |origin| origin + thumb_offset),
1184 padded_bounds
1185 .size
1186 .apply_along(axis, |_| thumb_end - thumb_offset),
1187 );
1188
1189 let needs_scroll_track = reserved_space.needs_scroll_track();
1190
1191 ScrollbarLayout {
1192 thumb_bounds,
1193 track_bounds: padded_bounds,
1194 axis,
1195 cursor_hitbox: window.insert_hitbox(
1196 if needs_scroll_track {
1197 padded_bounds
1198 } else {
1199 thumb_bounds
1200 },
1201 HitboxBehavior::BlockMouseExceptScroll,
1202 ),
1203 track_background: track_color
1204 .filter(|_| needs_scroll_track)
1205 .map(|color| (padded_bounds.dilate(SCROLLBAR_PADDING), color)),
1206 reserved_space,
1207 }
1208 })
1209 .collect()
1210 },
1211 parent_bounds_hitbox: window.insert_hitbox(bounds, HitboxBehavior::Normal),
1212 });
1213 if prepaint_state
1214 .as_ref()
1215 .is_some_and(|state| Some(state) != self.state.read(cx).last_prepaint_state.as_ref())
1216 {
1217 self.state
1218 .update(cx, |state, cx| state.show_scrollbars(window, cx));
1219 }
1220
1221 prepaint_state.map(|state| {
1222 let autohide_delta = self.state.read(cx).show_state.animation_progress().map(
1223 |(delta, delta_duration, should_invert)| {
1224 window.with_element_state(id.unwrap(), |state, window| {
1225 let state = state.unwrap_or_else(|| Instant::now());
1226 let current = Instant::now();
1227
1228 let new_delta = DELTA_MAX
1229 .min(delta + (current - state).div_duration_f32(delta_duration));
1230 self.state
1231 .update(cx, |state, _| state.show_state.set_delta(new_delta));
1232
1233 window.request_animation_frame();
1234 let delta = if should_invert {
1235 DELTA_MAX - delta
1236 } else {
1237 delta
1238 };
1239 (ease_in_out(delta), current)
1240 })
1241 },
1242 );
1243
1244 (state, autohide_delta)
1245 })
1246 }
1247
1248 fn paint(
1249 &mut self,
1250 _id: Option<&GlobalElementId>,
1251 _inspector_id: Option<&gpui::InspectorElementId>,
1252 Bounds { origin, size }: Bounds<Pixels>,
1253 _request_layout: &mut Self::RequestLayoutState,
1254 prepaint_state: &mut Self::PrepaintState,
1255 window: &mut Window,
1256 cx: &mut App,
1257 ) {
1258 let Some((prepaint_state, autohide_fade)) = prepaint_state.take() else {
1259 return;
1260 };
1261
1262 let bounds = Bounds::new(self.origin + origin, size);
1263 window.with_content_mask(Some(ContentMask { bounds }), |window| {
1264 let colors = cx.theme().colors();
1265
1266 let capture_phase;
1267
1268 if self.state.read(cx).visible() {
1269 let thumb_state = &self.state.read(cx).thumb_state;
1270
1271 if thumb_state.is_dragging() {
1272 capture_phase = DispatchPhase::Capture;
1273 } else {
1274 capture_phase = DispatchPhase::Bubble;
1275 }
1276
1277 for ScrollbarLayout {
1278 thumb_bounds,
1279 cursor_hitbox,
1280 axis,
1281 reserved_space,
1282 track_background,
1283 ..
1284 } in &prepaint_state.thumbs
1285 {
1286 const MAXIMUM_OPACITY: f32 = 0.7;
1287 let (thumb_base_color, hovered) = match thumb_state {
1288 ThumbState::Dragging(dragged_axis, _) if dragged_axis == axis => {
1289 (colors.scrollbar_thumb_active_background, false)
1290 }
1291 ThumbState::Hover(hovered_axis) if hovered_axis == axis => {
1292 (colors.scrollbar_thumb_hover_background, true)
1293 }
1294 _ => (colors.scrollbar_thumb_background, false),
1295 };
1296
1297 let blending_color = if hovered || reserved_space.needs_scroll_track() {
1298 track_background
1299 .map(|(_, background)| background)
1300 .unwrap_or(colors.surface_background)
1301 } else {
1302 let blend_color = colors.surface_background;
1303 blend_color.min(blend_color.alpha(MAXIMUM_OPACITY))
1304 };
1305
1306 let mut thumb_color = blending_color.blend(thumb_base_color);
1307
1308 if !hovered && let Some(fade) = autohide_fade {
1309 thumb_color.fade_out(fade);
1310 }
1311
1312 if let Some((track_bounds, color)) = track_background {
1313 let mut color = *color;
1314 if let Some(fade) = autohide_fade {
1315 color.fade_out(fade);
1316 }
1317
1318 window.paint_quad(quad(
1319 *track_bounds,
1320 Corners::default(),
1321 color,
1322 Edges::default(),
1323 Hsla::transparent_black(),
1324 BorderStyle::default(),
1325 ));
1326 }
1327
1328 window.paint_quad(quad(
1329 *thumb_bounds,
1330 Corners::all(Pixels::MAX).clamp_radii_for_quad_size(thumb_bounds.size),
1331 thumb_color,
1332 Edges::default(),
1333 Hsla::transparent_black(),
1334 BorderStyle::default(),
1335 ));
1336
1337 if thumb_state.is_dragging() {
1338 window.set_window_cursor_style(CursorStyle::Arrow);
1339 } else {
1340 window.set_cursor_style(CursorStyle::Arrow, cursor_hitbox);
1341 }
1342 }
1343 } else {
1344 capture_phase = DispatchPhase::Bubble;
1345 }
1346
1347 self.state.update(cx, |state, _| {
1348 state.last_prepaint_state = Some(prepaint_state)
1349 });
1350
1351 window.on_mouse_event({
1352 let state = self.state.clone();
1353
1354 move |event: &MouseDownEvent, phase, window, cx| {
1355 state.update(cx, |state, cx| {
1356 let Some(scrollbar_layout) = (phase == capture_phase
1357 && event.button == MouseButton::Left)
1358 .then(|| state.hit_for_position(&event.position))
1359 .flatten()
1360 else {
1361 return;
1362 };
1363
1364 let ScrollbarLayout {
1365 thumb_bounds, axis, ..
1366 } = scrollbar_layout;
1367
1368 if thumb_bounds.contains(&event.position) {
1369 let offset =
1370 event.position.along(*axis) - thumb_bounds.origin.along(*axis);
1371 state.set_dragging(*axis, offset, window, cx);
1372 } else {
1373 let scroll_handle = state.scroll_handle();
1374 let click_offset = scrollbar_layout.compute_click_offset(
1375 event.position,
1376 scroll_handle.max_offset(),
1377 ScrollbarMouseEvent::TrackClick,
1378 );
1379 state.set_offset(
1380 scroll_handle.offset().apply_along(*axis, |_| click_offset),
1381 cx,
1382 );
1383 };
1384
1385 cx.stop_propagation();
1386 });
1387 }
1388 });
1389
1390 window.on_mouse_event({
1391 let state = self.state.clone();
1392
1393 move |event: &ScrollWheelEvent, phase, window, cx| {
1394 state.update(cx, |state, cx| {
1395 if phase.capture() && state.parent_hovered(window) {
1396 state.update_hovered_thumb(&event.position, window, cx)
1397 }
1398 });
1399 }
1400 });
1401
1402 window.on_mouse_event({
1403 let state = self.state.clone();
1404
1405 move |event: &MouseMoveEvent, phase, window, cx| {
1406 if phase != capture_phase {
1407 return;
1408 }
1409
1410 match state.read(cx).thumb_state {
1411 ThumbState::Dragging(axis, drag_state) if event.dragging() => {
1412 if let Some(scrollbar_layout) = state.read(cx).thumb_for_axis(axis) {
1413 let scroll_handle = state.read(cx).scroll_handle();
1414 let drag_offset = scrollbar_layout.compute_click_offset(
1415 event.position,
1416 scroll_handle.max_offset(),
1417 ScrollbarMouseEvent::ThumbDrag(drag_state),
1418 );
1419 let new_offset =
1420 scroll_handle.offset().apply_along(axis, |_| drag_offset);
1421
1422 state.update(cx, |state, cx| state.set_offset(new_offset, cx));
1423 cx.stop_propagation();
1424 }
1425 }
1426 _ => state.update(cx, |state, cx| {
1427 match state.update_parent_hovered(window) {
1428 hover @ ParentHoverEvent::Entered
1429 | hover @ ParentHoverEvent::Within
1430 if event.pressed_button.is_none() =>
1431 {
1432 if matches!(hover, ParentHoverEvent::Entered) {
1433 state.show_scrollbars(window, cx);
1434 }
1435 state.update_hovered_thumb(&event.position, window, cx);
1436 if state.thumb_state != ThumbState::Inactive {
1437 cx.stop_propagation();
1438 }
1439 }
1440 ParentHoverEvent::Exited => {
1441 state.set_thumb_state(ThumbState::Inactive, window, cx);
1442 }
1443 _ => {}
1444 }
1445 }),
1446 }
1447 }
1448 });
1449
1450 window.on_mouse_event({
1451 let state = self.state.clone();
1452 move |event: &MouseUpEvent, phase, window, cx| {
1453 if phase != capture_phase {
1454 return;
1455 }
1456
1457 state.update(cx, |state, cx| {
1458 if state.is_dragging() {
1459 state.scroll_handle().drag_ended();
1460 }
1461
1462 if !state.parent_hovered(window) {
1463 state.schedule_auto_hide(window, cx);
1464 return;
1465 }
1466
1467 state.update_hovered_thumb(&event.position, window, cx);
1468 });
1469 }
1470 });
1471 })
1472 }
1473}
1474
1475impl<T: ScrollableHandle> IntoElement for ScrollbarElement<T> {
1476 type Element = Self;
1477
1478 fn into_element(self) -> Self::Element {
1479 self
1480 }
1481}