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