1use gpui::{
8 canvas, div, prelude::*, px, AnyElement, App, ElementId, IntoElement, ParentElement, Pixels,
9 RenderOnce, ScrollHandle, Styled, Window,
10};
11use herogpui_core::{element_id, Orientation};
12use herogpui_theme::ActiveTheme;
13
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
16pub enum ScrollShadowVariant {
17 #[default]
19 Fade,
20}
21
22#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
31pub enum ScrollShadowVisibility {
32 #[default]
34 Auto,
35 Both,
37 Top,
39 Bottom,
41 Left,
43 Right,
45 None,
47}
48
49impl ScrollShadowVisibility {
50 fn shows_start(self, orientation: Orientation) -> bool {
52 match self {
53 Self::Auto | Self::Both => true,
55 Self::Top => !orientation.is_horizontal(),
56 Self::Left => orientation.is_horizontal(),
57 _ => false,
58 }
59 }
60
61 fn shows_end(self, orientation: Orientation) -> bool {
63 match self {
64 Self::Auto | Self::Both => true,
65 Self::Bottom => !orientation.is_horizontal(),
66 Self::Right => orientation.is_horizontal(),
67 _ => false,
68 }
69 }
70}
71
72#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
74#[derive(IntoElement)]
75pub struct ScrollShadow {
76 id: ElementId,
77 orientation: Orientation,
78 size: Pixels,
80 offset: Pixels,
82 is_enabled: bool,
83 visibility: ScrollShadowVisibility,
84 on_visibility_change:
87 Option<std::sync::Arc<dyn Fn(&ScrollShadowVisibility, &mut Window, &mut App) + 'static>>,
88 max_h: Option<Pixels>,
89 max_w: Option<Pixels>,
90 gap: Pixels,
91 children: Vec<AnyElement>,
92 hide_scroll_bar: bool,
93 fill_axis: bool,
97 external_scroll: Option<ScrollHandle>,
100 sx: Option<Box<gpui::StyleRefinement>>,
102}
103
104impl ScrollShadow {
105 pub fn new(id: impl Into<ElementId>) -> Self {
107 Self {
108 id: id.into(),
109 orientation: Orientation::Vertical,
110 size: px(40.),
111 offset: px(0.),
112 is_enabled: true,
113 visibility: ScrollShadowVisibility::Auto,
114 on_visibility_change: None,
115 max_h: Some(px(240.)),
116 max_w: None,
117 gap: px(8.),
118 children: Vec::new(),
119 hide_scroll_bar: false,
120 fill_axis: false,
121 external_scroll: None,
122 sx: None,
123 }
124 }
125
126 pub fn hide_scroll_bar(mut self, v: bool) -> Self {
129 self.hide_scroll_bar = v;
130 self
131 }
132
133 pub(crate) fn scroll_handle(mut self, handle: ScrollHandle) -> Self {
135 self.external_scroll = Some(handle);
136 self
137 }
138
139 pub fn orientation(mut self, orientation: Orientation) -> Self {
141 self.orientation = orientation;
142 self
143 }
144
145 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
147 self.size = size.into();
148 self
149 }
150
151 pub fn offset(mut self, offset: impl Into<Pixels>) -> Self {
153 self.offset = offset.into();
154 self
155 }
156
157 pub fn is_enabled(mut self, v: bool) -> Self {
159 self.is_enabled = v;
160 self
161 }
162
163 pub fn on_visibility_change(
166 mut self,
167 handler: impl Fn(&ScrollShadowVisibility, &mut Window, &mut App) + 'static,
168 ) -> Self {
169 self.on_visibility_change = Some(std::sync::Arc::new(handler));
170 self
171 }
172
173 pub fn visibility(mut self, visibility: ScrollShadowVisibility) -> Self {
175 self.visibility = visibility;
176 self
177 }
178
179 pub(crate) fn unbounded_h(mut self) -> Self {
182 self.max_h = None;
183 self
184 }
185
186 pub(crate) fn fill_axis(mut self, value: bool) -> Self {
187 self.fill_axis = value;
188 self
189 }
190
191 pub fn max_h(mut self, v: impl Into<Pixels>) -> Self {
193 self.max_h = Some(v.into());
194 self
195 }
196
197 pub fn max_w(mut self, v: impl Into<Pixels>) -> Self {
199 self.max_w = Some(v.into());
200 self
201 }
202
203 pub fn gap(mut self, gap: impl Into<Pixels>) -> Self {
205 self.gap = gap.into();
206 self
207 }
208
209 pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
214 crate::util::refine_sx(&mut self.sx, style);
215 self
216 }
217}
218
219impl ParentElement for ScrollShadow {
220 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
221 self.children.extend(elements);
222 }
223}
224
225impl RenderOnce for ScrollShadow {
226 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
227 let scroll = self.external_scroll.clone().unwrap_or_else(|| {
230 window
231 .use_keyed_state(element_id::scoped(&self.id, "scroll"), cx, |_, _| {
232 ScrollHandle::new()
233 })
234 .read(cx)
235 .clone()
236 });
237 let reported = window.use_keyed_state(
238 element_id::scoped(&self.id, "shadow-visibility"),
239 cx,
240 |_, _| ScrollShadowVisibility::None,
241 );
242 let bg = cx.colors().background;
243 let horizontal = self.orientation.is_horizontal();
244
245 let mut scroller = div()
246 .id(self.id.clone())
247 .track_scroll(&scroll)
248 .restrict_scroll_to_axis()
249 .overflow_hidden()
250 .flex()
251 .gap(self.gap);
252
253 scroller = if horizontal {
254 scroller.flex_row().overflow_x_scroll()
255 } else {
256 scroller.flex_col().overflow_y_scroll()
257 };
258
259 if horizontal {
268 let wheel_scroll = scroll.clone();
269 scroller = scroller.on_scroll_wheel(move |event, _window, _cx| {
270 crate::util::shift_wheel_scroll_x(&wheel_scroll, event);
271 });
272 }
273
274 if self.fill_axis {
275 scroller = if horizontal {
276 scroller.w_full().min_w(px(0.))
277 } else {
278 scroller.h_full().min_h(px(0.))
279 };
280 }
281
282 if let Some(h) = self.max_h {
283 scroller = scroller.max_h(h);
284 }
285 if let Some(w) = self.max_w {
286 scroller = scroller.max_w(w);
287 }
288
289 scroller = scroller.children(self.children);
290
291 let bar = (!self.hide_scroll_bar).then(|| {
292 crate::scrollbar::Scrollbar::new(
293 element_id::scoped(&self.id, "scrollbar"),
294 scroll.clone(),
295 )
296 .orientation(self.orientation)
297 });
298
299 if !self.is_enabled || self.visibility == ScrollShadowVisibility::None {
300 let mut root = div().relative().child(scroller);
301 if let Some(bar) = bar {
302 root = root.child(bar);
303 }
304 return crate::util::apply_sx(root, &self.sx).into_any_element();
305 }
306
307 let offset = scroll.offset();
321 let max = scroll.max_offset();
322 let (scrolled, scroll_max) = if horizontal {
323 (
324 f32::from(offset.x).clamp(-f32::from(max.x), 0.0),
325 f32::from(max.x),
326 )
327 } else {
328 (
329 f32::from(offset.y).clamp(-f32::from(max.y), 0.0),
330 f32::from(max.y),
331 )
332 };
333 let (past_start, before_end) = (
334 scrolled < -f32::from(self.offset),
335 scrolled - f32::from(self.offset) > -scroll_max,
336 );
337 let resolved = if self.visibility == ScrollShadowVisibility::Auto {
338 match (past_start, before_end) {
339 (true, true) => ScrollShadowVisibility::Both,
340 (true, false) if horizontal => ScrollShadowVisibility::Left,
341 (true, false) => ScrollShadowVisibility::Top,
342 (false, true) if horizontal => ScrollShadowVisibility::Right,
343 (false, true) => ScrollShadowVisibility::Bottom,
344 (false, false) => ScrollShadowVisibility::None,
345 }
346 } else {
347 self.visibility
348 };
349 let reports_visibility = self.visibility == ScrollShadowVisibility::Auto;
350
351 let clear = bg.alpha(0.0);
356 let fade = |from_start: bool| {
357 let stops = if from_start { (bg, clear) } else { (clear, bg) };
358 let angle = if horizontal { 90.0 } else { 180.0 };
359 let mut el = div().absolute().bg(gpui::linear_gradient(
360 angle,
361 gpui::linear_color_stop(stops.0, 0.0),
362 gpui::linear_color_stop(stops.1, 1.0),
363 ));
364 el = if horizontal {
365 el.top_0().bottom_0().w(self.size)
366 } else {
367 el.left_0().right_0().h(self.size)
368 };
369 match (horizontal, from_start) {
370 (true, true) => el.left(self.offset),
371 (true, false) => el.right(self.offset),
372 (false, true) => el.top(self.offset),
373 (false, false) => el.bottom(self.offset),
374 }
375 };
376
377 let root = div()
378 .relative()
379 .child(scroller)
380 .when(resolved.shows_start(self.orientation), |el| {
381 el.child(fade(true))
382 })
383 .when(resolved.shows_end(self.orientation), |el| {
384 el.child(fade(false))
385 })
386 .when_some(bar, |el, bar| el.child(bar))
387 .when(reports_visibility, |el| {
388 let handler = self.on_visibility_change.clone();
391 el.child(
392 canvas(
393 move |_, window, cx| {
394 if *reported.read(cx) != resolved {
395 reported.update(cx, |value, cx| {
396 *value = resolved;
397 cx.notify();
398 });
399 if let Some(f) = &handler {
400 f(&resolved, window, cx);
401 }
402 }
403 },
404 |_, _, _, _| {},
405 )
406 .absolute()
407 .size(px(0.)),
408 )
409 });
410 crate::util::apply_sx(root, &self.sx).into_any_element()
411 }
412}
413
414crate::util::impl_component_styled!(ScrollShadow);