1use std::{cell::Cell, ops::Range, rc::Rc};
2
3use gpui::{
4 point, AnyView, Bounds, ContentMask, Hitbox, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
5 ScrollWheelEvent, Style, UniformListScrollHandle,
6};
7use ui::{prelude::*, px, relative, IntoElement};
8
9pub(crate) struct ProjectPanelScrollbar {
10 thumb: Range<f32>,
11 scroll: UniformListScrollHandle,
12 is_dragging_scrollbar: Rc<Cell<bool>>,
13 item_count: usize,
14 view: AnyView,
15}
16
17impl ProjectPanelScrollbar {
18 pub(crate) fn new(
19 thumb: Range<f32>,
20 scroll: UniformListScrollHandle,
21 is_dragging_scrollbar: Rc<Cell<bool>>,
22 view: AnyView,
23 item_count: usize,
24 ) -> Self {
25 Self {
26 thumb,
27 scroll,
28 is_dragging_scrollbar,
29 item_count,
30 view,
31 }
32 }
33}
34
35impl gpui::Element for ProjectPanelScrollbar {
36 type RequestLayoutState = ();
37
38 type PrepaintState = Hitbox;
39
40 fn id(&self) -> Option<ui::ElementId> {
41 None
42 }
43
44 fn request_layout(
45 &mut self,
46 _id: Option<&gpui::GlobalElementId>,
47 cx: &mut ui::WindowContext,
48 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
49 let mut style = Style::default();
50 style.flex_grow = 1.;
51 style.flex_shrink = 1.;
52 style.size.width = px(12.).into();
53 style.size.height = relative(1.).into();
54 (cx.request_layout(style, None), ())
55 }
56
57 fn prepaint(
58 &mut self,
59 _id: Option<&gpui::GlobalElementId>,
60 bounds: Bounds<ui::Pixels>,
61 _request_layout: &mut Self::RequestLayoutState,
62 cx: &mut ui::WindowContext,
63 ) -> Self::PrepaintState {
64 cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
65 cx.insert_hitbox(bounds, false)
66 })
67 }
68
69 fn paint(
70 &mut self,
71 _id: Option<&gpui::GlobalElementId>,
72 bounds: Bounds<ui::Pixels>,
73 _request_layout: &mut Self::RequestLayoutState,
74 _prepaint: &mut Self::PrepaintState,
75 cx: &mut ui::WindowContext,
76 ) {
77 cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
78 let colors = cx.theme().colors();
79 let scrollbar_background = colors.scrollbar_track_border;
80 let thumb_background = colors.scrollbar_thumb_background;
81 cx.paint_quad(gpui::fill(bounds, scrollbar_background));
82
83 let thumb_offset = self.thumb.start * bounds.size.height;
84 let thumb_end = self.thumb.end * bounds.size.height;
85
86 let thumb_percentage_size = self.thumb.end - self.thumb.start;
87 let thumb_bounds = {
88 let thumb_upper_left = point(bounds.origin.x, bounds.origin.y + thumb_offset);
89 let thumb_lower_right = point(
90 bounds.origin.x + bounds.size.width,
91 bounds.origin.y + thumb_end,
92 );
93 Bounds::from_corners(thumb_upper_left, thumb_lower_right)
94 };
95 cx.paint_quad(gpui::fill(thumb_bounds, thumb_background));
96 let scroll = self.scroll.clone();
97 let item_count = self.item_count;
98 cx.on_mouse_event({
99 let scroll = self.scroll.clone();
100 let is_dragging = self.is_dragging_scrollbar.clone();
101 move |event: &MouseDownEvent, phase, _cx| {
102 if phase.bubble() && bounds.contains(&event.position) {
103 if !thumb_bounds.contains(&event.position) {
104 let scroll = scroll.0.borrow();
105 if let Some(last_height) = scroll.last_item_height {
106 let max_offset = item_count as f32 * last_height;
107 let percentage =
108 (event.position.y - bounds.origin.y) / bounds.size.height;
109
110 let percentage = percentage.min(1. - thumb_percentage_size);
111 scroll
112 .base_handle
113 .set_offset(point(px(0.), -max_offset * percentage));
114 }
115 } else {
116 is_dragging.set(true);
117 }
118 }
119 }
120 });
121 cx.on_mouse_event({
122 let scroll = self.scroll.clone();
123 move |event: &ScrollWheelEvent, phase, cx| {
124 if phase.bubble() && bounds.contains(&event.position) {
125 let scroll = scroll.0.borrow_mut();
126 let current_offset = scroll.base_handle.offset();
127 scroll
128 .base_handle
129 .set_offset(current_offset + event.delta.pixel_delta(cx.line_height()));
130 }
131 }
132 });
133 let is_dragging = self.is_dragging_scrollbar.clone();
134 let view_id = self.view.entity_id();
135 cx.on_mouse_event(move |event: &MouseMoveEvent, _, cx| {
136 if event.dragging() && is_dragging.get() {
137 let scroll = scroll.0.borrow();
138 if let Some(last_height) = scroll.last_item_height {
139 let max_offset = item_count as f32 * last_height;
140 let percentage = (event.position.y - bounds.origin.y) / bounds.size.height;
141
142 let percentage = percentage.min(1. - thumb_percentage_size);
143 scroll
144 .base_handle
145 .set_offset(point(px(0.), -max_offset * percentage));
146 cx.notify(view_id);
147 }
148 } else {
149 is_dragging.set(false);
150 }
151 });
152 let is_dragging = self.is_dragging_scrollbar.clone();
153 cx.on_mouse_event(move |_event: &MouseUpEvent, phase, cx| {
154 if phase.bubble() {
155 is_dragging.set(false);
156 cx.notify(view_id);
157 }
158 });
159 })
160 }
161}
162
163impl IntoElement for ProjectPanelScrollbar {
164 type Element = Self;
165
166 fn into_element(self) -> Self::Element {
167 self
168 }
169}