conrod 0.30.0

An easy-to-use, immediate-mode, 2D GUI library
Documentation
//!
//!
//! A demonstration of designing a custom, third-party widget.
//!
//! In this case, we'll design a simple circular button.
//!
//! All of the custom widget design will occur within the `circular_button` module.
//!
//! We'll *use* our fancy circular button in the `main` function (below the circular_button module).
//!
//! Note that in this case, we use `piston_window` to draw our widget, however in practise you may
//! use any backend you wish.
//!
//! For more information, please see the `Widget` trait documentation.
//!


#[macro_use] extern crate conrod;
extern crate find_folder;
extern crate piston_window;
extern crate vecmath;


/// The module in which we'll implement our own custom circular button.
mod circular_button {
    use conrod::{
        CharacterCache,
        Circle,
        Color,
        Colorable,
        CommonBuilder,
        Dimensions,
        FontSize,
        IndexSlot,
        Labelable,
        Mouse,
        Point,
        Positionable,
        Scalar,
        Text,
        Theme,
        UpdateArgs,
        Widget,
        WidgetKind,
    };


    /// The type upon which we'll implement the `Widget` trait.
    pub struct CircularButton<'a, F> {
        /// An object that handles some of the dirty work of rendering a GUI. We don't
        /// really have to worry about it.
        common: CommonBuilder,
        /// Optional label string for the button.
        maybe_label: Option<&'a str>,
        /// Optional callback for when the button is pressed. If you want the button to
        /// do anything, this callback must exist.
        maybe_react: Option<F>,
        /// See the Style struct below.
        style: Style,
        /// Whether the button is currently enabled, i.e. whether it responds to
        /// user input.
        enabled: bool
    }

    /// Represents the unique styling for our CircularButton widget.
    #[derive(Clone, Debug, PartialEq)]
    pub struct Style {
        /// Color of the button.
        pub maybe_color: Option<Color>,
        /// Radius of the button.
        pub maybe_radius: Option<Scalar>,
        /// Color of the button's label.
        pub maybe_label_color: Option<Color>,
        /// Font size of the button's label.
        pub maybe_label_font_size: Option<u32>,
    }

    /// Represents the unique, cached state for our CircularButton widget.
    #[derive(Clone, Debug, PartialEq)]
    pub struct State {
        /// The current interaction state. See the Interaction enum below. See also
        /// get_new_interaction below, where we define all the logic for transitioning between
        /// interaction states.
        interaction: Interaction,
        /// An index to use for our **Circle** primitive graphics widget.
        circle_idx: IndexSlot,
        /// An index to use for our **Text** primitive graphics widget (for the label).
        text_idx: IndexSlot,
    }

    /// A `&'static str` that can be used to uniquely identify our widget type.
    pub const KIND: WidgetKind = "CircularButton";

    /// A type to keep track of interaction between updates.
    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    pub enum Interaction {
        Normal,
        Highlighted,
        Clicked,
    }

    impl Interaction {
        /// Alter the widget color depending on the current interaction.
        fn color(&self, color: Color) -> Color {
            match *self {
                /// The base color as defined in the Style struct, or a default provided
                /// by the current Theme if the Style has no color.
                Interaction::Normal => color,
                /// The Color object (from Elmesque) can calculate a highlighted version
                /// of itself. We don't have to use it, though. We could specify any color
                /// we want.
                Interaction::Highlighted => color.highlighted(),
                /// Ditto for clicked.
                Interaction::Clicked => color.clicked(),
            }
        }
    }

    /// Check the current interaction with the button. Takes into account whether the mouse is
    /// over the button and the previous interaction state.
    fn get_new_interaction(is_over: bool, prev: Interaction, mouse: Mouse) -> Interaction {
        use conrod::MouseButtonPosition::{Down, Up};
        use self::Interaction::{Normal, Highlighted, Clicked};
        match (is_over, prev, mouse.left.position) {
            // LMB is down over the button. But the button wasn't Highlighted last
            // update. This means the user clicked somewhere outside the button and
            // moved over the button holding LMB down. We do nothing in this case.
            (true,  Normal,  Down) => Normal,

            // LMB is down over the button. The button was either Highlighted or Clicked
            // last update. If it was highlighted before, that means the user clicked
            // just now, and we transition to the Clicked state. If it was clicked
            // before, that means the user is still holding LMB down from a previous
            // click, in which case the state remains Clicked.
            (true,  _,       Down) => Clicked,

            // LMB is up. The mouse is hovering over the button. Regardless of what the
            // state was last update, the state should definitely be Highlighted now.
            (true,  _,       Up)   => Highlighted,

            // LMB is down, the mouse is not over the button, but the previous state was
            // Clicked. That means the user clicked the button and then moved the mouse
            // outside the button while holding LMB down. The button stays Clicked.
            (false, Clicked, Down) => Clicked,

            // If none of the above applies, then nothing interesting is happening with
            // this button.
            _                      => Normal,
        }
    }

    /// Return whether or not a given point is over a circle at a given point on a
    /// Cartesian plane. We use this to determine whether the mouse is over the button.
    pub fn is_over_circ(circ_center: Point, mouse_point: Point, dim: Dimensions) -> bool {
        // Offset vector from the center of the circle to the mouse.
        let offset = ::vecmath::vec2_sub(mouse_point, circ_center);

        // If the length of the offset vector is less than or equal to the circle's
        // radius, then the mouse is inside the circle. We assume that dim is a square
        // bounding box around the circle, thus 2 * radius == dim[0] == dim[1].
        ::vecmath::vec2_len(offset) <= dim[0] / 2.0
    }

    impl<'a, F> CircularButton<'a, F> {
        /// Create a button context to be built upon.
        pub fn new() -> CircularButton<'a, F> {
            CircularButton {
                common: CommonBuilder::new(),
                maybe_react: None,
                maybe_label: None,
                style: Style::new(),
                enabled: true,
            }
        }

        /// Set the reaction for the Button. The reaction will be triggered upon release
        /// of the button. Like other Conrod configs, this returns self for chainability.
        pub fn react(mut self, reaction: F) -> Self {
            self.maybe_react = Some(reaction);
            self
        }

        /// If true, will allow user inputs.  If false, will disallow user inputs.  Like
        /// other Conrod configs, this returns self for chainability. Allow dead code
        /// because we never call this in the example.
        #[allow(dead_code)]
        pub fn enabled(mut self, flag: bool) -> Self {
            self.enabled = flag;
            self
        }
    }

    /// A custom Conrod widget must implement the Widget trait. See the **Widget** trait
    /// documentation for more details.
    impl<'a, F> Widget for CircularButton<'a, F>
        where F: FnMut()
    {
        /// The State struct that we defined above.
        type State = State;

        /// The Style struct that we defined above.
        type Style = Style;

        fn common(&self) -> &CommonBuilder {
            &self.common
        }

        fn common_mut(&mut self) -> &mut CommonBuilder {
            &mut self.common
        }

        fn unique_kind(&self) -> &'static str {
            KIND
        }

        fn init_state(&self) -> State {
            State {
                interaction: Interaction::Normal,
                circle_idx: IndexSlot::new(),
                text_idx: IndexSlot::new(),
            }
        }

        fn style(&self) -> Style {
            self.style.clone()
        }

        /// Update the state of the button. The state may or may not have changed since
        /// the last update. (E.g. it may have changed because the user moused over the
        /// button.) If the state has changed, return the new state. Else, return None.
        fn update<C: CharacterCache>(mut self, args: UpdateArgs<Self, C>) {
            let UpdateArgs { idx, state, rect, mut ui, style, .. } = args;
            let (xy, dim) = rect.xy_dim();
            let maybe_mouse = ui.input().maybe_mouse.map(|mouse| mouse.relative_to(xy));

            // Check whether or not a new interaction has occurred.
            let new_interaction = match (self.enabled, maybe_mouse) {
                (false, _) | (true, None) => Interaction::Normal,
                (true, Some(mouse)) => {
                    // Conrod does us a favor by transforming mouse.xy into this widget's
                    // local coordinate system. Because mouse.xy is in local coords,
                    // we must also pass the circle center in local coords. Thus we pass
                    // [0.0, 0.0] as the center.
                    //
                    // See above where we define is_over_circ.
                    let is_over = is_over_circ([0.0, 0.0], mouse.xy, dim);

                    // See above where we define get_new_interaction.
                    get_new_interaction(is_over, state.view().interaction, mouse)
                },
            };

            // If the mouse was released over the button, react. state.interaction is the
            // button's state as of a moment ago. new_interaction is the updated state as
            // of right now. So this if statement is saying: If the button was clicked a
            // moment ago, and it's now highlighted, then the button has been activated.
            if let (Interaction::Clicked, Interaction::Highlighted) =
                (state.view().interaction, new_interaction)
            {
                // Recall that our CircularButton struct includes maybe_react, which
                // stores either a reaction function or None. If maybe_react is Some, call
                // the function.
                if let Some(ref mut react) = self.maybe_react {
                    react();
                }
            }

            // Here we check to see whether or not our button should capture the mouse.
            //
            // Widgets can "capture" user input. If the button captures the mouse, then mouse
            // events will only be seen by the button. Other widgets will not see mouse events
            // until the button uncaptures the mouse.
            match (state.view().interaction, new_interaction) {
                // If the user has pressed the button we capture the mouse.
                (Interaction::Highlighted, Interaction::Clicked) => {
                    ui.capture_mouse();
                },
                // If the user releases the button, we uncapture the mouse.
                (Interaction::Clicked, Interaction::Highlighted) |
                (Interaction::Clicked, Interaction::Normal)      => {
                    ui.uncapture_mouse();
                },
                _ => (),
            }

            // Whenever we call `state.update` (as below), a flag is set within our `State`
            // indicating that there has been some mutation and that our widget requires a
            // re-draw. Thus, we only want to call `state.update` if there has been some change in
            // order to only re-draw when absolutely required.
            //
            // You can see how we do this below - we check if the state has changed before calling
            // `state.update`.

            // If the interaction has changed, set the new interaction.
            if state.view().interaction != new_interaction {
                state.update(|state| state.interaction = new_interaction);
            }

            // Finally, we'll describe how we want our widget drawn by simply instantiating the
            // necessary primitive graphics widgets.
            //
            // Conrod will automatically determine whether or not any changes have occurred and
            // whether or not any widgets need to be re-drawn.
            //
            // The primitive graphics widgets are special in that their unique state is used within
            // conrod's backend to do the actual drawing. This allows us to build up more complex
            // widgets by using these simple primitives with our familiar layout, coloring, etc
            // methods.
            //
            // If you notice that conrod is missing some sort of primitive graphics that you
            // require, please file an issue or open a PR so we can add it! :)

            // First, we'll draw the **Circle** with a radius that is half our given width.
            let radius = rect.w() / 2.0;
            let color = new_interaction.color(style.color(ui.theme()));
            let circle_idx = state.view().circle_idx.get(&mut ui);
            Circle::fill(radius)
                .middle_of(idx)
                .graphics_for(idx)
                .color(color)
                .set(circle_idx, &mut ui);

            // Now we'll instantiate our label using the **Text** widget.
            let label_color = style.label_color(ui.theme());
            let font_size = style.label_font_size(ui.theme());
            let text_idx = state.view().text_idx.get(&mut ui);
            if let Some(ref label) = self.maybe_label {
                Text::new(label)
                    .middle_of(idx)
                    .font_size(font_size)
                    .graphics_for(idx)
                    .color(label_color)
                    .set(text_idx, &mut ui);
            }
        }

    }

    impl Style {
        /// Construct the default Style.
        pub fn new() -> Style {
            Style {
                maybe_color: None,
                maybe_radius: None,
                maybe_label_color: None,
                maybe_label_font_size: None,
            }
        }

        /// Get the Color for an Element.
        pub fn color(&self, theme: &Theme) -> Color {
            self.maybe_color.or(theme.widget_style::<Self>(KIND).map(|default| {
                default.style.maybe_color.unwrap_or(theme.shape_color)
            })).unwrap_or(theme.shape_color)
        }

        /// Get the label Color for an Element.
        pub fn label_color(&self, theme: &Theme) -> Color {
            self.maybe_label_color.or(theme.widget_style::<Self>(KIND).map(|default| {
                default.style.maybe_label_color.unwrap_or(theme.label_color)
            })).unwrap_or(theme.label_color)
        }

        /// Get the label font size for an Element.
        pub fn label_font_size(&self, theme: &Theme) -> FontSize {
            self.maybe_label_font_size.or(theme.widget_style::<Self>(KIND).map(|default| {
                default.style.maybe_label_font_size.unwrap_or(theme.font_size_medium)
            })).unwrap_or(theme.font_size_medium)
        }
    }

    /// Provide the chainable color() configuration method.
    impl<'a, F> Colorable for CircularButton<'a, F> {
        fn color(mut self, color: Color) -> Self {
            self.style.maybe_color = Some(color);
            self
        }
    }

    /// Provide the chainable label(), label_color(), and label_font_size()
    /// configuration methods.
    impl<'a, F> Labelable<'a> for CircularButton<'a, F> {
        fn label(mut self, text: &'a str) -> Self {
            self.maybe_label = Some(text);
            self
        }
        fn label_color(mut self, color: Color) -> Self {
            self.style.maybe_label_color = Some(color);
            self
        }
        fn label_font_size(mut self, size: FontSize) -> Self {
            self.style.maybe_label_font_size = Some(size);
            self
        }
    }
}

