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 is_horizontal_rtl(&self, cx: &EventContext) -> bool {
23        self.orientation == Orientation::Horizontal
24            && cx.environment().direction.get() == Direction::RightToLeft
25    }
26
27    /// Create a new [Scrollbar] view.
28    pub fn new<F, V, R>(
29        cx: &mut Context,
30        value: V,
31        ratio: R,
32        orientation: Orientation,
33        callback: F,
34    ) -> Handle<Self>
35    where
36        V: Res<f32> + 'static,
37        R: Res<f32> + 'static,
38        F: 'static + Fn(&mut EventContext, f32),
39    {
40        let value = value.to_signal(cx);
41        let ratio = ratio.to_signal(cx);
42
43        Self {
44            value,
45            orientation,
46            reference_points: None,
47            on_changing: Some(Box::new(callback)),
48            scroll_to_cursor: false,
49            dragging: false,
50        }
51        .build(cx, move |cx| {
52            Element::new(cx)
53                .class("thumb")
54                .focusable(true)
55                .bind(value, move |handle| {
56                    let value = value.get();
57                    match orientation {
58                        Orientation::Horizontal => {
59                            handle.left(Units::Stretch(value)).right(Units::Stretch(1.0 - value))
60                        }
61                        Orientation::Vertical => {
62                            handle.top(Units::Stretch(value)).bottom(Units::Stretch(1.0 - value))
63                        }
64                    };
65                })
66                .bind(ratio, move |handle| {
67                    let ratio = ratio.get();
68                    match orientation {
69                        Orientation::Horizontal => handle.width(Units::Percentage(ratio * 100.0)),
70                        Orientation::Vertical => handle.height(Units::Percentage(ratio * 100.0)),
71                    };
72                })
73                .position_type(PositionType::Absolute);
74        })
75        .pointer_events(PointerEvents::Auto)
76        .orientation(orientation)
77        .role(Role::ScrollBar)
78    }
79
80    fn container_and_thumb_size(&self, cx: &mut EventContext) -> (f32, f32) {
81        let current = cx.current();
82        let child = cx.tree.get_child(current, 0).unwrap();
83        match &self.orientation {
84            Orientation::Horizontal => (cx.cache.get_width(current), cx.cache.get_width(child)),
85            Orientation::Vertical => (cx.cache.get_height(current), cx.cache.get_height(child)),
86        }
87    }
88
89    fn thumb_bounds(&self, cx: &mut EventContext) -> BoundingBox {
90        let child = cx.first_child();
91
92        cx.with_current(child, |cx| cx.bounds())
93    }
94
95    fn compute_new_value(&self, cx: &mut EventContext, physical_delta: f32, value_ref: f32) -> f32 {
96        // delta is moving within the negative space of the thumb: (1 - ratio) * container
97        let (size, thumb_size) = self.container_and_thumb_size(cx);
98        let negative_space = size - thumb_size;
99        if negative_space == 0.0 {
100            value_ref
101        } else {
102            // what percentage of negative space have we crossed?
103            let physical_delta =
104                if self.is_horizontal_rtl(cx) { -physical_delta } else { physical_delta };
105            let logical_delta = physical_delta / negative_space;
106            value_ref + logical_delta
107        }
108    }
109
110    fn change(&mut self, cx: &mut EventContext, new_val: f32) {
111        if let Some(callback) = &self.on_changing {
112            callback(cx, new_val.clamp(0.0, 1.0));
113        }
114    }
115}
116
117impl View for Scrollbar {
118    fn element(&self) -> Option<&'static str> {
119        Some("scrollbar")
120    }
121
122    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
123        event.map(|scrollbar_event, _| match scrollbar_event {
124            ScrollBarEvent::SetScrollToCursor(flag) => {
125                self.scroll_to_cursor = *flag;
126            }
127        });
128
129        event.map(|window_event, meta| {
130            let pos = match &self.orientation {
131                Orientation::Horizontal => cx.mouse.cursor_x,
132                Orientation::Vertical => cx.mouse.cursor_y,
133            };
134            match window_event {
135                WindowEvent::MouseDown(MouseButton::Left) => {
136                    if meta.target != cx.current() {
137                        self.reference_points = Some((pos, self.value.get()));
138                        cx.capture();
139                        cx.set_active(true);
140                        self.dragging = true;
141                        cx.with_current(Entity::root(), |cx| {
142                            cx.set_pointer_events(false);
143                        });
144                    } else if self.scroll_to_cursor {
145                        cx.capture();
146                        cx.set_active(true);
147                        self.dragging = true;
148                        cx.with_current(Entity::root(), |cx| {
149                            cx.set_pointer_events(false);
150                        });
151                        let thumb_bounds = self.thumb_bounds(cx);
152                        let bounds = cx.bounds();
153                        let sx = bounds.w - thumb_bounds.w;
154                        let sy = bounds.h - thumb_bounds.h;
155                        match self.orientation {
156                            Orientation::Horizontal => {
157                                let px = cx.mouse.cursor_x - cx.bounds().x - thumb_bounds.w / 2.0;
158                                let mut x = (px / sx).clamp(0.0, 1.0);
159                                if self.is_horizontal_rtl(cx) {
160                                    x = 1.0 - x;
161                                }
162                                if let Some(callback) = &self.on_changing {
163                                    (callback)(cx, x);
164                                }
165                            }
166
167                            Orientation::Vertical => {
168                                let py = cx.mouse.cursor_y - cx.bounds().y - thumb_bounds.h / 2.0;
169                                let y = (py / sy).clamp(0.0, 1.0);
170                                if let Some(callback) = &self.on_changing {
171                                    (callback)(cx, y);
172                                }
173                            }
174                        }
175                    } else {
176                        let (_, jump) = self.container_and_thumb_size(cx);
177                        // let (tx, ty, tw, th) = self.thumb_bounds(cx);
178                        let t = self.thumb_bounds(cx);
179                        let physical_delta = match &self.orientation {
180                            Orientation::Horizontal => {
181                                if cx.mouse.cursor_x < t.x {
182                                    -jump
183                                } else if cx.mouse.cursor_x >= t.x + t.w {
184                                    jump
185                                } else {
186                                    return;
187                                }
188                            }
189                            Orientation::Vertical => {
190                                if cx.mouse.cursor_y < t.y {
191                                    -jump
192                                } else if cx.mouse.cursor_y >= t.y + t.h {
193                                    jump
194                                } else {
195                                    return;
196                                }
197                            }
198                        };
199                        let changed = self.compute_new_value(cx, physical_delta, self.value.get());
200                        self.change(cx, changed);
201                    }
202                }
203
204                WindowEvent::MouseUp(MouseButton::Left) => {
205                    self.reference_points = None;
206                    cx.focus_with_visibility(false);
207                    cx.release();
208                    cx.set_active(false);
209                    self.dragging = false;
210                    cx.with_current(Entity::root(), |cx| {
211                        cx.set_pointer_events(true);
212                    });
213                }
214
215                WindowEvent::MouseMove(_, _) => {
216                    if self.dragging {
217                        if let Some((mouse_ref, value_ref)) = self.reference_points {
218                            let physical_delta = pos - mouse_ref;
219                            let changed = self.compute_new_value(cx, physical_delta, value_ref);
220                            self.change(cx, changed);
221                        } else if self.scroll_to_cursor {
222                            let thumb_bounds = self.thumb_bounds(cx);
223                            let bounds = cx.bounds();
224                            let sx = bounds.w - thumb_bounds.w;
225                            let sy = bounds.h - thumb_bounds.h;
226                            match self.orientation {
227                                Orientation::Horizontal => {
228                                    let px =
229                                        cx.mouse.cursor_x - cx.bounds().x - thumb_bounds.w / 2.0;
230                                    let mut x = (px / sx).clamp(0.0, 1.0);
231                                    if self.is_horizontal_rtl(cx) {
232                                        x = 1.0 - x;
233                                    }
234                                    if let Some(callback) = &self.on_changing {
235                                        (callback)(cx, x);
236                                    }
237                                }
238
239                                Orientation::Vertical => {
240                                    let py =
241                                        cx.mouse.cursor_y - cx.bounds().y - thumb_bounds.h / 2.0;
242                                    let y = (py / sy).clamp(0.0, 1.0);
243                                    if let Some(callback) = &self.on_changing {
244                                        (callback)(cx, y);
245                                    }
246                                }
247                            }
248                        }
249                    }
250                }
251
252                _ => {}
253            }
254        });
255    }
256}
257
258impl Handle<'_, Scrollbar> {
259    /// Sets whether the scrollbar should move to the cursor when pressed.
260    pub fn scroll_to_cursor(self, scroll_to_cursor: impl Res<bool> + 'static) -> Self {
261        let scroll_to_cursor = scroll_to_cursor.to_signal(self.cx);
262        self.bind(scroll_to_cursor, move |handle| {
263            handle.cx.emit(ScrollBarEvent::SetScrollToCursor(scroll_to_cursor.get()));
264        })
265    }
266}