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, 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, .. }) = self
757 .last_prepaint_state
758 .as_ref()
759 .and_then(|state| state.thumb_for_position(position))
760 {
761 ThumbState::Hover(axis)
762 } else {
763 ThumbState::Inactive
764 },
765 window,
766 cx,
767 );
768 }
769
770 fn set_thumb_state(&mut self, state: ThumbState, window: &mut Window, cx: &mut Context<Self>) {
771 if self.thumb_state != state {
772 if state == ThumbState::Inactive {
773 self.schedule_auto_hide(window, cx);
774 } else {
775 self.set_visibility(self.show_state.toggle_visible(self.show_behavior), cx);
776 self._auto_hide_task.take();
777 }
778 self.thumb_state = state;
779 cx.notify();
780 }
781 }
782
783 fn update_parent_hovered(&mut self, position: &Point<Pixels>) -> ParentHoverEvent {
784 let last_parent_hovered = self.mouse_in_parent;
785 self.mouse_in_parent = self.parent_hovered(position);
786 let state_changed = self.mouse_in_parent != last_parent_hovered;
787 match (self.mouse_in_parent, state_changed) {
788 (true, true) => ParentHoverEvent::Entered,
789 (true, false) => ParentHoverEvent::Within,
790 (false, true) => ParentHoverEvent::Exited,
791 (false, false) => ParentHoverEvent::Outside,
792 }
793 }
794
795 fn parent_hovered(&self, position: &Point<Pixels>) -> bool {
796 self.last_prepaint_state
797 .as_ref()
798 .is_some_and(|state| state.parent_bounds.contains(position))
799 }
800
801 fn hit_for_position(&self, position: &Point<Pixels>) -> Option<&ScrollbarLayout> {
802 self.last_prepaint_state
803 .as_ref()
804 .and_then(|state| state.hit_for_position(position))
805 }
806
807 fn thumb_for_axis(&self, axis: ScrollbarAxis) -> Option<&ScrollbarLayout> {
808 self.last_prepaint_state
809 .as_ref()
810 .and_then(|state| state.thumbs.iter().find(|thumb| thumb.axis == axis))
811 }
812
813 fn thumb_ranges(
814 &self,
815 ) -> impl Iterator<Item = (ScrollbarAxis, Range<f32>, ReservedSpace)> + '_ {
816 const MINIMUM_THUMB_SIZE: Pixels = px(25.);
817 let max_offset = self.scroll_handle().max_offset();
818 let viewport_size = self.scroll_handle().viewport().size;
819 let current_offset = self.scroll_handle().offset();
820
821 [ScrollbarAxis::Horizontal, ScrollbarAxis::Vertical]
822 .into_iter()
823 .filter(|&axis| self.visibility.along(axis).is_visible())
824 .flat_map(move |axis| {
825 let max_offset = max_offset.along(axis);
826 let viewport_size = viewport_size.along(axis);
827 if max_offset.is_zero() || viewport_size.is_zero() {
828 return None;
829 }
830 let content_size = viewport_size + max_offset;
831 let visible_percentage = viewport_size / content_size;
832 let thumb_size = MINIMUM_THUMB_SIZE.max(viewport_size * visible_percentage);
833 if thumb_size > viewport_size {
834 return None;
835 }
836 let current_offset = current_offset
837 .along(axis)
838 .clamp(-max_offset, Pixels::ZERO)
839 .abs();
840 let start_offset = (current_offset / max_offset) * (viewport_size - thumb_size);
841 let thumb_percentage_start = start_offset / viewport_size;
842 let thumb_percentage_end = (start_offset + thumb_size) / viewport_size;
843 Some((
844 axis,
845 thumb_percentage_start..thumb_percentage_end,
846 self.visibility.along(axis),
847 ))
848 })
849 }
850
851 fn visible(&self) -> bool {
852 self.show_state.is_visible()
853 }
854
855 #[inline]
856 fn disabled(&self) -> bool {
857 self.show_state.is_disabled()
858 }
859
860 fn notify_parent(&self, cx: &mut App) {
861 if let Some(entity_id) = self.notify_id {
862 cx.notify(entity_id);
863 }
864 }
865}
866
867impl<T: ScrollableHandle> Render for ScrollbarState<T> {
868 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
869 ScrollbarElement {
870 state: cx.entity(),
871 origin: Default::default(),
872 }
873 }
874}
875
876struct ScrollbarElement<T: ScrollableHandle> {
877 origin: Point<Pixels>,
878 state: Entity<ScrollbarState<T>>,
879}
880
881#[derive(Default, Debug, PartialEq, Eq)]
882enum ThumbState {
883 #[default]
884 Inactive,
885 Hover(ScrollbarAxis),
886 Dragging(ScrollbarAxis, Pixels),
887}
888
889impl ThumbState {
890 fn is_dragging(&self) -> bool {
891 matches!(*self, ThumbState::Dragging(..))
892 }
893}
894
895impl ScrollableHandle for UniformListScrollHandle {
896 fn max_offset(&self) -> Size<Pixels> {
897 self.0.borrow().base_handle.max_offset()
898 }
899
900 fn set_offset(&self, point: Point<Pixels>) {
901 self.0.borrow().base_handle.set_offset(point);
902 }
903
904 fn offset(&self) -> Point<Pixels> {
905 self.0.borrow().base_handle.offset()
906 }
907
908 fn viewport(&self) -> Bounds<Pixels> {
909 self.0.borrow().base_handle.bounds()
910 }
911}
912
913impl ScrollableHandle for ListState {
914 fn max_offset(&self) -> Size<Pixels> {
915 self.max_offset_for_scrollbar()
916 }
917
918 fn set_offset(&self, point: Point<Pixels>) {
919 self.set_offset_from_scrollbar(point);
920 }
921
922 fn offset(&self) -> Point<Pixels> {
923 self.scroll_px_offset_for_scrollbar()
924 }
925
926 fn drag_started(&self) {
927 self.scrollbar_drag_started();
928 }
929
930 fn drag_ended(&self) {
931 self.scrollbar_drag_ended();
932 }
933
934 fn viewport(&self) -> Bounds<Pixels> {
935 self.viewport_bounds()
936 }
937}
938
939impl ScrollableHandle for ScrollHandle {
940 fn max_offset(&self) -> Size<Pixels> {
941 self.max_offset()
942 }
943
944 fn set_offset(&self, point: Point<Pixels>) {
945 self.set_offset(point);
946 }
947
948 fn offset(&self) -> Point<Pixels> {
949 self.offset()
950 }
951
952 fn viewport(&self) -> Bounds<Pixels> {
953 self.bounds()
954 }
955}
956
957pub trait ScrollableHandle: 'static + Any + Sized {
958 fn max_offset(&self) -> Size<Pixels>;
959 fn set_offset(&self, point: Point<Pixels>);
960 fn offset(&self) -> Point<Pixels>;
961 fn viewport(&self) -> Bounds<Pixels>;
962 fn drag_started(&self) {}
963 fn drag_ended(&self) {}
964
965 fn scrollable_along(&self, axis: ScrollbarAxis) -> bool {
966 self.max_offset().along(axis) > Pixels::ZERO
967 }
968 fn content_size(&self) -> Size<Pixels> {
969 self.viewport().size + self.max_offset()
970 }
971}
972
973enum ScrollbarMouseEvent {
974 TrackClick,
975 ThumbDrag(Pixels),
976}
977
978struct ScrollbarLayout {
979 thumb_bounds: Bounds<Pixels>,
980 track_bounds: Bounds<Pixels>,
981 cursor_hitbox: Hitbox,
982 reserved_space: ReservedSpace,
983 track_background: Option<(Bounds<Pixels>, Hsla)>,
984 axis: ScrollbarAxis,
985}
986
987impl ScrollbarLayout {
988 fn compute_click_offset(
989 &self,
990 event_position: Point<Pixels>,
991 max_offset: Size<Pixels>,
992 event_type: ScrollbarMouseEvent,
993 ) -> Pixels {
994 let Self {
995 track_bounds,
996 thumb_bounds,
997 axis,
998 ..
999 } = self;
1000 let axis = *axis;
1001
1002 let viewport_size = track_bounds.size.along(axis);
1003 let thumb_size = thumb_bounds.size.along(axis);
1004 let thumb_offset = match event_type {
1005 ScrollbarMouseEvent::TrackClick => thumb_size / 2.,
1006 ScrollbarMouseEvent::ThumbDrag(thumb_offset) => thumb_offset,
1007 };
1008
1009 let thumb_start =
1010 (event_position.along(axis) - track_bounds.origin.along(axis) - thumb_offset)
1011 .clamp(px(0.), viewport_size - thumb_size);
1012
1013 let max_offset = max_offset.along(axis);
1014 let percentage = if viewport_size > thumb_size {
1015 thumb_start / (viewport_size - thumb_size)
1016 } else {
1017 0.
1018 };
1019
1020 -max_offset * percentage
1021 }
1022}
1023
1024impl PartialEq for ScrollbarLayout {
1025 fn eq(&self, other: &Self) -> bool {
1026 self.axis == other.axis && self.thumb_bounds == other.thumb_bounds
1027 }
1028}
1029
1030pub struct ScrollbarPrepaintState {
1031 parent_bounds: Bounds<Pixels>,
1032 thumbs: SmallVec<[ScrollbarLayout; 2]>,
1033}
1034
1035impl ScrollbarPrepaintState {
1036 fn thumb_for_position(&self, position: &Point<Pixels>) -> Option<&ScrollbarLayout> {
1037 self.thumbs
1038 .iter()
1039 .find(|info| info.thumb_bounds.contains(position))
1040 }
1041
1042 fn hit_for_position(&self, position: &Point<Pixels>) -> Option<&ScrollbarLayout> {
1043 self.thumbs.iter().find(|info| {
1044 if info.reserved_space.needs_scroll_track() {
1045 info.track_bounds.contains(position)
1046 } else {
1047 info.thumb_bounds.contains(position)
1048 }
1049 })
1050 }
1051}
1052
1053impl PartialEq for ScrollbarPrepaintState {
1054 fn eq(&self, other: &Self) -> bool {
1055 self.thumbs == other.thumbs
1056 }
1057}
1058
1059impl<T: ScrollableHandle> Element for ScrollbarElement<T> {
1060 type RequestLayoutState = ();
1061 type PrepaintState = Option<(ScrollbarPrepaintState, Option<f32>)>;
1062
1063 fn id(&self) -> Option<ElementId> {
1064 Some(("scrollbar_animation", self.state.entity_id()).into())
1065 }
1066
1067 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
1068 None
1069 }
1070
1071 fn request_layout(
1072 &mut self,
1073 _id: Option<&GlobalElementId>,
1074 _inspector_id: Option<&gpui::InspectorElementId>,
1075 window: &mut Window,
1076 cx: &mut App,
1077 ) -> (LayoutId, Self::RequestLayoutState) {
1078 let scrollbar_style = Style {
1079 position: Position::Absolute,
1080 inset: Edges::default(),
1081 size: size(relative(1.), relative(1.)).map(Into::into),
1082 ..Default::default()
1083 };
1084
1085 (window.request_layout(scrollbar_style, None, cx), ())
1086 }
1087
1088 fn prepaint(
1089 &mut self,
1090 id: Option<&GlobalElementId>,
1091 _inspector_id: Option<&gpui::InspectorElementId>,
1092 bounds: Bounds<Pixels>,
1093 _request_layout: &mut Self::RequestLayoutState,
1094 window: &mut Window,
1095 cx: &mut App,
1096 ) -> Self::PrepaintState {
1097 let prepaint_state = self
1098 .state
1099 .read(cx)
1100 .disabled()
1101 .not()
1102 .then(|| ScrollbarPrepaintState {
1103 parent_bounds: bounds,
1104 thumbs: {
1105 let thumb_ranges = self.state.read(cx).thumb_ranges().collect::<Vec<_>>();
1106 let width = self.state.read(cx).width.to_pixels();
1107
1108 let additional_padding = if thumb_ranges.len() == 2 {
1109 width
1110 } else {
1111 Pixels::ZERO
1112 };
1113
1114 thumb_ranges
1115 .into_iter()
1116 .map(|(axis, thumb_range, reserved_space)| {
1117 let track_anchor = match axis {
1118 ScrollbarAxis::Horizontal => Corner::BottomLeft,
1119 ScrollbarAxis::Vertical => Corner::TopRight,
1120 };
1121 let Bounds { origin, size } = Bounds::from_corner_and_size(
1122 track_anchor,
1123 bounds
1124 .corner(track_anchor)
1125 .apply_along(axis.invert(), |corner| {
1126 corner - SCROLLBAR_PADDING
1127 }),
1128 bounds.size.apply_along(axis.invert(), |_| width),
1129 );
1130 let scroll_track_bounds = Bounds::new(self.origin + origin, size);
1131
1132 let padded_bounds = scroll_track_bounds.extend(match axis {
1133 ScrollbarAxis::Horizontal => Edges {
1134 right: -SCROLLBAR_PADDING,
1135 left: -SCROLLBAR_PADDING,
1136 ..Default::default()
1137 },
1138 ScrollbarAxis::Vertical => Edges {
1139 top: -SCROLLBAR_PADDING,
1140 bottom: -SCROLLBAR_PADDING,
1141 ..Default::default()
1142 },
1143 });
1144
1145 let available_space =
1146 padded_bounds.size.along(axis) - additional_padding;
1147
1148 let thumb_offset = thumb_range.start * available_space;
1149 let thumb_end = thumb_range.end * available_space;
1150 let thumb_bounds = Bounds::new(
1151 padded_bounds
1152 .origin
1153 .apply_along(axis, |origin| origin + thumb_offset),
1154 padded_bounds
1155 .size
1156 .apply_along(axis, |_| thumb_end - thumb_offset),
1157 );
1158
1159 ScrollbarLayout {
1160 thumb_bounds,
1161 track_bounds: padded_bounds,
1162 axis,
1163 cursor_hitbox: window.insert_hitbox(
1164 if reserved_space.needs_scroll_track() {
1165 padded_bounds
1166 } else {
1167 thumb_bounds
1168 },
1169 HitboxBehavior::BlockMouseExceptScroll,
1170 ),
1171 track_background: reserved_space
1172 .track_color()
1173 .map(|color| (padded_bounds.dilate(SCROLLBAR_PADDING), color)),
1174 reserved_space,
1175 }
1176 })
1177 .collect()
1178 },
1179 });
1180 if prepaint_state
1181 .as_ref()
1182 .is_some_and(|state| Some(state) != self.state.read(cx).last_prepaint_state.as_ref())
1183 {
1184 self.state
1185 .update(cx, |state, cx| state.show_scrollbars(window, cx));
1186 }
1187
1188 prepaint_state.map(|state| {
1189 let autohide_delta = self.state.read(cx).show_state.animation_progress().map(
1190 |(delta, delta_duration, should_invert)| {
1191 window.with_element_state(id.unwrap(), |state, window| {
1192 let state = state.unwrap_or_else(|| Instant::now());
1193 let current = Instant::now();
1194
1195 let new_delta = DELTA_MAX
1196 .min(delta + (current - state).div_duration_f32(delta_duration));
1197 self.state
1198 .update(cx, |state, _| state.show_state.set_delta(new_delta));
1199
1200 window.request_animation_frame();
1201 let delta = if should_invert {
1202 DELTA_MAX - delta
1203 } else {
1204 delta
1205 };
1206 (ease_in_out(delta), current)
1207 })
1208 },
1209 );
1210
1211 (state, autohide_delta)
1212 })
1213 }
1214
1215 fn paint(
1216 &mut self,
1217 _id: Option<&GlobalElementId>,
1218 _inspector_id: Option<&gpui::InspectorElementId>,
1219 Bounds { origin, size }: Bounds<Pixels>,
1220 _request_layout: &mut Self::RequestLayoutState,
1221 prepaint_state: &mut Self::PrepaintState,
1222 window: &mut Window,
1223 cx: &mut App,
1224 ) {
1225 let Some((prepaint_state, autohide_fade)) = prepaint_state.take() else {
1226 return;
1227 };
1228
1229 let bounds = Bounds::new(self.origin + origin, size);
1230 window.with_content_mask(Some(ContentMask { bounds }), |window| {
1231 let colors = cx.theme().colors();
1232
1233 if self.state.read(cx).visible() {
1234 for ScrollbarLayout {
1235 thumb_bounds,
1236 cursor_hitbox,
1237 axis,
1238 reserved_space,
1239 track_background,
1240 ..
1241 } in &prepaint_state.thumbs
1242 {
1243 const MAXIMUM_OPACITY: f32 = 0.7;
1244 let thumb_state = &self.state.read(cx).thumb_state;
1245 let (thumb_base_color, hovered) = match thumb_state {
1246 ThumbState::Dragging(dragged_axis, _) if dragged_axis == axis => {
1247 (colors.scrollbar_thumb_active_background, false)
1248 }
1249 ThumbState::Hover(hovered_axis) if hovered_axis == axis => {
1250 (colors.scrollbar_thumb_hover_background, true)
1251 }
1252 _ => (colors.scrollbar_thumb_background, false),
1253 };
1254
1255 let blending_color = if hovered || reserved_space.needs_scroll_track() {
1256 track_background
1257 .map(|(_, background)| background)
1258 .unwrap_or(colors.surface_background)
1259 } else {
1260 let blend_color = colors.surface_background;
1261 blend_color.min(blend_color.alpha(MAXIMUM_OPACITY))
1262 };
1263
1264 let mut thumb_color = blending_color.blend(thumb_base_color);
1265
1266 if !hovered && let Some(fade) = autohide_fade {
1267 thumb_color.fade_out(fade);
1268 }
1269
1270 if let Some((track_bounds, color)) = track_background {
1271 window.paint_quad(quad(
1272 *track_bounds,
1273 Corners::default(),
1274 *color,
1275 Edges::default(),
1276 Hsla::transparent_black(),
1277 BorderStyle::default(),
1278 ));
1279 }
1280
1281 window.paint_quad(quad(
1282 *thumb_bounds,
1283 Corners::all(Pixels::MAX).clamp_radii_for_quad_size(thumb_bounds.size),
1284 thumb_color,
1285 Edges::default(),
1286 Hsla::transparent_black(),
1287 BorderStyle::default(),
1288 ));
1289
1290 if thumb_state.is_dragging() {
1291 window.set_window_cursor_style(CursorStyle::Arrow);
1292 } else {
1293 window.set_cursor_style(CursorStyle::Arrow, cursor_hitbox);
1294 }
1295 }
1296 }
1297
1298 self.state.update(cx, |state, _| {
1299 state.last_prepaint_state = Some(prepaint_state)
1300 });
1301
1302 window.on_mouse_event({
1303 let state = self.state.clone();
1304
1305 move |event: &MouseDownEvent, phase, window, cx| {
1306 state.update(cx, |state, cx| {
1307 let Some(scrollbar_layout) = (phase.capture()
1308 && event.button == MouseButton::Left)
1309 .then(|| state.hit_for_position(&event.position))
1310 .flatten()
1311 else {
1312 return;
1313 };
1314
1315 let ScrollbarLayout {
1316 thumb_bounds, axis, ..
1317 } = scrollbar_layout;
1318
1319 if thumb_bounds.contains(&event.position) {
1320 let offset =
1321 event.position.along(*axis) - thumb_bounds.origin.along(*axis);
1322 state.set_dragging(*axis, offset, window, cx);
1323 } else {
1324 let scroll_handle = state.scroll_handle();
1325 let click_offset = scrollbar_layout.compute_click_offset(
1326 event.position,
1327 scroll_handle.max_offset(),
1328 ScrollbarMouseEvent::TrackClick,
1329 );
1330 state.set_offset(
1331 scroll_handle.offset().apply_along(*axis, |_| click_offset),
1332 cx,
1333 );
1334 };
1335
1336 cx.stop_propagation();
1337 });
1338 }
1339 });
1340
1341 window.on_mouse_event({
1342 let state = self.state.clone();
1343
1344 move |event: &ScrollWheelEvent, phase, window, cx| {
1345 if phase.capture() {
1346 state.update(cx, |state, cx| {
1347 state.update_hovered_thumb(&event.position, window, cx)
1348 });
1349 }
1350 }
1351 });
1352
1353 window.on_mouse_event({
1354 let state = self.state.clone();
1355
1356 move |event: &MouseMoveEvent, phase, window, cx| {
1357 if !phase.capture() {
1358 return;
1359 }
1360
1361 match state.read(cx).thumb_state {
1362 ThumbState::Dragging(axis, drag_state) if event.dragging() => {
1363 if let Some(scrollbar_layout) = state.read(cx).thumb_for_axis(axis) {
1364 let scroll_handle = state.read(cx).scroll_handle();
1365 let drag_offset = scrollbar_layout.compute_click_offset(
1366 event.position,
1367 scroll_handle.max_offset(),
1368 ScrollbarMouseEvent::ThumbDrag(drag_state),
1369 );
1370 let new_offset =
1371 scroll_handle.offset().apply_along(axis, |_| drag_offset);
1372
1373 state.update(cx, |state, cx| state.set_offset(new_offset, cx));
1374 cx.stop_propagation();
1375 }
1376 }
1377 _ => state.update(cx, |state, cx| {
1378 match state.update_parent_hovered(&event.position) {
1379 hover @ ParentHoverEvent::Entered
1380 | hover @ ParentHoverEvent::Within
1381 if event.pressed_button.is_none() =>
1382 {
1383 if matches!(hover, ParentHoverEvent::Entered) {
1384 state.show_scrollbars(window, cx);
1385 }
1386 state.update_hovered_thumb(&event.position, window, cx);
1387 if state.thumb_state != ThumbState::Inactive {
1388 cx.stop_propagation();
1389 }
1390 }
1391 ParentHoverEvent::Exited => {
1392 state.set_thumb_state(ThumbState::Inactive, window, cx);
1393 }
1394 _ => {}
1395 }
1396 }),
1397 }
1398 }
1399 });
1400
1401 window.on_mouse_event({
1402 let state = self.state.clone();
1403 move |event: &MouseUpEvent, phase, window, cx| {
1404 if !phase.capture() {
1405 return;
1406 }
1407
1408 state.update(cx, |state, cx| {
1409 if state.is_dragging() {
1410 state.scroll_handle().drag_ended();
1411 }
1412
1413 if !state.parent_hovered(&event.position) {
1414 state.schedule_auto_hide(window, cx);
1415 return;
1416 }
1417
1418 state.update_hovered_thumb(&event.position, window, cx);
1419 });
1420 }
1421 });
1422 })
1423 }
1424}
1425
1426impl<T: ScrollableHandle> IntoElement for ScrollbarElement<T> {
1427 type Element = Self;
1428
1429 fn into_element(self) -> Self::Element {
1430 self
1431 }
1432}