gpui_component/scroll/
scrollable_mask.rs

1use gpui::{
2    px, relative, App, Axis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element, ElementId,
3    EntityId, GlobalElementId, Hitbox, Hsla, IntoElement, IsZero as _, LayoutId, PaintQuad, Pixels,
4    Point, Position, ScrollHandle, ScrollWheelEvent, Style, Window,
5};
6
7use crate::AxisExt;
8
9/// Make a scrollable mask element to cover the parent view with the mouse wheel event listening.
10///
11/// When the mouse wheel is scrolled, will move the `scroll_handle` scrolling with the `axis` direction.
12/// You can use this `scroll_handle` to control what you want to scroll.
13/// This is only can handle once axis scrolling.
14pub struct ScrollableMask {
15    view_id: EntityId,
16    axis: Axis,
17    scroll_handle: ScrollHandle,
18    debug: Option<Hsla>,
19}
20
21impl ScrollableMask {
22    /// Create a new scrollable mask element.
23    pub fn new(view_id: EntityId, axis: Axis, scroll_handle: &ScrollHandle) -> Self {
24        Self {
25            view_id,
26            scroll_handle: scroll_handle.clone(),
27            axis,
28            debug: None,
29        }
30    }
31
32    /// Enable the debug border, to show the mask bounds.
33    #[allow(dead_code)]
34    pub fn debug(mut self) -> Self {
35        self.debug = Some(gpui::yellow());
36        self
37    }
38}
39
40impl IntoElement for ScrollableMask {
41    type Element = Self;
42
43    fn into_element(self) -> Self::Element {
44        self
45    }
46}
47
48impl Element for ScrollableMask {
49    type RequestLayoutState = ();
50    type PrepaintState = Hitbox;
51
52    fn id(&self) -> Option<ElementId> {
53        None
54    }
55
56    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
57        None
58    }
59
60    fn request_layout(
61        &mut self,
62        _: Option<&GlobalElementId>,
63        _: Option<&gpui::InspectorElementId>,
64        window: &mut Window,
65        cx: &mut App,
66    ) -> (LayoutId, Self::RequestLayoutState) {
67        let mut style = Style::default();
68        // Set the layout style relative to the table view to get same size.
69        style.position = Position::Absolute;
70        style.flex_grow = 1.0;
71        style.flex_shrink = 1.0;
72        style.size.width = relative(1.).into();
73        style.size.height = relative(1.).into();
74
75        (window.request_layout(style, None, cx), ())
76    }
77
78    fn prepaint(
79        &mut self,
80        _: Option<&GlobalElementId>,
81        _: Option<&gpui::InspectorElementId>,
82        bounds: Bounds<Pixels>,
83        _: &mut Self::RequestLayoutState,
84        window: &mut Window,
85        _: &mut App,
86    ) -> Self::PrepaintState {
87        // Move y to bounds height to cover the parent view.
88        let cover_bounds = Bounds {
89            origin: Point {
90                x: bounds.origin.x,
91                y: bounds.origin.y - bounds.size.height,
92            },
93            size: bounds.size,
94        };
95
96        window.insert_hitbox(cover_bounds, gpui::HitboxBehavior::Normal)
97    }
98
99    fn paint(
100        &mut self,
101        _: Option<&GlobalElementId>,
102        _: Option<&gpui::InspectorElementId>,
103        _: Bounds<Pixels>,
104        _: &mut Self::RequestLayoutState,
105        hitbox: &mut Self::PrepaintState,
106        window: &mut Window,
107        _: &mut App,
108    ) {
109        let line_height = window.line_height();
110        let bounds = hitbox.bounds;
111
112        window.with_content_mask(Some(ContentMask { bounds }), |window| {
113            if let Some(color) = self.debug {
114                window.paint_quad(PaintQuad {
115                    bounds,
116                    border_widths: Edges::all(px(1.0)),
117                    border_color: color,
118                    background: gpui::transparent_white().into(),
119                    corner_radii: Corners::all(px(0.)),
120                    border_style: BorderStyle::default(),
121                });
122            }
123
124            window.on_mouse_event({
125                let view_id = self.view_id;
126                let is_horizontal = self.axis.is_horizontal();
127                let scroll_handle = self.scroll_handle.clone();
128                let hitbox = hitbox.clone();
129                let mouse_position = window.mouse_position();
130                let last_offset = scroll_handle.offset();
131
132                move |event: &ScrollWheelEvent, phase, window, cx| {
133                    if bounds.contains(&mouse_position)
134                        && phase.bubble()
135                        && hitbox.is_hovered(window)
136                    {
137                        let mut offset = scroll_handle.offset();
138                        let mut delta = event.delta.pixel_delta(line_height);
139
140                        // Limit for only one way scrolling at same time.
141                        // When use MacBook touchpad we may get both x and y delta,
142                        // only allows the one that more to scroll.
143                        if !delta.x.is_zero() && !delta.y.is_zero() {
144                            if delta.x.abs() > delta.y.abs() {
145                                delta.y = px(0.);
146                            } else {
147                                delta.x = px(0.);
148                            }
149                        }
150
151                        if is_horizontal {
152                            offset.x += delta.x;
153                        } else {
154                            offset.y += delta.y;
155                        }
156
157                        if last_offset != offset {
158                            scroll_handle.set_offset(offset);
159                            cx.notify(view_id);
160                            cx.stop_propagation();
161                        }
162                    }
163                }
164            });
165        });
166    }
167}