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