conrod_core/widget/
slider.rs

1//! A widget for selecting a single value along some linear range.
2
3use num::{Float, NumCast, ToPrimitive};
4use position::{Padding, Range, Rect, Scalar};
5use text;
6use widget;
7use widget::triangles::Triangle;
8use {Borderable, Color, Colorable, FontSize, Labelable, Positionable, Widget};
9
10/// Linear value selection.
11///
12/// If the slider's width is greater than it's height, it will automatically become a horizontal
13/// slider, otherwise it will be a vertical slider.
14///
15/// Its reaction is triggered if the value is updated or if the mouse button is released while
16/// the cursor is above the rectangle.
17#[derive(WidgetCommon_)]
18pub struct Slider<'a, T> {
19    #[conrod(common_builder)]
20    common: widget::CommonBuilder,
21    value: T,
22    min: T,
23    max: T,
24    /// The amount in which the slider's display should be skewed.
25    ///
26    /// Higher skew amounts (above 1.0) will weight lower values.
27    ///
28    /// Lower skew amounts (below 1.0) will weight heigher values.
29    ///
30    /// All skew amounts should be greater than 0.0.
31    pub skew: f32,
32    maybe_label: Option<&'a str>,
33    style: Style,
34    /// Whether or not user input is enabled for the Slider.
35    pub enabled: bool,
36}
37
38/// Graphical styling unique to the Slider widget.
39#[derive(Copy, Clone, Debug, Default, PartialEq, WidgetStyle_)]
40pub struct Style {
41    /// The color of the slidable rectangle.
42    #[conrod(default = "theme.shape_color")]
43    pub color: Option<Color>,
44    /// The length of the border around the edges of the slidable rectangle.
45    #[conrod(default = "theme.border_width")]
46    pub border: Option<Scalar>,
47    /// The color of the Slider's border.
48    #[conrod(default = "theme.border_color")]
49    pub border_color: Option<Color>,
50    /// The color of the Slider's label.
51    #[conrod(default = "theme.label_color")]
52    pub label_color: Option<Color>,
53    /// The font-size for the Slider's label.
54    #[conrod(default = "theme.font_size_medium")]
55    pub label_font_size: Option<FontSize>,
56    /// The ID of the font used to display the label.
57    #[conrod(default = "theme.font_id")]
58    pub label_font_id: Option<Option<text::font::Id>>,
59}
60
61widget_ids! {
62    struct Ids {
63        triangles,
64        label,
65    }
66}
67
68/// Represents the state of the Slider widget.
69pub struct State {
70    ids: Ids,
71}
72
73impl<'a, T> Slider<'a, T> {
74    /// Construct a new Slider widget.
75    pub fn new(value: T, min: T, max: T) -> Self {
76        Slider {
77            common: widget::CommonBuilder::default(),
78            style: Style::default(),
79            value: value,
80            min: min,
81            max: max,
82            skew: 1.0,
83            maybe_label: None,
84            enabled: true,
85        }
86    }
87
88    /// Specify the font used for displaying the label.
89    pub fn label_font_id(mut self, font_id: text::font::Id) -> Self {
90        self.style.label_font_id = Some(Some(font_id));
91        self
92    }
93
94    builder_methods! {
95        pub skew { skew = f32 }
96        pub enabled { enabled = bool }
97    }
98}
99
100impl<'a, T> Widget for Slider<'a, T>
101where
102    T: Float + NumCast + ToPrimitive,
103{
104    type State = State;
105    type Style = Style;
106    type Event = Option<T>;
107
108    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
109        State {
110            ids: Ids::new(id_gen),
111        }
112    }
113
114    fn style(&self) -> Self::Style {
115        self.style.clone()
116    }
117
118    fn kid_area(&self, args: widget::KidAreaArgs<Self>) -> widget::KidArea {
119        const LABEL_PADDING: Scalar = 10.0;
120        widget::KidArea {
121            rect: args.rect,
122            pad: Padding {
123                x: Range::new(LABEL_PADDING, LABEL_PADDING),
124                y: Range::new(LABEL_PADDING, LABEL_PADDING),
125            },
126        }
127    }
128
129    /// Update the state of the Slider.
130    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
131        use utils::{clamp, map_range, value_from_perc};
132
133        let widget::UpdateArgs {
134            id,
135            state,
136            rect,
137            style,
138            ui,
139            ..
140        } = args;
141        let Slider {
142            value,
143            min,
144            max,
145            skew,
146            maybe_label,
147            ..
148        } = self;
149
150        let is_horizontal = rect.w() > rect.h();
151        let border = style.border(ui.theme());
152        let inner_rect = rect.pad(border);
153
154        let new_value = if let Some(mouse) = ui.widget_input(id).mouse() {
155            if mouse.buttons.left().is_down() {
156                let mouse_abs_xy = mouse.abs_xy();
157                if is_horizontal {
158                    // Horizontal.
159                    let inner_w = inner_rect.w();
160                    let slider_w = mouse_abs_xy[0] - inner_rect.x.start;
161                    let perc = clamp(slider_w, 0.0, inner_w) / inner_w;
162                    let skewed_perc = (perc).powf(skew as f64);
163                    let w_perc = skewed_perc;
164                    value_from_perc(w_perc as f32, min, max)
165                } else {
166                    // Vertical.
167                    let inner_h = inner_rect.h();
168                    let slider_h = mouse_abs_xy[1] - inner_rect.y.start;
169                    let perc = clamp(slider_h, 0.0, inner_h) / inner_h;
170                    let skewed_perc = (perc).powf(skew as f64);
171                    let h_perc = skewed_perc;
172                    value_from_perc(h_perc as f32, min, max)
173                }
174            } else {
175                value
176            }
177        } else {
178            value
179        };
180
181        // The **Rectangle** for the border.
182        let interaction_color = |ui: &::ui::UiCell, color: Color| {
183            ui.widget_input(id)
184                .mouse()
185                .map(|mouse| {
186                    if mouse.buttons.left().is_down() {
187                        color.clicked()
188                    } else {
189                        color.highlighted()
190                    }
191                })
192                .unwrap_or(color)
193        };
194
195        // The **Rectangle** for the adjustable slider.
196        let value_perc = map_range(new_value, min, max, 0.0, 1.0);
197        let unskewed_perc = value_perc.powf(1.0 / skew as f64);
198        let (slider_rect, blank_rect) = if is_horizontal {
199            let left = inner_rect.x.start;
200            let slider = map_range(unskewed_perc, 0.0, 1.0, left, inner_rect.x.end);
201            let right = inner_rect.x.end;
202            let y = inner_rect.y;
203            let slider_rect = Rect {
204                x: Range::new(left, slider),
205                y: y,
206            };
207            let blank_rect = Rect {
208                x: Range::new(slider, right),
209                y: y,
210            };
211            (slider_rect, blank_rect)
212        } else {
213            let bottom = inner_rect.y.start;
214            let slider = map_range(unskewed_perc, 0.0, 1.0, bottom, inner_rect.y.end);
215            let top = inner_rect.y.end;
216            let x = inner_rect.x;
217            let slider_rect = Rect {
218                x: x,
219                y: Range::new(bottom, slider),
220            };
221            let blank_rect = Rect {
222                x: x,
223                y: Range::new(slider, top),
224            };
225            (slider_rect, blank_rect)
226        };
227
228        let border_triangles = widget::bordered_rectangle::border_triangles(rect, border);
229        let (a, b) = widget::rectangle::triangles(slider_rect);
230        let slider_triangles = [a, b];
231        let (a, b) = widget::rectangle::triangles(blank_rect);
232        let blank_triangles = [a, b];
233
234        let border_color = interaction_color(ui, style.border_color(ui.theme())).to_rgb();
235        let color = interaction_color(ui, style.color(ui.theme())).to_rgb();
236
237        // The border and blank triangles are the same color.
238        let border_colored_triangles = border_triangles
239            .as_ref()
240            .into_iter()
241            .flat_map(|tris| tris.iter().cloned())
242            .chain(blank_triangles.iter().cloned())
243            .map(|Triangle(ps)| {
244                Triangle([
245                    (ps[0], border_color),
246                    (ps[1], border_color),
247                    (ps[2], border_color),
248                ])
249            });
250
251        // Color the slider triangles.
252        let slider_colored_triangles = slider_triangles
253            .iter()
254            .cloned()
255            .map(|Triangle(ps)| Triangle([(ps[0], color), (ps[1], color), (ps[2], color)]));
256
257        // Chain all triangles together into a single iterator.
258        let triangles = border_colored_triangles.chain(slider_colored_triangles);
259
260        widget::Triangles::multi_color(triangles)
261            .with_bounding_rect(rect)
262            .graphics_for(id)
263            .parent(id)
264            .set(state.ids.triangles, ui);
265
266        // The **Text** for the slider's label (if it has one).
267        if let Some(label) = maybe_label {
268            let label_color = style.label_color(ui.theme());
269            let font_size = style.label_font_size(ui.theme());
270            let font_id = style.label_font_id(&ui.theme).or(ui.fonts.ids().next());
271            //const TEXT_PADDING: f64 = 10.0;
272            widget::Text::new(label)
273                .and_then(font_id, widget::Text::font_id)
274                .and(|text| {
275                    if is_horizontal {
276                        text.mid_left_of(id)
277                    } else {
278                        text.mid_bottom_of(id)
279                    }
280                })
281                .graphics_for(id)
282                .color(label_color)
283                .font_size(font_size)
284                .set(state.ids.label, ui);
285        }
286
287        // If the value has just changed, return the new value.
288        if value != new_value {
289            Some(new_value)
290        } else {
291            None
292        }
293    }
294}
295
296impl<'a, T> Colorable for Slider<'a, T> {
297    builder_method!(color { style.color = Some(Color) });
298}
299
300impl<'a, T> Borderable for Slider<'a, T> {
301    builder_methods! {
302        border { style.border = Some(Scalar) }
303        border_color { style.border_color = Some(Color) }
304    }
305}
306
307impl<'a, T> Labelable<'a> for Slider<'a, T> {
308    builder_methods! {
309        label { maybe_label = Some(&'a str) }
310        label_color { style.label_color = Some(Color) }
311        label_font_size { style.label_font_size = Some(FontSize) }
312    }
313}