1use std::{cell::Cell, ops::Range, rc::Rc};
2
3use gpui::{
4 point, quad, Bounds, ContentMask, Corners, Edges, EntityId, Hitbox, Hsla, MouseDownEvent,
5 MouseMoveEvent, MouseUpEvent, ScrollWheelEvent, Style, UniformListScrollHandle,
6};
7use ui::{prelude::*, px, relative, IntoElement};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub(crate) enum ScrollbarKind {
11 Horizontal,
12 Vertical,
13}
14
15pub(crate) struct ProjectPanelScrollbar {
16 thumb: Range<f32>,
17 scroll: UniformListScrollHandle,
18 // If Some(), there's an active drag, offset by percentage from the top of thumb.
19 scrollbar_drag_state: Rc<Cell<Option<f32>>>,
20 kind: ScrollbarKind,
21 parent_id: EntityId,
22}
23
24impl ProjectPanelScrollbar {
25 pub(crate) fn vertical(
26 thumb: Range<f32>,
27 scroll: UniformListScrollHandle,
28 scrollbar_drag_state: Rc<Cell<Option<f32>>>,
29 parent_id: EntityId,
30 ) -> Self {
31 Self {
32 thumb,
33 scroll,
34 scrollbar_drag_state,
35 kind: ScrollbarKind::Vertical,
36 parent_id,
37 }
38 }
39
40 pub(crate) fn horizontal(
41 thumb: Range<f32>,
42 scroll: UniformListScrollHandle,
43 scrollbar_drag_state: Rc<Cell<Option<f32>>>,
44 parent_id: EntityId,
45 ) -> Self {
46 Self {
47 thumb,
48 scroll,
49 scrollbar_drag_state,
50 kind: ScrollbarKind::Horizontal,
51 parent_id,
52 }
53 }
54}
55
56impl gpui::Element for ProjectPanelScrollbar {
57 type RequestLayoutState = ();
58
59 type PrepaintState = Hitbox;
60
61 fn id(&self) -> Option<ui::ElementId> {
62 None
63 }
64
65 fn request_layout(
66 &mut self,
67 _id: Option<&gpui::GlobalElementId>,
68 cx: &mut ui::WindowContext,
69 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
70 let mut style = Style::default();
71 style.flex_grow = 1.;
72 style.flex_shrink = 1.;
73 if self.kind == ScrollbarKind::Vertical {
74 style.size.width = px(12.).into();
75 style.size.height = relative(1.).into();
76 } else {
77 style.size.width = relative(1.).into();
78 style.size.height = px(12.).into();
79 }
80
81 (cx.request_layout(style, None), ())
82 }
83
84 fn prepaint(
85 &mut self,
86 _id: Option<&gpui::GlobalElementId>,
87 bounds: Bounds<ui::Pixels>,
88 _request_layout: &mut Self::RequestLayoutState,
89 cx: &mut ui::WindowContext,
90 ) -> Self::PrepaintState {
91 cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
92 cx.insert_hitbox(bounds, false)
93 })
94 }
95
96 fn paint(
97 &mut self,
98 _id: Option<&gpui::GlobalElementId>,
99 bounds: Bounds<ui::Pixels>,
100 _request_layout: &mut Self::RequestLayoutState,
101 _prepaint: &mut Self::PrepaintState,
102 cx: &mut ui::WindowContext,
103 ) {
104 cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
105 let colors = cx.theme().colors();
106 let thumb_background = colors.scrollbar_thumb_background;
107 let is_vertical = self.kind == ScrollbarKind::Vertical;
108 let extra_padding = px(5.0);
109 let padded_bounds = if is_vertical {
110 Bounds::from_corners(
111 bounds.origin + point(Pixels::ZERO, extra_padding),
112 bounds.lower_right() - point(Pixels::ZERO, extra_padding * 3),
113 )
114 } else {
115 Bounds::from_corners(
116 bounds.origin + point(extra_padding, Pixels::ZERO),
117 bounds.lower_right() - point(extra_padding * 3, Pixels::ZERO),
118 )
119 };
120
121 let mut thumb_bounds = if is_vertical {
122 let thumb_offset = self.thumb.start * padded_bounds.size.height;
123 let thumb_end = self.thumb.end * padded_bounds.size.height;
124 let thumb_upper_left = point(
125 padded_bounds.origin.x,
126 padded_bounds.origin.y + thumb_offset,
127 );
128 let thumb_lower_right = point(
129 padded_bounds.origin.x + padded_bounds.size.width,
130 padded_bounds.origin.y + thumb_end,
131 );
132 Bounds::from_corners(thumb_upper_left, thumb_lower_right)
133 } else {
134 let thumb_offset = self.thumb.start * padded_bounds.size.width;
135 let thumb_end = self.thumb.end * padded_bounds.size.width;
136 let thumb_upper_left = point(
137 padded_bounds.origin.x + thumb_offset,
138 padded_bounds.origin.y,
139 );
140 let thumb_lower_right = point(
141 padded_bounds.origin.x + thumb_end,
142 padded_bounds.origin.y + padded_bounds.size.height,
143 );
144 Bounds::from_corners(thumb_upper_left, thumb_lower_right)
145 };
146 let corners = if is_vertical {
147 thumb_bounds.size.width /= 1.5;
148 Corners::all(thumb_bounds.size.width / 2.0)
149 } else {
150 thumb_bounds.size.height /= 1.5;
151 Corners::all(thumb_bounds.size.height / 2.0)
152 };
153 cx.paint_quad(quad(
154 thumb_bounds,
155 corners,
156 thumb_background,
157 Edges::default(),
158 Hsla::transparent_black(),
159 ));
160
161 let scroll = self.scroll.clone();
162 let kind = self.kind;
163 let thumb_percentage_size = self.thumb.end - self.thumb.start;
164
165 cx.on_mouse_event({
166 let scroll = self.scroll.clone();
167 let is_dragging = self.scrollbar_drag_state.clone();
168 move |event: &MouseDownEvent, phase, _cx| {
169 if phase.bubble() && bounds.contains(&event.position) {
170 if !thumb_bounds.contains(&event.position) {
171 let scroll = scroll.0.borrow();
172 if let Some(item_size) = scroll.last_item_size {
173 match kind {
174 ScrollbarKind::Horizontal => {
175 let percentage = (event.position.x - bounds.origin.x)
176 / bounds.size.width;
177 let max_offset = item_size.contents.width;
178 let percentage = percentage.min(1. - thumb_percentage_size);
179 scroll.base_handle.set_offset(point(
180 -max_offset * percentage,
181 scroll.base_handle.offset().y,
182 ));
183 }
184 ScrollbarKind::Vertical => {
185 let percentage = (event.position.y - bounds.origin.y)
186 / bounds.size.height;
187 let max_offset = item_size.contents.height;
188 let percentage = percentage.min(1. - thumb_percentage_size);
189 scroll.base_handle.set_offset(point(
190 scroll.base_handle.offset().x,
191 -max_offset * percentage,
192 ));
193 }
194 }
195 }
196 } else {
197 let thumb_offset = if is_vertical {
198 (event.position.y - thumb_bounds.origin.y) / bounds.size.height
199 } else {
200 (event.position.x - thumb_bounds.origin.x) / bounds.size.width
201 };
202 is_dragging.set(Some(thumb_offset));
203 }
204 }
205 }
206 });
207 cx.on_mouse_event({
208 let scroll = self.scroll.clone();
209 move |event: &ScrollWheelEvent, phase, cx| {
210 if phase.bubble() && bounds.contains(&event.position) {
211 let scroll = scroll.0.borrow_mut();
212 let current_offset = scroll.base_handle.offset();
213
214 scroll
215 .base_handle
216 .set_offset(current_offset + event.delta.pixel_delta(cx.line_height()));
217 }
218 }
219 });
220 let drag_state = self.scrollbar_drag_state.clone();
221 let view_id = self.parent_id;
222 let kind = self.kind;
223 cx.on_mouse_event(move |event: &MouseMoveEvent, _, cx| {
224 if let Some(drag_state) = drag_state.get().filter(|_| event.dragging()) {
225 let scroll = scroll.0.borrow();
226 if let Some(item_size) = scroll.last_item_size {
227 match kind {
228 ScrollbarKind::Horizontal => {
229 let max_offset = item_size.contents.width;
230 let percentage = (event.position.x - bounds.origin.x)
231 / bounds.size.width
232 - drag_state;
233
234 let percentage = percentage.min(1. - thumb_percentage_size);
235 scroll.base_handle.set_offset(point(
236 -max_offset * percentage,
237 scroll.base_handle.offset().y,
238 ));
239 }
240 ScrollbarKind::Vertical => {
241 let max_offset = item_size.contents.height;
242 let percentage = (event.position.y - bounds.origin.y)
243 / bounds.size.height
244 - drag_state;
245
246 let percentage = percentage.min(1. - thumb_percentage_size);
247 scroll.base_handle.set_offset(point(
248 scroll.base_handle.offset().x,
249 -max_offset * percentage,
250 ));
251 }
252 };
253
254 cx.notify(view_id);
255 }
256 } else {
257 drag_state.set(None);
258 }
259 });
260 let is_dragging = self.scrollbar_drag_state.clone();
261 cx.on_mouse_event(move |_event: &MouseUpEvent, phase, cx| {
262 if phase.bubble() {
263 is_dragging.set(None);
264 cx.notify(view_id);
265 }
266 });
267 })
268 }
269}
270
271impl IntoElement for ProjectPanelScrollbar {
272 type Element = Self;
273
274 fn into_element(self) -> Self::Element {
275 self
276 }
277}