scrollbar.rs

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