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