vizia_core/views/
scrollview.rs

1use std::sync::Arc;
2
3use crate::binding::RatioLens;
4use crate::prelude::*;
5
6pub(crate) const SCROLL_SENSITIVITY: f32 = 20.0;
7
8/// Events for setting the properties of a scroll view.
9pub enum ScrollEvent {
10    /// Sets the progress of scroll position between 0 and 1 for the x axis
11    SetX(f32),
12    /// Sets the progress of scroll position between 0 and 1 for the y axis
13    SetY(f32),
14    /// Adds given progress to scroll position for the x axis and clamps between 0 and 1
15    ScrollX(f32),
16    /// Adds given progress to scroll position for the y axis and clamps between 0 and 1
17    ScrollY(f32),
18    /// Sets the size for the inner scroll-content view which holds the content
19    ChildGeo(f32, f32),
20}
21
22/// A container a view which allows the user to scroll any overflowed content.
23#[derive(Lens, Data, Clone)]
24pub struct ScrollView {
25    /// Progress of scroll position between 0 and 1 for the x axis
26    pub scroll_x: f32,
27    /// Progress of scroll position between 0 and 1 for the y axis
28    pub scroll_y: f32,
29
30    /// Callback called when the scrollview is scrolled.
31    #[lens(ignore)]
32    pub on_scroll: Option<Arc<dyn Fn(&mut EventContext, f32, f32) + Send + Sync>>,
33
34    /// Width of the inner VStack which holds the content (typically bigger than container_width)
35    pub inner_width: f32,
36    /// Height of the inner VStack which holds the content (typically bigger than container_height)
37    pub inner_height: f32,
38    /// Width of the outer `ScrollView` which wraps the inner (typically smaller than inner_width)
39    pub container_width: f32,
40    /// Height of the outer `ScrollView` which wraps the inner (typically smaller than inner_height)
41    pub container_height: f32,
42    /// Whether the scrollbar should move to the cursor when pressed.
43    pub scroll_to_cursor: bool,
44    /// Whether the horizontal scrollbar should be visible.
45    pub show_horizontal_scrollbar: bool,
46    /// Whether the vertical scrollbar should be visible.
47    pub show_vertical_scrollbar: bool,
48}
49
50impl ScrollView {
51    /// Creates a new [ScrollView].
52    pub fn new<F>(cx: &mut Context, content: F) -> Handle<Self>
53    where
54        F: 'static + FnOnce(&mut Context),
55    {
56        Self {
57            scroll_to_cursor: false,
58            scroll_x: 0.0,
59            scroll_y: 0.0,
60            on_scroll: None,
61            inner_width: 0.0,
62            inner_height: 0.0,
63            container_width: 0.0,
64            container_height: 0.0,
65            show_horizontal_scrollbar: true,
66            show_vertical_scrollbar: true,
67        }
68        .build(cx, move |cx| {
69            ScrollContent::new(cx, content);
70
71            Binding::new(cx, ScrollView::show_vertical_scrollbar, |cx, show_scrollbar| {
72                if show_scrollbar.get(cx) {
73                    Scrollbar::new(
74                        cx,
75                        ScrollView::scroll_y,
76                        RatioLens::new(ScrollView::container_height, ScrollView::inner_height),
77                        Orientation::Vertical,
78                        |cx, value| {
79                            cx.emit(ScrollEvent::SetY(value));
80                        },
81                    )
82                    .position_type(PositionType::Absolute)
83                    .scroll_to_cursor(Self::scroll_to_cursor);
84                }
85            });
86
87            Binding::new(cx, ScrollView::show_vertical_scrollbar, |cx, show_scrollbar| {
88                if show_scrollbar.get(cx) {
89                    Scrollbar::new(
90                        cx,
91                        ScrollView::scroll_x,
92                        RatioLens::new(ScrollView::container_width, ScrollView::inner_width),
93                        Orientation::Horizontal,
94                        |cx, value| {
95                            cx.emit(ScrollEvent::SetX(value));
96                        },
97                    )
98                    .position_type(PositionType::Absolute)
99                    .scroll_to_cursor(Self::scroll_to_cursor);
100                }
101            });
102        })
103        .bind(ScrollView::root, |mut handle, data| {
104            let data = data.get(&handle);
105            let scale_factor = handle.context().scale_factor();
106            let top = ((data.inner_height - data.container_height) * data.scroll_y).round()
107                / scale_factor;
108            let left =
109                ((data.inner_width - data.container_width) * data.scroll_x).round() / scale_factor;
110            handle.horizontal_scroll(-left.abs()).vertical_scroll(-top.abs());
111        })
112        .toggle_class(
113            "h-scroll",
114            ScrollView::root.map(|data| data.container_width < data.inner_width),
115        )
116        .toggle_class(
117            "v-scroll",
118            ScrollView::root.map(|data| data.container_height < data.inner_height),
119        )
120    }
121
122    fn reset(&mut self) {
123        if self.inner_width == self.container_width {
124            self.scroll_x = 0.0;
125        }
126
127        if self.inner_height == self.container_height {
128            self.scroll_y = 0.0;
129        }
130    }
131}
132
133impl View for ScrollView {
134    fn element(&self) -> Option<&'static str> {
135        Some("scrollview")
136    }
137
138    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
139        event.map(|scroll_update, meta| {
140            match scroll_update {
141                ScrollEvent::ScrollX(f) => {
142                    self.scroll_x = (self.scroll_x + *f).clamp(0.0, 1.0);
143
144                    if let Some(callback) = &self.on_scroll {
145                        (callback)(cx, self.scroll_x, self.scroll_y);
146                    }
147                }
148
149                ScrollEvent::ScrollY(f) => {
150                    self.scroll_y = (self.scroll_y + *f).clamp(0.0, 1.0);
151                    if let Some(callback) = &self.on_scroll {
152                        (callback)(cx, self.scroll_x, self.scroll_y);
153                    }
154                }
155
156                ScrollEvent::SetX(f) => {
157                    self.scroll_x = *f;
158                    if let Some(callback) = &self.on_scroll {
159                        (callback)(cx, self.scroll_x, self.scroll_y);
160                    }
161                }
162
163                ScrollEvent::SetY(f) => {
164                    self.scroll_y = *f;
165                    if let Some(callback) = &self.on_scroll {
166                        (callback)(cx, self.scroll_x, self.scroll_y);
167                    }
168                }
169
170                ScrollEvent::ChildGeo(w, h) => {
171                    let bounds = cx.bounds();
172                    let scale_factor = cx.scale_factor();
173
174                    if self.inner_width != 0.0
175                        && self.inner_height != 0.0
176                        && self.container_width != 0.0
177                        && self.container_height != 0.0
178                    {
179                        let top = ((self.inner_height - self.container_height) * self.scroll_y)
180                            .round()
181                            / scale_factor;
182                        let left = ((self.inner_width - self.container_width) * self.scroll_x)
183                            .round()
184                            / scale_factor;
185
186                        self.container_width = bounds.width();
187                        self.container_height = bounds.height();
188                        self.inner_width = *w;
189                        self.inner_height = *h;
190
191                        self.scroll_y = ((top * scale_factor)
192                            / (self.inner_height - self.container_height))
193                            .clamp(0.0, 1.0);
194                        self.scroll_x = ((left * scale_factor)
195                            / (self.inner_width - self.container_width))
196                            .clamp(0.0, 1.0);
197
198                        if let Some(callback) = &self.on_scroll {
199                            (callback)(cx, self.scroll_x, self.scroll_y);
200                        }
201
202                        self.reset();
203                    }
204
205                    self.inner_width = *w;
206                    self.inner_height = *h;
207                    self.reset();
208                }
209            }
210
211            // Prevent scroll events propagating to any parent scrollviews.
212            // TODO: This might be desired behavior when the scrollview is scrolled all the way.
213            meta.consume();
214        });
215
216        event.map(|window_event, meta| match window_event {
217            WindowEvent::GeometryChanged(geo) => {
218                if geo.contains(GeoChanged::WIDTH_CHANGED)
219                    || geo.contains(GeoChanged::HEIGHT_CHANGED)
220                {
221                    let bounds = cx.bounds();
222                    let scale_factor = cx.scale_factor();
223
224                    if self.inner_width != 0.0
225                        && self.inner_height != 0.0
226                        && self.container_width != 0.0
227                        && self.container_height != 0.0
228                    {
229                        let top = ((self.inner_height - self.container_height) * self.scroll_y)
230                            .round()
231                            / scale_factor;
232                        let left = ((self.inner_width - self.container_width) * self.scroll_x)
233                            .round()
234                            / scale_factor;
235
236                        self.container_width = bounds.width();
237                        self.container_height = bounds.height();
238
239                        self.scroll_y = ((top * scale_factor)
240                            / (self.inner_height - self.container_height))
241                            .clamp(0.0, 1.0);
242                        self.scroll_x = ((left * scale_factor)
243                            / (self.inner_width - self.container_width))
244                            .clamp(0.0, 1.0);
245                        if let Some(callback) = &self.on_scroll {
246                            (callback)(cx, self.scroll_x, self.scroll_y);
247                        }
248
249                        self.reset();
250                    }
251
252                    self.container_width = bounds.width();
253                    self.container_height = bounds.height();
254                }
255            }
256
257            WindowEvent::MouseScroll(x, y) => {
258                cx.set_active(true);
259                let (x, y) = if cx.modifiers.shift() { (-*y, -*x) } else { (-*x, -*y) };
260
261                // What percentage of the negative space does this cross?
262                if x != 0.0 && self.inner_width > self.container_width {
263                    let negative_space = self.inner_width - self.container_width;
264                    if negative_space != 0.0 {
265                        let logical_delta = x * SCROLL_SENSITIVITY / negative_space;
266                        cx.emit(ScrollEvent::ScrollX(logical_delta));
267                    }
268                    // Prevent event propagating to ancestor scrollviews.
269                    meta.consume();
270                }
271                if y != 0.0 && self.inner_height > self.container_height {
272                    let negative_space = self.inner_height - self.container_height;
273                    if negative_space != 0.0 {
274                        let logical_delta = y * SCROLL_SENSITIVITY / negative_space;
275                        cx.emit(ScrollEvent::ScrollY(logical_delta));
276                    }
277                    // Prevent event propagating to ancestor scrollviews.
278                    meta.consume();
279                }
280            }
281
282            WindowEvent::MouseOut => {
283                cx.set_active(false);
284            }
285
286            _ => {}
287        });
288    }
289}
290
291impl Handle<'_, ScrollView> {
292    /// Sets a callback which will be called when a scrollview is scrolled, either with the mouse wheel, touchpad, or using the scroll bars.
293    pub fn on_scroll(
294        self,
295        callback: impl Fn(&mut EventContext, f32, f32) + 'static + Send + Sync,
296    ) -> Self {
297        self.modify(|scrollview: &mut ScrollView| scrollview.on_scroll = Some(Arc::new(callback)))
298    }
299
300    /// Sets whether the scrollbar should move to the cursor when pressed.
301    pub fn scroll_to_cursor(self, scroll_to_cursor: bool) -> Self {
302        self.modify(|scrollview: &mut ScrollView| scrollview.scroll_to_cursor = scroll_to_cursor)
303    }
304
305    /// Set the horizontal scroll position of the [ScrollView]. Accepts a value or lens to an 'f32' between 0 and 1.
306    pub fn scroll_x(self, scrollx: impl Res<f32>) -> Self {
307        self.bind(scrollx, |handle, scrollx| {
308            let sx = scrollx.get(&handle);
309            handle.modify(|scrollview| scrollview.scroll_x = sx);
310        })
311    }
312
313    /// Set the vertical scroll position of the [ScrollView]. Accepts a value or lens to an 'f32' between 0 and 1.
314    pub fn scroll_y(self, scrollx: impl Res<f32>) -> Self {
315        self.bind(scrollx, |handle, scrolly| {
316            let sy = scrolly.get(&handle);
317            handle.modify(|scrollview| scrollview.scroll_y = sy);
318        })
319    }
320
321    /// Sets whether the horizontal scrollbar should be visible.
322    pub fn show_horizontal_scrollbar(self, flag: impl Res<bool>) -> Self {
323        self.bind(flag, |handle, show_scrollbar| {
324            let s = show_scrollbar.get(&handle);
325            handle.modify(|scrollview| scrollview.show_horizontal_scrollbar = s);
326        })
327    }
328
329    /// Sets whether the vertical scrollbar should be visible.
330    pub fn show_vertical_scrollbar(self, flag: impl Res<bool>) -> Self {
331        self.bind(flag, |handle, show_scrollbar| {
332            let s = show_scrollbar.get(&handle);
333            handle.modify(|scrollview| scrollview.show_vertical_scrollbar = s);
334        })
335    }
336}
337
338struct ScrollContent {}
339
340impl ScrollContent {
341    pub fn new(cx: &mut Context, content: impl FnOnce(&mut Context)) -> Handle<Self> {
342        Self {}.build(cx, content)
343    }
344}
345
346impl View for ScrollContent {
347    fn element(&self) -> Option<&'static str> {
348        Some("scroll-content")
349    }
350
351    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
352        event.map(|window_event, _| match window_event {
353            WindowEvent::GeometryChanged(geo) => {
354                if geo.contains(GeoChanged::WIDTH_CHANGED)
355                    || geo.contains(GeoChanged::HEIGHT_CHANGED)
356                {
357                    let bounds = cx.bounds();
358                    // If the width or height have changed then send this back up to the ScrollData.
359                    cx.emit(ScrollEvent::ChildGeo(bounds.w, bounds.h));
360                }
361            }
362
363            _ => {}
364        });
365    }
366}