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