vizia_core/views/
scrollbar.rs1use crate::context::TreeProps;
2use crate::prelude::*;
3
4pub 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 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 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 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 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 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}