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 let needs_scroll_track = reserved_space.needs_scroll_track();
1175
1176 ScrollbarLayout {
1177 thumb_bounds,
1178 track_bounds: padded_bounds,
1179 axis,
1180 cursor_hitbox: window.insert_hitbox(
1181 if needs_scroll_track {
1182 padded_bounds
1183 } else {
1184 thumb_bounds
1185 },
1186 HitboxBehavior::BlockMouseExceptScroll,
1187 ),
1188 track_background: track_color
1189 .filter(|_| needs_scroll_track)
1190 .map(|color| (padded_bounds.dilate(SCROLLBAR_PADDING), color)),
1191 reserved_space,
1192 }
1193 })
1194 .collect()
1195 },
1196 parent_bounds_hitbox: window.insert_hitbox(bounds, HitboxBehavior::Normal),
1197 });
1198 if prepaint_state
1199 .as_ref()
1200 .is_some_and(|state| Some(state) != self.state.read(cx).last_prepaint_state.as_ref())
1201 {
1202 self.state
1203 .update(cx, |state, cx| state.show_scrollbars(window, cx));
1204 }
1205
1206 prepaint_state.map(|state| {
1207 let autohide_delta = self.state.read(cx).show_state.animation_progress().map(
1208 |(delta, delta_duration, should_invert)| {
1209 window.with_element_state(id.unwrap(), |state, window| {
1210 let state = state.unwrap_or_else(|| Instant::now());
1211 let current = Instant::now();
1212
1213 let new_delta = DELTA_MAX
1214 .min(delta + (current - state).div_duration_f32(delta_duration));
1215 self.state
1216 .update(cx, |state, _| state.show_state.set_delta(new_delta));
1217
1218 window.request_animation_frame();
1219 let delta = if should_invert {
1220 DELTA_MAX - delta
1221 } else {
1222 delta
1223 };
1224 (ease_in_out(delta), current)
1225 })
1226 },
1227 );
1228
1229 (state, autohide_delta)
1230 })
1231 }
1232
1233 fn paint(
1234 &mut self,
1235 _id: Option<&GlobalElementId>,
1236 _inspector_id: Option<&gpui::InspectorElementId>,
1237 Bounds { origin, size }: Bounds<Pixels>,
1238 _request_layout: &mut Self::RequestLayoutState,
1239 prepaint_state: &mut Self::PrepaintState,
1240 window: &mut Window,
1241 cx: &mut App,
1242 ) {
1243 let Some((prepaint_state, autohide_fade)) = prepaint_state.take() else {
1244 return;
1245 };
1246
1247 let bounds = Bounds::new(self.origin + origin, size);
1248 window.with_content_mask(Some(ContentMask { bounds }), |window| {
1249 let colors = cx.theme().colors();
1250
1251 let capture_phase;
1252
1253 if self.state.read(cx).visible() {
1254 let thumb_state = &self.state.read(cx).thumb_state;
1255
1256 if thumb_state.is_dragging() {
1257 capture_phase = DispatchPhase::Capture;
1258 } else {
1259 capture_phase = DispatchPhase::Bubble;
1260 }
1261
1262 for ScrollbarLayout {
1263 thumb_bounds,
1264 cursor_hitbox,
1265 axis,
1266 reserved_space,
1267 track_background,
1268 ..
1269 } in &prepaint_state.thumbs
1270 {
1271 const MAXIMUM_OPACITY: f32 = 0.7;
1272 let (thumb_base_color, hovered) = match thumb_state {
1273 ThumbState::Dragging(dragged_axis, _) if dragged_axis == axis => {
1274 (colors.scrollbar_thumb_active_background, false)
1275 }
1276 ThumbState::Hover(hovered_axis) if hovered_axis == axis => {
1277 (colors.scrollbar_thumb_hover_background, true)
1278 }
1279 _ => (colors.scrollbar_thumb_background, false),
1280 };
1281
1282 let blending_color = if hovered || reserved_space.needs_scroll_track() {
1283 track_background
1284 .map(|(_, background)| background)
1285 .unwrap_or(colors.surface_background)
1286 } else {
1287 let blend_color = colors.surface_background;
1288 blend_color.min(blend_color.alpha(MAXIMUM_OPACITY))
1289 };
1290
1291 let mut thumb_color = blending_color.blend(thumb_base_color);
1292
1293 if !hovered && let Some(fade) = autohide_fade {
1294 thumb_color.fade_out(fade);
1295 }
1296
1297 if let Some((track_bounds, color)) = track_background {
1298 let mut color = *color;
1299 if let Some(fade) = autohide_fade {
1300 color.fade_out(fade);
1301 }
1302
1303 window.paint_quad(quad(
1304 *track_bounds,
1305 Corners::default(),
1306 color,
1307 Edges::default(),
1308 Hsla::transparent_black(),
1309 BorderStyle::default(),
1310 ));
1311 }
1312
1313 window.paint_quad(quad(
1314 *thumb_bounds,
1315 Corners::all(Pixels::MAX).clamp_radii_for_quad_size(thumb_bounds.size),
1316 thumb_color,
1317 Edges::default(),
1318 Hsla::transparent_black(),
1319 BorderStyle::default(),
1320 ));
1321
1322 if thumb_state.is_dragging() {
1323 window.set_window_cursor_style(CursorStyle::Arrow);
1324 } else {
1325 window.set_cursor_style(CursorStyle::Arrow, cursor_hitbox);
1326 }
1327 }
1328 } else {
1329 capture_phase = DispatchPhase::Bubble;
1330 }
1331
1332 self.state.update(cx, |state, _| {
1333 state.last_prepaint_state = Some(prepaint_state)
1334 });
1335
1336 window.on_mouse_event({
1337 let state = self.state.clone();
1338
1339 move |event: &MouseDownEvent, phase, window, cx| {
1340 state.update(cx, |state, cx| {
1341 let Some(scrollbar_layout) = (phase == capture_phase
1342 && event.button == MouseButton::Left)
1343 .then(|| state.hit_for_position(&event.position))
1344 .flatten()
1345 else {
1346 return;
1347 };
1348
1349 let ScrollbarLayout {
1350 thumb_bounds, axis, ..
1351 } = scrollbar_layout;
1352
1353 if thumb_bounds.contains(&event.position) {
1354 let offset =
1355 event.position.along(*axis) - thumb_bounds.origin.along(*axis);
1356 state.set_dragging(*axis, offset, window, cx);
1357 } else {
1358 let scroll_handle = state.scroll_handle();
1359 let click_offset = scrollbar_layout.compute_click_offset(
1360 event.position,
1361 scroll_handle.max_offset(),
1362 ScrollbarMouseEvent::TrackClick,
1363 );
1364 state.set_offset(
1365 scroll_handle.offset().apply_along(*axis, |_| click_offset),
1366 cx,
1367 );
1368 };
1369
1370 cx.stop_propagation();
1371 });
1372 }
1373 });
1374
1375 window.on_mouse_event({
1376 let state = self.state.clone();
1377
1378 move |event: &ScrollWheelEvent, phase, window, cx| {
1379 state.update(cx, |state, cx| {
1380 if phase.capture() && state.parent_hovered(window) {
1381 state.update_hovered_thumb(&event.position, window, cx)
1382 }
1383 });
1384 }
1385 });
1386
1387 window.on_mouse_event({
1388 let state = self.state.clone();
1389
1390 move |event: &MouseMoveEvent, phase, window, cx| {
1391 if phase != capture_phase {
1392 return;
1393 }
1394
1395 match state.read(cx).thumb_state {
1396 ThumbState::Dragging(axis, drag_state) if event.dragging() => {
1397 if let Some(scrollbar_layout) = state.read(cx).thumb_for_axis(axis) {
1398 let scroll_handle = state.read(cx).scroll_handle();
1399 let drag_offset = scrollbar_layout.compute_click_offset(
1400 event.position,
1401 scroll_handle.max_offset(),
1402 ScrollbarMouseEvent::ThumbDrag(drag_state),
1403 );
1404 let new_offset =
1405 scroll_handle.offset().apply_along(axis, |_| drag_offset);
1406
1407 state.update(cx, |state, cx| state.set_offset(new_offset, cx));
1408 cx.stop_propagation();
1409 }
1410 }
1411 _ => state.update(cx, |state, cx| {
1412 match state.update_parent_hovered(window) {
1413 hover @ ParentHoverEvent::Entered
1414 | hover @ ParentHoverEvent::Within
1415 if event.pressed_button.is_none() =>
1416 {
1417 if matches!(hover, ParentHoverEvent::Entered) {
1418 state.show_scrollbars(window, cx);
1419 }
1420 state.update_hovered_thumb(&event.position, window, cx);
1421 if state.thumb_state != ThumbState::Inactive {
1422 cx.stop_propagation();
1423 }
1424 }
1425 ParentHoverEvent::Exited => {
1426 state.set_thumb_state(ThumbState::Inactive, window, cx);
1427 }
1428 _ => {}
1429 }
1430 }),
1431 }
1432 }
1433 });
1434
1435 window.on_mouse_event({
1436 let state = self.state.clone();
1437 move |event: &MouseUpEvent, phase, window, cx| {
1438 if phase != capture_phase {
1439 return;
1440 }
1441
1442 state.update(cx, |state, cx| {
1443 if state.is_dragging() {
1444 state.scroll_handle().drag_ended();
1445 }
1446
1447 if !state.parent_hovered(window) {
1448 state.schedule_auto_hide(window, cx);
1449 return;
1450 }
1451
1452 state.update_hovered_thumb(&event.position, window, cx);
1453 });
1454 }
1455 });
1456 })
1457 }
1458}
1459
1460impl<T: ScrollableHandle> IntoElement for ScrollbarElement<T> {
1461 type Element = Self;
1462
1463 fn into_element(self) -> Self::Element {
1464 self
1465 }
1466}