1use std::{
2 cell::Cell,
3 ops::Deref,
4 rc::Rc,
5 time::{Duration, Instant},
6};
7
8use crate::{ActiveTheme, AxisExt};
9use gpui::{
10 fill, point, px, relative, size, App, Axis, BorderStyle, Bounds, ContentMask, Corner,
11 CursorStyle, Edges, Element, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InspectorElementId,
12 IntoElement, IsZero, LayoutId, ListState, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
13 PaintQuad, Pixels, Point, Position, ScrollHandle, ScrollWheelEvent, Size, Style, Timer,
14 UniformListScrollHandle, Window,
15};
16use schemars::JsonSchema;
17use serde::{Deserialize, Serialize};
18
19const WIDTH: Pixels = px(2. * 2. + 8.);
21const MIN_THUMB_SIZE: f32 = 48.;
22
23const THUMB_WIDTH: Pixels = px(6.);
24const THUMB_RADIUS: Pixels = px(6. / 2.);
25const THUMB_INSET: Pixels = px(2.);
26
27const THUMB_ACTIVE_WIDTH: Pixels = px(8.);
28const THUMB_ACTIVE_RADIUS: Pixels = px(8. / 2.);
29const THUMB_ACTIVE_INSET: Pixels = px(2.);
30
31const FADE_OUT_DURATION: f32 = 3.0;
32const FADE_OUT_DELAY: f32 = 2.0;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default, JsonSchema)]
36pub enum ScrollbarShow {
37 #[default]
39 Scrolling,
40 Hover,
42 Always,
44}
45
46impl ScrollbarShow {
47 fn is_hover(&self) -> bool {
48 matches!(self, Self::Hover)
49 }
50
51 fn is_always(&self) -> bool {
52 matches!(self, Self::Always)
53 }
54}
55
56pub trait ScrollHandleOffsetable {
58 fn offset(&self) -> Point<Pixels>;
60 fn set_offset(&self, offset: Point<Pixels>);
62 fn content_size(&self) -> Size<Pixels>;
64 fn start_drag(&self) {}
66 fn end_drag(&self) {}
68}
69
70impl ScrollHandleOffsetable for ScrollHandle {
71 fn offset(&self) -> Point<Pixels> {
72 self.offset()
73 }
74
75 fn set_offset(&self, offset: Point<Pixels>) {
76 self.set_offset(offset);
77 }
78
79 fn content_size(&self) -> Size<Pixels> {
80 self.max_offset() + self.bounds().size
81 }
82}
83
84impl ScrollHandleOffsetable for UniformListScrollHandle {
85 fn offset(&self) -> Point<Pixels> {
86 self.0.borrow().base_handle.offset()
87 }
88
89 fn set_offset(&self, offset: Point<Pixels>) {
90 self.0.borrow_mut().base_handle.set_offset(offset)
91 }
92
93 fn content_size(&self) -> Size<Pixels> {
94 let base_handle = &self.0.borrow().base_handle;
95 base_handle.max_offset() + base_handle.bounds().size
96 }
97}
98
99impl ScrollHandleOffsetable for ListState {
100 fn offset(&self) -> Point<Pixels> {
101 self.scroll_px_offset_for_scrollbar()
102 }
103
104 fn set_offset(&self, offset: Point<Pixels>) {
105 self.set_offset_from_scrollbar(offset);
106 }
107
108 fn content_size(&self) -> Size<Pixels> {
109 self.viewport_bounds().size + self.max_offset_for_scrollbar()
110 }
111
112 fn start_drag(&self) {
113 self.scrollbar_drag_started();
114 }
115
116 fn end_drag(&self) {
117 self.scrollbar_drag_ended();
118 }
119}
120
121#[doc(hidden)]
122#[derive(Debug, Clone)]
123pub struct ScrollbarState(Rc<Cell<ScrollbarStateInner>>);
124
125#[doc(hidden)]
126#[derive(Debug, Clone, Copy)]
127pub struct ScrollbarStateInner {
128 hovered_axis: Option<Axis>,
129 hovered_on_thumb: Option<Axis>,
130 dragged_axis: Option<Axis>,
131 drag_pos: Point<Pixels>,
132 last_scroll_offset: Point<Pixels>,
133 last_scroll_time: Option<Instant>,
134 last_update: Instant,
136 idle_timer_scheduled: bool,
137}
138
139impl Default for ScrollbarState {
140 fn default() -> Self {
141 Self(Rc::new(Cell::new(ScrollbarStateInner {
142 hovered_axis: None,
143 hovered_on_thumb: None,
144 dragged_axis: None,
145 drag_pos: point(px(0.), px(0.)),
146 last_scroll_offset: point(px(0.), px(0.)),
147 last_scroll_time: None,
148 last_update: Instant::now(),
149 idle_timer_scheduled: false,
150 })))
151 }
152}
153
154impl Deref for ScrollbarState {
155 type Target = Rc<Cell<ScrollbarStateInner>>;
156
157 fn deref(&self) -> &Self::Target {
158 &self.0
159 }
160}
161
162impl ScrollbarStateInner {
163 fn with_drag_pos(&self, axis: Axis, pos: Point<Pixels>) -> Self {
164 let mut state = *self;
165 if axis.is_vertical() {
166 state.drag_pos.y = pos.y;
167 } else {
168 state.drag_pos.x = pos.x;
169 }
170
171 state.dragged_axis = Some(axis);
172 state
173 }
174
175 fn with_unset_drag_pos(&self) -> Self {
176 let mut state = *self;
177 state.dragged_axis = None;
178 state
179 }
180
181 fn with_hovered(&self, axis: Option<Axis>) -> Self {
182 let mut state = *self;
183 state.hovered_axis = axis;
184 if axis.is_some() {
185 state.last_scroll_time = Some(std::time::Instant::now());
186 }
187 state
188 }
189
190 fn with_hovered_on_thumb(&self, axis: Option<Axis>) -> Self {
191 let mut state = *self;
192 state.hovered_on_thumb = axis;
193 if self.is_scrollbar_visible() {
194 if axis.is_some() {
195 state.last_scroll_time = Some(std::time::Instant::now());
196 }
197 }
198 state
199 }
200
201 fn with_last_scroll(
202 &self,
203 last_scroll_offset: Point<Pixels>,
204 last_scroll_time: Option<Instant>,
205 ) -> Self {
206 let mut state = *self;
207 state.last_scroll_offset = last_scroll_offset;
208 state.last_scroll_time = last_scroll_time;
209 state
210 }
211
212 fn with_last_scroll_time(&self, t: Option<Instant>) -> Self {
213 let mut state = *self;
214 state.last_scroll_time = t;
215 state
216 }
217
218 fn with_last_update(&self, t: Instant) -> Self {
219 let mut state = *self;
220 state.last_update = t;
221 state
222 }
223
224 fn with_idle_timer_scheduled(&self, scheduled: bool) -> Self {
225 let mut state = *self;
226 state.idle_timer_scheduled = scheduled;
227 state
228 }
229
230 fn is_scrollbar_visible(&self) -> bool {
231 if self.dragged_axis.is_some() {
233 return true;
234 }
235
236 if let Some(last_time) = self.last_scroll_time {
237 let elapsed = Instant::now().duration_since(last_time).as_secs_f32();
238 elapsed < FADE_OUT_DURATION
239 } else {
240 false
241 }
242 }
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq)]
247pub enum ScrollbarAxis {
248 Vertical,
250 Horizontal,
252 Both,
254}
255
256impl From<Axis> for ScrollbarAxis {
257 fn from(axis: Axis) -> Self {
258 match axis {
259 Axis::Vertical => Self::Vertical,
260 Axis::Horizontal => Self::Horizontal,
261 }
262 }
263}
264
265impl ScrollbarAxis {
266 #[inline]
268 pub fn is_vertical(&self) -> bool {
269 matches!(self, Self::Vertical)
270 }
271
272 #[inline]
274 pub fn is_horizontal(&self) -> bool {
275 matches!(self, Self::Horizontal)
276 }
277
278 #[inline]
280 pub fn is_both(&self) -> bool {
281 matches!(self, Self::Both)
282 }
283
284 #[inline]
286 pub fn has_vertical(&self) -> bool {
287 matches!(self, Self::Vertical | Self::Both)
288 }
289
290 #[inline]
292 pub fn has_horizontal(&self) -> bool {
293 matches!(self, Self::Horizontal | Self::Both)
294 }
295
296 #[inline]
297 fn all(&self) -> Vec<Axis> {
298 match self {
299 Self::Vertical => vec![Axis::Vertical],
300 Self::Horizontal => vec![Axis::Horizontal],
301 Self::Both => vec![Axis::Horizontal, Axis::Vertical],
304 }
305 }
306}
307
308pub struct Scrollbar {
310 axis: ScrollbarAxis,
311 scrollbar_show: Option<ScrollbarShow>,
312 scroll_handle: Rc<dyn ScrollHandleOffsetable>,
313 state: ScrollbarState,
314 scroll_size: Option<Size<Pixels>>,
315 max_fps: usize,
320}
321
322impl Scrollbar {
323 fn new(
324 axis: impl Into<ScrollbarAxis>,
325 state: &ScrollbarState,
326 scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
327 ) -> Self {
328 Self {
329 state: state.clone(),
330 axis: axis.into(),
331 scrollbar_show: None,
332 scroll_handle: Rc::new(scroll_handle.clone()),
333 max_fps: 120,
334 scroll_size: None,
335 }
336 }
337
338 pub fn both(
340 state: &ScrollbarState,
341 scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
342 ) -> Self {
343 Self::new(ScrollbarAxis::Both, state, scroll_handle)
344 }
345
346 pub fn horizontal(
348 state: &ScrollbarState,
349 scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
350 ) -> Self {
351 Self::new(ScrollbarAxis::Horizontal, state, scroll_handle)
352 }
353
354 pub fn vertical(
356 state: &ScrollbarState,
357 scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
358 ) -> Self {
359 Self::new(ScrollbarAxis::Vertical, state, scroll_handle)
360 }
361
362 pub fn uniform_scroll(
364 state: &ScrollbarState,
365 scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
366 ) -> Self {
367 Self::new(ScrollbarAxis::Vertical, state, scroll_handle)
368 }
369
370 pub fn scrollbar_show(mut self, scrollbar_show: ScrollbarShow) -> Self {
372 self.scrollbar_show = Some(scrollbar_show);
373 self
374 }
375
376 pub fn scroll_size(mut self, scroll_size: Size<Pixels>) -> Self {
380 self.scroll_size = Some(scroll_size);
381 self
382 }
383
384 pub fn axis(mut self, axis: impl Into<ScrollbarAxis>) -> Self {
386 self.axis = axis.into();
387 self
388 }
389
390 pub(crate) fn max_fps(mut self, max_fps: usize) -> Self {
396 self.max_fps = max_fps.clamp(30, 120);
397 self
398 }
399
400 pub(crate) const fn width() -> Pixels {
402 WIDTH
403 }
404
405 fn style_for_active(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
406 (
407 cx.theme().scrollbar_thumb_hover,
408 cx.theme().scrollbar,
409 cx.theme().border,
410 THUMB_ACTIVE_WIDTH,
411 THUMB_ACTIVE_INSET,
412 THUMB_ACTIVE_RADIUS,
413 )
414 }
415
416 fn style_for_hovered_thumb(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
417 (
418 cx.theme().scrollbar_thumb_hover,
419 cx.theme().scrollbar,
420 cx.theme().border,
421 THUMB_ACTIVE_WIDTH,
422 THUMB_ACTIVE_INSET,
423 THUMB_ACTIVE_RADIUS,
424 )
425 }
426
427 fn style_for_hovered_bar(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
428 (
429 cx.theme().scrollbar_thumb,
430 cx.theme().scrollbar,
431 gpui::transparent_black(),
432 THUMB_ACTIVE_WIDTH,
433 THUMB_ACTIVE_INSET,
434 THUMB_ACTIVE_RADIUS,
435 )
436 }
437
438 fn style_for_normal(&self, cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
439 let scrollbar_show = self.scrollbar_show.unwrap_or(cx.theme().scrollbar_show);
440 let (width, inset, radius) = match scrollbar_show {
441 ScrollbarShow::Scrolling => (THUMB_WIDTH, THUMB_INSET, THUMB_RADIUS),
442 _ => (THUMB_ACTIVE_WIDTH, THUMB_ACTIVE_INSET, THUMB_ACTIVE_RADIUS),
443 };
444
445 (
446 cx.theme().scrollbar_thumb,
447 cx.theme().scrollbar,
448 gpui::transparent_black(),
449 width,
450 inset,
451 radius,
452 )
453 }
454
455 fn style_for_idle(&self, cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
456 let scrollbar_show = self.scrollbar_show.unwrap_or(cx.theme().scrollbar_show);
457 let (width, inset, radius) = match scrollbar_show {
458 ScrollbarShow::Scrolling => (THUMB_WIDTH, THUMB_INSET, THUMB_RADIUS),
459 _ => (THUMB_ACTIVE_WIDTH, THUMB_ACTIVE_INSET, THUMB_ACTIVE_RADIUS),
460 };
461
462 (
463 gpui::transparent_black(),
464 gpui::transparent_black(),
465 gpui::transparent_black(),
466 width,
467 inset,
468 radius,
469 )
470 }
471}
472
473impl IntoElement for Scrollbar {
474 type Element = Self;
475
476 fn into_element(self) -> Self::Element {
477 self
478 }
479}
480
481#[doc(hidden)]
482pub struct PrepaintState {
483 hitbox: Hitbox,
484 states: Vec<AxisPrepaintState>,
485}
486
487#[doc(hidden)]
488pub struct AxisPrepaintState {
489 axis: Axis,
490 bar_hitbox: Hitbox,
491 bounds: Bounds<Pixels>,
492 radius: Pixels,
493 bg: Hsla,
494 border: Hsla,
495 thumb_bounds: Bounds<Pixels>,
496 thumb_fill_bounds: Bounds<Pixels>,
498 thumb_bg: Hsla,
499 scroll_size: Pixels,
500 container_size: Pixels,
501 thumb_size: Pixels,
502 margin_end: Pixels,
503}
504
505impl Element for Scrollbar {
506 type RequestLayoutState = ();
507 type PrepaintState = PrepaintState;
508
509 fn id(&self) -> Option<gpui::ElementId> {
510 None
511 }
512
513 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
514 None
515 }
516
517 fn request_layout(
518 &mut self,
519 _: Option<&GlobalElementId>,
520 _: Option<&InspectorElementId>,
521 window: &mut Window,
522 cx: &mut App,
523 ) -> (LayoutId, Self::RequestLayoutState) {
524 let mut style = Style::default();
525 style.position = Position::Absolute;
526 style.flex_grow = 1.0;
527 style.flex_shrink = 1.0;
528 style.size.width = relative(1.).into();
529 style.size.height = relative(1.).into();
530
531 (window.request_layout(style, None, cx), ())
532 }
533
534 fn prepaint(
535 &mut self,
536 _: Option<&GlobalElementId>,
537 _: Option<&InspectorElementId>,
538 bounds: Bounds<Pixels>,
539 _: &mut Self::RequestLayoutState,
540 window: &mut Window,
541 cx: &mut App,
542 ) -> Self::PrepaintState {
543 let hitbox = window.with_content_mask(Some(ContentMask { bounds }), |window| {
544 window.insert_hitbox(bounds, HitboxBehavior::Normal)
545 });
546
547 let mut states = vec![];
548 let mut has_both = self.axis.is_both();
549 let scroll_size = self
550 .scroll_size
551 .unwrap_or(self.scroll_handle.content_size());
552
553 for axis in self.axis.all().into_iter() {
554 let is_vertical = axis.is_vertical();
555 let (scroll_area_size, container_size, scroll_position) = if is_vertical {
556 (
557 scroll_size.height,
558 hitbox.size.height,
559 self.scroll_handle.offset().y,
560 )
561 } else {
562 (
563 scroll_size.width,
564 hitbox.size.width,
565 self.scroll_handle.offset().x,
566 )
567 };
568
569 let margin_end = if has_both && !is_vertical {
571 WIDTH
572 } else {
573 px(0.)
574 };
575
576 if scroll_area_size <= container_size {
578 has_both = false;
579 continue;
580 }
581
582 let thumb_length =
583 (container_size / scroll_area_size * container_size).max(px(MIN_THUMB_SIZE));
584 let thumb_start = -(scroll_position / (scroll_area_size - container_size)
585 * (container_size - margin_end - thumb_length));
586 let thumb_end = (thumb_start + thumb_length).min(container_size - margin_end);
587
588 let bounds = Bounds {
589 origin: if is_vertical {
590 point(hitbox.origin.x + hitbox.size.width - WIDTH, hitbox.origin.y)
591 } else {
592 point(
593 hitbox.origin.x,
594 hitbox.origin.y + hitbox.size.height - WIDTH,
595 )
596 },
597 size: gpui::Size {
598 width: if is_vertical {
599 WIDTH
600 } else {
601 hitbox.size.width
602 },
603 height: if is_vertical {
604 hitbox.size.height
605 } else {
606 WIDTH
607 },
608 },
609 };
610
611 let scrollbar_show = self.scrollbar_show.unwrap_or(cx.theme().scrollbar_show);
612 let state = self.state.clone();
613 let is_always_to_show = scrollbar_show.is_always();
614 let is_hover_to_show = scrollbar_show.is_hover();
615 let is_hovered_on_bar = state.get().hovered_axis == Some(axis);
616 let is_hovered_on_thumb = state.get().hovered_on_thumb == Some(axis);
617 let is_offset_changed = state.get().last_scroll_offset != self.scroll_handle.offset();
618
619 let (thumb_bg, bar_bg, bar_border, thumb_width, inset, radius) =
620 if state.get().dragged_axis == Some(axis) {
621 Self::style_for_active(cx)
622 } else if is_hover_to_show && (is_hovered_on_bar || is_hovered_on_thumb) {
623 if is_hovered_on_thumb {
624 Self::style_for_hovered_thumb(cx)
625 } else {
626 Self::style_for_hovered_bar(cx)
627 }
628 } else if is_offset_changed {
629 self.style_for_normal(cx)
630 } else if is_always_to_show {
631 if is_hovered_on_thumb {
632 Self::style_for_hovered_thumb(cx)
633 } else {
634 Self::style_for_hovered_bar(cx)
635 }
636 } else {
637 let mut idle_state = self.style_for_idle(cx);
638 if let Some(last_time) = state.get().last_scroll_time {
640 let elapsed = Instant::now().duration_since(last_time).as_secs_f32();
641 if is_hovered_on_bar {
642 state.set(state.get().with_last_scroll_time(Some(Instant::now())));
643 idle_state = if is_hovered_on_thumb {
644 Self::style_for_hovered_thumb(cx)
645 } else {
646 Self::style_for_hovered_bar(cx)
647 };
648 } else if elapsed < FADE_OUT_DELAY {
649 idle_state.0 = cx.theme().scrollbar_thumb;
650
651 if !state.get().idle_timer_scheduled {
652 let state = state.clone();
653 state.set(state.get().with_idle_timer_scheduled(true));
654 let current_view = window.current_view();
655 let next_delay = Duration::from_secs_f32(FADE_OUT_DELAY - elapsed);
656 window
657 .spawn(cx, async move |cx| {
658 Timer::after(next_delay).await;
659 state.set(state.get().with_idle_timer_scheduled(false));
660 cx.update(|_, cx| cx.notify(current_view)).ok();
661 })
662 .detach();
663 }
664 } else if elapsed < FADE_OUT_DURATION {
665 let opacity = 1.0 - (elapsed - FADE_OUT_DELAY).powi(10);
666 idle_state.0 = cx.theme().scrollbar_thumb.opacity(opacity);
667
668 window.request_animation_frame();
669 }
670 }
671
672 idle_state
673 };
674
675 let thumb_length = thumb_end - thumb_start - inset * 2;
677 let thumb_bounds = if is_vertical {
678 Bounds::from_corner_and_size(
679 Corner::TopRight,
680 bounds.top_right() + point(-inset, inset + thumb_start),
681 size(WIDTH, thumb_length),
682 )
683 } else {
684 Bounds::from_corner_and_size(
685 Corner::BottomLeft,
686 bounds.bottom_left() + point(inset + thumb_start, -inset),
687 size(thumb_length, WIDTH),
688 )
689 };
690
691 let thumb_fill_bounds = if is_vertical {
693 Bounds::from_corner_and_size(
694 Corner::TopRight,
695 bounds.top_right() + point(-inset, inset + thumb_start),
696 size(thumb_width, thumb_length),
697 )
698 } else {
699 Bounds::from_corner_and_size(
700 Corner::BottomLeft,
701 bounds.bottom_left() + point(inset + thumb_start, -inset),
702 size(thumb_length, thumb_width),
703 )
704 };
705
706 let bar_hitbox = window.with_content_mask(Some(ContentMask { bounds }), |window| {
707 window.insert_hitbox(bounds, gpui::HitboxBehavior::Normal)
708 });
709
710 states.push(AxisPrepaintState {
711 axis,
712 bar_hitbox,
713 bounds,
714 radius,
715 bg: bar_bg,
716 border: bar_border,
717 thumb_bounds,
718 thumb_fill_bounds,
719 thumb_bg,
720 scroll_size: scroll_area_size,
721 container_size,
722 thumb_size: thumb_length,
723 margin_end,
724 })
725 }
726
727 PrepaintState { hitbox, states }
728 }
729
730 fn paint(
731 &mut self,
732 _: Option<&GlobalElementId>,
733 _: Option<&InspectorElementId>,
734 _: Bounds<Pixels>,
735 _: &mut Self::RequestLayoutState,
736 prepaint: &mut Self::PrepaintState,
737 window: &mut Window,
738 cx: &mut App,
739 ) {
740 let scrollbar_show = self.scrollbar_show.unwrap_or(cx.theme().scrollbar_show);
741 let view_id = window.current_view();
742 let hitbox_bounds = prepaint.hitbox.bounds;
743 let is_visible = self.state.get().is_scrollbar_visible() || scrollbar_show.is_always();
744 let is_hover_to_show = scrollbar_show.is_hover();
745
746 if self.scroll_handle.offset() != self.state.get().last_scroll_offset {
748 self.state.set(
749 self.state
750 .get()
751 .with_last_scroll(self.scroll_handle.offset(), Some(Instant::now())),
752 );
753 cx.notify(view_id);
754 }
755
756 window.with_content_mask(
757 Some(ContentMask {
758 bounds: hitbox_bounds,
759 }),
760 |window| {
761 for state in prepaint.states.iter() {
762 let axis = state.axis;
763 let mut radius = state.radius;
764 if cx.theme().radius.is_zero() {
765 radius = px(0.);
766 }
767 let bounds = state.bounds;
768 let thumb_bounds = state.thumb_bounds;
769 let scroll_area_size = state.scroll_size;
770 let container_size = state.container_size;
771 let thumb_size = state.thumb_size;
772 let margin_end = state.margin_end;
773 let is_vertical = axis.is_vertical();
774
775 window.set_cursor_style(CursorStyle::default(), &state.bar_hitbox);
776
777 window.paint_layer(hitbox_bounds, |cx| {
778 cx.paint_quad(fill(state.bounds, state.bg));
779
780 cx.paint_quad(PaintQuad {
781 bounds,
782 corner_radii: (0.).into(),
783 background: gpui::transparent_black().into(),
784 border_widths: if is_vertical {
785 Edges {
786 top: px(0.),
787 right: px(0.),
788 bottom: px(0.),
789 left: px(0.),
790 }
791 } else {
792 Edges {
793 top: px(0.),
794 right: px(0.),
795 bottom: px(0.),
796 left: px(0.),
797 }
798 },
799 border_color: state.border,
800 border_style: BorderStyle::default(),
801 });
802
803 cx.paint_quad(
804 fill(state.thumb_fill_bounds, state.thumb_bg).corner_radii(radius),
805 );
806 });
807
808 window.on_mouse_event({
809 let state = self.state.clone();
810 let scroll_handle = self.scroll_handle.clone();
811
812 move |event: &ScrollWheelEvent, phase, _, cx| {
813 if phase.bubble() && hitbox_bounds.contains(&event.position) {
814 if scroll_handle.offset() != state.get().last_scroll_offset {
815 state.set(state.get().with_last_scroll(
816 scroll_handle.offset(),
817 Some(Instant::now()),
818 ));
819 cx.notify(view_id);
820 }
821 }
822 }
823 });
824
825 let safe_range = (-scroll_area_size + container_size)..px(0.);
826
827 if is_hover_to_show || is_visible {
828 window.on_mouse_event({
829 let state = self.state.clone();
830 let scroll_handle = self.scroll_handle.clone();
831
832 move |event: &MouseDownEvent, phase, _, cx| {
833 if phase.bubble() && bounds.contains(&event.position) {
834 cx.stop_propagation();
835
836 if thumb_bounds.contains(&event.position) {
837 let pos = event.position - thumb_bounds.origin;
839
840 scroll_handle.start_drag();
841 state.set(state.get().with_drag_pos(axis, pos));
842
843 cx.notify(view_id);
844 } else {
845 let offset = scroll_handle.offset();
848 let percentage = if is_vertical {
849 (event.position.y - thumb_size / 2. - bounds.origin.y)
850 / (bounds.size.height - thumb_size)
851 } else {
852 (event.position.x - thumb_size / 2. - bounds.origin.x)
853 / (bounds.size.width - thumb_size)
854 }
855 .min(1.);
856
857 if is_vertical {
858 scroll_handle.set_offset(point(
859 offset.x,
860 (-scroll_area_size * percentage)
861 .clamp(safe_range.start, safe_range.end),
862 ));
863 } else {
864 scroll_handle.set_offset(point(
865 (-scroll_area_size * percentage)
866 .clamp(safe_range.start, safe_range.end),
867 offset.y,
868 ));
869 }
870 }
871 }
872 }
873 });
874 }
875
876 window.on_mouse_event({
877 let scroll_handle = self.scroll_handle.clone();
878 let state = self.state.clone();
879 let max_fps_duration = Duration::from_millis((1000 / self.max_fps) as u64);
880
881 move |event: &MouseMoveEvent, _, _, cx| {
882 let mut notify = false;
883 let need_hover_to_update = is_hover_to_show || is_visible;
886 if bounds.contains(&event.position) && need_hover_to_update {
888 state.set(state.get().with_hovered(Some(axis)));
889
890 if state.get().hovered_axis != Some(axis) {
891 notify = true;
892 }
893 } else {
894 if state.get().hovered_axis == Some(axis) {
895 if state.get().hovered_axis.is_some() {
896 state.set(state.get().with_hovered(None));
897 notify = true;
898 }
899 }
900 }
901
902 if thumb_bounds.contains(&event.position) {
904 if state.get().hovered_on_thumb != Some(axis) {
905 state.set(state.get().with_hovered_on_thumb(Some(axis)));
906 notify = true;
907 }
908 } else {
909 if state.get().hovered_on_thumb == Some(axis) {
910 state.set(state.get().with_hovered_on_thumb(None));
911 notify = true;
912 }
913 }
914
915 if state.get().dragged_axis == Some(axis) && event.dragging() {
917 cx.stop_propagation();
919
920 let drag_pos = state.get().drag_pos;
923
924 let percentage = (if is_vertical {
925 (event.position.y - drag_pos.y - bounds.origin.y)
926 / (bounds.size.height - thumb_size)
927 } else {
928 (event.position.x - drag_pos.x - bounds.origin.x)
929 / (bounds.size.width - thumb_size - margin_end)
930 })
931 .clamp(0., 1.);
932
933 let offset = if is_vertical {
934 point(
935 scroll_handle.offset().x,
936 (-(scroll_area_size - container_size) * percentage)
937 .clamp(safe_range.start, safe_range.end),
938 )
939 } else {
940 point(
941 (-(scroll_area_size - container_size) * percentage)
942 .clamp(safe_range.start, safe_range.end),
943 scroll_handle.offset().y,
944 )
945 };
946
947 if (scroll_handle.offset().y - offset.y).abs() > px(1.)
948 || (scroll_handle.offset().x - offset.x).abs() > px(1.)
949 {
950 if state.get().last_update.elapsed() > max_fps_duration {
952 scroll_handle.set_offset(offset);
953 state.set(state.get().with_last_update(Instant::now()));
954 notify = true;
955 }
956 }
957 }
958
959 if notify {
960 cx.notify(view_id);
961 }
962 }
963 });
964
965 window.on_mouse_event({
966 let scroll_handle = self.scroll_handle.clone();
967 let state = self.state.clone();
968
969 move |_event: &MouseUpEvent, phase, _, cx| {
970 if phase.bubble() {
971 scroll_handle.end_drag();
972 state.set(state.get().with_unset_drag_pos());
973 cx.notify(view_id);
974 }
975 }
976 });
977 }
978 },
979 );
980 }
981}