use super::*;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct Hover {
over: bool,
out: bool,
}
impl Hover {
pub(crate) fn held(self) -> bool {
self.over && !self.out
}
}
pub(super) type SetHover = Rc<dyn Fn(Hover, &mut Window, &mut App)>;
pub(crate) fn hover_track<E: StatefulInteractiveElement>(
track: E,
hover: Rc<Cell<Hover>>,
changed: impl Fn(&mut Window, &mut App) + 'static,
) -> E {
let set: SetHover = {
let hover = hover.clone();
Rc::new(move |next, window, cx| {
if next != hover.get() {
hover.set(next);
changed(window, cx);
}
})
};
let (over, exit, moved) = (hover.clone(), hover.clone(), hover);
let (set_exit, set_moved) = (set.clone(), set.clone());
track
.on_hover(move |hovered, window, cx| {
let over = Hover {
over: *hovered,
..over.get()
};
set(over, window, cx)
})
.on_mouse_exit(move |_, window, cx| {
let out = Hover {
out: true,
..exit.get()
};
set_exit(out, window, cx)
})
.on_mouse_move(move |_, window, cx| {
let back = Hover {
out: false,
..moved.get()
};
set_moved(back, window, cx)
})
}
pub(super) fn track(id: &SharedString, place: Place, axis: Axis) -> Stateful<Div> {
let debug_id = id.clone();
let el = div()
.debug_selector(move || format!("{debug_id}-track"))
.id(SharedString::from(format!("{id}-track")))
.block_mouse_except_scroll()
.absolute()
.flex();
match axis {
Axis::Vertical => el
.top(BAR_INSET)
.right(place.near())
.bottom(BAR_INSET + place.end)
.w(px(TRACK)),
Axis::Horizontal => el
.left(BAR_INSET)
.right(BAR_INSET + place.end)
.bottom(place.near())
.h(px(TRACK)),
}
}
pub fn thumb(
viewport: Pixels,
max_offset: Pixels,
offset: Pixels,
min: Pixels,
) -> Option<Range<Pixels>> {
if viewport <= px(0.0) || max_offset <= px(0.0) {
return None;
}
let content = viewport + max_offset;
let size = min.max(viewport * (viewport / content));
if size > viewport {
return None;
}
let travelled = offset.clamp(-max_offset, px(0.0)).abs();
let start = (travelled / max_offset) * (viewport - size);
Some(start..start + size)
}
pub(crate) fn thumb_in_track(
viewport: Pixels,
max_offset: Pixels,
offset: Pixels,
track: Pixels,
) -> Option<Range<Pixels>> {
if track <= px(0.) {
return None;
}
let scale = track / viewport;
let range = thumb(viewport, max_offset, offset, MIN_THUMB / scale)?;
Some(range.start * scale..range.end * scale)
}
pub fn offset_for_thumb(top: Pixels, viewport: Pixels, max_offset: Pixels, size: Pixels) -> Pixels {
let travel = viewport - size;
if travel <= px(0.0) || max_offset <= px(0.0) {
return px(0.0);
}
-(max_offset * (top / travel).clamp(0.0, 1.0))
}
#[derive(Clone)]
pub struct ScrollbarDrag(pub SharedString);
#[derive(Clone)]
pub struct ScrollbarState {
grab: Rc<Cell<Option<Pixels>>>,
painter: Painter,
}
impl ScrollbarState {
pub fn new(painter: Painter) -> Self {
Self {
grab: Rc::new(Cell::new(None)),
painter,
}
}
fn begin(&self, handle: &Scroller, event: &gpui::MouseDownEvent, end_inset: Pixels) {
let viewport = handle.bounds().size.height;
if let Some(range) = thumb_in_track(
viewport,
handle.max_offset().y,
handle.offset().y,
viewport - 2. * BAR_INSET - end_inset,
) {
self.grab.set(Some(
(event.position.y - handle.bounds().top() - BAR_INSET - range.start)
.clamp(px(0.), range.end - range.start),
));
}
}
pub fn dragging(&self) -> bool {
self.grab.get().is_some()
}
fn drag(
&self,
track_id: &SharedString,
handle: &Scroller,
event: &DragMoveEvent<ScrollbarDrag>,
end_inset: Pixels,
cx: &mut App,
) {
if event.drag(cx).0 != *track_id {
return;
}
let viewport = handle.bounds().size.height;
let max_offset = handle.max_offset().y;
let Some(range) = thumb_in_track(
viewport,
max_offset,
handle.offset().y,
viewport - 2. * BAR_INSET - end_inset,
) else {
return;
};
let size = range.end - range.start;
let pointer = event.event.position.y - event.bounds.top();
let grab = self.grab.get().unwrap_or_else(|| {
let grab = (pointer - range.start).clamp(px(0.0), size);
self.grab.set(Some(grab));
grab
});
let offset = offset_for_thumb(
pointer - grab,
viewport - 2. * BAR_INSET - end_inset,
max_offset,
size,
);
handle.set_offset(point(handle.offset().x, offset));
self.painter.notify(cx);
}
}
#[derive(Clone, Copy)]
pub(super) struct Place {
pub(super) end: Pixels,
pub(super) margin: Pixels,
}
impl Default for Place {
fn default() -> Self {
Self {
end: px(0.),
margin: px(MARGIN),
}
}
}
impl Place {
pub(super) fn near(self) -> Pixels {
(self.margin - px(TRACK - THUMB) * 0.5).max(px(0.))
}
pub(super) fn inner(self) -> Pixels {
self.margin - self.near()
}
}
pub fn scrollbar(
id: impl Into<SharedString>,
handle: impl Into<Scroller>,
state: &ScrollbarState,
) -> gpui::AnyElement {
scrollbar_placed(id.into(), &handle.into(), state, Place::default())
}
pub(super) fn scrollbar_placed(
id: SharedString,
handle: &Scroller,
state: &ScrollbarState,
place: Place,
) -> gpui::AnyElement {
let end_inset = place.end;
let viewport = handle.bounds().size.height;
let max_offset = handle.max_offset().y;
let Some(range) = thumb_in_track(
viewport,
max_offset,
handle.offset().y,
viewport - 2. * BAR_INSET - end_inset,
) else {
return Empty.into_any_element();
};
let size = range.end - range.start;
let dragging = state.dragging();
let track_id = id.clone();
let drag_handle = handle.clone();
let drag_state = state.clone();
let release_state = state.clone();
let press_state = state.clone();
let press_handle = handle.clone();
let released = move |_: &gpui::MouseUpEvent, _: &mut Window, _: &mut App| {
release_state.grab.set(None);
};
let thumb_debug_id = id.clone();
track(&id, place, Axis::Vertical)
.on_drag_move(move |event, _, cx| {
drag_state.drag(&track_id, &drag_handle, event, end_inset, cx);
})
.on_mouse_up(MouseButton::Left, released.clone())
.on_mouse_up_out(MouseButton::Left, released)
.child(
div()
.debug_selector(move || format!("{thumb_debug_id}-thumb"))
.id(SharedString::from(format!("{id}-thumb")))
.absolute()
.top(range.start)
.h(size)
.right(place.inner())
.w(px(THUMB))
.rounded_full()
.bg(if dragging { ink(0.38) } else { ink(0.2) })
.hover(|s| s.bg(ink(0.32)))
.on_mouse_down(MouseButton::Left, move |event, _, cx| {
press_state.begin(&press_handle, event, end_inset);
press_state.painter.notify(cx);
})
.on_drag(ScrollbarDrag(id.clone()), |_, _, _, cx| cx.new(|_| Empty)),
)
.child(
canvas(
move |bounds, window, _| {
if (bounds.size.height + 2. * BAR_INSET + end_inset - viewport).abs() > px(0.5)
{
window.request_animation_frame();
}
},
|_, _, _, _| {},
)
.absolute()
.size_full(),
)
.into_any_element()
}
pub fn rail_fits(room: Pixels) -> bool {
room >= px(RAIL_ROOM)
}
pub fn rail(
id: impl Into<SharedString>,
handle: &ScrollHandle,
count: usize,
room: Pixels,
) -> gpui::AnyElement {
if count == 0 || !rail_fits(room) {
return Empty.into_any_element();
}
let id = id.into();
let at = handle.top_item();
div()
.absolute()
.top_0()
.bottom_0()
.left(px(RAIL_INSET))
.flex()
.flex_col()
.items_center()
.justify_center()
.gap(px(MARK_GAP))
.overflow_hidden()
.children((0..count).map(|ix| {
let handle = handle.clone();
div()
.id(SharedString::from(format!("{id}-{ix}")))
.w(px(MARK))
.h(px(MARK_THICK))
.rounded_full()
.bg(if ix == at { ink(0.6) } else { ink(0.2) })
.cursor_pointer()
.hover(|mark| mark.bg(ink(0.32)))
.on_click(move |_, window, _| {
handle.scroll_to_item(ix);
window.refresh();
})
}))
.into_any_element()
}
pub const TRANSIENT_IDLE: Duration = Duration::from_millis(1000);
#[derive(Clone)]
pub struct TransientState {
cell: Rc<Cell<(Pixels, Pixels, u64)>>,
hover: Rc<Cell<Hover>>,
bar: ScrollbarState,
}
impl TransientState {
pub fn new(painter: Painter) -> Self {
Self {
cell: Rc::new(Cell::new(Default::default())),
hover: Rc::default(),
bar: ScrollbarState::new(painter),
}
}
}
pub fn transient(
id: impl Into<SharedString>,
handle: impl Into<Scroller>,
state: &TransientState,
reduce_motion: bool,
) -> gpui::AnyElement {
transient_placed(
id.into(),
&handle.into(),
state,
reduce_motion,
Place::default(),
)
}
pub(super) fn transient_placed(
id: SharedString,
handle: &Scroller,
state: &TransientState,
reduce_motion: bool,
place: Place,
) -> gpui::AnyElement {
let end_inset = place.end;
let viewport = handle.bounds().size.height;
let max_offset = handle.max_offset().y;
let Some(range) = thumb_in_track(
viewport,
max_offset,
handle.offset().y,
viewport - 2. * BAR_INSET - end_inset,
) else {
return Empty.into_any_element();
};
let size = range.end - range.start;
let mut cell = state.cell.get();
if (handle.offset().y - cell.0).abs() > px(0.5) || (max_offset - cell.1).abs() > px(0.5) {
cell.0 = handle.offset().y;
cell.1 = max_offset;
cell.2 += 1;
state.cell.set(cell);
}
let generation = cell.2;
let dragging = state.bar.dragging();
let track_id = id.clone();
let drag_handle = handle.clone();
let drag_state = state.clone();
let release_state = state.clone();
let press_state = state.clone();
let press_handle = handle.clone();
let released = move |_: &gpui::MouseUpEvent, _: &mut Window, _: &mut App| {
release_state.bar.grab.set(None);
};
let thumb_debug_id = id.clone();
let track = track(&id, place, Axis::Vertical)
.on_drag_move(move |event, _, cx| {
drag_state
.bar
.drag(&track_id, &drag_handle, event, end_inset, cx);
})
.on_mouse_up(MouseButton::Left, released.clone())
.on_mouse_up_out(MouseButton::Left, released)
.map(|track| {
if reduce_motion {
track
} else {
let hover_state = state.clone();
let hover_painter = state.bar.painter;
hover_track(track, state.hover.clone(), move |_, cx| {
let mut cell = hover_state.cell.get();
cell.2 += 1;
hover_state.cell.set(cell);
hover_painter.notify(cx);
})
}
});
let thumb = div()
.debug_selector(move || format!("{thumb_debug_id}-thumb"))
.id(SharedString::from(format!("{id}-thumb")))
.absolute()
.top(range.start)
.h(size)
.right(place.inner())
.w(px(THUMB))
.rounded_full()
.bg(if dragging { ink(0.38) } else { ink(0.2) })
.hover(|s| s.bg(ink(0.32)))
.on_mouse_down(MouseButton::Left, move |event, _, cx| {
press_state.bar.begin(&press_handle, event, end_inset);
press_state.bar.painter.notify(cx);
})
.on_drag(ScrollbarDrag(id.clone()), |_, _, _, cx| cx.new(|_| Empty));
let thumb: gpui::AnyElement = if reduce_motion {
thumb.into_any_element()
} else {
let anim = state.clone();
thumb
.with_animation(
ElementId::from(format!("{id}-fade-{generation}")),
Animation::new(TRANSIENT_IDLE),
move |el, p| {
if anim.hover.get().held() || anim.bar.dragging() {
el
} else if p < 1.0 {
el.opacity(1.0 - p)
} else {
el.hidden()
}
},
)
.into_any_element()
};
track
.child(thumb)
.child(
canvas(
move |bounds, window, _| {
if (bounds.size.height + 2. * BAR_INSET + end_inset - viewport).abs() > px(0.5)
{
window.request_animation_frame();
}
},
|_, _, _, _| {},
)
.absolute()
.size_full(),
)
.into_any_element()
}