conrod_core/widget/
button.rs

1//! The `Button` widget and related items.
2
3use image;
4use position::{self, Align, Rect, Scalar};
5use text;
6use widget;
7use {Borderable, Color, Colorable, FontSize, Labelable, Positionable, Sizeable, UiCell, Widget};
8
9/// A pressable button widget whose reaction is triggered upon release.
10#[derive(Clone, WidgetCommon_)]
11pub struct Button<'a, S> {
12    #[conrod(common_builder)]
13    common: widget::CommonBuilder,
14    maybe_label: Option<&'a str>,
15    /// Whether the `Button` is a `Flat` color or an `Image`.
16    pub show: S,
17    /// Unique styling parameters for the Button.
18    pub style: Style,
19    /// Whether or not user input is enabled.
20    enabled: bool,
21}
22
23/// Unique styling for the Button.
24#[derive(Copy, Clone, Debug, Default, PartialEq, WidgetStyle_)]
25pub struct Style {
26    /// Color of the Button's pressable area.
27    #[conrod(default = "theme.shape_color")]
28    pub color: Option<Color>,
29    /// Width of the border surrounding the button
30    #[conrod(default = "theme.border_width")]
31    pub border: Option<Scalar>,
32    /// The color of the border.
33    #[conrod(default = "theme.border_color")]
34    pub border_color: Option<Color>,
35    /// The color of the Button's label.
36    #[conrod(default = "theme.label_color")]
37    pub label_color: Option<Color>,
38    /// The font size of the Button's label.
39    #[conrod(default = "theme.font_size_medium")]
40    pub label_font_size: Option<FontSize>,
41    /// The ID of the font used to display the label.
42    #[conrod(default = "theme.font_id")]
43    pub label_font_id: Option<Option<text::font::Id>>,
44    /// The label's typographic alignment over the *x* axis.
45    #[conrod(default = "text::Justify::Center")]
46    pub label_justify: Option<text::Justify>,
47    /// The position of the title bar's `Label` widget over the *x* axis.
48    #[conrod(default = "position::Relative::Align(Align::Middle)")]
49    pub label_x: Option<position::Relative>,
50    /// The position of the title bar's `Label` widget over the *y* axis.
51    #[conrod(default = "position::Relative::Align(Align::Middle)")]
52    pub label_y: Option<position::Relative>,
53}
54
55widget_ids! {
56    /// Identifiers for a "flat" button.
57    #[allow(missing_docs, missing_copy_implementations)]
58    pub struct FlatIds {
59        rectangle,
60        label,
61    }
62}
63
64widget_ids! {
65    /// Identifiers for an image button.
66    #[allow(missing_docs, missing_copy_implementations)]
67    pub struct ImageIds {
68        image,
69        label,
70    }
71}
72
73/// The `Button` simply displays a flat color.
74#[derive(Copy, Clone, Default, PartialEq, Debug)]
75pub struct Flat {
76    /// Allows specifying a color to use when the mouse hovers over the button.
77    ///
78    /// By default, this is `color.highlighted()` where `color` is the button's regular color.
79    pub hover_color: Option<Color>,
80    /// Allows specifying a color to use when the mouse presses the button.
81    ///
82    /// By default, this is `color.clicked()` where `color` is the button's regular color.
83    pub press_color: Option<Color>,
84}
85
86/// The `Button` displays an `Image` on top.
87#[derive(Copy, Clone)]
88pub struct Image {
89    /// The id of the `Image` to be used.
90    pub image_id: image::Id,
91    /// The image displayed when the mouse hovers over the button.
92    pub hover_image_id: Option<image::Id>,
93    /// The image displayed when the mouse has captured and is pressing the button.
94    pub press_image_id: Option<image::Id>,
95    /// If `Some`, maps the image's luminance to this `Color`.
96    pub color: ImageColor,
97    /// The rectangular area of the original source image that should be displayed.
98    pub src_rect: Option<Rect>,
99}
100
101/// The coloring of the `Image`.
102#[derive(Copy, Clone, Debug)]
103pub enum ImageColor {
104    /// The image's luminance will be mapped to this color.
105    Normal(Color),
106    /// The image's luminance will be mapped to this color.
107    ///
108    /// The color will change slightly upon interaction to provide visual feedback.
109    WithFeedback(Color),
110    /// The image's regular color will be used.
111    None,
112}
113
114#[derive(Copy, Clone)]
115enum Interaction {
116    Idle,
117    Hover,
118    Press,
119}
120
121/// The `Event` type yielded by the `Button` widget.
122///
123/// Represents the number of times that the `Button` has been clicked with the left mouse button
124/// since the last update.
125#[derive(Clone, Debug)]
126#[allow(missing_copy_implementations)]
127pub struct TimesClicked(pub u16);
128
129impl TimesClicked {
130    /// `true` if the `Button` was clicked one or more times.
131    pub fn was_clicked(self) -> bool {
132        self.0 > 0
133    }
134}
135
136impl Iterator for TimesClicked {
137    type Item = ();
138    fn next(&mut self) -> Option<Self::Item> {
139        if self.0 > 0 {
140            self.0 -= 1;
141            Some(())
142        } else {
143            None
144        }
145    }
146}
147
148impl<'a> Button<'a, Image> {
149    /// Begin building a button displaying the given `Image` on top.
150    pub fn image(image_id: image::Id) -> Self {
151        let image = Image {
152            image_id: image_id,
153            hover_image_id: None,
154            press_image_id: None,
155            src_rect: None,
156            color: ImageColor::None,
157        };
158        Self::new_internal(image)
159    }
160
161    /// The rectangular area of the image that we wish to display.
162    ///
163    /// If this method is not called, the entire image will be used.
164    pub fn source_rectangle(mut self, rect: Rect) -> Self {
165        self.show.src_rect = Some(rect);
166        self
167    }
168
169    /// Map the `Image`'s luminance to the given color.
170    pub fn image_color(mut self, color: Color) -> Self {
171        self.show.color = ImageColor::Normal(color);
172        self
173    }
174
175    /// Map the `Image`'s luminance to the given color.
176    ///
177    /// The color will change slightly when the button is highlighted or clicked to give the user
178    /// some visual feedback.
179    pub fn image_color_with_feedback(mut self, color: Color) -> Self {
180        self.show.color = ImageColor::WithFeedback(color);
181        self
182    }
183
184    /// The image displayed while the mouse hovers over the `Button`.
185    pub fn hover_image(mut self, id: image::Id) -> Self {
186        self.show.hover_image_id = Some(id);
187        self
188    }
189
190    /// The image displayed while the `Button` is pressed.
191    pub fn press_image(mut self, id: image::Id) -> Self {
192        self.show.press_image_id = Some(id);
193        self
194    }
195}
196
197impl<'a> Button<'a, Flat> {
198    /// Begin building a flat-colored `Button` widget.
199    pub fn new() -> Self {
200        Self::new_internal(Flat::default())
201    }
202
203    /// Override the default button style
204    pub fn with_style(mut self, s: Style) -> Self {
205        self.style = s;
206        self
207    }
208
209    /// Specify a color to use when the mouse hovers over the button.
210    ///
211    /// By default, this is `color.highlighted()` where `color` is the button's regular color.
212    pub fn hover_color(mut self, color: Color) -> Self {
213        self.show.hover_color = Some(color);
214        self
215    }
216
217    /// Specify a color to use when the mouse presses the button.
218    ///
219    /// By default, this is `color.clicked()` where `color` is the button's regular color.
220    pub fn press_color(mut self, color: Color) -> Self {
221        self.show.press_color = Some(color);
222        self
223    }
224}
225
226impl<'a, S> Button<'a, S> {
227    /// Create a button context to be built upon.
228    fn new_internal(show: S) -> Self {
229        Button {
230            common: widget::CommonBuilder::default(),
231            show: show,
232            maybe_label: None,
233            style: Style::default(),
234            enabled: true,
235        }
236    }
237
238    /// Specify the font used for displaying the label.
239    pub fn label_font_id(mut self, font_id: text::font::Id) -> Self {
240        self.style.label_font_id = Some(Some(font_id));
241        self
242    }
243
244    /// Align the label to the left of the `Button`'s surface.
245    pub fn left_justify_label(mut self) -> Self {
246        self.style.label_justify = Some(text::Justify::Left);
247        self
248    }
249
250    /// Align the label to the mid-left of the `Button`'s surface.
251    ///
252    /// This is the default label alignment.
253    pub fn center_justify_label(mut self) -> Self {
254        self.style.label_justify = Some(text::Justify::Center);
255        self
256    }
257
258    /// Align the label to the mid-left of the `Button`'s surface.
259    pub fn right_justify_label(mut self) -> Self {
260        self.style.label_justify = Some(text::Justify::Right);
261        self
262    }
263
264    /// Specify the label's position relatively to `Button` along the *x* axis.
265    pub fn label_x(mut self, x: position::Relative) -> Self {
266        self.style.label_x = Some(x);
267        self
268    }
269
270    /// Specify the label's position relatively to `Button` along the *y* axis.
271    pub fn label_y(mut self, y: position::Relative) -> Self {
272        self.style.label_y = Some(y);
273        self
274    }
275
276    builder_methods! {
277        pub enabled { enabled = bool }
278    }
279}
280
281impl<'a> Widget for Button<'a, Flat> {
282    type State = FlatIds;
283    type Style = Style;
284    type Event = TimesClicked;
285
286    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
287        FlatIds::new(id_gen)
288    }
289
290    fn style(&self) -> Style {
291        self.style.clone()
292    }
293
294    /// Update the state of the Button.
295    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
296        let widget::UpdateArgs {
297            id,
298            state,
299            style,
300            rect,
301            ui,
302            ..
303        } = args;
304        let Button {
305            show, maybe_label, ..
306        } = self;
307
308        let (interaction, times_triggered) = interaction_and_times_triggered(id, ui);
309        let color = match interaction {
310            Interaction::Idle => style.color(&ui.theme),
311            Interaction::Hover => show
312                .hover_color
313                .unwrap_or_else(|| style.color(&ui.theme).highlighted()),
314            Interaction::Press => show
315                .press_color
316                .unwrap_or_else(|| style.color(&ui.theme).clicked()),
317        };
318
319        bordered_rectangle(id, state.rectangle, rect, color, style, ui);
320
321        // Label widget.
322        if let Some(l) = maybe_label {
323            label(id, state.label, l, style, ui);
324        }
325
326        TimesClicked(times_triggered)
327    }
328}
329
330impl<'a> Widget for Button<'a, Image> {
331    type State = ImageIds;
332    type Style = Style;
333    type Event = TimesClicked;
334
335    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
336        ImageIds::new(id_gen)
337    }
338
339    fn style(&self) -> Style {
340        self.style.clone()
341    }
342
343    /// Update the state of the Button.
344    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
345        let widget::UpdateArgs {
346            id,
347            state,
348            style,
349            rect,
350            ui,
351            ..
352        } = args;
353        let Button {
354            show, maybe_label, ..
355        } = self;
356
357        let (interaction, times_triggered) = interaction_and_times_triggered(id, ui);
358
359        // Instantiate the image.
360        let Image {
361            image_id,
362            press_image_id,
363            hover_image_id,
364            src_rect,
365            color,
366        } = show;
367
368        // Determine the correct image to display.
369        let image_id = match interaction {
370            Interaction::Idle => image_id,
371            Interaction::Hover => hover_image_id.unwrap_or(image_id),
372            Interaction::Press => press_image_id.or(hover_image_id).unwrap_or(image_id),
373        };
374
375        let (x, y, w, h) = rect.x_y_w_h();
376        let mut image = widget::Image::new(image_id)
377            .x_y(x, y)
378            .w_h(w, h)
379            .parent(id)
380            .graphics_for(id);
381        image.src_rect = src_rect;
382        image.style.maybe_color = match color {
383            ImageColor::Normal(color) => Some(Some(color)),
384            ImageColor::WithFeedback(color) => ui
385                .widget_input(id)
386                .mouse()
387                .map(|mouse| {
388                    if mouse.buttons.left().is_down() {
389                        Some(color.clicked())
390                    } else {
391                        Some(color.highlighted())
392                    }
393                })
394                .or(Some(Some(color))),
395            ImageColor::None => None,
396        };
397        image.set(state.image, ui);
398
399        if let Some(s) = maybe_label {
400            label(id, state.label, s, style, ui);
401        }
402
403        TimesClicked(times_triggered)
404    }
405}
406
407fn interaction_and_times_triggered(button_id: widget::Id, ui: &UiCell) -> (Interaction, u16) {
408    let input = ui.widget_input(button_id);
409    let mouse_interaction = input.mouse().map_or(Interaction::Idle, |mouse| {
410        if mouse.buttons.left().is_down() {
411            if ui.global_input().current.widget_under_mouse == Some(button_id) {
412                Interaction::Press
413            } else {
414                Interaction::Idle
415            }
416        } else {
417            Interaction::Hover
418        }
419    });
420    let interaction = match mouse_interaction {
421        Interaction::Idle | Interaction::Hover => {
422            let is_touch_press = ui
423                .global_input()
424                .current
425                .touch
426                .values()
427                .any(|t| t.start.widget == Some(button_id) && t.widget == Some(button_id));
428            if is_touch_press {
429                Interaction::Press
430            } else {
431                mouse_interaction
432            }
433        }
434        Interaction::Press => Interaction::Press,
435    };
436    let times_triggered = (input.clicks().left().count() + input.taps().count()) as u16;
437    (interaction, times_triggered)
438}
439
440fn bordered_rectangle(
441    button_id: widget::Id,
442    rectangle_id: widget::Id,
443    rect: Rect,
444    color: Color,
445    style: &Style,
446    ui: &mut UiCell,
447) {
448    // BorderedRectangle widget.
449    let dim = rect.dim();
450    let border = style.border(&ui.theme);
451    let border_color = style.border_color(&ui.theme);
452    widget::BorderedRectangle::new(dim)
453        .middle_of(button_id)
454        .graphics_for(button_id)
455        .color(color)
456        .border(border)
457        .border_color(border_color)
458        .set(rectangle_id, ui);
459}
460
461fn label(button_id: widget::Id, label_id: widget::Id, label: &str, style: &Style, ui: &mut UiCell) {
462    let color = style.label_color(&ui.theme);
463    let font_size = style.label_font_size(&ui.theme);
464    let x = style.label_x(&ui.theme);
465    let y = style.label_y(&ui.theme);
466    let justify = style.label_justify(&ui.theme);
467    let font_id = style.label_font_id(&ui.theme).or(ui.fonts.ids().next());
468    widget::Text::new(label)
469        .and_then(font_id, widget::Text::font_id)
470        .x_position_relative_to(button_id, x)
471        .y_position_relative_to(button_id, y)
472        .justify(justify)
473        .parent(button_id)
474        .graphics_for(button_id)
475        .color(color)
476        .font_size(font_size)
477        .set(label_id, ui);
478}
479
480impl<'a, S> Colorable for Button<'a, S> {
481    builder_method!(color { style.color = Some(Color) });
482}
483
484impl<'a, S> Borderable for Button<'a, S> {
485    builder_methods! {
486        border { style.border = Some(Scalar) }
487        border_color { style.border_color = Some(Color) }
488    }
489}
490
491impl<'a, S> Labelable<'a> for Button<'a, S> {
492    builder_methods! {
493        label { maybe_label = Some(&'a str) }
494        label_color { style.label_color = Some(Color) }
495        label_font_size { style.label_font_size = Some(FontSize) }
496    }
497}