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