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: &Scroller, 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: &Scroller,
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: impl Into<Scroller>,
291 state: &ScrollbarState,
292) -> gpui::AnyElement {
293 scrollbar_placed(id.into(), &handle.into(), state, Place::default())
294}
295
296pub(super) fn scrollbar_placed(
297 id: SharedString,
298 handle: &Scroller,
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: impl Into<Scroller>,
471 state: &TransientState,
472 reduce_motion: bool,
473) -> gpui::AnyElement {
474 transient_placed(
475 id.into(),
476 &handle.into(),
477 state,
478 reduce_motion,
479 Place::default(),
480 )
481}
482
483pub(super) fn transient_placed(
484 id: SharedString,
485 handle: &Scroller,
486 state: &TransientState,
487 reduce_motion: bool,
488 place: Place,
489) -> gpui::AnyElement {
490 let end_inset = place.end;
491 let viewport = handle.bounds().size.height;
492 let max_offset = handle.max_offset().y;
493 let Some(range) = thumb_in_track(
494 viewport,
495 max_offset,
496 handle.offset().y,
497 viewport - 2. * BAR_INSET - end_inset,
498 ) else {
499 return Empty.into_any_element();
500 };
501 let size = range.end - range.start;
502
503 let mut cell = state.cell.get();
507 if (handle.offset().y - cell.0).abs() > px(0.5) || (max_offset - cell.1).abs() > px(0.5) {
508 cell.0 = handle.offset().y;
509 cell.1 = max_offset;
510 cell.2 += 1;
511 state.cell.set(cell);
512 }
513 let generation = cell.2;
514 let dragging = state.bar.dragging();
515
516 let track_id = id.clone();
517 let drag_handle = handle.clone();
518 let drag_state = state.clone();
519 let release_state = state.clone();
520 let press_state = state.clone();
521 let press_handle = handle.clone();
522 let released = move |_: &gpui::MouseUpEvent, _: &mut Window, _: &mut App| {
523 release_state.bar.grab.set(None);
524 };
525
526 let thumb_debug_id = id.clone();
527 let track = track(&id, place, Axis::Vertical)
528 .on_drag_move(move |event, _, cx| {
529 drag_state
530 .bar
531 .drag(&track_id, &drag_handle, event, end_inset, cx);
532 })
533 .on_mouse_up(MouseButton::Left, released.clone())
536 .on_mouse_up_out(MouseButton::Left, released)
537 .map(|track| {
538 if reduce_motion {
539 track
540 } else {
541 let hover_state = state.clone();
544 let hover_painter = state.bar.painter;
545 hover_track(track, state.hover.clone(), move |_, cx| {
546 let mut cell = hover_state.cell.get();
547 cell.2 += 1;
548 hover_state.cell.set(cell);
549 hover_painter.notify(cx);
550 })
551 }
552 });
553
554 let thumb = div()
555 .debug_selector(move || format!("{thumb_debug_id}-thumb"))
556 .id(SharedString::from(format!("{id}-thumb")))
557 .absolute()
558 .top(range.start)
559 .h(size)
560 .right(place.inner())
561 .w(px(THUMB))
562 .rounded_full()
563 .bg(if dragging { ink(0.38) } else { ink(0.2) })
564 .hover(|s| s.bg(ink(0.32)))
565 .on_mouse_down(MouseButton::Left, move |event, _, cx| {
566 press_state.bar.begin(&press_handle, event, end_inset);
567 press_state.bar.painter.notify(cx);
568 })
569 .on_drag(ScrollbarDrag(id.clone()), |_, _, _, cx| cx.new(|_| Empty));
570
571 let thumb: gpui::AnyElement = if reduce_motion {
572 thumb.into_any_element()
573 } else {
574 let anim = state.clone();
578 thumb
579 .with_animation(
580 ElementId::from(format!("{id}-fade-{generation}")),
581 Animation::new(TRANSIENT_IDLE),
582 move |el, p| {
583 if anim.hover.get().held() || anim.bar.dragging() {
584 el
585 } else if p < 1.0 {
586 el.opacity(1.0 - p)
587 } else {
588 el.hidden()
589 }
590 },
591 )
592 .into_any_element()
593 };
594
595 track
596 .child(thumb)
597 .child(
598 canvas(
599 move |bounds, window, _| {
600 if (bounds.size.height + 2. * BAR_INSET + end_inset - viewport).abs() > px(0.5)
605 {
606 window.request_animation_frame();
607 }
608 },
609 |_, _, _, _| {},
610 )
611 .absolute()
612 .size_full(),
613 )
614 .into_any_element()
615}