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