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