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