scrollbar.rs

  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}