1use std::{
2 any::Any,
3 fmt::Debug,
4 ops::Not,
5 time::{Duration, Instant},
6};
7
8use gpui::{
9 Along, App, AppContext as _, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask, Context,
10 Corner, Corners, CursorStyle, DispatchPhase, Div, Edges, Element, ElementId, Entity, EntityId,
11 GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
12 LayoutId, ListState, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Negate,
13 ParentElement, Pixels, Point, Position, Render, ScrollHandle, ScrollWheelEvent, Size, Stateful,
14 StatefulInteractiveElement, Style, Styled, Task, UniformListDecoration,
15 UniformListScrollHandle, Window, ease_in_out, prelude::FluentBuilder as _, px, quad, relative,
16 size,
17};
18use settings::SettingsStore;
19use smallvec::SmallVec;
20use theme::ActiveTheme as _;
21use util::ResultExt;
22
23use std::ops::Range;
24
25use crate::scrollbars::{ScrollbarAutoHide, ScrollbarVisibility, ShowScrollbar};
26
27const SCROLLBAR_HIDE_DELAY_INTERVAL: Duration = Duration::from_secs(1);
28const SCROLLBAR_HIDE_DURATION: Duration = Duration::from_millis(400);
29const SCROLLBAR_SHOW_DURATION: Duration = Duration::from_millis(50);
30
31pub const EDITOR_SCROLLBAR_WIDTH: Pixels = ScrollbarWidth::Editor.to_pixels();
32const SCROLLBAR_PADDING: Pixels = px(4.);
33const BORDER_WIDTH: Pixels = px(1.);
34
35pub mod scrollbars {
36 use gpui::{App, Global};
37 use schemars::JsonSchema;
38 use serde::{Deserialize, Serialize};
39 use settings::Settings;
40
41 /// When to show the scrollbar in the editor.
42 ///
43 /// Default: auto
44 #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
45 #[serde(rename_all = "snake_case")]
46 pub enum ShowScrollbar {
47 /// Show the scrollbar if there's important information or
48 /// follow the system's configured behavior.
49 #[default]
50 Auto,
51 /// Match the system's configured behavior.
52 System,
53 /// Always show the scrollbar.
54 Always,
55 /// Never show the scrollbar.
56 Never,
57 }
58
59 impl From<settings::ShowScrollbar> for ShowScrollbar {
60 fn from(value: settings::ShowScrollbar) -> Self {
61 match value {
62 settings::ShowScrollbar::Auto => ShowScrollbar::Auto,
63 settings::ShowScrollbar::System => ShowScrollbar::System,
64 settings::ShowScrollbar::Always => ShowScrollbar::Always,
65 settings::ShowScrollbar::Never => ShowScrollbar::Never,
66 }
67 }
68 }
69
70 pub trait GlobalSetting {
71 fn get_value(cx: &App) -> &Self;
72 }
73
74 impl<T: Settings> GlobalSetting for T {
75 fn get_value(cx: &App) -> &T {
76 T::get_global(cx)
77 }
78 }
79
80 pub trait ScrollbarVisibility: GlobalSetting + 'static {
81 fn visibility(&self, cx: &App) -> ShowScrollbar;
82 }
83
84 #[derive(Default)]
85 pub struct ScrollbarAutoHide(pub bool);
86
87 impl ScrollbarAutoHide {
88 pub fn should_hide(&self) -> bool {
89 self.0
90 }
91 }
92
93 impl Global for ScrollbarAutoHide {}
94}
95
96fn get_scrollbar_state<T>(
97 mut config: Scrollbars<T>,
98 caller_location: &'static std::panic::Location,
99 window: &mut Window,
100 cx: &mut App,
101) -> Entity<ScrollbarStateWrapper<T>>
102where
103 T: ScrollableHandle,
104{
105 let element_id = config.id.take().unwrap_or_else(|| caller_location.into());
106 let track_color = config.track_color;
107 let border_color = config.border_color;
108
109 let state = 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 state.update(cx, |state, cx| {
117 state.0.update(cx, |state, _cx| {
118 state.update_colors(track_color, border_color)
119 })
120 });
121 state
122}
123
124pub trait WithScrollbar: Sized {
125 type Output;
126
127 fn custom_scrollbars<T>(
128 self,
129 config: Scrollbars<T>,
130 window: &mut Window,
131 cx: &mut App,
132 ) -> Self::Output
133 where
134 T: ScrollableHandle;
135
136 // TODO: account for these cases properly
137 // #[track_caller]
138 // fn horizontal_scrollbar(self, window: &mut Window, cx: &mut App) -> Self::Output {
139 // self.custom_scrollbars(
140 // Scrollbars::new(ScrollAxes::Horizontal).ensure_id(core::panic::Location::caller()),
141 // window,
142 // cx,
143 // )
144 // }
145
146 // #[track_caller]
147 // fn vertical_scrollbar(self, window: &mut Window, cx: &mut App) -> Self::Output {
148 // self.custom_scrollbars(
149 // Scrollbars::new(ScrollAxes::Vertical).ensure_id(core::panic::Location::caller()),
150 // window,
151 // cx,
152 // )
153 // }
154
155 #[track_caller]
156 fn vertical_scrollbar_for<ScrollHandle: ScrollableHandle + Clone>(
157 self,
158 scroll_handle: &ScrollHandle,
159 window: &mut Window,
160 cx: &mut App,
161 ) -> Self::Output {
162 self.custom_scrollbars(
163 Scrollbars::new(ScrollAxes::Vertical)
164 .tracked_scroll_handle(scroll_handle)
165 .ensure_id(core::panic::Location::caller()),
166 window,
167 cx,
168 )
169 }
170}
171
172impl WithScrollbar for Stateful<Div> {
173 type Output = Self;
174
175 #[track_caller]
176 fn custom_scrollbars<T>(
177 self,
178 config: Scrollbars<T>,
179 window: &mut Window,
180 cx: &mut App,
181 ) -> Self::Output
182 where
183 T: ScrollableHandle,
184 {
185 render_scrollbar(
186 get_scrollbar_state(config, std::panic::Location::caller(), window, cx),
187 self,
188 cx,
189 )
190 }
191}
192
193impl WithScrollbar for Div {
194 type Output = Stateful<Div>;
195
196 #[track_caller]
197 fn custom_scrollbars<T>(
198 self,
199 config: Scrollbars<T>,
200 window: &mut Window,
201 cx: &mut App,
202 ) -> Self::Output
203 where
204 T: ScrollableHandle,
205 {
206 let scrollbar = get_scrollbar_state(config, std::panic::Location::caller(), window, cx);
207 // We know this ID stays consistent as long as the element is rendered for
208 // consecutive frames, which is sufficient for our use case here
209 let scrollbar_entity_id = scrollbar.entity_id();
210
211 render_scrollbar(
212 scrollbar,
213 self.id(("track-scroll", scrollbar_entity_id)),
214 cx,
215 )
216 }
217}
218
219fn render_scrollbar<T>(
220 scrollbar: Entity<ScrollbarStateWrapper<T>>,
221 div: Stateful<Div>,
222 cx: &App,
223) -> Stateful<Div>
224where
225 T: ScrollableHandle,
226{
227 let state = &scrollbar.read(cx).0;
228
229 div.when_some(state.read(cx).handle_to_track(), |this, handle| {
230 this.track_scroll(handle).when_some(
231 state.read(cx).visible_axes(),
232 |this, axes| match axes {
233 ScrollAxes::Horizontal => this.overflow_x_scroll(),
234 ScrollAxes::Vertical => this.overflow_y_scroll(),
235 ScrollAxes::Both => this.overflow_scroll(),
236 },
237 )
238 })
239 .when_some(
240 state
241 .read(cx)
242 .space_to_reserve_for(ScrollbarAxis::Horizontal),
243 |this, space| this.pb(space),
244 )
245 .when_some(
246 state.read(cx).space_to_reserve_for(ScrollbarAxis::Vertical),
247 |this, space| this.pr(space),
248 )
249 .child(state.clone())
250}
251
252impl<T: ScrollableHandle> UniformListDecoration for ScrollbarStateWrapper<T> {
253 fn compute(
254 &self,
255 _visible_range: Range<usize>,
256 _bounds: Bounds<Pixels>,
257 scroll_offset: Point<Pixels>,
258 _item_height: Pixels,
259 _item_count: usize,
260 _window: &mut Window,
261 _cx: &mut App,
262 ) -> gpui::AnyElement {
263 ScrollbarElement {
264 origin: scroll_offset.negate(),
265 state: self.0.clone(),
266 }
267 .into_any()
268 }
269}
270
271// impl WithScrollbar for UniformList {
272// type Output = Self;
273
274// #[track_caller]
275// fn custom_scrollbars<S, T>(
276// self,
277// config: Scrollbars<S, T>,
278// window: &mut Window,
279// cx: &mut App,
280// ) -> Self::Output
281// where
282// S: ScrollbarVisibilitySetting,
283// T: ScrollableHandle,
284// {
285// let scrollbar = get_scrollbar_state(config, std::panic::Location::caller(), window, cx);
286// self.when_some(
287// scrollbar.read_with(cx, |wrapper, cx| {
288// wrapper
289// .0
290// .read(cx)
291// .handle_to_track::<UniformListScrollHandle>()
292// .cloned()
293// }),
294// |this, handle| this.track_scroll(handle),
295// )
296// .with_decoration(scrollbar)
297// }
298// }
299
300#[derive(Copy, Clone, PartialEq, Eq)]
301enum ShowBehavior {
302 Always,
303 Autohide,
304 Never,
305}
306
307impl ShowBehavior {
308 fn from_setting(setting: ShowScrollbar, cx: &mut App) -> Self {
309 match setting {
310 ShowScrollbar::Never => Self::Never,
311 ShowScrollbar::Auto => Self::Autohide,
312 ShowScrollbar::System => {
313 if cx.default_global::<ScrollbarAutoHide>().should_hide() {
314 Self::Autohide
315 } else {
316 Self::Always
317 }
318 }
319 ShowScrollbar::Always => Self::Always,
320 }
321 }
322}
323
324pub enum ScrollAxes {
325 Horizontal,
326 Vertical,
327 Both,
328}
329
330impl ScrollAxes {
331 fn apply_to<T>(self, point: Point<T>, value: T) -> Point<T>
332 where
333 T: Debug + Default + PartialEq + Clone,
334 {
335 match self {
336 Self::Horizontal => point.apply_along(ScrollbarAxis::Horizontal, |_| value),
337 Self::Vertical => point.apply_along(ScrollbarAxis::Vertical, |_| value),
338 Self::Both => Point::new(value.clone(), value),
339 }
340 }
341}
342
343#[derive(Clone, Debug, Default, PartialEq)]
344enum ReservedSpace {
345 #[default]
346 None,
347 Thumb,
348 Track,
349 StableTrack,
350}
351
352impl ReservedSpace {
353 fn is_visible(&self) -> bool {
354 *self != ReservedSpace::None
355 }
356
357 fn needs_scroll_track(&self) -> bool {
358 matches!(self, Self::Track | Self::StableTrack)
359 }
360
361 fn needs_space_reserved(&self, max_offset: Pixels) -> bool {
362 match self {
363 Self::StableTrack => true,
364 Self::Track => !max_offset.is_zero(),
365 _ => false,
366 }
367 }
368}
369
370#[derive(Debug, Default, Clone, Copy)]
371enum ScrollbarWidth {
372 #[default]
373 Normal,
374 Editor,
375 Small,
376 XSmall,
377}
378
379impl ScrollbarWidth {
380 const fn to_pixels(&self) -> Pixels {
381 match self {
382 ScrollbarWidth::Editor => px(15.),
383 ScrollbarWidth::Normal => px(8.),
384 ScrollbarWidth::Small => px(6.),
385 ScrollbarWidth::XSmall => px(4.),
386 }
387 }
388}
389
390#[derive(Clone)]
391enum Handle<T: ScrollableHandle> {
392 Tracked(T),
393 Untracked(fn() -> T),
394}
395
396#[derive(Clone, Copy, Default, PartialEq)]
397pub enum ScrollbarStyle {
398 #[default]
399 Rounded,
400 Editor,
401}
402
403#[derive(Clone)]
404pub struct Scrollbars<T: ScrollableHandle = ScrollHandle> {
405 id: Option<ElementId>,
406 get_visibility: fn(&App) -> ShowScrollbar,
407 tracked_entity: Option<Option<EntityId>>,
408 scrollable_handle: Handle<T>,
409 visibility: Point<ReservedSpace>,
410 style: Option<ScrollbarStyle>,
411 track_color: Option<Hsla>,
412 border_color: Option<Hsla>,
413 scrollbar_width: ScrollbarWidth,
414}
415
416impl Scrollbars {
417 pub fn new(show_along: ScrollAxes) -> Self {
418 Self::new_with_setting(show_along, |_| ShowScrollbar::default())
419 }
420
421 pub fn always_visible(show_along: ScrollAxes) -> Self {
422 Self::new_with_setting(show_along, |_| ShowScrollbar::Always)
423 }
424
425 pub fn for_settings<S: ScrollbarVisibility>() -> Scrollbars {
426 Scrollbars::new_with_setting(ScrollAxes::Both, |cx| S::get_value(cx).visibility(cx))
427 }
428}
429
430impl Scrollbars {
431 fn new_with_setting(show_along: ScrollAxes, get_visibility: fn(&App) -> ShowScrollbar) -> Self {
432 Self {
433 id: None,
434 get_visibility,
435 scrollable_handle: Handle::Untracked(ScrollHandle::new),
436 tracked_entity: None,
437 visibility: show_along.apply_to(Default::default(), ReservedSpace::Thumb),
438 style: None,
439 track_color: None,
440 border_color: None,
441 scrollbar_width: ScrollbarWidth::Normal,
442 }
443 }
444}
445
446impl<ScrollHandle: ScrollableHandle> Scrollbars<ScrollHandle> {
447 pub fn id(mut self, id: impl Into<ElementId>) -> Self {
448 self.id = Some(id.into());
449 self
450 }
451
452 fn ensure_id(mut self, id: impl Into<ElementId>) -> Self {
453 if self.id.is_none() {
454 self.id = Some(id.into());
455 }
456 self
457 }
458
459 /// Notify the current context whenever this scrollbar gets a scroll event
460 pub fn notify_content(mut self) -> Self {
461 self.tracked_entity = Some(None);
462 self
463 }
464
465 /// Set a parent model which should be notified whenever this scrollbar gets a scroll event.
466 pub fn tracked_entity(mut self, entity_id: EntityId) -> Self {
467 self.tracked_entity = Some(Some(entity_id));
468 self
469 }
470
471 pub fn tracked_scroll_handle<TrackedHandle: ScrollableHandle>(
472 self,
473 tracked_scroll_handle: &TrackedHandle,
474 ) -> Scrollbars<TrackedHandle> {
475 let Self {
476 id,
477 tracked_entity: tracked_entity_id,
478 scrollbar_width,
479 visibility,
480 get_visibility,
481 track_color,
482 border_color,
483 style,
484 ..
485 } = self;
486
487 Scrollbars {
488 scrollable_handle: Handle::Tracked(tracked_scroll_handle.clone()),
489 id,
490 tracked_entity: tracked_entity_id,
491 visibility,
492 scrollbar_width,
493 track_color,
494 border_color,
495 get_visibility,
496 style,
497 }
498 }
499
500 pub fn show_along(mut self, along: ScrollAxes) -> Self {
501 self.visibility = along.apply_to(self.visibility, ReservedSpace::Thumb);
502 self
503 }
504
505 pub fn style(mut self, style: ScrollbarStyle) -> Self {
506 self.style = Some(style);
507 self
508 }
509
510 pub fn with_track_along(mut self, along: ScrollAxes, background_color: Hsla) -> Self {
511 self.visibility = along.apply_to(self.visibility, ReservedSpace::Track);
512 self.track_color = Some(background_color);
513 self
514 }
515
516 pub fn with_stable_track_along(
517 mut self,
518 along: ScrollAxes,
519 background_color: Hsla,
520 track_border_color: Option<Hsla>,
521 ) -> Self {
522 self.visibility = along.apply_to(self.visibility, ReservedSpace::StableTrack);
523 self.track_color = Some(background_color);
524 self.border_color = track_border_color;
525 self
526 }
527
528 pub fn width_sm(mut self) -> Self {
529 self.scrollbar_width = ScrollbarWidth::Small;
530 self
531 }
532
533 pub fn width_xs(mut self) -> Self {
534 self.scrollbar_width = ScrollbarWidth::XSmall;
535 self
536 }
537
538 pub fn width_editor(mut self) -> Self {
539 self.scrollbar_width = ScrollbarWidth::Editor;
540 self
541 }
542}
543
544#[derive(PartialEq, Clone, Debug)]
545enum VisibilityState {
546 Visible,
547 Animating { showing: bool, delta: f32 },
548 ThumbHidden,
549 Hidden,
550 Disabled,
551}
552
553enum AnimationState {
554 InProgress {
555 current_delta: f32,
556 animation_duration: Duration,
557 showing: bool,
558 },
559 Stale,
560}
561
562const DELTA_MAX: f32 = 1.0;
563
564impl VisibilityState {
565 fn from_behavior(behavior: ShowBehavior) -> Self {
566 match behavior {
567 ShowBehavior::Always => Self::Visible,
568 ShowBehavior::Never => Self::Disabled,
569 ShowBehavior::Autohide => Self::for_show(),
570 }
571 }
572
573 fn for_show() -> Self {
574 Self::Animating {
575 showing: true,
576 delta: Default::default(),
577 }
578 }
579
580 fn for_autohide() -> Self {
581 Self::Animating {
582 showing: Default::default(),
583 delta: Default::default(),
584 }
585 }
586
587 fn is_visible(&self) -> bool {
588 matches!(
589 self,
590 Self::Visible | Self::Animating { .. } | Self::ThumbHidden
591 )
592 }
593
594 #[inline]
595 fn is_disabled(&self) -> bool {
596 *self == VisibilityState::Disabled
597 }
598
599 fn animation_state(&self) -> Option<AnimationState> {
600 match self {
601 Self::ThumbHidden => Some(AnimationState::Stale),
602 Self::Animating { showing, delta } => Some(AnimationState::InProgress {
603 current_delta: *delta,
604 animation_duration: if *showing {
605 SCROLLBAR_SHOW_DURATION
606 } else {
607 SCROLLBAR_HIDE_DURATION
608 },
609 showing: *showing,
610 }),
611 _ => None,
612 }
613 }
614
615 fn set_delta(&mut self, new_delta: f32, keep_track_visible: bool) {
616 match self {
617 Self::Animating { showing, delta } if new_delta >= DELTA_MAX => {
618 if *showing {
619 *self = Self::Visible;
620 } else if keep_track_visible {
621 *self = Self::ThumbHidden;
622 } else {
623 *self = Self::Hidden;
624 }
625 }
626 Self::Animating { delta, .. } => *delta = new_delta,
627 _ => {}
628 }
629 }
630
631 fn toggle_visible(&self, show_behavior: ShowBehavior) -> Self {
632 match self {
633 Self::Hidden | Self::ThumbHidden => {
634 if show_behavior == ShowBehavior::Autohide {
635 Self::for_show()
636 } else {
637 Self::Visible
638 }
639 }
640 Self::Animating {
641 showing: false,
642 delta: progress,
643 } => Self::Animating {
644 showing: true,
645 delta: DELTA_MAX - progress,
646 },
647 _ => self.clone(),
648 }
649 }
650}
651
652enum ParentHoverEvent {
653 Within,
654 Entered,
655 Exited,
656 Outside,
657}
658
659#[derive(Clone)]
660struct TrackColors {
661 background: Hsla,
662 border: Option<Hsla>,
663}
664
665impl TrackColors {
666 fn has_border(&self) -> bool {
667 self.border.is_some()
668 }
669}
670
671/// This is used to ensure notifies within the state do not notify the parent
672/// unintentionally.
673struct ScrollbarStateWrapper<T: ScrollableHandle>(Entity<ScrollbarState<T>>);
674
675/// A scrollbar state that should be persisted across frames.
676struct ScrollbarState<T: ScrollableHandle = ScrollHandle> {
677 thumb_state: ThumbState,
678 notify_id: Option<EntityId>,
679 manually_added: bool,
680 scroll_handle: T,
681 width: ScrollbarWidth,
682 show_behavior: ShowBehavior,
683 get_visibility: fn(&App) -> ShowScrollbar,
684 visibility: Point<ReservedSpace>,
685 track_color: Option<TrackColors>,
686 show_state: VisibilityState,
687 style: ScrollbarStyle,
688 mouse_in_parent: bool,
689 last_prepaint_state: Option<ScrollbarPrepaintState>,
690 _auto_hide_task: Option<Task<()>>,
691}
692
693impl<T: ScrollableHandle> ScrollbarState<T> {
694 fn new_from_config(
695 config: Scrollbars<T>,
696 parent_id: EntityId,
697 window: &mut Window,
698 cx: &mut Context<Self>,
699 ) -> Self {
700 cx.observe_global_in::<SettingsStore>(window, Self::settings_changed)
701 .detach();
702
703 let (manually_added, scroll_handle) = match config.scrollable_handle {
704 Handle::Tracked(handle) => (true, handle),
705 Handle::Untracked(func) => (false, func()),
706 };
707
708 let show_behavior = ShowBehavior::from_setting((config.get_visibility)(cx), cx);
709 ScrollbarState {
710 thumb_state: Default::default(),
711 notify_id: config.tracked_entity.map(|id| id.unwrap_or(parent_id)),
712 manually_added,
713 scroll_handle,
714 width: config.scrollbar_width,
715 visibility: config.visibility,
716 track_color: config.track_color.map(|color| TrackColors {
717 background: color,
718 border: config.border_color,
719 }),
720 show_behavior,
721 get_visibility: config.get_visibility,
722 style: config.style.unwrap_or_default(),
723 show_state: VisibilityState::from_behavior(show_behavior),
724 mouse_in_parent: true,
725 last_prepaint_state: None,
726 _auto_hide_task: None,
727 }
728 }
729
730 fn settings_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
731 self.set_show_behavior(
732 ShowBehavior::from_setting((self.get_visibility)(cx), cx),
733 window,
734 cx,
735 );
736 }
737
738 /// Schedules a scrollbar auto hide if no auto hide is currently in progress yet.
739 fn schedule_auto_hide(&mut self, window: &mut Window, cx: &mut Context<Self>) {
740 if self._auto_hide_task.is_none() {
741 self._auto_hide_task = (self.visible() && self.show_behavior == ShowBehavior::Autohide)
742 .then(|| {
743 cx.spawn_in(window, async move |scrollbar_state, cx| {
744 cx.background_executor()
745 .timer(SCROLLBAR_HIDE_DELAY_INTERVAL)
746 .await;
747 scrollbar_state
748 .update(cx, |state, cx| {
749 if state.thumb_state == ThumbState::Inactive {
750 state.set_visibility(VisibilityState::for_autohide(), cx);
751 }
752 state._auto_hide_task.take();
753 })
754 .log_err();
755 })
756 });
757 }
758 }
759
760 fn show_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Self>) {
761 let visibility = self.show_state.toggle_visible(self.show_behavior);
762 self.set_visibility(visibility, cx);
763 self._auto_hide_task.take();
764 self.schedule_auto_hide(window, cx);
765 }
766
767 fn set_show_behavior(
768 &mut self,
769 behavior: ShowBehavior,
770 window: &mut Window,
771 cx: &mut Context<Self>,
772 ) {
773 if self.show_behavior != behavior {
774 self.show_behavior = behavior;
775 self.set_visibility(VisibilityState::from_behavior(behavior), cx);
776 self.schedule_auto_hide(window, cx);
777 cx.notify();
778 }
779 }
780
781 fn set_visibility(&mut self, visibility: VisibilityState, cx: &mut Context<Self>) {
782 if self.show_state != visibility {
783 self.show_state = visibility;
784 cx.notify();
785 }
786 }
787
788 #[inline]
789 fn visible_axes(&self) -> Option<ScrollAxes> {
790 match (&self.visibility.x, &self.visibility.y) {
791 (ReservedSpace::None, ReservedSpace::None) => None,
792 (ReservedSpace::None, _) => Some(ScrollAxes::Vertical),
793 (_, ReservedSpace::None) => Some(ScrollAxes::Horizontal),
794 _ => Some(ScrollAxes::Both),
795 }
796 }
797
798 fn space_to_reserve_for(&self, axis: ScrollbarAxis) -> Option<Pixels> {
799 (self.show_state.is_disabled().not()
800 && self
801 .visibility
802 .along(axis)
803 .needs_space_reserved(self.scroll_handle().max_offset().along(axis)))
804 .then(|| self.space_to_reserve())
805 }
806
807 fn space_to_reserve(&self) -> Pixels {
808 self.width.to_pixels() + 2 * SCROLLBAR_PADDING
809 }
810
811 fn handle_to_track<Handle: ScrollableHandle>(&self) -> Option<&Handle> {
812 (!self.manually_added)
813 .then(|| (self.scroll_handle() as &dyn Any).downcast_ref::<Handle>())
814 .flatten()
815 }
816
817 fn scroll_handle(&self) -> &T {
818 &self.scroll_handle
819 }
820
821 fn set_offset(&mut self, offset: Point<Pixels>, cx: &mut Context<Self>) {
822 self.scroll_handle.set_offset(offset);
823 self.notify_parent(cx);
824 cx.notify();
825 }
826
827 fn is_dragging(&self) -> bool {
828 self.thumb_state.is_dragging()
829 }
830
831 fn set_dragging(
832 &mut self,
833 axis: ScrollbarAxis,
834 drag_offset: Pixels,
835 window: &mut Window,
836 cx: &mut Context<Self>,
837 ) {
838 self.set_thumb_state(ThumbState::Dragging(axis, drag_offset), window, cx);
839 self.scroll_handle().drag_started();
840 }
841
842 fn update_hovered_thumb(
843 &mut self,
844 position: &Point<Pixels>,
845 window: &mut Window,
846 cx: &mut Context<Self>,
847 ) {
848 self.set_thumb_state(
849 if let Some(&ScrollbarLayout { axis, .. }) =
850 self.last_prepaint_state.as_ref().and_then(|state| {
851 state
852 .thumb_for_position(position)
853 .filter(|thumb| thumb.cursor_hitbox.is_hovered(window))
854 })
855 {
856 ThumbState::Hover(axis)
857 } else {
858 ThumbState::Inactive
859 },
860 window,
861 cx,
862 );
863 }
864
865 fn set_thumb_state(&mut self, state: ThumbState, window: &mut Window, cx: &mut Context<Self>) {
866 if self.thumb_state != state {
867 if state == ThumbState::Inactive {
868 self.schedule_auto_hide(window, cx);
869 } else {
870 self.set_visibility(self.show_state.toggle_visible(self.show_behavior), cx);
871 self._auto_hide_task.take();
872 }
873 self.thumb_state = state;
874 cx.notify();
875 }
876 }
877
878 fn update_parent_hovered(&mut self, window: &Window) -> ParentHoverEvent {
879 let last_parent_hovered = self.mouse_in_parent;
880 self.mouse_in_parent = self.parent_hovered(window);
881 let state_changed = self.mouse_in_parent != last_parent_hovered;
882 match (self.mouse_in_parent, state_changed) {
883 (true, true) => ParentHoverEvent::Entered,
884 (true, false) => ParentHoverEvent::Within,
885 (false, true) => ParentHoverEvent::Exited,
886 (false, false) => ParentHoverEvent::Outside,
887 }
888 }
889
890 fn update_colors(&mut self, track_color: Option<Hsla>, border_color: Option<Hsla>) {
891 self.track_color = track_color.map(|color| TrackColors {
892 background: color,
893 border: border_color,
894 });
895 }
896
897 fn parent_hovered(&self, window: &Window) -> bool {
898 self.last_prepaint_state
899 .as_ref()
900 .is_some_and(|state| state.parent_bounds_hitbox.is_hovered(window))
901 }
902
903 fn hit_for_position(&self, position: &Point<Pixels>) -> Option<&ScrollbarLayout> {
904 self.last_prepaint_state
905 .as_ref()
906 .and_then(|state| state.hit_for_position(position))
907 }
908
909 fn thumb_for_axis(&self, axis: ScrollbarAxis) -> Option<&ScrollbarLayout> {
910 self.last_prepaint_state
911 .as_ref()
912 .and_then(|state| state.thumbs.iter().find(|thumb| thumb.axis == axis))
913 }
914
915 fn thumb_ranges(
916 &self,
917 ) -> impl Iterator<Item = (ScrollbarAxis, Range<f32>, ReservedSpace)> + '_ {
918 const MINIMUM_THUMB_SIZE: Pixels = px(25.);
919 let max_offset = self.scroll_handle().max_offset();
920 let viewport_size = self.scroll_handle().viewport().size;
921 let current_offset = self.scroll_handle().offset();
922
923 [ScrollbarAxis::Horizontal, ScrollbarAxis::Vertical]
924 .into_iter()
925 .filter(|&axis| self.visibility.along(axis).is_visible())
926 .flat_map(move |axis| {
927 let max_offset = max_offset.along(axis);
928 let viewport_size = viewport_size.along(axis);
929 if max_offset.is_zero() || viewport_size.is_zero() {
930 return None;
931 }
932 let content_size = viewport_size + max_offset;
933 let visible_percentage = viewport_size / content_size;
934 let thumb_size = MINIMUM_THUMB_SIZE.max(viewport_size * visible_percentage);
935 if thumb_size > viewport_size {
936 return None;
937 }
938 let current_offset = current_offset
939 .along(axis)
940 .clamp(-max_offset, Pixels::ZERO)
941 .abs();
942 let start_offset = (current_offset / max_offset) * (viewport_size - thumb_size);
943 let thumb_percentage_start = start_offset / viewport_size;
944 let thumb_percentage_end = (start_offset + thumb_size) / viewport_size;
945 Some((
946 axis,
947 thumb_percentage_start..thumb_percentage_end,
948 self.visibility.along(axis),
949 ))
950 })
951 }
952
953 fn visible(&self) -> bool {
954 self.show_state.is_visible()
955 }
956
957 #[inline]
958 fn disabled(&self) -> bool {
959 self.show_state.is_disabled()
960 }
961
962 fn notify_parent(&self, cx: &mut App) {
963 if let Some(entity_id) = self.notify_id {
964 cx.notify(entity_id);
965 }
966 }
967}
968
969impl<T: ScrollableHandle> Render for ScrollbarState<T> {
970 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
971 ScrollbarElement {
972 state: cx.entity(),
973 origin: Default::default(),
974 }
975 }
976}
977
978struct ScrollbarElement<T: ScrollableHandle> {
979 origin: Point<Pixels>,
980 state: Entity<ScrollbarState<T>>,
981}
982
983#[derive(Default, Debug, PartialEq, Eq)]
984enum ThumbState {
985 #[default]
986 Inactive,
987 Hover(ScrollbarAxis),
988 Dragging(ScrollbarAxis, Pixels),
989}
990
991impl ThumbState {
992 fn is_dragging(&self) -> bool {
993 matches!(*self, ThumbState::Dragging(..))
994 }
995}
996
997impl ScrollableHandle for UniformListScrollHandle {
998 fn max_offset(&self) -> Size<Pixels> {
999 self.0.borrow().base_handle.max_offset()
1000 }
1001
1002 fn set_offset(&self, point: Point<Pixels>) {
1003 self.0.borrow().base_handle.set_offset(point);
1004 }
1005
1006 fn offset(&self) -> Point<Pixels> {
1007 self.0.borrow().base_handle.offset()
1008 }
1009
1010 fn viewport(&self) -> Bounds<Pixels> {
1011 self.0.borrow().base_handle.bounds()
1012 }
1013}
1014
1015impl ScrollableHandle for ListState {
1016 fn max_offset(&self) -> Size<Pixels> {
1017 self.max_offset_for_scrollbar()
1018 }
1019
1020 fn set_offset(&self, point: Point<Pixels>) {
1021 self.set_offset_from_scrollbar(point);
1022 }
1023
1024 fn offset(&self) -> Point<Pixels> {
1025 self.scroll_px_offset_for_scrollbar()
1026 }
1027
1028 fn drag_started(&self) {
1029 self.scrollbar_drag_started();
1030 }
1031
1032 fn drag_ended(&self) {
1033 self.scrollbar_drag_ended();
1034 }
1035
1036 fn viewport(&self) -> Bounds<Pixels> {
1037 self.viewport_bounds()
1038 }
1039}
1040
1041impl ScrollableHandle for ScrollHandle {
1042 fn max_offset(&self) -> Size<Pixels> {
1043 self.max_offset()
1044 }
1045
1046 fn set_offset(&self, point: Point<Pixels>) {
1047 self.set_offset(point);
1048 }
1049
1050 fn offset(&self) -> Point<Pixels> {
1051 self.offset()
1052 }
1053
1054 fn viewport(&self) -> Bounds<Pixels> {
1055 self.bounds()
1056 }
1057}
1058
1059pub trait ScrollableHandle: 'static + Any + Sized + Clone {
1060 fn max_offset(&self) -> Size<Pixels>;
1061 fn set_offset(&self, point: Point<Pixels>);
1062 fn offset(&self) -> Point<Pixels>;
1063 fn viewport(&self) -> Bounds<Pixels>;
1064 fn drag_started(&self) {}
1065 fn drag_ended(&self) {}
1066
1067 fn scrollable_along(&self, axis: ScrollbarAxis) -> bool {
1068 self.max_offset().along(axis) > Pixels::ZERO
1069 }
1070 fn content_size(&self) -> Size<Pixels> {
1071 self.viewport().size + self.max_offset()
1072 }
1073}
1074
1075enum ScrollbarMouseEvent {
1076 TrackClick,
1077 ThumbDrag(Pixels),
1078}
1079
1080struct ScrollbarLayout {
1081 thumb_bounds: Bounds<Pixels>,
1082 track_bounds: Bounds<Pixels>,
1083 cursor_hitbox: Hitbox,
1084 reserved_space: ReservedSpace,
1085 track_config: Option<(Bounds<Pixels>, TrackColors)>,
1086 axis: ScrollbarAxis,
1087}
1088
1089impl ScrollbarLayout {
1090 fn compute_click_offset(
1091 &self,
1092 event_position: Point<Pixels>,
1093 max_offset: Size<Pixels>,
1094 event_type: ScrollbarMouseEvent,
1095 ) -> Pixels {
1096 let Self {
1097 track_bounds,
1098 thumb_bounds,
1099 axis,
1100 ..
1101 } = self;
1102 let axis = *axis;
1103
1104 let viewport_size = track_bounds.size.along(axis);
1105 let thumb_size = thumb_bounds.size.along(axis);
1106 let thumb_offset = match event_type {
1107 ScrollbarMouseEvent::TrackClick => thumb_size / 2.,
1108 ScrollbarMouseEvent::ThumbDrag(thumb_offset) => thumb_offset,
1109 };
1110
1111 let thumb_start =
1112 (event_position.along(axis) - track_bounds.origin.along(axis) - thumb_offset)
1113 .clamp(px(0.), viewport_size - thumb_size);
1114
1115 let max_offset = max_offset.along(axis);
1116 let percentage = if viewport_size > thumb_size {
1117 thumb_start / (viewport_size - thumb_size)
1118 } else {
1119 0.
1120 };
1121
1122 -max_offset * percentage
1123 }
1124}
1125
1126impl PartialEq for ScrollbarLayout {
1127 fn eq(&self, other: &Self) -> bool {
1128 self.axis == other.axis && self.thumb_bounds == other.thumb_bounds
1129 }
1130}
1131
1132pub struct ScrollbarPrepaintState {
1133 parent_bounds_hitbox: Hitbox,
1134 thumbs: SmallVec<[ScrollbarLayout; 2]>,
1135}
1136
1137impl ScrollbarPrepaintState {
1138 fn thumb_for_position(&self, position: &Point<Pixels>) -> Option<&ScrollbarLayout> {
1139 self.thumbs
1140 .iter()
1141 .find(|info| info.thumb_bounds.contains(position))
1142 }
1143
1144 fn hit_for_position(&self, position: &Point<Pixels>) -> Option<&ScrollbarLayout> {
1145 self.thumbs.iter().find(|info| {
1146 if info.reserved_space.needs_scroll_track() {
1147 info.track_bounds.contains(position)
1148 } else {
1149 info.thumb_bounds.contains(position)
1150 }
1151 })
1152 }
1153}
1154
1155impl PartialEq for ScrollbarPrepaintState {
1156 fn eq(&self, other: &Self) -> bool {
1157 self.thumbs == other.thumbs
1158 }
1159}
1160
1161impl<T: ScrollableHandle> Element for ScrollbarElement<T> {
1162 type RequestLayoutState = ();
1163 type PrepaintState = Option<(ScrollbarPrepaintState, Option<f32>)>;
1164
1165 fn id(&self) -> Option<ElementId> {
1166 Some(("scrollbar_animation", self.state.entity_id()).into())
1167 }
1168
1169 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
1170 None
1171 }
1172
1173 fn request_layout(
1174 &mut self,
1175 _id: Option<&GlobalElementId>,
1176 _inspector_id: Option<&gpui::InspectorElementId>,
1177 window: &mut Window,
1178 cx: &mut App,
1179 ) -> (LayoutId, Self::RequestLayoutState) {
1180 let scrollbar_style = Style {
1181 position: Position::Absolute,
1182 inset: Edges::default(),
1183 size: size(relative(1.), relative(1.)).map(Into::into),
1184 ..Default::default()
1185 };
1186
1187 (window.request_layout(scrollbar_style, None, cx), ())
1188 }
1189
1190 fn prepaint(
1191 &mut self,
1192 id: Option<&GlobalElementId>,
1193 _inspector_id: Option<&gpui::InspectorElementId>,
1194 bounds: Bounds<Pixels>,
1195 _request_layout: &mut Self::RequestLayoutState,
1196 window: &mut Window,
1197 cx: &mut App,
1198 ) -> Self::PrepaintState {
1199 let prepaint_state =
1200 self.state
1201 .read(cx)
1202 .disabled()
1203 .not()
1204 .then(|| ScrollbarPrepaintState {
1205 thumbs: {
1206 let state = self.state.read(cx);
1207 let thumb_ranges = state.thumb_ranges().collect::<SmallVec<[_; 2]>>();
1208 let width = state.width.to_pixels();
1209 let track_color = state.track_color.as_ref();
1210
1211 let additional_padding = if thumb_ranges.len() == 2 {
1212 width
1213 } else {
1214 Pixels::ZERO
1215 };
1216
1217 thumb_ranges
1218 .into_iter()
1219 .map(|(axis, thumb_range, reserved_space)| {
1220 let track_anchor = match axis {
1221 ScrollbarAxis::Horizontal => Corner::BottomLeft,
1222 ScrollbarAxis::Vertical => Corner::TopRight,
1223 };
1224
1225 let scroll_track_bounds = Bounds::from_corner_and_size(
1226 track_anchor,
1227 self.origin + bounds.corner(track_anchor),
1228 bounds.size.apply_along(axis.invert(), |_| {
1229 width
1230 + match state.style {
1231 ScrollbarStyle::Rounded => 2 * SCROLLBAR_PADDING,
1232 ScrollbarStyle::Editor => Pixels::ZERO,
1233 }
1234 }),
1235 );
1236
1237 let has_border = track_color
1238 .is_some_and(|track_colors| track_colors.has_border());
1239
1240 // Rounded style needs a bit of padding, whereas for editor scrollbars,
1241 // we want the full length of the track
1242 let thumb_container_bounds = match state.style {
1243 ScrollbarStyle::Rounded => {
1244 scroll_track_bounds.dilate(-SCROLLBAR_PADDING)
1245 }
1246 ScrollbarStyle::Editor if has_border => scroll_track_bounds
1247 .extend(match axis {
1248 ScrollbarAxis::Horizontal => Edges {
1249 top: -BORDER_WIDTH,
1250 ..Default::default()
1251 },
1252
1253 ScrollbarAxis::Vertical => Edges {
1254 left: -BORDER_WIDTH,
1255 ..Default::default()
1256 },
1257 }),
1258 ScrollbarStyle::Editor => scroll_track_bounds,
1259 };
1260
1261 let available_space =
1262 thumb_container_bounds.size.along(axis) - additional_padding;
1263
1264 let thumb_offset = thumb_range.start * available_space;
1265 let thumb_end = thumb_range.end * available_space;
1266 let thumb_bounds = Bounds::new(
1267 thumb_container_bounds
1268 .origin
1269 .apply_along(axis, |origin| origin + thumb_offset),
1270 thumb_container_bounds
1271 .size
1272 .apply_along(axis, |_| thumb_end - thumb_offset),
1273 );
1274
1275 let needs_scroll_track = reserved_space.needs_scroll_track();
1276
1277 ScrollbarLayout {
1278 thumb_bounds,
1279 track_bounds: thumb_container_bounds,
1280 axis,
1281 cursor_hitbox: window.insert_hitbox(
1282 if needs_scroll_track {
1283 if has_border && state.style == ScrollbarStyle::Editor {
1284 scroll_track_bounds
1285 } else {
1286 thumb_container_bounds
1287 }
1288 } else {
1289 thumb_bounds
1290 },
1291 HitboxBehavior::BlockMouseExceptScroll,
1292 ),
1293 track_config: track_color
1294 .filter(|_| needs_scroll_track)
1295 .map(|color| (scroll_track_bounds, color.clone())),
1296 reserved_space,
1297 }
1298 })
1299 .collect()
1300 },
1301 parent_bounds_hitbox: window.insert_hitbox(bounds, HitboxBehavior::Normal),
1302 });
1303 if prepaint_state
1304 .as_ref()
1305 .is_some_and(|state| Some(state) != self.state.read(cx).last_prepaint_state.as_ref())
1306 {
1307 self.state
1308 .update(cx, |state, cx| state.show_scrollbars(window, cx));
1309 }
1310
1311 prepaint_state.map(|state| {
1312 let autohide_delta = self
1313 .state
1314 .read(cx)
1315 .show_state
1316 .animation_state()
1317 .map(|state| match state {
1318 AnimationState::InProgress {
1319 current_delta,
1320 animation_duration: delta_duration,
1321 showing: should_invert,
1322 } => window.with_element_state(id.unwrap(), |state, window| {
1323 let state = state.unwrap_or_else(|| Instant::now());
1324 let current = Instant::now();
1325
1326 let new_delta = DELTA_MAX.min(
1327 current_delta + (current - state).div_duration_f32(delta_duration),
1328 );
1329 self.state.update(cx, |state, _| {
1330 let has_border = state
1331 .track_color
1332 .as_ref()
1333 .is_some_and(|track_colors| track_colors.has_border());
1334 state.show_state.set_delta(new_delta, has_border)
1335 });
1336
1337 window.request_animation_frame();
1338 let delta = if should_invert {
1339 DELTA_MAX - current_delta
1340 } else {
1341 current_delta
1342 };
1343 (ease_in_out(delta), current)
1344 }),
1345 AnimationState::Stale => 1.0,
1346 });
1347
1348 (state, autohide_delta)
1349 })
1350 }
1351
1352 fn paint(
1353 &mut self,
1354 _id: Option<&GlobalElementId>,
1355 _inspector_id: Option<&gpui::InspectorElementId>,
1356 Bounds { origin, size }: Bounds<Pixels>,
1357 _request_layout: &mut Self::RequestLayoutState,
1358 prepaint_state: &mut Self::PrepaintState,
1359 window: &mut Window,
1360 cx: &mut App,
1361 ) {
1362 let Some((prepaint_state, autohide_fade)) = prepaint_state.take() else {
1363 return;
1364 };
1365
1366 let bounds = Bounds::new(self.origin + origin, size);
1367 window.with_content_mask(Some(ContentMask { bounds }), |window| {
1368 let colors = cx.theme().colors();
1369
1370 let capture_phase;
1371
1372 if self.state.read(cx).visible() {
1373 let state = self.state.read(cx);
1374 let thumb_state = &state.thumb_state;
1375 let style = state.style;
1376
1377 if thumb_state.is_dragging() {
1378 capture_phase = DispatchPhase::Capture;
1379 } else {
1380 capture_phase = DispatchPhase::Bubble;
1381 }
1382
1383 for ScrollbarLayout {
1384 thumb_bounds,
1385 cursor_hitbox,
1386 axis,
1387 reserved_space,
1388 track_config,
1389 ..
1390 } in &prepaint_state.thumbs
1391 {
1392 const MAXIMUM_OPACITY: f32 = 0.7;
1393 let (thumb_base_color, hovered) = match thumb_state {
1394 ThumbState::Dragging(dragged_axis, _) if dragged_axis == axis => {
1395 (colors.scrollbar_thumb_active_background, false)
1396 }
1397 ThumbState::Hover(hovered_axis) if hovered_axis == axis => {
1398 (colors.scrollbar_thumb_hover_background, true)
1399 }
1400 _ => (colors.scrollbar_thumb_background, false),
1401 };
1402
1403 let blend_color = track_config
1404 .as_ref()
1405 .map(|(_, colors)| colors.background)
1406 .unwrap_or(colors.surface_background);
1407
1408 let blending_color = if hovered || reserved_space.needs_scroll_track() {
1409 blend_color
1410 } else {
1411 blend_color.min(blend_color.alpha(MAXIMUM_OPACITY))
1412 };
1413
1414 let mut thumb_color = blending_color.blend(thumb_base_color);
1415
1416 if !hovered && let Some(fade) = autohide_fade {
1417 thumb_color.fade_out(fade);
1418 }
1419
1420 if let Some((track_bounds, colors)) = track_config {
1421 let has_border = colors.border.is_some();
1422
1423 let mut track_color = colors.background;
1424 if let Some(fade) = autohide_fade
1425 && !has_border
1426 {
1427 track_color.fade_out(fade);
1428 }
1429
1430 let border_edges = has_border
1431 .then(|| match axis {
1432 ScrollbarAxis::Horizontal => Edges {
1433 top: BORDER_WIDTH,
1434 ..Default::default()
1435 },
1436 ScrollbarAxis::Vertical => Edges {
1437 left: BORDER_WIDTH,
1438 ..Default::default()
1439 },
1440 })
1441 .unwrap_or_default();
1442
1443 window.paint_quad(quad(
1444 *track_bounds,
1445 Corners::default(),
1446 track_color,
1447 border_edges,
1448 colors.border.unwrap_or_else(Hsla::transparent_black),
1449 BorderStyle::Solid,
1450 ));
1451 }
1452
1453 window.paint_quad(quad(
1454 *thumb_bounds,
1455 match style {
1456 ScrollbarStyle::Rounded => Corners::all(Pixels::MAX)
1457 .clamp_radii_for_quad_size(thumb_bounds.size),
1458 ScrollbarStyle::Editor => Corners::default(),
1459 },
1460 thumb_color,
1461 Edges::default(),
1462 Hsla::transparent_black(),
1463 BorderStyle::default(),
1464 ));
1465
1466 if thumb_state.is_dragging() {
1467 window.set_window_cursor_style(CursorStyle::Arrow);
1468 } else {
1469 window.set_cursor_style(CursorStyle::Arrow, cursor_hitbox);
1470 }
1471 }
1472 } else {
1473 capture_phase = DispatchPhase::Bubble;
1474 }
1475
1476 self.state.update(cx, |state, _| {
1477 state.last_prepaint_state = Some(prepaint_state)
1478 });
1479
1480 window.on_mouse_event({
1481 let state = self.state.clone();
1482
1483 move |event: &MouseDownEvent, phase, window, cx| {
1484 state.update(cx, |state, cx| {
1485 let Some(scrollbar_layout) = (phase == capture_phase
1486 && event.button == MouseButton::Left)
1487 .then(|| state.hit_for_position(&event.position))
1488 .flatten()
1489 else {
1490 return;
1491 };
1492
1493 let ScrollbarLayout {
1494 thumb_bounds, axis, ..
1495 } = scrollbar_layout;
1496
1497 if thumb_bounds.contains(&event.position) {
1498 let offset =
1499 event.position.along(*axis) - thumb_bounds.origin.along(*axis);
1500 state.set_dragging(*axis, offset, window, cx);
1501 } else {
1502 let scroll_handle = state.scroll_handle();
1503 let click_offset = scrollbar_layout.compute_click_offset(
1504 event.position,
1505 scroll_handle.max_offset(),
1506 ScrollbarMouseEvent::TrackClick,
1507 );
1508 state.set_offset(
1509 scroll_handle.offset().apply_along(*axis, |_| click_offset),
1510 cx,
1511 );
1512 };
1513
1514 cx.stop_propagation();
1515 });
1516 }
1517 });
1518
1519 window.on_mouse_event({
1520 let state = self.state.clone();
1521
1522 move |event: &ScrollWheelEvent, phase, window, cx| {
1523 state.update(cx, |state, cx| {
1524 if phase.capture() && state.parent_hovered(window) {
1525 state.update_hovered_thumb(&event.position, window, cx)
1526 }
1527 });
1528 }
1529 });
1530
1531 window.on_mouse_event({
1532 let state = self.state.clone();
1533
1534 move |event: &MouseMoveEvent, phase, window, cx| {
1535 if phase != capture_phase {
1536 return;
1537 }
1538
1539 match state.read(cx).thumb_state {
1540 ThumbState::Dragging(axis, drag_state) if event.dragging() => {
1541 if let Some(scrollbar_layout) = state.read(cx).thumb_for_axis(axis) {
1542 let scroll_handle = state.read(cx).scroll_handle();
1543 let drag_offset = scrollbar_layout.compute_click_offset(
1544 event.position,
1545 scroll_handle.max_offset(),
1546 ScrollbarMouseEvent::ThumbDrag(drag_state),
1547 );
1548 let new_offset =
1549 scroll_handle.offset().apply_along(axis, |_| drag_offset);
1550
1551 state.update(cx, |state, cx| state.set_offset(new_offset, cx));
1552 cx.stop_propagation();
1553 }
1554 }
1555 _ => state.update(cx, |state, cx| {
1556 match state.update_parent_hovered(window) {
1557 hover @ ParentHoverEvent::Entered
1558 | hover @ ParentHoverEvent::Within
1559 if event.pressed_button.is_none() =>
1560 {
1561 if matches!(hover, ParentHoverEvent::Entered) {
1562 state.show_scrollbars(window, cx);
1563 }
1564 state.update_hovered_thumb(&event.position, window, cx);
1565 if state.thumb_state != ThumbState::Inactive {
1566 cx.stop_propagation();
1567 }
1568 }
1569 ParentHoverEvent::Exited => {
1570 state.set_thumb_state(ThumbState::Inactive, window, cx);
1571 }
1572 _ => {}
1573 }
1574 }),
1575 }
1576 }
1577 });
1578
1579 window.on_mouse_event({
1580 let state = self.state.clone();
1581 move |event: &MouseUpEvent, phase, window, cx| {
1582 if phase != capture_phase {
1583 return;
1584 }
1585
1586 state.update(cx, |state, cx| {
1587 if state.is_dragging() {
1588 state.scroll_handle().drag_ended();
1589 }
1590
1591 if !state.parent_hovered(window) {
1592 state.schedule_auto_hide(window, cx);
1593 return;
1594 }
1595
1596 state.update_hovered_thumb(&event.position, window, cx);
1597 });
1598 }
1599 });
1600 })
1601 }
1602}
1603
1604impl<T: ScrollableHandle> IntoElement for ScrollbarElement<T> {
1605 type Element = Self;
1606
1607 fn into_element(self) -> Self::Element {
1608 self
1609 }
1610}