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