//! 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;
/// The module in which we'll implement our own custom circular button.
mod circular_button {
use conrod::{
self,
Backend,
Circle,
Color,
Colorable,
CommonBuilder,
Dimensions,
FontSize,
IndexSlot,
Labelable,
Point,
Positionable,
Text,
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
}
/// A `&'static str` that can be used to uniquely identify our widget type.
pub const KIND: WidgetKind = "CircularButton";
// We use the `widget_style!` macro to vastly simplify the definition and implementation of the
// widget's associated `Style` type. This generates both a `Style` struct, as well as an
// implementation that automatically retrieves defaults from the provided theme.
//
// See the documenation of the macro for a more details.
widget_style!{
KIND;
/// Represents the unique styling for our CircularButton widget.
style Style {
/// Color of the button.
- color: Color { theme.shape_color }
/// Color of the button's label.
- label_color: Color { theme.label_color }
/// Font size of the button's label.
- label_font_size: FontSize { theme.font_size_medium }
}
}
/// Represents the unique, cached state for our CircularButton widget.
#[derive(Clone, Debug, PartialEq)]
pub struct State {
/// 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,
}
/// 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 = conrod::utils::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].
let distance = (offset[0].powf(2.0) + offset[1].powf(2.0)).sqrt();
let radius = dim[0] / 2.0;
distance <= radius
}
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 using the `widget_style!` macro.
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 {
circle_idx: IndexSlot::new(),
text_idx: IndexSlot::new(),
}
}
fn style(&self) -> Style {
self.style.clone()
}
/// Update the state of the button by handling any input that has occurred since the last
/// update.
fn update<B: Backend>(self, args: UpdateArgs<Self, B>) {
let UpdateArgs { idx, state, rect, mut ui, style, .. } = args;
let color = {
let input = ui.widget_input(idx);
// If the button was clicked, call the user's `react` function.
if input.clicks().left().next().is_some() {
if let Some(mut react) = self.maybe_react {
react();
}
}
let color = style.color(ui.theme());
input.mouse()
.map(|mouse| {
if is_over_circ([0.0, 0.0], mouse.rel_xy(), rect.dim()) {
if mouse.buttons.left().is_down() {
color.clicked()
} else {
color.highlighted()
}
} else {
color
}
})
.unwrap_or(color)
};
// 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 circle_idx = state.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.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);
}
}
}
/// Provide the chainable color() configuration method.
impl<'a, F> Colorable for CircularButton<'a, F> {
fn color(mut self, color: Color) -> Self {
self.style.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.label_color = Some(color);
self
}
fn label_font_size(mut self, size: FontSize) -> Self {
self.style.label_font_size = Some(size);
self
}
}
}
pub fn main() {
use conrod::{self, Colorable, Labelable, Positionable, Sizeable, Widget};
use piston_window::{EventLoop, PistonWindow, OpenGL, UpdateEvent, WindowSettings};
use self::circular_button::CircularButton;
// Conrod is backend agnostic. Here, we define the `piston_window` backend to use for our `Ui`.
type Backend = (piston_window::G2dTexture<'static>, piston_window::Glyphs);
type Ui = conrod::Ui<Backend>;
// Change this to OpenGL::V2_1 if not working.
let opengl = OpenGL::V3_2;
// 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 mut window: PistonWindow = WindowSettings::new("Control Panel", [1200, 800])
.opengl(opengl)
.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 = piston_window::Glyphs::new(&font_path, window.factory.clone()).unwrap();
let theme = conrod::Theme::default();
Ui::new(glyph_cache, theme)
};
window.set_ups(60);
while let Some(e) = window.next() {
// Pass each `Event` to the `Ui`.
ui.handle_event(e.clone());
e.update(|_| ui.set_widgets(|ref mut 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);
// 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,
}
// 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.
window.draw_2d(&e, |c, g| ui.draw_if_changed(c, g))
}
}