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