Skip to main content

vizia_core/views/
scrollbar.rs

1use crate::context::TreeProps;
2use crate::prelude::*;
3
4/// A view which represents a bar that can be dragged to manipulate a scrollview.
5pub struct Scrollbar {
6    value: Signal<f32>,
7    orientation: Orientation,
8
9    reference_points: Option<(f32, f32)>,
10    dragging: bool,
11
12    on_changing: Option<Box<dyn Fn(&mut EventContext, f32)>>,
13
14    scroll_to_cursor: bool,
15}
16
17enum ScrollBarEvent {
18    SetScrollToCursor(bool),
19}
20
21impl Scrollbar {
22    fn apply_thumb_translate(&self, cx: &mut EventContext) {
23        let child = cx.first_child();
24        let (size, thumb_size) = self.container_and_thumb_size(cx);
25        let movable = (size - thumb_size).max(0.0);
26        let scale_factor = cx.scale_factor().max(f32::EPSILON);
27        let offset = (self.value.get().clamp(0.0, 1.0) * movable) / scale_factor;
28
29        cx.with_current(child, |cx| match self.orientation {
30            Orientation::Horizontal => cx.set_translate(Translate {
31                x: LengthOrPercentage::Length(Length::px(offset)),
32                y: LengthOrPercentage::Length(Length::px(0.0)),
33            }),
34            Orientation::Vertical => cx.set_translate(Translate {
35                x: LengthOrPercentage::Length(Length::px(0.0)),
36                y: LengthOrPercentage::Length(Length::px(offset)),
37            }),
38        });
39    }
40
41    /// Create a new [Scrollbar] view.
42    pub fn new<F, V, R>(
43        cx: &mut Context,
44        value: V,
45        ratio: R,
46        orientation: Orientation,
47        callback: F,
48    ) -> Handle<Self>
49    where
50        V: Res<f32> + 'static,
51        R: Res<f32> + 'static,
52        F: 'static + Fn(&mut EventContext, f32),
53    {
54        let value = value.to_signal(cx);
55        let ratio = ratio.to_signal(cx);
56        let translate_state: Memo<(f32, f32)> = Memo::new(move |_| (value.get(), ratio.get()));
57
58        Self {
59            value,
60            orientation,
61            reference_points: None,
62            on_changing: Some(Box::new(callback)),
63            scroll_to_cursor: false,
64            dragging: false,
65        }
66        .build(cx, move |cx| {
67            Element::new(cx)
68                .class("thumb")
69                .focusable(true)
70                .bind(translate_state, move |mut handle| {
71                    let (value, _ratio) = translate_state.get();
72                    let thumb = handle.entity();
73                    let track = handle.parent();
74                    let cx = handle.context();
75                    let (size, thumb_size) = match orientation {
76                        Orientation::Horizontal => {
77                            (cx.cache.get_width(track), cx.cache.get_width(thumb))
78                        }
79                        Orientation::Vertical => {
80                            (cx.cache.get_height(track), cx.cache.get_height(thumb))
81                        }
82                    };
83                    let movable = (size.max(0.0) - thumb_size.clamp(0.0, size.max(0.0))).max(0.0);
84                    let scale_factor = cx.scale_factor().max(f32::EPSILON);
85                    let offset = (value.clamp(0.0, 1.0) * movable) / scale_factor;
86
87                    match orientation {
88                        Orientation::Horizontal => handle.translate(Translate {
89                            x: LengthOrPercentage::Length(Length::px(offset)),
90                            y: LengthOrPercentage::Length(Length::px(0.0)),
91                        }),
92                        Orientation::Vertical => handle.translate(Translate {
93                            x: LengthOrPercentage::Length(Length::px(0.0)),
94                            y: LengthOrPercentage::Length(Length::px(offset)),
95                        }),
96                    };
97                })
98                .bind(ratio, move |handle| {
99                    let ratio = ratio.get();
100                    match orientation {
101                        Orientation::Horizontal => handle.width(Units::Percentage(ratio * 100.0)),
102                        Orientation::Vertical => handle.height(Units::Percentage(ratio * 100.0)),
103                    };
104                })
105                .position_type(PositionType::Absolute);
106        })
107        .pointer_events(PointerEvents::Auto)
108        .orientation(orientation)
109        .role(Role::ScrollBar)
110    }
111
112    fn container_and_thumb_size(&self, cx: &mut EventContext) -> (f32, f32) {
113        let current = cx.current();
114        let child = cx.tree.get_child(current, 0).unwrap();
115        let (size, thumb_size) = match &self.orientation {
116            Orientation::Horizontal => (cx.cache.get_width(current), cx.cache.get_width(child)),
117            Orientation::Vertical => (cx.cache.get_height(current), cx.cache.get_height(child)),
118        };
119
120        let size = size.max(0.0);
121        let thumb_size = thumb_size.clamp(0.0, size);
122        (size, thumb_size)
123    }
124
125    fn thumb_bounds(&self, cx: &mut EventContext) -> BoundingBox {
126        let child = cx.first_child();
127        cx.with_current(child, |cx| cx.bounds())
128    }
129
130    fn compute_new_value(&self, cx: &mut EventContext, physical_delta: f32, value_ref: f32) -> f32 {
131        // delta is moving within the negative space of the thumb: (1 - ratio) * container
132        let (size, thumb_size) = self.container_and_thumb_size(cx);
133        let negative_space = (size - thumb_size).max(0.0);
134        if negative_space <= f32::EPSILON {
135            value_ref
136        } else {
137            // what percentage of negative space have we crossed?
138            let logical_delta = physical_delta / negative_space;
139            value_ref + logical_delta
140        }
141    }
142
143    fn change(&mut self, cx: &mut EventContext, new_val: f32) {
144        if let Some(callback) = &self.on_changing {
145            callback(cx, new_val.clamp(0.0, 1.0));
146        }
147    }
148}
149
150impl View for Scrollbar {
151    fn element(&self) -> Option<&'static str> {
152        Some("scrollbar")
153    }
154
155    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
156        event.map(|scrollbar_event, _| match scrollbar_event {
157            ScrollBarEvent::SetScrollToCursor(flag) => {
158                self.scroll_to_cursor = *flag;
159            }
160        });
161
162        event.map(|window_event, meta| {
163            let pos = match &self.orientation {
164                Orientation::Horizontal => cx.mouse.cursor_x,
165                Orientation::Vertical => cx.mouse.cursor_y,
166            };
167            match window_event {
168                WindowEvent::MouseDown(MouseButton::Left) => {
169                    if meta.target != cx.current() {
170                        self.reference_points = Some((pos, self.value.get()));
171                        cx.capture();
172                        cx.set_active(true);
173                        self.dragging = true;
174                        cx.with_current(Entity::root(), |cx| {
175                            cx.set_pointer_events(false);
176                        });
177                    } else if self.scroll_to_cursor {
178                        cx.capture();
179                        cx.set_active(true);
180                        self.dragging = true;
181                        cx.with_current(Entity::root(), |cx| {
182                            cx.set_pointer_events(false);
183                        });
184                        let thumb_bounds = self.thumb_bounds(cx);
185                        let bounds = cx.bounds();
186                        let sx = bounds.w - thumb_bounds.w;
187                        let sy = bounds.h - thumb_bounds.h;
188                        match self.orientation {
189                            Orientation::Horizontal => {
190                                let px = cx.mouse.cursor_x - bounds.x - thumb_bounds.w / 2.0;
191                                let x = if sx <= f32::EPSILON {
192                                    0.0
193                                } else {
194                                    (px / sx).clamp(0.0, 1.0)
195                                };
196                                if let Some(callback) = &self.on_changing {
197                                    (callback)(cx, x);
198                                }
199                            }
200
201                            Orientation::Vertical => {
202                                let py = cx.mouse.cursor_y - bounds.y - thumb_bounds.h / 2.0;
203                                let y = if sy <= f32::EPSILON {
204                                    0.0
205                                } else {
206                                    (py / sy).clamp(0.0, 1.0)
207                                };
208                                if let Some(callback) = &self.on_changing {
209                                    (callback)(cx, y);
210                                }
211                            }
212                        }
213                    } else {
214                        let (_, jump) = self.container_and_thumb_size(cx);
215                        // let (tx, ty, tw, th) = self.thumb_bounds(cx);
216                        let t = self.thumb_bounds(cx);
217                        let physical_delta = match &self.orientation {
218                            Orientation::Horizontal => {
219                                if cx.mouse.cursor_x < t.x {
220                                    -jump
221                                } else if cx.mouse.cursor_x >= t.x + t.w {
222                                    jump
223                                } else {
224                                    return;
225                                }
226                            }
227                            Orientation::Vertical => {
228                                if cx.mouse.cursor_y < t.y {
229                                    -jump
230                                } else if cx.mouse.cursor_y >= t.y + t.h {
231                                    jump
232                                } else {
233                                    return;
234                                }
235                            }
236                        };
237                        let changed = self.compute_new_value(cx, physical_delta, self.value.get());
238                        self.change(cx, changed);
239                    }
240                }
241
242                WindowEvent::MouseUp(MouseButton::Left) => {
243                    self.reference_points = None;
244                    cx.focus_with_visibility(false);
245                    cx.release();
246                    cx.set_active(false);
247                    self.dragging = false;
248                    cx.with_current(Entity::root(), |cx| {
249                        cx.set_pointer_events(true);
250                    });
251                }
252
253                WindowEvent::MouseMove(_, _) => {
254                    if self.dragging {
255                        if let Some((mouse_ref, value_ref)) = self.reference_points {
256                            let physical_delta = pos - mouse_ref;
257                            let changed = self.compute_new_value(cx, physical_delta, value_ref);
258                            self.change(cx, changed);
259                        } else if self.scroll_to_cursor {
260                            let thumb_bounds = self.thumb_bounds(cx);
261                            let bounds = cx.bounds();
262                            let sx = bounds.w - thumb_bounds.w;
263                            let sy = bounds.h - thumb_bounds.h;
264                            match self.orientation {
265                                Orientation::Horizontal => {
266                                    let px = cx.mouse.cursor_x - bounds.x - thumb_bounds.w / 2.0;
267                                    let x = if sx <= f32::EPSILON {
268                                        0.0
269                                    } else {
270                                        (px / sx).clamp(0.0, 1.0)
271                                    };
272                                    if let Some(callback) = &self.on_changing {
273                                        (callback)(cx, x);
274                                    }
275                                }
276
277                                Orientation::Vertical => {
278                                    let py = cx.mouse.cursor_y - bounds.y - thumb_bounds.h / 2.0;
279                                    let y = if sy <= f32::EPSILON {
280                                        0.0
281                                    } else {
282                                        (py / sy).clamp(0.0, 1.0)
283                                    };
284                                    if let Some(callback) = &self.on_changing {
285                                        (callback)(cx, y);
286                                    }
287                                }
288                            }
289                        }
290                    }
291                }
292
293                WindowEvent::GeometryChanged(_) => {
294                    self.apply_thumb_translate(cx);
295                }
296
297                _ => {}
298            }
299        });
300    }
301}
302
303impl Handle<'_, Scrollbar> {
304    /// Sets whether the scrollbar should move to the cursor when pressed.
305    pub fn scroll_to_cursor(self, scroll_to_cursor: impl Res<bool> + 'static) -> Self {
306        let scroll_to_cursor = scroll_to_cursor.to_signal(self.cx);
307        self.bind(scroll_to_cursor, move |handle| {
308            handle.cx.emit(ScrollBarEvent::SetScrollToCursor(scroll_to_cursor.get()));
309        })
310    }
311}