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