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