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