gpui_component/scroll/
scrollable_mask.rs

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