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