fn main() {
    use piston_window::{EventLoop, Glyphs, PistonWindow, OpenGL, UpdateEvent, WindowSettings};
    use conrod::{Colorable, Labelable, Positionable, Sizeable, Widget};
    use circular_button::CircularButton;

    // PistonWindow has two type parameters, but the default type is
    // PistonWindow<T = (), W: Window = GlutinWindow>. To change the Piston backend,
    // specify a different type in the let binding, e.g.
    // let window: PistonWindow<(), Sdl2Window>.
    let window: PistonWindow = WindowSettings::new("Control Panel", [1200, 800])
        .opengl(OpenGL::V3_2)
        .exit_on_esc(true)
        .build().unwrap();

    // Conrod's main object.
    let mut ui = {
        // Load a font. `Glyphs` is provided to us via piston_window and gfx, though you may use
        // any type that implements `CharacterCache`.
        let assets = find_folder::Search::ParentsThenKids(3, 3)
            .for_folder("assets").unwrap();
        let font_path = assets.join("fonts/NotoSans/NotoSans-Regular.ttf");
        let glyph_cache = Glyphs::new(&font_path, window.factory.borrow().clone()).unwrap();
        conrod::Ui::new(glyph_cache, conrod::Theme::default())
    };

    for e in window.ups(60) {
        // Pass each `Event` to the `Ui`.
        ui.handle_event(e.event.as_ref().unwrap());

        e.update(|_| ui.set_widgets(|ui| {

            // Sets a color to clear the background with before the Ui draws our widget.
            conrod::Canvas::new().color(conrod::color::DARK_RED).set(BACKGROUND, ui);

            // Create an instance of our custom widget.
            CircularButton::new()
                .color(conrod::color::rgb(0.0, 0.3, 0.1))
                .middle_of(BACKGROUND)
                .w_h(256.0, 256.0)
                .label_color(conrod::color::WHITE)
                .label("Circular Button")
                // This is called when the user clicks the button.
                .react(|| println!("Click"))
                // Add the widget to the conrod::Ui. This schedules the widget it to be
                // drawn when we call Ui::draw.
                .set(CIRCLE_BUTTON, ui);
        }));

        // Draws the whole Ui (in this case, just our widget) whenever a change occurs.
        e.draw_2d(|c, g| ui.draw_if_changed(c, g))
    }
}


// The `widget_ids` macro is a easy, safe way of generating unique `WidgetId`s.
widget_ids! {
    // An ID for the background widget, upon which we'll place our custom button.
    BACKGROUND,
    // The WidgetId we'll use to plug our widget into the `Ui`.
    CIRCLE_BUTTON,
}