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