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