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