1use super::*;
4
5#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
18pub(crate) struct Hover {
19 over: bool,
20 out: bool,
21}
22
23impl Hover {
24 pub(crate) fn held(self) -> bool {
25 self.over && !self.out
26 }
27}
28
29pub(super) type SetHover = Rc<dyn Fn(Hover, &mut Window, &mut App)>;
30
31pub(crate) fn hover_track<E: StatefulInteractiveElement>(
33 track: E,
34 hover: Rc<Cell<Hover>>,
35 changed: impl Fn(&mut Window, &mut App) + 'static,
36) -> E {
37 let set: SetHover = {
38 let hover = hover.clone();
39 Rc::new(move |next, window, cx| {
40 if next != hover.get() {
41 hover.set(next);
42 changed(window, cx);
43 }
44 })
45 };
46 let (over, exit, moved) = (hover.clone(), hover.clone(), hover);
47 let (set_exit, set_moved) = (set.clone(), set.clone());
48 track
49 .on_hover(move |hovered, window, cx| {
50 let over = Hover {
51 over: *hovered,
52 ..over.get()
53 };
54 set(over, window, cx)
55 })
56 .on_mouse_exit(move |_, window, cx| {
57 let out = Hover {
58 out: true,
59 ..exit.get()
60 };
61 set_exit(out, window, cx)
62 })
63 .on_mouse_move(move |_, window, cx| {
64 let back = Hover {
65 out: false,
66 ..moved.get()
67 };
68 set_moved(back, window, cx)
69 })
70}
71
72pub(super) fn track(id: &SharedString, place: Place, axis: Axis) -> Stateful<Div> {
73 let debug_id = id.clone();
74 let el = div()
75 .debug_selector(move || format!("{debug_id}-track"))
76 .id(SharedString::from(format!("{id}-track")))
77 .block_mouse_except_scroll()
78 .absolute()
79 .flex();
80 match axis {
81 Axis::Vertical => el
82 .top(BAR_INSET)
83 .right(place.near())
84 .bottom(BAR_INSET + place.end)
85 .w(px(TRACK)),
86 Axis::Horizontal => el
87 .left(BAR_INSET)
88 .right(BAR_INSET + place.end)
89 .bottom(place.near())
90 .h(px(TRACK)),
91 }
92}
93
94pub fn thumb(
101 viewport: Pixels,
102 max_offset: Pixels,
103 offset: Pixels,
104 min: Pixels,
105) -> Option<Range<Pixels>> {
106 if viewport <= px(0.0) || max_offset <= px(0.0) {
107 return None;
108 }
109 let content = viewport + max_offset;
110 let size = min.max(viewport * (viewport / content));
111 if size > viewport {
112 return None;
113 }
114 let travelled = offset.clamp(-max_offset, px(0.0)).abs();
116 let start = (travelled / max_offset) * (viewport - size);
117 Some(start..start + size)
118}
119
120pub(crate) fn thumb_in_track(
121 viewport: Pixels,
122 max_offset: Pixels,
123 offset: Pixels,
124 track: Pixels,
125) -> Option<Range<Pixels>> {
126 if track <= px(0.) {
127 return None;
128 }
129 let scale = track / viewport;
130 let range = thumb(viewport, max_offset, offset, MIN_THUMB / scale)?;
131 Some(range.start * scale..range.end * scale)
132}
133
134pub fn offset_for_thumb(top: Pixels, viewport: Pixels, max_offset: Pixels, size: Pixels) -> Pixels {
139 let travel = viewport - size;
140 if travel <= px(0.0) || max_offset <= px(0.0) {
141 return px(0.0);
142 }
143 -(max_offset * (top / travel).clamp(0.0, 1.0))
144}
145
146#[derive(Clone)]
150pub struct ScrollbarDrag(pub SharedString);
151
152#[derive(Clone)]
159pub struct ScrollbarState {
160 grab: Rc<Cell<Option<Pixels>>>,
161 painter: Painter,
164}
165
166impl ScrollbarState {
167 pub fn new(painter: Painter) -> Self {
168 Self {
169 grab: Rc::new(Cell::new(None)),
170 painter,
171 }
172 }
173
174 fn begin(&self, handle: &ScrollHandle, event: &gpui::MouseDownEvent, end_inset: Pixels) {
175 let viewport = handle.bounds().size.height;
176 if let Some(range) = thumb_in_track(
177 viewport,
178 handle.max_offset().y,
179 handle.offset().y,
180 viewport - 2. * BAR_INSET - end_inset,
181 ) {
182 self.grab.set(Some(
183 (event.position.y - handle.bounds().top() - BAR_INSET - range.start)
184 .clamp(px(0.), range.end - range.start),
185 ));
186 }
187 }
188
189 pub fn dragging(&self) -> bool {
191 self.grab.get().is_some()
192 }
193
194 fn drag(
197 &self,
198 track_id: &SharedString,
199 handle: &ScrollHandle,
200 event: &DragMoveEvent<ScrollbarDrag>,
201 end_inset: Pixels,
202 cx: &mut App,
203 ) {
204 if event.drag(cx).0 != *track_id {
207 return;
208 }
209 let viewport = handle.bounds().size.height;
210 let max_offset = handle.max_offset().y;
211 let Some(range) = thumb_in_track(
212 viewport,
213 max_offset,
214 handle.offset().y,
215 viewport - 2. * BAR_INSET - end_inset,
216 ) else {
217 return;
218 };
219 let size = range.end - range.start;
220 let pointer = event.event.position.y - event.bounds.top();
221 let grab = self.grab.get().unwrap_or_else(|| {
227 let grab = (pointer - range.start).clamp(px(0.0), size);
228 self.grab.set(Some(grab));
229 grab
230 });
231 let offset = offset_for_thumb(
232 pointer - grab,
233 viewport - 2. * BAR_INSET - end_inset,
234 max_offset,
235 size,
236 );
237 handle.set_offset(point(handle.offset().x, offset));
238 self.painter.notify(cx);
239 }
240}
241
242#[derive(Clone, Copy)]
245pub(super) struct Place {
246 pub(super) end: Pixels,
248 pub(super) margin: Pixels,
250}
251
252impl Default for Place {
253 fn default() -> Self {
254 Self {
255 end: px(0.),
256 margin: px(MARGIN),
257 }
258 }
259}
260
261impl Place {
262 pub(super) fn near(self) -> Pixels {
265 (self.margin - px(TRACK - THUMB) * 0.5).max(px(0.))
266 }
267
268 pub(super) fn inner(self) -> Pixels {
270 self.margin - self.near()
271 }
272}
273
274pub fn scrollbar(
289 id: impl Into<SharedString>,
290 handle: &ScrollHandle,
291 state: &ScrollbarState,
292) -> gpui::AnyElement {
293 scrollbar_placed(id.into(), handle, state, Place::default())
294}
295
296pub(super) fn scrollbar_placed(
297 id: SharedString,
298 handle: &ScrollHandle,
299 state: &ScrollbarState,
300 place: Place,
301) -> gpui::AnyElement {
302 let end_inset = place.end;
303 let viewport = handle.bounds().size.height;
304 let max_offset = handle.max_offset().y;
305 let Some(range) = thumb_in_track(
306 viewport,
307 max_offset,
308 handle.offset().y,
309 viewport - 2. * BAR_INSET - end_inset,
310 ) else {
311 return Empty.into_any_element();
312 };
313 let size = range.end - range.start;
314 let dragging = state.dragging();
315
316 let track_id = id.clone();
317 let drag_handle = handle.clone();
318 let drag_state = state.clone();
319 let release_state = state.clone();
320 let press_state = state.clone();
321 let press_handle = handle.clone();
322 let released = move |_: &gpui::MouseUpEvent, _: &mut Window, _: &mut App| {
323 release_state.grab.set(None);
324 };
325
326 let thumb_debug_id = id.clone();
327 track(&id, place, Axis::Vertical)
328 .on_drag_move(move |event, _, cx| {
329 drag_state.drag(&track_id, &drag_handle, event, end_inset, cx);
330 })
331 .on_mouse_up(MouseButton::Left, released.clone())
334 .on_mouse_up_out(MouseButton::Left, released)
335 .child(
336 div()
337 .debug_selector(move || format!("{thumb_debug_id}-thumb"))
338 .id(SharedString::from(format!("{id}-thumb")))
339 .absolute()
340 .top(range.start)
341 .h(size)
342 .right(place.inner())
343 .w(px(THUMB))
344 .rounded_full()
345 .bg(if dragging { ink(0.38) } else { ink(0.2) })
346 .hover(|s| s.bg(ink(0.32)))
347 .on_mouse_down(MouseButton::Left, move |event, _, cx| {
348 press_state.begin(&press_handle, event, end_inset);
349 press_state.painter.notify(cx);
350 })
351 .on_drag(ScrollbarDrag(id.clone()), |_, _, _, cx| cx.new(|_| Empty)),
352 )
353 .child(
354 canvas(
355 move |bounds, window, _| {
356 if (bounds.size.height + 2. * BAR_INSET + end_inset - viewport).abs() > px(0.5)
361 {
362 window.request_animation_frame();
363 }
364 },
365 |_, _, _, _| {},
366 )
367 .absolute()
368 .size_full(),
369 )
370 .into_any_element()
371}
372
373pub fn rail_fits(room: Pixels) -> bool {
376 room >= px(RAIL_ROOM)
377}
378
379pub fn rail(
394 id: impl Into<SharedString>,
395 handle: &ScrollHandle,
396 count: usize,
397 room: Pixels,
398) -> gpui::AnyElement {
399 if count == 0 || !rail_fits(room) {
400 return Empty.into_any_element();
401 }
402 let id = id.into();
403 let at = handle.top_item();
404 div()
405 .absolute()
406 .top_0()
407 .bottom_0()
408 .left(px(RAIL_INSET))
409 .flex()
410 .flex_col()
411 .items_center()
412 .justify_center()
413 .gap(px(MARK_GAP))
414 .overflow_hidden()
415 .children((0..count).map(|ix| {
416 let handle = handle.clone();
417 div()
418 .id(SharedString::from(format!("{id}-{ix}")))
419 .w(px(MARK))
420 .h(px(MARK_THICK))
421 .rounded_full()
422 .bg(if ix == at { ink(0.6) } else { ink(0.2) })
423 .cursor_pointer()
424 .hover(|mark| mark.bg(ink(0.32)))
425 .on_click(move |_, window, _| {
426 handle.scroll_to_item(ix);
427 window.refresh();
428 })
429 }))
430 .into_any_element()
431}
432
433pub const TRANSIENT_IDLE: Duration = Duration::from_millis(1000);
440
441#[derive(Clone)]
447pub struct TransientState {
448 cell: Rc<Cell<(Pixels, Pixels, u64)>>,
449 hover: Rc<Cell<Hover>>,
450 bar: ScrollbarState,
451}
452
453impl TransientState {
454 pub fn new(painter: Painter) -> Self {
455 Self {
456 cell: Rc::new(Cell::new(Default::default())),
457 hover: Rc::default(),
458 bar: ScrollbarState::new(painter),
459 }
460 }
461}
462
463pub fn transient(
469 id: impl Into<SharedString>,
470 handle: &ScrollHandle,
471 state: &TransientState,
472 reduce_motion: bool,
473) -> gpui::AnyElement {
474 transient_placed(id.into(), handle, state, reduce_motion, Place::default())
475}
476
477pub(super) fn transient_placed(
478 id: SharedString,
479 handle: &ScrollHandle,
480 state: &TransientState,
481 reduce_motion: bool,
482 place: Place,
483) -> gpui::AnyElement {
484 let end_inset = place.end;
485 let viewport = handle.bounds().size.height;
486 let max_offset = handle.max_offset().y;
487 let Some(range) = thumb_in_track(
488 viewport,
489 max_offset,
490 handle.offset().y,
491 viewport - 2. * BAR_INSET - end_inset,
492 ) else {
493 return Empty.into_any_element();
494 };
495 let size = range.end - range.start;
496
497 let mut cell = state.cell.get();
501 if (handle.offset().y - cell.0).abs() > px(0.5) || (max_offset - cell.1).abs() > px(0.5) {
502 cell.0 = handle.offset().y;
503 cell.1 = max_offset;
504 cell.2 += 1;
505 state.cell.set(cell);
506 }
507 let generation = cell.2;
508 let dragging = state.bar.dragging();
509
510 let track_id = id.clone();
511 let drag_handle = handle.clone();
512 let drag_state = state.clone();
513 let release_state = state.clone();
514 let press_state = state.clone();
515 let press_handle = handle.clone();
516 let released = move |_: &gpui::MouseUpEvent, _: &mut Window, _: &mut App| {
517 release_state.bar.grab.set(None);
518 };
519
520 let thumb_debug_id = id.clone();
521 let track = track(&id, place, Axis::Vertical)
522 .on_drag_move(move |event, _, cx| {
523 drag_state
524 .bar
525 .drag(&track_id, &drag_handle, event, end_inset, cx);
526 })
527 .on_mouse_up(MouseButton::Left, released.clone())
530 .on_mouse_up_out(MouseButton::Left, released)
531 .map(|track| {
532 if reduce_motion {
533 track
534 } else {
535 let hover_state = state.clone();
538 let hover_painter = state.bar.painter;
539 hover_track(track, state.hover.clone(), move |_, cx| {
540 let mut cell = hover_state.cell.get();
541 cell.2 += 1;
542 hover_state.cell.set(cell);
543 hover_painter.notify(cx);
544 })
545 }
546 });
547
548 let thumb = div()
549 .debug_selector(move || format!("{thumb_debug_id}-thumb"))
550 .id(SharedString::from(format!("{id}-thumb")))
551 .absolute()
552 .top(range.start)
553 .h(size)
554 .right(place.inner())
555 .w(px(THUMB))
556 .rounded_full()
557 .bg(if dragging { ink(0.38) } else { ink(0.2) })
558 .hover(|s| s.bg(ink(0.32)))
559 .on_mouse_down(MouseButton::Left, move |event, _, cx| {
560 press_state.bar.begin(&press_handle, event, end_inset);
561 press_state.bar.painter.notify(cx);
562 })
563 .on_drag(ScrollbarDrag(id.clone()), |_, _, _, cx| cx.new(|_| Empty));
564
565 let thumb: gpui::AnyElement = if reduce_motion {
566 thumb.into_any_element()
567 } else {
568 let anim = state.clone();
572 thumb
573 .with_animation(
574 ElementId::from(format!("{id}-fade-{generation}")),
575 Animation::new(TRANSIENT_IDLE),
576 move |el, p| {
577 if anim.hover.get().held() || anim.bar.dragging() {
578 el
579 } else if p < 1.0 {
580 el.opacity(1.0 - p)
581 } else {
582 el.hidden()
583 }
584 },
585 )
586 .into_any_element()
587 };
588
589 track
590 .child(thumb)
591 .child(
592 canvas(
593 move |bounds, window, _| {
594 if (bounds.size.height + 2. * BAR_INSET + end_inset - viewport).abs() > px(0.5)
599 {
600 window.request_animation_frame();
601 }
602 },
603 |_, _, _, _| {},
604 )
605 .absolute()
606 .size_full(),
607 )
608 .into_any_element()
609}