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 .justify_center(),
87 Axis::Horizontal => el
88 .left(BAR_INSET)
89 .right(BAR_INSET + place.end)
90 .bottom(place.near())
91 .h(px(TRACK))
92 .items_center(),
93 }
94}
95
96pub fn thumb(
103 viewport: Pixels,
104 max_offset: Pixels,
105 offset: Pixels,
106 min: Pixels,
107) -> Option<Range<Pixels>> {
108 if viewport <= px(0.0) || max_offset <= px(0.0) {
109 return None;
110 }
111 let content = viewport + max_offset;
112 let size = min.max(viewport * (viewport / content));
113 if size > viewport {
114 return None;
115 }
116 let travelled = offset.clamp(-max_offset, px(0.0)).abs();
118 let start = (travelled / max_offset) * (viewport - size);
119 Some(start..start + size)
120}
121
122pub(crate) fn thumb_in_track(
123 viewport: Pixels,
124 max_offset: Pixels,
125 offset: Pixels,
126 track: Pixels,
127) -> Option<Range<Pixels>> {
128 if track <= px(0.) {
129 return None;
130 }
131 let scale = track / viewport;
132 let range = thumb(viewport, max_offset, offset, MIN_THUMB / scale)?;
133 Some(range.start * scale..range.end * scale)
134}
135
136pub fn offset_for_thumb(top: Pixels, viewport: Pixels, max_offset: Pixels, size: Pixels) -> Pixels {
141 let travel = viewport - size;
142 if travel <= px(0.0) || max_offset <= px(0.0) {
143 return px(0.0);
144 }
145 -(max_offset * (top / travel).clamp(0.0, 1.0))
146}
147
148#[derive(Clone)]
152pub struct ScrollbarDrag(pub SharedString);
153
154#[derive(Clone)]
161pub struct ScrollbarState {
162 grab: Rc<Cell<Option<Pixels>>>,
163 painter: Painter,
166}
167
168impl ScrollbarState {
169 pub fn new(painter: Painter) -> Self {
170 Self {
171 grab: Rc::new(Cell::new(None)),
172 painter,
173 }
174 }
175
176 fn begin(&self, handle: &ScrollHandle, event: &gpui::MouseDownEvent, end_inset: Pixels) {
177 let viewport = handle.bounds().size.height;
178 if let Some(range) = thumb_in_track(
179 viewport,
180 handle.max_offset().y,
181 handle.offset().y,
182 viewport - 2. * BAR_INSET - end_inset,
183 ) {
184 self.grab.set(Some(
185 (event.position.y - handle.bounds().top() - BAR_INSET - range.start)
186 .clamp(px(0.), range.end - range.start),
187 ));
188 }
189 }
190
191 pub fn dragging(&self) -> bool {
193 self.grab.get().is_some()
194 }
195
196 fn drag(
199 &self,
200 track_id: &SharedString,
201 handle: &ScrollHandle,
202 event: &DragMoveEvent<ScrollbarDrag>,
203 end_inset: Pixels,
204 cx: &mut App,
205 ) {
206 if event.drag(cx).0 != *track_id {
209 return;
210 }
211 let viewport = handle.bounds().size.height;
212 let max_offset = handle.max_offset().y;
213 let Some(range) = thumb_in_track(
214 viewport,
215 max_offset,
216 handle.offset().y,
217 viewport - 2. * BAR_INSET - end_inset,
218 ) else {
219 return;
220 };
221 let size = range.end - range.start;
222 let pointer = event.event.position.y - event.bounds.top();
223 let grab = self.grab.get().unwrap_or_else(|| {
229 let grab = (pointer - range.start).clamp(px(0.0), size);
230 self.grab.set(Some(grab));
231 grab
232 });
233 let offset = offset_for_thumb(
234 pointer - grab,
235 viewport - 2. * BAR_INSET - end_inset,
236 max_offset,
237 size,
238 );
239 handle.set_offset(point(handle.offset().x, offset));
240 self.painter.notify(cx);
241 }
242}
243
244#[derive(Clone, Copy)]
247pub(super) struct Place {
248 pub(super) end: Pixels,
250 pub(super) channel: Pixels,
252}
253
254impl Default for Place {
255 fn default() -> Self {
256 Self {
257 end: px(0.),
258 channel: px(CHANNEL),
259 }
260 }
261}
262
263impl Place {
264 fn near(self) -> Pixels {
266 ((self.channel - px(TRACK)) * 0.5).max(px(0.))
267 }
268}
269
270pub fn scrollbar(
285 id: impl Into<SharedString>,
286 handle: &ScrollHandle,
287 state: &ScrollbarState,
288) -> gpui::AnyElement {
289 scrollbar_placed(id.into(), handle, state, Place::default())
290}
291
292pub(super) fn scrollbar_placed(
293 id: SharedString,
294 handle: &ScrollHandle,
295 state: &ScrollbarState,
296 place: Place,
297) -> gpui::AnyElement {
298 let end_inset = place.end;
299 let viewport = handle.bounds().size.height;
300 let max_offset = handle.max_offset().y;
301 let Some(range) = thumb_in_track(
302 viewport,
303 max_offset,
304 handle.offset().y,
305 viewport - 2. * BAR_INSET - end_inset,
306 ) else {
307 return Empty.into_any_element();
308 };
309 let size = range.end - range.start;
310 let dragging = state.dragging();
311
312 let track_id = id.clone();
313 let drag_handle = handle.clone();
314 let drag_state = state.clone();
315 let release_state = state.clone();
316 let press_state = state.clone();
317 let press_handle = handle.clone();
318 let released = move |_: &gpui::MouseUpEvent, _: &mut Window, _: &mut App| {
319 release_state.grab.set(None);
320 };
321
322 let thumb_debug_id = id.clone();
323 track(&id, place, Axis::Vertical)
324 .on_drag_move(move |event, _, cx| {
325 drag_state.drag(&track_id, &drag_handle, event, end_inset, cx);
326 })
327 .on_mouse_up(MouseButton::Left, released.clone())
330 .on_mouse_up_out(MouseButton::Left, released)
331 .child(
332 div()
333 .debug_selector(move || format!("{thumb_debug_id}-thumb"))
334 .id(SharedString::from(format!("{id}-thumb")))
335 .absolute()
336 .top(range.start)
337 .h(size)
338 .w(px(THUMB))
339 .rounded_full()
340 .bg(if dragging { ink(0.38) } else { ink(0.2) })
341 .hover(|s| s.bg(ink(0.32)))
342 .on_mouse_down(MouseButton::Left, move |event, _, cx| {
343 press_state.begin(&press_handle, event, end_inset);
344 press_state.painter.notify(cx);
345 })
346 .on_drag(ScrollbarDrag(id.clone()), |_, _, _, cx| cx.new(|_| Empty)),
347 )
348 .child(
349 canvas(
350 move |bounds, window, _| {
351 if (bounds.size.height + 2. * BAR_INSET + end_inset - viewport).abs() > px(0.5)
356 {
357 window.request_animation_frame();
358 }
359 },
360 |_, _, _, _| {},
361 )
362 .absolute()
363 .size_full(),
364 )
365 .into_any_element()
366}
367
368pub fn rail_fits(room: Pixels) -> bool {
371 room >= px(RAIL_ROOM)
372}
373
374pub fn rail(
389 id: impl Into<SharedString>,
390 handle: &ScrollHandle,
391 count: usize,
392 room: Pixels,
393) -> gpui::AnyElement {
394 if count == 0 || !rail_fits(room) {
395 return Empty.into_any_element();
396 }
397 let id = id.into();
398 let at = handle.top_item();
399 div()
400 .absolute()
401 .top_0()
402 .bottom_0()
403 .left(px(RAIL_INSET))
404 .flex()
405 .flex_col()
406 .items_center()
407 .justify_center()
408 .gap(px(MARK_GAP))
409 .overflow_hidden()
410 .children((0..count).map(|ix| {
411 let handle = handle.clone();
412 div()
413 .id(SharedString::from(format!("{id}-{ix}")))
414 .w(px(MARK))
415 .h(px(MARK_THICK))
416 .rounded_full()
417 .bg(if ix == at { ink(0.6) } else { ink(0.2) })
418 .cursor_pointer()
419 .hover(|mark| mark.bg(ink(0.32)))
420 .on_click(move |_, window, _| {
421 handle.scroll_to_item(ix);
422 window.refresh();
423 })
424 }))
425 .into_any_element()
426}
427
428pub const TRANSIENT_IDLE: Duration = Duration::from_millis(1000);
435
436#[derive(Clone)]
442pub struct TransientState {
443 cell: Rc<Cell<(Pixels, Pixels, u64)>>,
444 hover: Rc<Cell<Hover>>,
445 bar: ScrollbarState,
446}
447
448impl TransientState {
449 pub fn new(painter: Painter) -> Self {
450 Self {
451 cell: Rc::new(Cell::new(Default::default())),
452 hover: Rc::default(),
453 bar: ScrollbarState::new(painter),
454 }
455 }
456}
457
458pub fn transient(
464 id: impl Into<SharedString>,
465 handle: &ScrollHandle,
466 state: &TransientState,
467 reduce_motion: bool,
468) -> gpui::AnyElement {
469 transient_placed(id.into(), handle, state, reduce_motion, Place::default())
470}
471
472pub(super) fn transient_placed(
473 id: SharedString,
474 handle: &ScrollHandle,
475 state: &TransientState,
476 reduce_motion: bool,
477 place: Place,
478) -> gpui::AnyElement {
479 let end_inset = place.end;
480 let viewport = handle.bounds().size.height;
481 let max_offset = handle.max_offset().y;
482 let Some(range) = thumb_in_track(
483 viewport,
484 max_offset,
485 handle.offset().y,
486 viewport - 2. * BAR_INSET - end_inset,
487 ) else {
488 return Empty.into_any_element();
489 };
490 let size = range.end - range.start;
491
492 let mut cell = state.cell.get();
496 if (handle.offset().y - cell.0).abs() > px(0.5) || (max_offset - cell.1).abs() > px(0.5) {
497 cell.0 = handle.offset().y;
498 cell.1 = max_offset;
499 cell.2 += 1;
500 state.cell.set(cell);
501 }
502 let generation = cell.2;
503 let dragging = state.bar.dragging();
504
505 let track_id = id.clone();
506 let drag_handle = handle.clone();
507 let drag_state = state.clone();
508 let release_state = state.clone();
509 let press_state = state.clone();
510 let press_handle = handle.clone();
511 let released = move |_: &gpui::MouseUpEvent, _: &mut Window, _: &mut App| {
512 release_state.bar.grab.set(None);
513 };
514
515 let thumb_debug_id = id.clone();
516 let track = track(&id, place, Axis::Vertical)
517 .on_drag_move(move |event, _, cx| {
518 drag_state
519 .bar
520 .drag(&track_id, &drag_handle, event, end_inset, cx);
521 })
522 .on_mouse_up(MouseButton::Left, released.clone())
525 .on_mouse_up_out(MouseButton::Left, released)
526 .map(|track| {
527 if reduce_motion {
528 track
529 } else {
530 let hover_state = state.clone();
533 let hover_painter = state.bar.painter;
534 hover_track(track, state.hover.clone(), move |_, cx| {
535 let mut cell = hover_state.cell.get();
536 cell.2 += 1;
537 hover_state.cell.set(cell);
538 hover_painter.notify(cx);
539 })
540 }
541 });
542
543 let thumb = div()
544 .debug_selector(move || format!("{thumb_debug_id}-thumb"))
545 .id(SharedString::from(format!("{id}-thumb")))
546 .absolute()
547 .top(range.start)
548 .h(size)
549 .w(px(THUMB))
550 .rounded_full()
551 .bg(if dragging { ink(0.38) } else { ink(0.2) })
552 .hover(|s| s.bg(ink(0.32)))
553 .on_mouse_down(MouseButton::Left, move |event, _, cx| {
554 press_state.bar.begin(&press_handle, event, end_inset);
555 press_state.bar.painter.notify(cx);
556 })
557 .on_drag(ScrollbarDrag(id.clone()), |_, _, _, cx| cx.new(|_| Empty));
558
559 let thumb: gpui::AnyElement = if reduce_motion {
560 thumb.into_any_element()
561 } else {
562 let anim = state.clone();
566 thumb
567 .with_animation(
568 ElementId::from(format!("{id}-fade-{generation}")),
569 Animation::new(TRANSIENT_IDLE),
570 move |el, p| {
571 if anim.hover.get().held() || anim.bar.dragging() {
572 el
573 } else if p < 1.0 {
574 el.opacity(1.0 - p)
575 } else {
576 el.hidden()
577 }
578 },
579 )
580 .into_any_element()
581 };
582
583 track
584 .child(thumb)
585 .child(
586 canvas(
587 move |bounds, window, _| {
588 if (bounds.size.height + 2. * BAR_INSET + end_inset - viewport).abs() > px(0.5)
593 {
594 window.request_animation_frame();
595 }
596 },
597 |_, _, _, _| {},
598 )
599 .absolute()
600 .size_full(),
601 )
602 .into_any_element()
603}