use fluidattacks_designs_domain::scrollbar::{
offset_for, thumb, RADIUS, THUMB, TRACK, WIDE_WIDTH, WIDTH,
};
use gpui::{
canvas, div, fill, point, px, rgb, size, App, Bounds, ElementId, Entity, EntityId, IntoElement,
ListState, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point,
RenderOnce, ScrollHandle, Styled as _, Window,
};
use crate::theme::ActiveTheme as _;
pub fn stop_dominant_axis(
axis: ScrollAxis,
) -> impl Fn(&gpui::ScrollWheelEvent, &mut Window, &mut App) + 'static {
move |event, window, cx| {
let delta = event.delta.pixel_delta(window.line_height());
let horizontal = f32::from(delta.x).abs() > f32::from(delta.y).abs();
let dominant = match axis {
ScrollAxis::Horizontal => horizontal,
ScrollAxis::Vertical => !horizontal,
};
if dominant {
cx.stop_propagation();
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ScrollAxis {
Vertical,
Horizontal,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Axis {
Vertical,
Horizontal,
}
#[derive(Clone)]
enum Source {
Plain(ScrollHandle),
Rows(ListState),
}
impl Source {
fn offset(&self) -> Point<Pixels> {
match self {
Self::Plain(handle) => handle.offset(),
Self::Rows(rows) => rows.scroll_px_offset_for_scrollbar(),
}
}
fn max_offset(&self) -> Point<Pixels> {
match self {
Self::Plain(handle) => handle.max_offset(),
Self::Rows(rows) => rows.max_offset_for_scrollbar(),
}
}
fn set_offset(&self, offset: Point<Pixels>) {
match self {
Self::Plain(handle) => handle.set_offset(offset),
Self::Rows(rows) => rows.set_offset_from_scrollbar(offset),
}
}
fn grab(&self) {
if let Self::Rows(rows) = self {
rows.scrollbar_drag_started();
}
}
fn release(&self) {
if let Self::Rows(rows) = self {
rows.scrollbar_drag_ended();
}
}
}
#[derive(IntoElement)]
pub struct Scrollbar {
id: ElementId,
source: Source,
track: bool,
axis: Axis,
breadth: f32,
corner: bool,
}
impl Scrollbar {
pub fn vertical(id: impl Into<ElementId>, handle: &ScrollHandle) -> Self {
Self::vertical_over(id, Source::Plain(handle.clone()))
}
pub(crate) fn rows(id: impl Into<ElementId>, rows: &ListState) -> Self {
Self::vertical_over(id, Source::Rows(rows.clone()))
}
fn vertical_over(id: impl Into<ElementId>, source: Source) -> Self {
Self {
id: id.into(),
source,
track: true,
axis: Axis::Vertical,
breadth: WIDTH,
corner: false,
}
}
#[must_use]
pub fn horizontal(id: impl Into<ElementId>, handle: &ScrollHandle) -> Self {
Self {
id: id.into(),
source: Source::Plain(handle.clone()),
track: true,
axis: Axis::Horizontal,
breadth: WIDTH,
corner: false,
}
}
#[must_use]
pub const fn track_transparent(mut self) -> Self {
self.track = false;
self
}
#[must_use]
pub const fn wide(mut self) -> Self {
self.breadth = WIDE_WIDTH;
self
}
#[must_use]
pub const fn reserve_corner(mut self) -> Self {
self.corner = true;
self
}
}
#[derive(Clone)]
struct Bar {
source: Source,
grip: Entity<Option<f32>>,
view: EntityId,
track: bool,
axis: Axis,
breadth: f32,
tones: (u32, u32),
}
#[derive(Clone, Copy)]
struct Geometry {
bounds: Bounds<Pixels>,
thumb: Bounds<Pixels>,
container: f32,
content: f32,
}
impl Geometry {
fn along(&self, axis: Axis, position: Point<Pixels>) -> f32 {
match axis {
Axis::Vertical => f32::from(position.y) - f32::from(self.bounds.origin.y),
Axis::Horizontal => f32::from(position.x) - f32::from(self.bounds.origin.x),
}
}
#[must_use]
fn grab(&self, axis: Axis, position: Point<Pixels>) -> f32 {
match axis {
Axis::Vertical => f32::from(position.y) - f32::from(self.thumb.origin.y),
Axis::Horizontal => f32::from(position.x) - f32::from(self.thumb.origin.x),
}
}
#[must_use]
fn thumb_length(&self, axis: Axis) -> f32 {
match axis {
Axis::Vertical => f32::from(self.thumb.size.height),
Axis::Horizontal => f32::from(self.thumb.size.width),
}
}
}
fn scroll_to(geometry: &Geometry, bar: &Bar, top: f32, cx: &mut App) {
let offset = px(offset_for(geometry.container, geometry.content, top));
let current = bar.source.offset();
let target = match bar.axis {
Axis::Vertical => point(current.x, offset),
Axis::Horizontal => point(offset, current.y),
};
bar.source.set_offset(target);
cx.notify(bar.view);
}
fn handle_mouse(geometry: Geometry, bar: &Bar, window: &mut Window) {
window.on_mouse_event({
let bar = bar.clone();
move |event: &MouseDownEvent, phase, _, cx| {
if !(phase.bubble() && geometry.bounds.contains(&event.position)) {
return;
}
cx.stop_propagation();
if geometry.thumb.contains(&event.position) {
let within = geometry.grab(bar.axis, event.position);
bar.source.grab();
bar.grip.update(cx, |grip, _| *grip = Some(within));
} else {
let half = geometry.thumb_length(bar.axis) / 2.0;
let top = geometry.along(bar.axis, event.position) - half;
scroll_to(&geometry, &bar, top, cx);
}
}
});
window.on_mouse_event({
let bar = bar.clone();
move |event: &MouseMoveEvent, _, _, cx| {
let Some(within) = *bar.grip.read(cx) else {
return;
};
if !event.dragging() {
return;
}
cx.stop_propagation();
let top = geometry.along(bar.axis, event.position) - within;
scroll_to(&geometry, &bar, top, cx);
}
});
window.on_mouse_event({
let bar = bar.clone();
move |_: &MouseUpEvent, phase, _, cx| {
if phase.bubble() && bar.grip.read(cx).is_some() {
bar.source.release();
bar.grip.update(cx, |grip, _| *grip = None);
}
}
});
}
fn paint(bounds: Bounds<Pixels>, bar: &Bar, window: &mut Window) {
let (container, max_offset, offset) = match bar.axis {
Axis::Vertical => (
bounds.size.height,
bar.source.max_offset().y,
bar.source.offset().y,
),
Axis::Horizontal => (
bounds.size.width,
bar.source.max_offset().x,
bar.source.offset().x,
),
};
let container = f32::from(container);
let content = f32::from(max_offset) + container;
let Some((top, length)) = thumb(container, content, f32::from(offset)) else {
return;
};
let (track, thumb_color) = bar.tones;
if bar.track {
window.paint_quad(fill(bounds, rgb(track)));
}
let thumb_bounds = match bar.axis {
Axis::Vertical => Bounds {
origin: point(bounds.origin.x, px(f32::from(bounds.origin.y) + top)),
size: size(px(bar.breadth), px(length)),
},
Axis::Horizontal => Bounds {
origin: point(px(f32::from(bounds.origin.x) + top), bounds.origin.y),
size: size(px(length), px(bar.breadth)),
},
};
window.paint_quad(fill(thumb_bounds, rgb(thumb_color)).corner_radii(px(RADIUS)));
let geometry = Geometry {
bounds,
thumb: thumb_bounds,
container,
content,
};
handle_mouse(geometry, bar, window);
}
impl RenderOnce for Scrollbar {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let bar = Bar {
source: self.source,
grip: window.use_keyed_state(self.id, cx, |_, _| None),
view: window.current_view(),
track: self.track,
axis: self.axis,
breadth: self.breadth,
tones: (cx.theme().shade(TRACK), cx.theme().shade(THUMB)),
};
let strip = div().absolute();
let end = px(if self.corner { self.breadth } else { 0.0 });
let strip = match self.axis {
Axis::Vertical => strip.top_0().right_0().bottom(end).w(px(self.breadth)),
Axis::Horizontal => strip.bottom_0().left_0().right(end).h(px(self.breadth)),
};
strip.child(
canvas(
|_, _, _| (),
move |bounds, (), paint_window, _paint_cx| {
paint(bounds, &bar, paint_window);
},
)
.size_full(),
)
}
}