conrod_core/widget/
scrollbar.rs

1//! A widget that allows for manually scrolling via dragging the mouse.
2
3use graph;
4use position::{Dimension, Range, Rect, Scalar};
5use std;
6use utils;
7use widget::scroll::{self, X, Y};
8use widget::{self, Widget};
9use {Color, Colorable, Positionable, Ui};
10
11/// A widget that allows for scrolling via dragging the mouse.
12#[derive(WidgetCommon_)]
13pub struct Scrollbar<A> {
14    #[conrod(common_builder)]
15    common: widget::CommonBuilder,
16    style: Style,
17    widget: widget::Id,
18    axis: std::marker::PhantomData<A>,
19}
20
21/// The axis that is scrolled by the `Scrollbar`.
22pub trait Axis: scroll::Axis + Sized {
23    /// The `Rect` for a scroll "track" with the given `thickness` for a container with the given
24    /// `Rect`.
25    fn track_rect(container: Rect, thickness: Scalar) -> Rect;
26    /// The `Rect` for a scroll handle given both `Range`s.
27    fn handle_rect(perpendicular_track_range: Range, handle_range: Range) -> Rect;
28    /// Retrieve the related `scroll::State` for the axis from a given widget container.
29    fn scroll_state(widget: &graph::Container) -> Option<&scroll::State<Self>>;
30    /// Determine a default *x* dimension for the scrollbar in the case that no specific width is
31    /// given.
32    fn default_x_dimension(scrollbar: &Scrollbar<Self>, ui: &Ui) -> Dimension;
33    /// Determine a default *y* dimension for the scrollbar in the case that no specific height is
34    /// given.
35    fn default_y_dimension(scrollbar: &Scrollbar<Self>, ui: &Ui) -> Dimension;
36    /// Convert a given `Scalar` along the axis into two dimensions.
37    fn to_2d(scalar: Scalar) -> [Scalar; 2];
38}
39
40/// Styling for the DropDownList, necessary for constructing its renderable Element.
41#[derive(Copy, Clone, Debug, Default, PartialEq, WidgetStyle_)]
42pub struct Style {
43    /// Color of the widget.
44    #[conrod(default = "theme.border_color")]
45    pub color: Option<Color>,
46    /// The "thickness" of the scrollbar's track and handle `Rect`s.
47    #[conrod(default = "10.0")]
48    pub thickness: Option<Scalar>,
49    /// When true, the `Scrollbar` will only be visible when:
50    ///
51    /// - The target scrollable widget is being scrolled.
52    /// - The mouse is over the scrollbar.
53    #[conrod(default = "false")]
54    pub auto_hide: Option<bool>,
55}
56
57widget_ids! {
58    struct Ids {
59        track,
60        handle,
61    }
62}
63
64/// The state of the `Scrollbar`.
65pub struct State {
66    ids: Ids,
67}
68
69impl<A> Scrollbar<A> {
70    /// Begin building a new scrollbar widget.
71    fn new(widget: widget::Id) -> Self {
72        Scrollbar {
73            common: widget::CommonBuilder::default(),
74            style: Style::default(),
75            widget: widget,
76            axis: std::marker::PhantomData,
77        }
78    }
79
80    /// By default, this is set to `false`.
81    ///
82    /// When false, the `Scrollbar` will always be visible.
83    ///
84    /// When true, the `Scrollbar` will only be visible when:
85    ///
86    /// - The target scrollable widget is actually scrollable and:
87    /// - The target scrollable widget is being scrolled.
88    /// - The scrollbar is capturing the mouse.
89    pub fn auto_hide(mut self, auto_hide: bool) -> Self {
90        self.style.auto_hide = Some(auto_hide);
91        self
92    }
93
94    /// Build the widget with the given `thickness`.
95    ///
96    /// This value sets the width of vertical scrollbars, or the height of horizontal scrollbars.
97    ///
98    /// By default, this is `10.0`.
99    pub fn thickness(mut self, thickness: Scalar) -> Self {
100        self.style.thickness = Some(thickness);
101        self
102    }
103}
104
105impl Scrollbar<X> {
106    /// Begin building a new scrollbar widget that scrolls along the *X* axis along the range of
107    /// the scrollable widget at the given Id.
108    pub fn x_axis(widget: widget::Id) -> Self {
109        Scrollbar::new(widget)
110            .align_middle_x_of(widget)
111            .align_bottom_of(widget)
112    }
113}
114
115impl Scrollbar<Y> {
116    /// Begin building a new scrollbar widget that scrolls along the *Y* axis along the range of
117    /// the scrollable widget at the given Id.
118    pub fn y_axis(widget: widget::Id) -> Self {
119        Scrollbar::new(widget)
120            .align_middle_y_of(widget)
121            .align_right_of(widget)
122    }
123}
124
125impl<A> Widget for Scrollbar<A>
126where
127    A: Axis,
128{
129    type State = State;
130    type Style = Style;
131    type Event = ();
132
133    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
134        State {
135            ids: Ids::new(id_gen),
136        }
137    }
138
139    fn style(&self) -> Self::Style {
140        self.style.clone()
141    }
142
143    fn default_x_dimension(&self, ui: &Ui) -> Dimension {
144        A::default_x_dimension(self, ui)
145    }
146
147    fn default_y_dimension(&self, ui: &Ui) -> Dimension {
148        A::default_y_dimension(self, ui)
149    }
150
151    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
152        let widget::UpdateArgs {
153            id,
154            state,
155            rect,
156            style,
157            ui,
158            ..
159        } = args;
160        let Scrollbar { widget, .. } = self;
161
162        // Only continue if the widget that we want to scroll has some scroll state.
163        let (offset_bounds, offset, scrollable_range_len, is_scrolling) =
164            match ui.widget_graph().widget(widget) {
165                Some(widget) => match A::scroll_state(widget) {
166                    Some(scroll) => (
167                        scroll.offset_bounds,
168                        scroll.offset,
169                        scroll.scrollable_range_len,
170                        scroll.is_scrolling,
171                    ),
172                    None => return,
173                },
174                None => return,
175            };
176
177        // Calculates the `Rect` for a scroll "handle" sitting on the given `track` with an offset
178        // and length that represents the given `Axis`' `state`.
179        let handle_rect = {
180            let perpendicular_track_range = A::perpendicular_range(rect);
181            let track_range = A::parallel_range(rect);
182            let track_len = track_range.len();
183            let len = if scrollable_range_len == 0.0 {
184                track_len
185            } else {
186                utils::clamp(
187                    track_len * (1.0 - offset_bounds.len() / scrollable_range_len),
188                    0.0,
189                    track_len,
190                )
191            };
192            let handle_range = Range::from_pos_and_len(0.0, len);
193            let pos = {
194                let pos_min = handle_range.align_start_of(track_range).middle();
195                let pos_max = handle_range.align_end_of(track_range).middle();
196                let pos_bounds = Range::new(pos_min, pos_max);
197                offset_bounds.map_value_to(offset, &pos_bounds)
198            };
199            let range = Range::from_pos_and_len(pos, len);
200            A::handle_rect(perpendicular_track_range, range)
201        };
202
203        let handle_range = A::parallel_range(handle_rect);
204        let handle_pos_range_len = || {
205            let track_range = A::parallel_range(rect);
206            let handle_pos_at_start = handle_range.align_start_of(track_range).middle();
207            let handle_pos_at_end = handle_range.align_end_of(track_range).middle();
208            let handle_pos_range = Range::new(handle_pos_at_start, handle_pos_at_end);
209            handle_pos_range.len()
210        };
211
212        // Sum all offset yielded by `Press` and `Drag` events.
213        let mut additional_offset = 0.0;
214        for widget_event in ui.widget_input(id).events() {
215            use event;
216            use input;
217
218            match widget_event {
219                // If the track is pressed, snap the handle to that part of the track and scroll
220                // accordingly with the handle's Range clamped to the track's Range.
221                event::Widget::Press(press) => {
222                    if let event::Button::Mouse(input::MouseButton::Left, xy) = press.button {
223                        let abs_xy = utils::vec2_add(xy, rect.xy());
224                        if rect.is_over(abs_xy) && !handle_rect.is_over(abs_xy) {
225                            let handle_pos_range_len = handle_pos_range_len();
226                            let offset_range_len = offset_bounds.len();
227                            let mouse_scalar = A::mouse_scalar(abs_xy);
228                            let pos_offset = mouse_scalar - handle_range.middle();
229                            let offset = utils::map_range(
230                                pos_offset,
231                                0.0,
232                                handle_pos_range_len,
233                                0.0,
234                                offset_range_len,
235                            );
236                            additional_offset += -offset;
237                        }
238                    }
239                }
240
241                // Check for the handle being dragged across the track.
242                event::Widget::Drag(drag) if drag.button == input::MouseButton::Left => {
243                    let handle_pos_range_len = handle_pos_range_len();
244                    let offset_range_len = offset_bounds.len();
245                    let from_scalar = A::mouse_scalar(drag.from);
246                    let to_scalar = A::mouse_scalar(drag.to);
247                    let pos_offset = to_scalar - from_scalar;
248                    let offset = utils::map_range(
249                        pos_offset,
250                        0.0,
251                        handle_pos_range_len,
252                        0.0,
253                        offset_range_len,
254                    );
255                    additional_offset += -offset;
256                }
257
258                _ => (),
259            }
260        }
261
262        // Scroll the given widget by the accumulated additional offset.
263        if additional_offset != 0.0 && !additional_offset.is_nan() {
264            ui.scroll_widget(widget, A::to_2d(additional_offset));
265        }
266
267        // Don't draw the scrollbar if auto_hide is on and there is no interaction.
268        let auto_hide = style.auto_hide(ui.theme());
269        let not_scrollable = offset_bounds.magnitude().is_sign_positive();
270        if auto_hide {
271            let no_offset = additional_offset == 0.0;
272            let no_mouse_interaction = ui.widget_input(id).mouse().is_none();
273            if not_scrollable || (!is_scrolling && no_offset && no_mouse_interaction) {
274                return;
275            }
276        }
277
278        let color = style.color(ui.theme());
279        let color = if not_scrollable {
280            color
281        } else {
282            ui.widget_input(id)
283                .mouse()
284                .map(|m| {
285                    if m.buttons.left().is_down() {
286                        color.clicked()
287                    } else {
288                        color.highlighted()
289                    }
290                })
291                .unwrap_or_else(|| color)
292        };
293
294        // The `Track` widget along which the handle will slide.
295        let track_color = color.alpha(0.25);
296        widget::Rectangle::fill(rect.dim())
297            .xy(rect.xy())
298            .color(track_color)
299            .graphics_for(id)
300            .parent(id)
301            .set(state.ids.track, ui);
302
303        // The `Handle` widget used as a graphical representation of the part of the scrollbar that
304        // can be dragged over the track.
305        widget::Rectangle::fill(handle_rect.dim())
306            .xy(handle_rect.xy())
307            .color(color)
308            .graphics_for(id)
309            .parent(id)
310            .set(state.ids.handle, ui);
311    }
312}
313
314impl Axis for X {
315    fn track_rect(container: Rect, thickness: Scalar) -> Rect {
316        let h = thickness;
317        let w = container.w();
318        let x = container.x();
319        Rect::from_xy_dim([x, 0.0], [w, h]).align_bottom_of(container)
320    }
321
322    fn handle_rect(perpendicular_track_range: Range, handle_range: Range) -> Rect {
323        Rect {
324            x: handle_range,
325            y: perpendicular_track_range,
326        }
327    }
328
329    fn scroll_state(widget: &graph::Container) -> Option<&scroll::State<Self>> {
330        widget.maybe_x_scroll_state.as_ref()
331    }
332
333    fn default_x_dimension(scrollbar: &Scrollbar<Self>, _ui: &Ui) -> Dimension {
334        Dimension::Of(scrollbar.widget, None)
335    }
336
337    fn default_y_dimension(scrollbar: &Scrollbar<Self>, ui: &Ui) -> Dimension {
338        Dimension::Absolute(scrollbar.style.thickness(&ui.theme))
339    }
340
341    fn to_2d(scalar: Scalar) -> [Scalar; 2] {
342        [scalar, 0.0]
343    }
344}
345
346impl Axis for Y {
347    fn track_rect(container: Rect, thickness: Scalar) -> Rect {
348        let w = thickness;
349        let h = container.h();
350        let y = container.y();
351        Rect::from_xy_dim([0.0, y], [w, h]).align_right_of(container)
352    }
353
354    fn handle_rect(perpendicular_track_range: Range, handle_range: Range) -> Rect {
355        Rect {
356            x: perpendicular_track_range,
357            y: handle_range,
358        }
359    }
360
361    fn scroll_state(widget: &graph::Container) -> Option<&scroll::State<Self>> {
362        widget.maybe_y_scroll_state.as_ref()
363    }
364
365    fn default_x_dimension(scrollbar: &Scrollbar<Self>, ui: &Ui) -> Dimension {
366        Dimension::Absolute(scrollbar.style.thickness(&ui.theme))
367    }
368
369    fn default_y_dimension(scrollbar: &Scrollbar<Self>, _ui: &Ui) -> Dimension {
370        Dimension::Of(scrollbar.widget, None)
371    }
372
373    fn to_2d(scalar: Scalar) -> [Scalar; 2] {
374        [0.0, scalar]
375    }
376}
377
378impl<A> Colorable for Scrollbar<A> {
379    builder_method!(color { style.color = Some(Color) });
380}