1use super::{self as scroll, ScrollbarState, TransientState};
4use gpui::{
5 self, Animation, AnimationExt, AnyElement, App, Axis, Div, DragMoveEvent, Empty, Global,
6 IntoElement, MouseButton, Pixels, RenderOnce, ScrollHandle, SharedString, Stateful, Window,
7 canvas, div, point, prelude::*, px,
8};
9use motion::Painter;
10use std::{cell::Cell, rc::Rc};
11use theme::ink;
12
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15pub enum Visibility {
16 #[default]
17 Scrolling,
18 Always,
19 Never,
20}
21impl Global for Visibility {}
22
23pub fn visibility(cx: &App) -> Visibility {
24 cx.try_global::<Visibility>().copied().unwrap_or_default()
25}
26
27pub fn set_visibility(value: Visibility, cx: &mut App) {
29 cx.set_global(value);
30 cx.refresh_windows();
31}
32
33#[derive(IntoElement)]
34pub struct Overlay {
35 id: SharedString,
36 handle: ScrollHandle,
37 axis: Axis,
38 visibility: Option<Visibility>,
39 place: scroll::Place,
40}
41
42impl Overlay {
43 pub fn new(id: impl Into<SharedString>, handle: &ScrollHandle, axis: Axis) -> Self {
45 Self {
46 id: id.into(),
47 handle: handle.clone(),
48 axis,
49 visibility: None,
50 place: scroll::Place::default(),
51 }
52 }
53
54 pub fn end_inset(mut self, inset: Pixels) -> Self {
56 self.place.end = inset.max(px(0.));
57 self
58 }
59
60 pub fn margin(mut self, margin: Pixels) -> Self {
63 self.place.margin = margin.max(px(0.));
64 self
65 }
66
67 pub fn visibility(mut self, visibility: Visibility) -> Self {
69 self.visibility = Some(visibility);
70 self
71 }
72}
73
74#[derive(IntoElement)]
76pub struct Viewport {
77 id: SharedString,
78 content: Stateful<Div>,
79 axis: Axis,
80 fill: bool,
81 handle: Option<ScrollHandle>,
82}
83
84impl Viewport {
85 pub fn new(id: impl Into<SharedString>, content: Stateful<Div>, axis: Axis) -> Self {
86 Self {
87 id: id.into(),
88 content,
89 axis,
90 fill: false,
91 handle: None,
92 }
93 }
94
95 pub fn fill(mut self) -> Self {
97 self.fill = true;
98 self
99 }
100
101 pub fn track_scroll(mut self, handle: &ScrollHandle) -> Self {
106 self.handle = Some(handle.clone());
107 self
108 }
109}
110
111impl RenderOnce for Viewport {
112 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
113 let handle = match self.handle {
114 Some(handle) => handle,
115 None => window
116 .use_keyed_state(
117 SharedString::from(format!("{}-handle", self.id)),
118 cx,
119 |_, _| ScrollHandle::new(),
120 )
121 .read(cx)
122 .clone(),
123 };
124 let axes = match self.axis {
125 Axis::Vertical => scroll::Axes::Vertical,
126 Axis::Horizontal => scroll::Axes::Horizontal,
127 };
128 div()
129 .relative()
130 .w_full()
131 .min_w_0()
132 .when(self.fill, |el| el.flex_1().min_h_0().flex().flex_col())
133 .child(scroll::scrolls(self.content, axes).track_scroll(&handle))
134 .child(Overlay::new(self.id, &handle, self.axis))
135 }
136}
137
138struct State {
139 steady: ScrollbarState,
140 transient: TransientState,
141 horizontal: Rc<Cell<Horizontal>>,
142 horizontal_hover: Rc<Cell<scroll::Hover>>,
143}
144
145#[derive(Clone, Copy, Default)]
146struct Horizontal {
147 offset: Pixels,
148 max: Pixels,
149 generation: usize,
150 grab: Option<Pixels>,
151}
152
153impl RenderOnce for Overlay {
154 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
155 let mode = self.visibility.unwrap_or_else(|| visibility(cx));
156 if mode == Visibility::Never {
157 return Empty.into_any_element();
158 }
159 let state = window.use_keyed_state(
160 SharedString::from(format!("{}-state", self.id)),
161 cx,
162 |_, cx| State {
163 steady: ScrollbarState::new(Painter::of(cx)),
164 transient: TransientState::new(Painter::of(cx)),
165 horizontal: Rc::default(),
166 horizontal_hover: Rc::default(),
167 },
168 );
169 let held = state.read(cx);
170 let always = mode == Visibility::Always || cx.reduce_motion();
171 let inner = match self.axis {
172 Axis::Vertical if always => {
173 scroll::scrollbar_placed(self.id, &self.handle, &held.steady, self.place)
174 }
175 Axis::Vertical => {
176 scroll::transient_placed(self.id, &self.handle, &held.transient, false, self.place)
177 }
178 Axis::Horizontal => horizontal(
179 self.id,
180 &self.handle,
181 held.horizontal.clone(),
182 held.horizontal_hover.clone(),
183 always,
184 self.place,
185 ),
186 };
187 let handle = self.handle;
188 let before = (handle.bounds(), handle.max_offset(), handle.offset());
189 div()
191 .absolute()
192 .inset_0()
193 .child(inner)
194 .child(
195 canvas(
196 move |_, window, _| {
197 if before != (handle.bounds(), handle.max_offset(), handle.offset()) {
198 window.request_animation_frame();
199 }
200 },
201 |_, _, _, _| {},
202 )
203 .absolute()
204 .size_full(),
205 )
206 .into_any_element()
207 }
208}
209
210#[derive(Clone)]
211struct HorizontalDrag(SharedString);
212
213fn horizontal(
214 id: SharedString,
215 handle: &ScrollHandle,
216 state: Rc<Cell<Horizontal>>,
217 hover: Rc<Cell<scroll::Hover>>,
218 always: bool,
219 place: scroll::Place,
220) -> AnyElement {
221 let end_inset = place.end;
222 let viewport = handle.bounds().size.width;
223 let max = handle.max_offset().x;
224 let Some(range) = scroll::thumb_in_track(
225 viewport,
226 max,
227 handle.offset().x,
228 viewport - 2. * scroll::BAR_INSET - end_inset,
229 ) else {
230 return Empty.into_any_element();
231 };
232 let size = range.end - range.start;
233 let mut held = state.get();
234 if (held.offset - handle.offset().x).abs() > px(0.5) || (held.max - max).abs() > px(0.5) {
235 held.offset = handle.offset().x;
236 held.max = max;
237 held.generation += 1;
238 state.set(held);
239 }
240 let drag_id = id.clone();
241 let drag_handle = handle.clone();
242 let drag_state = state.clone();
243 let release_state = state.clone();
244 let release = move |_: &gpui::MouseUpEvent, window: &mut Window, _: &mut App| {
245 let mut held = release_state.get();
246 held.grab = None;
247 held.generation += 1;
248 release_state.set(held);
249 window.refresh();
250 };
251 let hover_state = state.clone();
252 let track = scroll::track(&id, place, Axis::Horizontal);
253 let track = scroll::hover_track(track, hover.clone(), move |window, _| {
254 let mut held = hover_state.get();
255 held.generation += 1;
256 hover_state.set(held);
257 window.refresh();
258 })
259 .on_drag_move(move |event: &DragMoveEvent<HorizontalDrag>, window, cx| {
260 if event.drag(cx).0 != drag_id {
261 return;
262 }
263 let viewport = drag_handle.bounds().size.width;
264 let max = drag_handle.max_offset().x;
265 let Some(range) = scroll::thumb_in_track(
266 viewport,
267 max,
268 drag_handle.offset().x,
269 viewport - 2. * scroll::BAR_INSET - end_inset,
270 ) else {
271 return;
272 };
273 let pointer = event.event.position.x - event.bounds.left();
274 let mut held = drag_state.get();
275 let grab = *held
276 .grab
277 .get_or_insert((pointer - range.start).clamp(px(0.), range.end - range.start));
278 drag_state.set(held);
279 let x = scroll::offset_for_thumb(
280 pointer - grab,
281 viewport - 2. * scroll::BAR_INSET - end_inset,
282 max,
283 range.end - range.start,
284 );
285 drag_handle.set_offset(point(x, drag_handle.offset().y));
286 window.refresh();
287 })
288 .on_mouse_up(MouseButton::Left, release.clone())
289 .on_mouse_up_out(MouseButton::Left, release);
290 let thumb_debug_id = id.clone();
291 let press_state = state.clone();
292 let press_handle = handle.clone();
293 let thumb = div()
294 .debug_selector(move || format!("{thumb_debug_id}-thumb"))
295 .id(SharedString::from(format!("{id}-thumb")))
296 .absolute()
297 .left(range.start)
298 .w(size)
299 .bottom(place.inner())
300 .h(px(scroll::THUMB))
301 .rounded_full()
302 .bg(ink(0.2))
303 .hover(|s| s.bg(ink(0.32)))
304 .on_mouse_down(MouseButton::Left, move |event, window, _| {
305 let mut held = press_state.get();
306 held.grab = Some(
307 (event.position.x - press_handle.bounds().left() - scroll::BAR_INSET - range.start)
308 .clamp(px(0.), size),
309 );
310 press_state.set(held);
311 window.refresh();
312 })
313 .on_drag(HorizontalDrag(id.clone()), |_, _, _, cx| cx.new(|_| Empty));
314 let thumb = if always {
315 thumb.into_any_element()
316 } else {
317 thumb
318 .with_animation(
319 SharedString::from(format!("{id}-fade-{}", held.generation)),
320 Animation::new(scroll::TRANSIENT_IDLE),
321 move |el, p| {
322 if hover.get().held() || state.get().grab.is_some() {
323 el
324 } else if p < 1. {
325 el.opacity(1. - p)
326 } else {
327 el.hidden()
328 }
329 },
330 )
331 .into_any_element()
332 };
333 track.child(thumb).into_any_element()
334}