makepad_widget/
scrollbar.rs

1use makepad_render::*;
2use crate::widgetstyle::*;
3
4#[derive(Clone)]
5pub struct ScrollBar {
6    
7    pub bg: Quad,
8    pub bar_size: f32,
9    pub min_handle_size: f32, //minimum size of the handle in pixels
10    pub axis: Axis,
11    pub animator: Animator,
12    pub use_vertical_finger_scroll: bool,
13    pub _visible: bool,
14    pub smoothing: Option<f32>,
15    pub _bg_area: Area,
16    pub _bar_side_margin: f32,
17    pub _view_area: Area,
18    pub _view_total: f32, // the total view area
19    pub _view_visible: f32, // the visible view area
20    pub _scroll_size: f32, // the size of the scrollbar
21    pub _scroll_pos: f32, // scrolling position non normalised
22    
23    pub _scroll_target: f32,
24    pub _scroll_delta: f32,
25    
26    pub _drag_point: Option<f32>, // the point in pixels where we are dragging
27}
28
29#[derive(Clone, PartialEq, Debug)]
30pub enum ScrollBarEvent {
31    None,
32    Scroll {scroll_pos: f32, view_total: f32, view_visible: f32},
33    ScrollDone
34}
35
36impl ScrollBar {
37    pub fn proto(cx: &mut Cx) -> Self {
38        Self {
39            bar_size: 12.0,
40            min_handle_size: 30.0,
41            smoothing: None,
42            
43            axis: Axis::Horizontal,
44            animator: Animator::default(),
45            bg: Quad {
46                z: 10.,
47                ..Quad::proto(cx)
48            },
49            use_vertical_finger_scroll: false,
50            _visible: false,
51            
52            _view_area: Area::Empty,
53            _view_total: 0.0,
54            _view_visible: 0.0,
55            _bar_side_margin: 6.0,
56            _scroll_size: 0.0,
57            _scroll_pos: 0.0,
58            
59            _scroll_target: 0.0,
60            _scroll_delta: 0.0,
61            
62            _drag_point: None,
63            _bg_area: Area::Empty,
64        }
65    }
66    
67    pub fn bar_size() -> FloatId {uid!()}
68    pub fn instance_is_vertical() -> InstanceFloat {uid!()}
69    pub fn instance_norm_handle() -> InstanceFloat {uid!()}
70    pub fn instance_norm_scroll() -> InstanceFloat {uid!()}
71    
72    pub fn anim_default() -> AnimId {uid!()}
73    pub fn anim_over() -> AnimId {uid!()}
74    pub fn anim_down() -> AnimId {uid!()}
75    pub fn shader_bg() -> ShaderId {uid!()}
76    
77    pub fn style(cx: &mut Cx, opt: &StyleOptions) {
78        Self::bar_size().set(cx, 12. * opt.scale.powf(0.5));
79        
80        Self::anim_default().set(cx, Anim::new(Play::Cut {duration: 0.5}, vec![
81            Track::color(Quad::instance_color(), Ease::Lin, vec![(1.0, Theme::color_scrollbar_base().get(cx))])
82        ]));
83        
84        Self::anim_over().set(cx, Anim::new(Play::Cut {duration: 0.05}, vec![
85            Track::color(Quad::instance_color(), Ease::Lin, vec![(1.0, Theme::color_scrollbar_over().get(cx))])
86        ]));
87        
88        Self::anim_down().set(cx, Anim::new(Play::Cut {duration: 0.05}, vec![
89            Track::color(Quad::instance_color(), Ease::Lin, vec![(1.0, Theme::color_scrollbar_down().get(cx))])
90        ]));
91        
92        Self::shader_bg().set(cx, Quad::def_quad_shader().compose(shader_ast!({
93            
94            let is_vertical: Self::instance_is_vertical();
95            let norm_handle: Self::instance_norm_handle();
96            let norm_scroll: Self::instance_norm_scroll();
97            
98            const border_radius: float = 1.5;
99            
100            fn pixel() -> vec4 {
101                df_viewport(pos * vec2(w, h));
102                if is_vertical > 0.5 {
103                    df_box(1., h * norm_scroll, w * 0.5, h * norm_handle, border_radius);
104                }
105                else {
106                    df_box(w * norm_scroll, 1., w * norm_handle, h * 0.5, border_radius);
107                }
108                return df_fill_keep(color);
109            }
110        })));
111    }
112    // reads back normalized scroll position info
113    pub fn get_normalized_scroll_pos(&self) -> (f32, f32) {
114        // computed handle size normalized
115        let vy = self._view_visible / self._view_total;
116        if !self._visible {
117            return (0.0, 0.0);
118        }
119        let norm_handle = vy.max(self.min_handle_size / self._scroll_size);
120        let norm_scroll = (1. - norm_handle) * ((self._scroll_pos / self._view_total) / (1. - vy));
121        return (norm_scroll, norm_handle)
122    }
123    
124    // sets the scroll pos from finger position
125    pub fn set_scroll_pos_from_finger(&mut self, cx: &mut Cx, finger: f32) -> ScrollBarEvent {
126        let vy = self._view_visible / self._view_total;
127        let norm_handle = vy.max(self.min_handle_size / self._scroll_size);
128        
129        let new_scroll_pos = (
130            (self._view_total * (1. - vy) * (finger / self._scroll_size)) / (1. - norm_handle)
131        ).max(0.).min(self._view_total - self._view_visible);
132        
133        // lets snap new_scroll_pos
134        let changed = self._scroll_pos != new_scroll_pos;
135        self._scroll_pos = new_scroll_pos;
136        self._scroll_target = new_scroll_pos;
137        if changed {
138            self.update_shader_scroll_pos(cx);
139            return self.make_scroll_event();
140        }
141        return ScrollBarEvent::None;
142    }
143    
144    // writes the norm_scroll value into the shader
145    pub fn update_shader_scroll_pos(&mut self, cx: &mut Cx) {
146        let (norm_scroll, _) = self.get_normalized_scroll_pos();
147        self._bg_area.write_float(cx, Self::instance_norm_scroll(), norm_scroll);
148    }
149    
150    // turns scroll_pos into an event on this.event
151    pub fn make_scroll_event(&mut self) -> ScrollBarEvent {
152        ScrollBarEvent::Scroll {
153            scroll_pos: self._scroll_pos,
154            view_total: self._view_total,
155            view_visible: self._view_visible
156        }
157    }
158    
159    pub fn move_towards_scroll_target(&mut self, cx: &mut Cx) -> bool {
160        
161        if self.smoothing.is_none() {
162            return false;
163        }
164        if (self._scroll_target - self._scroll_pos).abs() < 0.01 {
165            return false
166        }
167        if self._scroll_pos > self._scroll_target { // go back
168            self._scroll_pos = self._scroll_pos + (self.smoothing.unwrap() * self._scroll_delta).min(-1.);
169            if self._scroll_pos <= self._scroll_target { // hit the target
170                self._scroll_pos = self._scroll_target;
171                self.update_shader_scroll_pos(cx);
172                return false;
173            }
174        }
175        else { // go forward
176            self._scroll_pos = self._scroll_pos + (self.smoothing.unwrap() * self._scroll_delta).max(1.);
177            if self._scroll_pos > self._scroll_target { // hit the target
178                self._scroll_pos = self._scroll_target;
179                self.update_shader_scroll_pos(cx);
180                return false;
181            }
182        }
183        self.update_shader_scroll_pos(cx);
184        true
185    }
186    
187    pub fn get_scroll_pos(&self) -> f32 {
188        return self._scroll_pos;
189    }
190    
191    pub fn set_scroll_pos(&mut self, cx: &mut Cx, scroll_pos: f32) -> bool {
192        // clamp scroll_pos to
193        let scroll_pos = scroll_pos.min(self._view_total - self._view_visible).max(0.);
194        
195        if self._scroll_pos != scroll_pos {
196            self._scroll_pos = scroll_pos;
197            self._scroll_target = scroll_pos;
198            self.update_shader_scroll_pos(cx);
199            cx.next_frame(self._bg_area);
200            return true
201        };
202        return false
203    }
204    
205    pub fn get_scroll_target(&mut self) -> f32 {
206        return self._scroll_target
207    }
208    
209    pub fn set_scroll_view_total(&mut self, _cx: &mut Cx, view_total: f32) {
210        self._view_total = view_total;
211    }
212    
213    pub fn get_scroll_view_total(&self) -> f32 {
214        return self._view_total;
215    }
216    
217    pub fn set_scroll_target(&mut self, cx: &mut Cx, scroll_pos_target: f32) -> bool {
218        // clamp scroll_pos to
219        
220        let new_target = scroll_pos_target.min(self._view_total - self._view_visible).max(0.);
221        if self._scroll_target != new_target {
222            self._scroll_target = new_target;
223            self._scroll_delta = new_target - self._scroll_pos;
224            cx.next_frame(self._bg_area);
225            return true
226        };
227        return false
228    }
229    
230    pub fn scroll_into_view(&mut self, cx: &mut Cx, pos: f32, size: f32) {
231        if pos < self._scroll_pos { // scroll up
232            let scroll_to = pos;
233            if self.smoothing.is_none() {
234                self.set_scroll_pos(cx, scroll_to);
235            }
236            else {
237                self.set_scroll_target(cx, scroll_to);
238            }
239        }
240        else if pos + size > self._scroll_pos + self._view_visible { // scroll down
241            let scroll_to = (pos + size) - self._view_visible;
242            if pos + size > self._view_total { // resize _view_total if need be
243                self._view_total = pos + size;
244            }
245            if self.smoothing.is_none() {
246                self.set_scroll_pos(cx, scroll_to);
247            }
248            else {
249                self.set_scroll_target(cx, scroll_to);
250            }
251        }
252    }
253    
254    pub fn handle_scroll_bar(&mut self, cx: &mut Cx, event: &mut Event) -> ScrollBarEvent {
255        // lets check if our view-area gets a mouse-scroll.
256        match event {
257            Event::FingerScroll(fe) => if !fe.handled {
258                let rect = self._view_area.get_rect(cx);
259                if rect.contains(fe.abs.x, fe.abs.y) { // handle mousewheel
260                    // we should scroll in either x or y
261                    let scroll = match self.axis {
262                        Axis::Horizontal => if self.use_vertical_finger_scroll {fe.scroll.y}else {fe.scroll.x},
263                        Axis::Vertical => fe.scroll.y
264                    };
265                    if !self.smoothing.is_none() {
266                        let scroll_pos_target = self.get_scroll_target();
267                        if self.set_scroll_target(cx, scroll_pos_target + scroll) {
268                            fe.handled = true;
269                        };
270                        self.move_towards_scroll_target(cx); // take the first step now
271                        return self.make_scroll_event();
272                    }
273                    else {
274                        let scroll_pos = self.get_scroll_pos();
275                        if self.set_scroll_pos(cx, scroll_pos + scroll) {
276                            fe.handled = true;
277                        }
278                        return self.make_scroll_event();
279                    }
280                }
281            },
282            
283            _ => ()
284        };
285        if self._visible {
286            match event.hits(cx, self._bg_area, HitOpt::default()) {
287                Event::Animate(ae) => {
288                    self.animator.calc_area(cx, self._bg_area, ae.time);
289                },
290                Event::AnimEnded(_) => self.animator.end(),
291                Event::Frame(_ae) => {
292                    
293                    if self.move_towards_scroll_target(cx) {
294                        cx.next_frame(self._bg_area);
295                    }
296                    return self.make_scroll_event()
297                },
298                Event::FingerDown(fe) => {
299                    self.animator.play_anim(cx, Self::anim_down().get(cx));
300                    let rel = match self.axis {
301                        Axis::Horizontal => fe.rel.x,
302                        Axis::Vertical => fe.rel.y
303                    };
304                    cx.set_down_mouse_cursor(MouseCursor::Default);
305                    let (norm_scroll, norm_handle) = self.get_normalized_scroll_pos();
306                    let bar_start = norm_scroll * self._scroll_size;
307                    let bar_size = norm_handle * self._scroll_size;
308                    if rel < bar_start || rel > bar_start + bar_size { // clicked outside
309                        self._drag_point = Some(bar_size * 0.5);
310                        return self.set_scroll_pos_from_finger(cx, rel - self._drag_point.unwrap());
311                    }
312                    else { // clicked on
313                        self._drag_point = Some(rel - bar_start); // store the drag delta
314                    }
315                },
316                Event::FingerHover(fe) => {
317                    if self._drag_point.is_none() {
318                        cx.set_hover_mouse_cursor(MouseCursor::Default);
319                        match fe.hover_state {
320                            HoverState::In => {
321                                self.animator.play_anim(cx, Self::anim_over().get(cx));
322                            },
323                            HoverState::Out => {
324                                self.animator.play_anim(cx, Self::anim_default().get(cx));
325                            },
326                            _ => ()
327                        }
328                    }
329                },
330                Event::FingerUp(fe) => {
331                    self._drag_point = None;
332                    if fe.is_over {
333                        if !fe.is_touch {
334                            self.animator.play_anim(cx, Self::anim_over().get(cx));
335                        }
336                        else {
337                            self.animator.play_anim(cx, Self::anim_default().get(cx));
338                        }
339                    }
340                    else {
341                        self.animator.play_anim(cx, Self::anim_default().get(cx));
342                    }
343                    return ScrollBarEvent::ScrollDone;
344                },
345                Event::FingerMove(fe) => {
346                    // helper called by event code to scroll from a finger
347                    if self._drag_point.is_none() {
348                        // state should never occur.
349                        //println!("Invalid state in scrollbar, fingerMove whilst drag_point is none")
350                    }
351                    else {
352                        match self.axis {
353                            Axis::Horizontal => {
354                                return self.set_scroll_pos_from_finger(cx, fe.rel.x - self._drag_point.unwrap());
355                            },
356                            Axis::Vertical => {
357                                return self.set_scroll_pos_from_finger(cx, fe.rel.y - self._drag_point.unwrap());
358                            }
359                        }
360                    }
361                },
362                _ => ()
363            };
364        }
365        
366        ScrollBarEvent::None
367    }
368    
369    pub fn draw_scroll_bar(&mut self, cx: &mut Cx, axis: Axis, view_area: Area, view_rect: Rect, view_total: Vec2) -> f32 {
370        self.animator.init(cx, | cx | Self::anim_default().get(cx));
371        self.bg.shader = Self::shader_bg().get(cx);
372        self.bg.color = self.animator.last_color(cx, Quad::instance_color());
373        self._bg_area = Area::Empty;
374        self._view_area = view_area;
375        self.axis = axis;
376        self.bar_size = Self::bar_size().get(cx);
377        match self.axis {
378            Axis::Horizontal => {
379                self._visible = view_total.x > view_rect.w + 0.1;
380                self._scroll_size = if view_total.y > view_rect.h + 0.1 {
381                    view_rect.w - self.bar_size
382                }
383                else {
384                    view_rect.w
385                } -self._bar_side_margin * 2.;
386                self._view_total = view_total.x;
387                self._view_visible = view_rect.w;
388                self._scroll_pos = self._scroll_pos.min(self._view_total - self._view_visible).max(0.);
389                
390                if self._visible {
391                    let bg_inst = self.bg.draw_quad_rel(
392                        cx,
393                        Rect {
394                            x: self._bar_side_margin,
395                            y: view_rect.h - self.bar_size,
396                            w: self._scroll_size,
397                            h: self.bar_size,
398                        }
399                    );
400                    bg_inst.set_do_scroll(cx, false, false);
401                    //is_vertical
402                    let (norm_scroll, norm_handle) = self.get_normalized_scroll_pos();
403                    bg_inst.push_float(cx, 0.0);
404                    bg_inst.push_float(cx, norm_handle);
405                    bg_inst.push_float(cx, norm_scroll);
406                    self._bg_area = bg_inst.into();
407                }
408            },
409            Axis::Vertical => {
410                // compute if we need a horizontal one
411                self._visible = view_total.y > view_rect.h + 0.1;
412                self._scroll_size = if view_total.x > view_rect.w + 0.1 {
413                    view_rect.h - self.bar_size
414                }
415                else {
416                    view_rect.h
417                } -self._bar_side_margin * 2.;
418                self._view_total = view_total.y;
419                self._view_visible = view_rect.h;
420                self._scroll_pos = self._scroll_pos.min(self._view_total - self._view_visible).max(0.);
421                if self._visible {
422                    let bg_inst = self.bg.draw_quad_rel(
423                        cx,
424                        Rect {
425                            x: view_rect.w - self.bar_size,
426                            y: self._bar_side_margin,
427                            w: self.bar_size,
428                            h: self._scroll_size
429                        }
430                    );
431                    bg_inst.set_do_scroll(cx, false, false);
432                    //is_vertical
433                    let (norm_scroll, norm_handle) = self.get_normalized_scroll_pos();
434                    bg_inst.push_float(cx, 1.0);
435                    bg_inst.push_float(cx, norm_handle);
436                    bg_inst.push_float(cx, norm_scroll);
437                    self._bg_area = bg_inst.into();
438                }
439            }
440        }
441        
442        // push the var added to the sb shader
443        if self._visible {
444            self.animator.set_area(cx, self._bg_area); // if our area changed, update animation
445        }
446        
447        // see if we need to clamp
448        let clamped_pos = self._scroll_pos.min(self._view_total - self._view_visible).max(0.);
449        if clamped_pos != self._scroll_pos {
450            self._scroll_pos = clamped_pos;
451            self._scroll_target = clamped_pos;
452            // ok so this means we 'scrolled' this can give a problem for virtual viewport widgets
453            cx.next_frame(self._bg_area);
454        }
455        
456        self._scroll_pos
457    }
458}