diff --git a/Cargo.toml b/Cargo.toml index 6515a0cc2..03335f9cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,9 @@ git = "https://github.com/PistonDevelopers/opengl_graphics.git" [dependencies.clock_ticks] git = "https://github.com/tomaka/clock_ticks" +[dependencies.color] +git = "https://github.com/PistonDevelopers/color/" + [dependencies.elmesque] git = "https://github.com/mitchmindtree/elmesque" diff --git a/examples/all_widgets.rs b/examples/all_widgets.rs index ea00d30aa..ad0bbbb92 100644 --- a/examples/all_widgets.rs +++ b/examples/all_widgets.rs @@ -1,3 +1,15 @@ +//! +//! +//! A demonstration of all widgets available in Conrod. +//! +//! +//! Don't be put off by the number of method calls, they are only for demonstration and almost all +//! of them are optional. Conrod supports `Theme`s, so if you don't give it an argument, it will +//! check the current `Theme` within the `Ui` and retrieve defaults from there. +//! +//! + + extern crate piston; extern crate conrod; extern crate graphics; @@ -10,7 +22,6 @@ use conrod::{ Button, Color, Colorable, - Drawable, DropDownList, EnvelopeEditor, Frameable, @@ -20,11 +31,12 @@ use conrod::{ Point, Positionable, Slider, - Shapeable, + Sizeable, TextBox, Theme, Toggle, Ui, + UiId, WidgetMatrix, XYPad, }; @@ -35,7 +47,6 @@ use piston::event::*; use piston::window::{WindowSettings, Size}; use glutin_window::GlutinWindow; use std::path::Path; -use vecmath::vec2_add; /// This struct holds all of the variables used to demonstrate /// application data being passed through the widgets. If some @@ -52,7 +63,7 @@ struct DemoApp { toggle_label: String, /// The number of pixels between the left side of the window /// and the title. - title_padding: f64, + title_pad: f64, /// The height of the vertical sliders (we will play with this /// using a number_dialer). v_slider_height: f64, @@ -81,7 +92,7 @@ impl DemoApp { bg_color: rgb(0.2, 0.35, 0.45), show_button: false, toggle_label: "OFF".to_string(), - title_padding: 50.0, + title_pad: -374.0, v_slider_height: 230.0, frame_width: 1.0, bool_matrix: vec![ vec![true, true, true, true, true, true, true, true], @@ -98,7 +109,7 @@ impl DemoApp { "Green".to_string(), "Blue".to_string()], selected_idx: None, - circle_pos: [700.0, 200.0], + circle_pos: [560.0, 310.0], envelopes: vec![(vec![ [0.0, 0.0], [0.1, 17000.0], [0.25, 8000.0], @@ -119,7 +130,7 @@ fn main() { opengl, WindowSettings::new( "Hello Conrod".to_string(), - Size { width: 1180, height: 580 } + Size { width: 1100, height: 550 } ) .exit_on_esc(true) .samples(4) @@ -143,32 +154,36 @@ fn main() { } } + + /// Draw the User Interface. -fn draw_ui<'a>(gl: &mut GlGraphics, - ui: &mut Ui>, - demo: &mut DemoApp) { +fn draw_ui<'a>(gl: &mut GlGraphics, ui: &mut Ui>, demo: &mut DemoApp) { // Draw the background. Background::new().color(demo.bg_color).draw(ui, gl); + // Calculate x and y coords for title (temporary until `Canvas`es are implemented, see #380). + let title_x = demo.title_pad - (ui.win_w / 2.0) + 185.0; + let title_y = (ui.win_h / 2.0) - 50.0; + // Label example. Label::new("Widget Demonstration") - .position(demo.title_padding, 30.0) + .xy(title_x, title_y) .size(32) .color(demo.bg_color.plain_contrast()) - .draw(ui, gl); + .set(TITLE, ui); if demo.show_button { // Button widget example button(UiId). - Button::new(0) - .dimensions(90.0, 60.0) - .position(50.0, 115.0) + Button::new() + .dimensions(200.0, 50.0) + .xy(140.0 - (ui.win_w / 2.0), title_y - 70.0) .rgb(0.4, 0.75, 0.6) .frame(demo.frame_width) .label("PRESS") .callback(|| demo.bg_color = color::random()) - .draw(ui, gl); + .set(BUTTON, ui) } @@ -176,7 +191,7 @@ fn draw_ui<'a>(gl: &mut GlGraphics, else { // Create the label for the slider. - let pad = demo.title_padding as i16; + let pad = demo.title_pad as i16; let pad_string = pad.to_string(); let label = { let mut text = "Padding: ".to_string(); @@ -185,25 +200,28 @@ fn draw_ui<'a>(gl: &mut GlGraphics, }; // Slider widget example slider(UiId, value, min, max). - Slider::new(1, pad as f32, 10.0, 910.0) + Slider::new(pad as f32, 30.0, 700.0) .dimensions(200.0, 50.0) - .position(50.0, 115.0) + .xy(140.0 - (ui.win_w / 2.0), title_y - 70.0) .rgb(0.5, 0.3, 0.6) .frame(demo.frame_width) .label(&label) .label_color(white()) - .callback(|new_pad: f32| demo.title_padding = new_pad as f64) - .draw(ui, gl); + .callback(|new_pad: f32| demo.title_pad = new_pad as f64) + .set(TITLE_PAD_SLIDER, ui); } // Clone the label toggle to be drawn. let label = demo.toggle_label.clone(); + // Keep track of the currently shown widget. + let shown_widget = if demo.show_button { BUTTON } else { TITLE_PAD_SLIDER }; + // Toggle widget example toggle(UiId, value). - Toggle::new(2, demo.show_button) + Toggle::new(demo.show_button) .dimensions(75.0, 75.0) - .down(20.0, ui) + .down(20.0) .rgb(0.6, 0.25, 0.75) .frame(demo.frame_width) .label(&label) @@ -215,7 +233,7 @@ fn draw_ui<'a>(gl: &mut GlGraphics, false => "OFF".to_string() } }) - .draw(ui, gl); + .set(TOGGLE, ui); // Let's draw a slider for each color element. // 0 => red, 1 => green, 2 => blue. @@ -240,9 +258,9 @@ fn draw_ui<'a>(gl: &mut GlGraphics, if label.len() > 4 { label.truncate(4); } // Slider widget examples. slider(UiId, value, min, max) - Slider::new(3 + i as u64, value, 0.0, 1.0) + if i == 0 { Slider::new(value, 0.0, 1.0).down(25.0) } + else { Slider::new(value, 0.0, 1.0).right(20.0) } .dimensions(40.0, demo.v_slider_height) - .position(50.0 + i as f64 * 60.0, 300.0) .color(color) .frame(demo.frame_width) .label(&label) @@ -252,41 +270,41 @@ fn draw_ui<'a>(gl: &mut GlGraphics, 1 => demo.bg_color.set_green(color), _ => demo.bg_color.set_blue(color), }) - .draw(ui, gl); + .set(COLOR_SLIDER + i, ui); } // Number Dialer widget example. number_dialer(UiId, value, min, max, precision) - NumberDialer::new(6, demo.v_slider_height, 25.0, 250.0, 1u8) + NumberDialer::new(demo.v_slider_height, 25.0, 250.0, 1u8) .dimensions(260.0, 60.0) - .position(300.0, 115.0) + .right_from(shown_widget, 30.0) .color(demo.bg_color.invert()) .frame(demo.frame_width) .label("Height (px)") .label_color(demo.bg_color.invert().plain_contrast()) .callback(|new_height| demo.v_slider_height = new_height) - .draw(ui, gl); + .set(SLIDER_HEIGHT, ui); // Number Dialer widget example. number_dialer(UiId, value, min, max, precision) - NumberDialer::new(7, demo.frame_width, 0.0, 15.0, 2u8) + NumberDialer::new(demo.frame_width, 0.0, 15.0, 2u8) .dimensions(260.0, 60.0) - .down(20.0, ui) + .down(20.0) .color(demo.bg_color.invert().plain_contrast()) .frame(demo.frame_width) .frame_color(demo.bg_color.plain_contrast()) .label("Frame Width (px)") .label_color(demo.bg_color.plain_contrast()) .callback(|new_width| demo.frame_width = new_width) - .draw(ui, gl); + .set(FRAME_WIDTH, ui); // A demonstration using widget_matrix to easily draw // a matrix of any kind of widget. let (cols, rows) = (8, 8); WidgetMatrix::new(cols, rows) + .down(20.0) .dimensions(260.0, 260.0) // matrix width and height. - .position(300.0, 270.0) // matrix position. - .each_widget(|num, col, row, pos, dim| { // This is called for every widget. + .each_widget(ui, |ui, num, col, row, pos, dim| { // This is called for every widget. // Color effect for fun. let (r, g, b, a) = ( @@ -298,13 +316,13 @@ fn draw_ui<'a>(gl: &mut GlGraphics, // Now draw the widgets with the given callback. let val = demo.bool_matrix[col][row]; - Toggle::new(8 + num as u64, val) + Toggle::new(val) .dim(dim) .point(pos) .rgba(r, g, b, a) .frame(demo.frame_width) .callback(|new_val: bool| demo.bool_matrix[col][row] = new_val) - .draw(ui, gl); + .set(TOGGLE_MATRIX + num, ui); }); @@ -329,9 +347,9 @@ fn draw_ui<'a>(gl: &mut GlGraphics, gl); // A demonstration using drop_down_list. - DropDownList::new(75, &mut demo.ddl_colors, &mut demo.selected_idx) + DropDownList::new(&mut demo.ddl_colors, &mut demo.selected_idx) .dimensions(150.0, 40.0) - .right_from(6u64, 50.0, ui) // Position right from widget 6 by 50 pixels. + .right_from(SLIDER_HEIGHT, 30.0) // Position right from widget 6 by 50 pixels. .color(ddl_color) .frame(demo.frame_width) .frame_color(ddl_color.plain_contrast()) @@ -340,75 +358,79 @@ fn draw_ui<'a>(gl: &mut GlGraphics, .callback(|selected_idx: &mut Option, new_idx, _string| { *selected_idx = Some(new_idx) }) - .draw(ui, gl); + .set(COLOR_SELECT, ui); // Draw an xy_pad. - XYPad::new(76, // UiId - demo.circle_pos[0], 745.0, 595.0, // x range. + XYPad::new(demo.circle_pos[0], 550.0, 700.0, // x range. demo.circle_pos[1], 320.0, 170.0) // y range. .dimensions(150.0, 150.0) - .down(225.0, ui) + .right_from(TOGGLE_MATRIX + 63, 30.0) + .align_bottom() // Align to the bottom of the last TOGGLE_MATRIX element. .color(ddl_color) .frame(demo.frame_width) .frame_color(white()) .label("Circle Position") .label_color(ddl_color.plain_contrast().alpha(0.5)) .line_width(2.0) - .value_font_size(18u32) .callback(|new_x, new_y| { demo.circle_pos[0] = new_x; demo.circle_pos[1] = new_y; }) - .draw(ui, gl); + .set(CIRCLE_POSITION, ui); - // Let's use the widget matrix to draw one column of two envelope_editors, - // each with its own text_box. - let (cols, rows) = (1, 2); - WidgetMatrix::new(cols, rows) - .position(810.0, 115.0) - .dimensions(320.0, 425.0) - .each_widget(|num, _col, _row, pos, dim| { // This is called for every widget. - use conrod::Drawable; - - let &mut (ref mut env, ref mut text) = &mut demo.envelopes[num]; - let text_box_height = dim[1] / 4.0; - let env_editor_height = dim[1] - text_box_height; - let env_editor_pos = vec2_add(pos, [0.0, text_box_height]); - let env_label_color = demo.bg_color.invert().plain_contrast().alpha(0.5); - let env_y_max = match num { 0 => 20000.0, _ => 1.0 }; - let tbox_uiid = 77 + (num * 2) as u64; - let env_uiid = tbox_uiid + 1u64; - let env_skew_y = match num { 0 => 3.0, _ => 1.0 }; - - // Draw a TextBox. text_box(UiId, &mut String, FontSize) - TextBox::new(tbox_uiid, text) - .font_size(20) - .dimensions(dim[0], text_box_height - 10.0) - .point(pos) - .frame(demo.frame_width) - .frame_color(demo.bg_color.invert().plain_contrast()) - .color(demo.bg_color.invert()) - .callback(|_string: &mut String|{}) - .draw(ui, gl); - - // Draw an EnvelopeEditor. - EnvelopeEditor::new(env_uiid, // UiId - env, // vector of `E: EnvelopePoint`s. - 0.0, 1.0, 0.0, env_y_max) // x_min, x_max, y_min, y_max. - .dimensions(dim[0], env_editor_height - 10.0) - .point(env_editor_pos) - .skew_y(env_skew_y) - .color(demo.bg_color.invert()) - .frame(demo.frame_width) - .frame_color(demo.bg_color.invert().plain_contrast()) - .label(&text) - .label_color(env_label_color) - .point_radius(6.0) - .line_width(2.0) - .callback(|_points: &mut Vec, _idx: usize|{}) - .draw(ui, gl); + // Draw two TextBox and EnvelopeEditor pairs to the right of the DropDownList flowing downward. + for i in 0..2 { - }); // End of matrix widget callback. + let &mut (ref mut env, ref mut text) = &mut demo.envelopes[i]; + + // Draw a TextBox. text_box(&mut String, FontSize) + if i == 0 { TextBox::new(text).right_from(COLOR_SELECT, 30.0) } + else { TextBox::new(text) } + .font_size(20) + .dimensions(320.0, 40.0) + .frame(demo.frame_width) + .frame_color(demo.bg_color.invert().plain_contrast()) + .color(demo.bg_color.invert()) + .callback(|_string: &mut String|{}) + .set(ENVELOPE_EDITOR + (i * 2), ui); + + let env_y_max = match i { 0 => 20_000.0, _ => 1.0 }; + let env_skew_y = match i { 0 => 3.0, _ => 1.0 }; + + // Draw an EnvelopeEditor. (Vec, x_min, x_max, y_min, y_max). + EnvelopeEditor::new(env, 0.0, 1.0, 0.0, env_y_max) + .down(10.0) + .dimensions(320.0, 150.0) + .skew_y(env_skew_y) + .color(demo.bg_color.invert()) + .frame(demo.frame_width) + .frame_color(demo.bg_color.invert().plain_contrast()) + .label(&text) + .label_color(demo.bg_color.invert().plain_contrast().alpha(0.5)) + .point_radius(6.0) + .line_width(2.0) + .callback(|_points: &mut Vec, _idx: usize|{}) + .set(ENVELOPE_EDITOR + (i * 2) + 1, ui); + + } + + // Draw our Ui! + ui.draw(gl); } + +// As each widget must have it's own unique identifier, it can be useful to create these +// identifiers relative to each other in order to make refactoring easier. +const TITLE: UiId = 0; +const BUTTON: UiId = TITLE + 1; +const TITLE_PAD_SLIDER: UiId = BUTTON + 1; +const TOGGLE: UiId = TITLE_PAD_SLIDER + 1; +const COLOR_SLIDER: UiId = TOGGLE + 1; +const SLIDER_HEIGHT: UiId = COLOR_SLIDER + 3; +const FRAME_WIDTH: UiId = SLIDER_HEIGHT + 1; +const TOGGLE_MATRIX: UiId = FRAME_WIDTH + 1; +const COLOR_SELECT: UiId = TOGGLE_MATRIX + 64; +const CIRCLE_POSITION: UiId = COLOR_SELECT + 1; +const ENVELOPE_EDITOR: UiId = CIRCLE_POSITION + 1; + diff --git a/examples/counter.rs b/examples/counter.rs index 71487e07b..883f9faeb 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -3,17 +3,7 @@ extern crate glutin_window; extern crate opengl_graphics; extern crate piston; -use conrod::{ - Background, - Button, - Colorable, - Shapeable, - Drawable, - Label, - Positionable, - Theme, - Ui -}; +use conrod::{Background, Button, Colorable, Labelable, Sizeable, Theme, Ui}; use glutin_window::GlutinWindow; use opengl_graphics::{ GlGraphics, OpenGL }; use opengl_graphics::glyph_cache::GlyphCache; @@ -40,7 +30,7 @@ fn main() { let glyph_cache = GlyphCache::new(&font_path).unwrap(); let ui = &mut Ui::new(glyph_cache, theme); - let mut count = 0; + let mut count: u32 = 0; for event in event_iter { ui.handle_event(&event); @@ -48,10 +38,14 @@ fn main() { gl.draw(args.viewport(), |_, gl| { // Draw the background. - Background::new().rgba(0.2, 0.25, 0.4, 1.0).draw(ui, gl); + Background::new().rgb(0.2, 0.25, 0.4).draw(ui, gl); - // Draw the counter. - counter(gl, ui, &mut count) + // Draw the button and increment count if pressed.. + Button::new() + .dimensions(80.0, 80.0) + .label(&count.to_string()) + .callback(|| count += 1) + .set(0, ui); }); } @@ -59,19 +53,3 @@ fn main() { } -/// Function for drawing the counter widget. -fn counter<'a>(gl: &mut GlGraphics, - ui: &mut Ui>, - count: &mut u32) { - - // Draw the value. - Label::new(&count.to_string()).position(10.0, 10.0).draw(ui, gl); - - // Draw the button and increment count if pressed.. - Button::new(0) - .position(110.0, 10.0) - .dimensions(80.0, 80.0) - .callback(|| *count += 1) - .draw(ui, gl) - -} diff --git a/src/background.rs b/src/background.rs index 4d05dcb83..0ca1b2d70 100644 --- a/src/background.rs +++ b/src/background.rs @@ -1,6 +1,5 @@ use color::{Color, Colorable}; -use draw::Drawable; use graphics::{self, Graphics}; use graphics::character::CharacterCache; use ui::Ui; @@ -20,17 +19,8 @@ impl Background { } } -} - -impl Colorable for Background { - fn color(mut self, color: Color) -> Self { - self.maybe_color = Some(color); - self - } -} - -impl Drawable for Background { - fn draw(&mut self, ui: &mut Ui, graphics: &mut B) + /// Draw the background. + pub fn draw(&mut self, ui: &mut Ui, graphics: &mut B) where B: Graphics::Texture>, C: CharacterCache @@ -38,5 +28,13 @@ impl Drawable for Background { let color = self.maybe_color.unwrap_or(ui.theme.background_color); graphics::clear(color.to_fsa(), graphics); } + +} + +impl Colorable for Background { + fn color(mut self, color: Color) -> Self { + self.maybe_color = Some(color); + self + } } diff --git a/src/color.rs b/src/color.rs deleted file mode 100644 index c77dea331..000000000 --- a/src/color.rs +++ /dev/null @@ -1,468 +0,0 @@ -//! -//! Inspiration taken from [elm-lang's color module] -//! (https://github.com/elm-lang/core/blob/62b22218c42fb8ccc996c86bea450a14991ab815/src/Color.elm) -//! -//! -//! Module for working with colors. Includes [RGB](https://en.wikipedia.org/wiki/RGB_color_model) -//! and [HSL](http://en.wikipedia.org/wiki/HSL_and_HSV) creation, gradients and built-in names. -//! - -use num::Float; -use rustc_serialize::hex::ToHex; -use std::ascii::AsciiExt; -use std::f32::consts::PI; -use utils::{clampf32, degrees, fmod, min, max, turns}; - - -/// Color supporting RGB and HSL variants. -#[derive(Copy, Clone, RustcEncodable, RustcDecodable)] -pub enum Color { - /// Red, Green, Blue, Alpha - All values' scales represented between 0.0 and 1.0. - Rgba(f32, f32, f32, f32), - /// Hue, Saturation, Lightness, Alpha - all valuess scales represented between 0.0 and 1.0. - Hsla(f32, f32, f32, f32), -} -/// Regional spelling alias. -pub type Colour = Color; - - -/// Create RGB colors with an alpha component for transparency. -/// The alpha component is specified with numbers between 0 and 1. -#[inline] -pub fn rgba(r: f32, g: f32, b: f32, a: f32) -> Color { - Color::Rgba(r, g, b, a) -} - - -/// Create RGB colors from numbers between 0.0 and 1.0. -#[inline] -pub fn rgb(r: f32, g: f32, b: f32) -> Color { - Color::Rgba(r, g, b, 1.0) -} - - -/// Create RGB colors from numbers between 0 and 255 inclusive. -/// The alpha component is specified with numbers between 0 and 1. -#[inline] -pub fn rgba_bytes(r: u8, g: u8, b: u8, a: f32) -> Color { - Color::Rgba(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a) -} - - -/// Create RGB colors from numbers between 0 and 255 inclusive. -#[inline] -pub fn rgb_bytes(r: u8, g: u8, b: u8) -> Color { - rgba_bytes(r, g, b, 1.0) -} - - -/// Create [HSL colors](http://en.wikipedia.org/wiki/HSL_and_HSV) with an alpha component for -/// transparency. -#[inline] -pub fn hsla(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Color { - Color::Hsla(hue - turns((hue / (2.0 * PI)).floor()), saturation, lightness, alpha) -} - - -/// Create [HSL colors](http://en.wikipedia.org/wiki/HSL_and_HSV). This gives you access to colors -/// more like a color wheel, where all hues are arranged in a circle that you specify with radians. -/// -/// red = hsl(degrees(0.0) , 1.0 , 0.5) -/// green = hsl(degrees(120.0) , 1.0 , 0.5) -/// blue = hsl(degrees(240.0) , 1.0 , 0.5) -/// pastel_red = hsl(degrees(0.0) , 0.7 , 0.7) -/// -/// To cycle through all colors, just cycle through degrees. The saturation level is how vibrant -/// the color is, like a dial between grey and bright colors. The lightness level is a dial between -/// white and black. -#[inline] -pub fn hsl(hue: f32, saturation: f32, lightness: f32) -> Color { - hsla(hue, saturation, lightness, 1.0) -} - - -/// Produce a gray based on the input. 0.0 is white, 1.0 is black. -pub fn grayscale(p: f32) -> Color { - Color::Hsla(0.0, 0.0, 1.0-p, 1.0) -} -/// Produce a gray based on the input. 0.0 is white, 1.0 is black. -pub fn greyscale(p: f32) -> Color { - Color::Hsla(0.0, 0.0, 1.0-p, 1.0) -} - - -/// Construct a random color. -pub fn random() -> Color { - rgb(::rand::random(), ::rand::random(), ::rand::random()) -} - - -impl Color { - - /// Produce a complementary color. The two colors will accent each other. This is the same as - /// rotating the hue by 180 degrees. - pub fn complement(self) -> Color { - match self { - Color::Hsla(h, s, l, a) => hsla(h + degrees(180.0), s, l, a), - Color::Rgba(r, g, b, a) => { - let (h, s, l) = rgb_to_hsl(r, g, b); - hsla(h + degrees(180.0), s, l, a) - }, - } - } - - /// Calculate and return the luminance of the Color. - pub fn luminance(&self) -> f32 { - match *self { - Color::Rgba(r, g, b, _) => (r + g + b) / 3.0, - Color::Hsla(_, _, l, _) => l, - } - } - - /// Return either black or white, depending which contrasts the Color the most. This will be - /// useful for determining a readable color for text on any given background Color. - pub fn plain_contrast(self) -> Color { - if self.luminance() > 0.5 { black() } else { white() } - } - - /// Extract the components of a color in the HSL format. - pub fn to_hsl(self) -> Hsla { - match self { - Color::Hsla(h, s, l, a) => Hsla(h, s, l, a), - Color::Rgba(r, g, b, a) => { - let (h, s, l) = rgb_to_hsl(r, g, b); - Hsla(h, s, l, a) - }, - } - } - - /// Extract the components of a color in the RGB format. - pub fn to_rgb(self) -> Rgba { - match self { - Color::Rgba(r, g, b, a) => Rgba(r, g, b, a), - Color::Hsla(h, s, l, a) => { - let (r, g, b) = hsl_to_rgb(h, s, l); - Rgba(r, g, b, a) - }, - } - } - - /// Extract the components of a color in the RGB format within a fixed-size array. - pub fn to_fsa(self) -> [f32; 4] { - let Rgba(r, g, b, a) = self.to_rgb(); - [r, g, b, a] - } - - /// Same as `to_fsa`, except r, g, b and a are represented in byte form. - pub fn to_byte_fsa(self) -> [u8; 4] { - let Rgba(r, g, b, a) = self.to_rgb(); - [f32_to_byte(r), f32_to_byte(g), f32_to_byte(b), f32_to_byte(a)] - } - - /// Return the hex representation of this color in the format #RRGGBBAA - /// e.g. `Color(1.0, 0.0, 5.0, 1.0) == "#FF0080FF"` - pub fn to_hex(self) -> String { - let vals = self.to_byte_fsa(); - let hex = vals.to_hex().to_ascii_uppercase(); - format!("#{}", &hex) - } - - /// Return the same color but with the given luminance. - pub fn with_luminance(self, l: f32) -> Color { - let Hsla(h, s, _, a) = self.to_hsl(); - Color::Hsla(h, s, l, a) - } - - /// Return the same color but with the alpha multiplied by the given alpha. - pub fn alpha(self, alpha: f32) -> Color { - match self { - Color::Rgba(r, g, b, a) => Color::Rgba(r, g, b, a * alpha), - Color::Hsla(h, s, l, a) => Color::Hsla(h, s, l, a * alpha), - } - } - - /// Return the same color but with the given alpha. - pub fn with_alpha(self, a: f32) -> Color { - match self { - Color::Rgba(r, g, b, _) => Color::Rgba(r, g, b, a), - Color::Hsla(h, s, l, _) => Color::Hsla(h, s, l, a), - } - } - - /// Return a highlighted version of the current Color. - pub fn highlighted(self) -> Color { - let luminance = self.luminance(); - let Rgba(r, g, b, a) = self.to_rgb(); - let (r, g, b) = { - if luminance > 0.8 { (r - 0.2, g - 0.2, b - 0.2) } - else if luminance < 0.2 { (r + 0.2, g + 0.2, b + 0.2) } - else { - (clampf32((1.0 - r) * 0.5 * r + r), - clampf32((1.0 - g) * 0.1 * g + g), - clampf32((1.0 - b) * 0.1 * b + b)) - } - }; - let a = clampf32((1.0 - a) * 0.5 + a); - rgba(r, g, b, a) - } - - /// Return a clicked version of the current Color. - pub fn clicked(&self) -> Color { - let luminance = self.luminance(); - let Rgba(r, g, b, a) = self.to_rgb(); - let (r, g, b) = { - if luminance > 0.8 { (r , g - 0.2, b - 0.2) } - else if luminance < 0.2 { (r + 0.4, g + 0.2, b + 0.2) } - else { - (clampf32((1.0 - r) * 0.75 + r), - clampf32((1.0 - g) * 0.25 + g), - clampf32((1.0 - b) * 0.25 + b)) - } - }; - let a = clampf32((1.0 - a) * 0.75 + a); - rgba(r, g, b, a) - } - - /// Return the Color's invert. - pub fn invert(self) -> Color { - let Rgba(r, g, b, a) = self.to_rgb(); - rgba((r - 1.0).abs(), (g - 1.0).abs(), (b - 1.0).abs(), a) - } - - /// Return the red value. - pub fn red(&self) -> f32 { - let Rgba(r, _, _, _) = self.to_rgb(); - r - } - - /// Return the green value. - pub fn green(&self) -> f32 { - let Rgba(_, g, _, _) = self.to_rgb(); - g - } - - /// Return the blue value. - pub fn blue(&self) -> f32 { - let Rgba(_, _, b, _) = self.to_rgb(); - b - } - - /// Set the red value. - pub fn set_red(&mut self, r: f32) { - let Rgba(_, g, b, a) = self.to_rgb(); - *self = rgba(r, g, b, a); - } - - /// Set the green value. - pub fn set_green(&mut self, g: f32) { - let Rgba(r, _, b, a) = self.to_rgb(); - *self = rgba(r, g, b, a); - } - - /// Set the blue value. - pub fn set_blue(&mut self, b: f32) { - let Rgba(r, g, _, a) = self.to_rgb(); - *self = rgba(r, g, b, a); - } - -} - - -/// The parts of HSL along with an alpha for transparency. -#[derive(Copy, Clone, Debug)] -pub struct Hsla(pub f32, pub f32, pub f32, pub f32); - - -/// The parts of RGB along with an alpha for transparency. -#[derive(Copy, Clone, Debug)] -pub struct Rgba(pub f32, pub f32, pub f32, pub f32); - - -/// Convert an f32 color to a byte. -#[inline] -pub fn f32_to_byte(c: f32) -> u8 { (c * 255.0) as u8 } - - -/// Pure function for converting rgb to hsl. -pub fn rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) { - let c_max = max(max(r, g), b); - let c_min = min(min(r, g), b); - let c = c_max - c_min; - let hue = degrees(60.0) * if c_max == r { fmod(((g - b) / c), 6) } - else if c_max == g { ((b - r) / c) + 2.0 } - else { ((r - g) / c) + 4.0 }; - let lightness = (c_max + c_min) / 2.0; - let saturation = if lightness == 0.0 { 0.0 } - else { c / (1.0 - (2.0 * lightness - 1.0).abs()) }; - (hue, saturation, lightness) -} - - -/// Pure function for converting hsl to rgb. -pub fn hsl_to_rgb(hue: f32, saturation: f32, lightness: f32) -> (f32, f32, f32) { - let chroma = (1.0 - (2.0 * lightness - 1.0).abs()) * saturation; - let hue = hue / degrees(60.0); - let x = chroma * (1.0 - (fmod(hue, 2) - 1.0).abs()); - let (r, g, b) = match hue { - hue if hue < 0.0 => (0.0, 0.0, 0.0), - hue if hue < 1.0 => (chroma, x, 0.0), - hue if hue < 2.0 => (x, chroma, 0.0), - hue if hue < 3.0 => (0.0, chroma, x), - hue if hue < 4.0 => (0.0, x, chroma), - hue if hue < 5.0 => (x, 0.0, chroma), - hue if hue < 6.0 => (chroma, 0.0, x), - _ => (0.0, 0.0, 0.0), - }; - let m = lightness - chroma / 2.0; - (r + m, g + m, b + m) -} - - -/// Linear or Radial Gradient. -#[derive(Clone, Debug)] -pub enum Gradient { - /// Takes a start and end point and then a series of color stops that indicate how to - /// interpolate between the start and end points. - Linear((f64, f64), (f64, f64), Vec<(f64, Color)>), - /// First takes a start point and inner radius. Then takes an end point and outer radius. - /// It then takes a series of color stops that indicate how to interpolate between the - /// inner and outer circles. - Radial((f64, f64), f64, (f64, f64), f64, Vec<(f64, Color)>), -} - - -/// Create a linear gradient. -pub fn linear(start: (f64, f64), end: (f64, f64), colors: Vec<(f64, Color)>) -> Gradient { - Gradient::Linear(start, end, colors) -} - - -/// Create a radial gradient. -pub fn radial(start: (f64, f64), start_r: f64, - end: (f64, f64), end_r: f64, - colors: Vec<(f64, Color)>) -> Gradient { - Gradient::Radial(start, start_r, end, end_r, colors) -} - - -/// Built-in colors. -/// -/// These colors come from the -/// [Tango palette](http://tango.freedesktop.org/Tango_Icon_Theme_Guidelines) which provides -/// aesthetically reasonable defaults for colors. Each color also comes with a light and dark -/// version. - -/// Scarlet Red - Light - #EF2929 -pub fn light_red() -> Color { rgb_bytes(239 , 41 , 41 ) } -/// Scarlet Red - Regular - #CC0000 -pub fn red() -> Color { rgb_bytes(204 , 0 , 0 ) } -/// Scarlet Red - Dark - #A30000 -pub fn dark_red() -> Color { rgb_bytes(164 , 0 , 0 ) } - -/// Orange - Light - #FCAF3E -pub fn light_orange() -> Color { rgb_bytes(252 , 175 , 62 ) } -/// Orange - Regular - #F57900 -pub fn orange() -> Color { rgb_bytes(245 , 121 , 0 ) } -/// Orange - Dark - #CE5C00 -pub fn dark_orange() -> Color { rgb_bytes(206 , 92 , 0 ) } - -/// Butter - Light - #FCE94F -pub fn light_yellow() -> Color { rgb_bytes(255 , 233 , 79 ) } -/// Butter - Regular - #EDD400 -pub fn yellow() -> Color { rgb_bytes(237 , 212 , 0 ) } -/// Butter - Dark - #C4A000 -pub fn dark_yellow() -> Color { rgb_bytes(196 , 160 , 0 ) } - -/// Chameleon - Light - #8AE234 -pub fn light_green() -> Color { rgb_bytes(138 , 226 , 52 ) } -/// Chameleon - Regular - #73D216 -pub fn green() -> Color { rgb_bytes(115 , 210 , 22 ) } -/// Chameleon - Dark - #4E9A06 -pub fn dark_green() -> Color { rgb_bytes(78 , 154 , 6 ) } - -/// Sky Blue - Light - #729FCF -pub fn light_blue() -> Color { rgb_bytes(114 , 159 , 207) } -/// Sky Blue - Regular - #3465A4 -pub fn blue() -> Color { rgb_bytes(52 , 101 , 164) } -/// Sky Blue - Dark - #204A87 -pub fn dark_blue() -> Color { rgb_bytes(32 , 74 , 135) } - -/// Plum - Light - #AD7FA8 -pub fn light_purple() -> Color { rgb_bytes(173 , 127 , 168) } -/// Plum - Regular - #75507B -pub fn purple() -> Color { rgb_bytes(117 , 80 , 123) } -/// Plum - Dark - #5C3566 -pub fn dark_purple() -> Color { rgb_bytes(92 , 53 , 102) } - -/// Chocolate - Light - #E9B96E -pub fn light_brown() -> Color { rgb_bytes(233 , 185 , 110) } -/// Chocolate - Regular - #C17D11 -pub fn brown() -> Color { rgb_bytes(193 , 125 , 17 ) } -/// Chocolate - Dark - #8F5902 -pub fn dark_brown() -> Color { rgb_bytes(143 , 89 , 2 ) } - -/// Straight Black. -pub fn black() -> Color { rgb_bytes(0 , 0 , 0 ) } -/// Straight White. -pub fn white() -> Color { rgb_bytes(255 , 255 , 255) } - -/// Alluminium - Light -pub fn light_gray() -> Color { rgb_bytes(238 , 238 , 236) } -/// Alluminium - Regular -pub fn gray() -> Color { rgb_bytes(211 , 215 , 207) } -/// Alluminium - Dark -pub fn dark_gray() -> Color { rgb_bytes(186 , 189 , 182) } - -/// Aluminium - Light - #EEEEEC -pub fn light_grey() -> Color { rgb_bytes(238 , 238 , 236) } -/// Aluminium - Regular - #D3D7CF -pub fn grey() -> Color { rgb_bytes(211 , 215 , 207) } -/// Aluminium - Dark - #BABDB6 -pub fn dark_grey() -> Color { rgb_bytes(186 , 189 , 182) } - -/// Charcoal - Light - #888A85 -pub fn light_charcoal() -> Color { rgb_bytes(136 , 138 , 133) } -/// Charcoal - Regular - #555753 -pub fn charcoal() -> Color { rgb_bytes(85 , 87 , 83 ) } -/// Charcoal - Dark - #2E3436 -pub fn dark_charcoal() -> Color { rgb_bytes(46 , 52 , 54 ) } - - - -impl ::std::fmt::Debug for Color { - fn fmt(&self, fmt: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> { - let hex = self.clone().to_hex(); - fmt.pad(&hex) - } -} - - -/// Widgets that can be colored. -pub trait Colorable: Sized { - - /// Set the color of the widget. - fn color(self, color: Color) -> Self; - - /// Set the color of the widget from rgba values. - fn rgba(self, r: f32, g: f32, b: f32, a: f32) -> Self { - self.color(rgba(r, g, b, a)) - } - - /// Set the color of the widget from rgb values. - fn rgb(self, r: f32, g: f32, b: f32) -> Self { - self.color(rgb(r, g, b)) - } - - /// Set the color of the widget from hsla values. - fn hsla(self, h: f32, s: f32, l: f32, a: f32) -> Self { - self.color(hsla(h, s, l, a)) - } - - /// Set the color of the widget from hsl values. - fn hsl(self, h: f32, s: f32, l: f32) -> Self { - self.color(hsl(h, s, l)) - } - -} - - diff --git a/src/dimensions.rs b/src/dimensions.rs deleted file mode 100644 index 26362dc3c..000000000 --- a/src/dimensions.rs +++ /dev/null @@ -1,4 +0,0 @@ - -/// General use 2D spatial dimensions. -pub type Dimensions = [f64; 2]; - diff --git a/src/draw.rs b/src/draw.rs deleted file mode 100644 index c3adcd919..000000000 --- a/src/draw.rs +++ /dev/null @@ -1,15 +0,0 @@ - -use graphics::Graphics; -use graphics::character::CharacterCache; -use Ui; - -/// Widgets that are renderable. -pub trait Drawable { - - /// Draw a widget using the given graphics backend. - fn draw(&mut self, ui: &mut Ui, graphics: &mut B) - where - B: Graphics::Texture>, - C: CharacterCache; - -} diff --git a/src/lib.rs b/src/lib.rs index c1858a4b8..7580390c9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ #[macro_use] extern crate bitflags; extern crate clock_ticks; +extern crate color as color_lib; extern crate elmesque; extern crate graphics; #[macro_use] extern crate piston; @@ -17,6 +18,8 @@ extern crate rustc_serialize; extern crate vecmath; extern crate num; +pub use color_lib as color; + pub use widget::button::Button; pub use widget::drop_down_list::DropDownList; pub use widget::envelope_editor::EnvelopeEditor; @@ -31,13 +34,9 @@ pub use widget::xy_pad::XYPad; pub use background::Background; pub use color::{Color, Colorable}; -pub use dimensions::Dimensions; -pub use draw::Drawable; pub use frame::{Framing, Frameable}; pub use label::Labelable; -pub use point::Point; -pub use position::Positionable; -pub use shape::Shapeable; +pub use position::{Depth, Direction, Dimensions, Point, Position, Positionable, Sizeable}; pub use theme::Theme; pub use ui::{Ui, UiId}; @@ -45,16 +44,10 @@ pub use ui::{Ui, UiId}; mod macros; mod background; -pub mod color; -mod dimensions; -mod draw; mod frame; mod label; mod mouse; -mod point; mod position; -mod rectangle; -mod shape; mod theme; mod ui; mod utils; diff --git a/src/macros.rs b/src/macros.rs index 1e806f71e..9e735e8d1 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -26,25 +26,5 @@ macro_rules! widget_fns( } } - /// Set the state for the widget in the Ui. - fn set_state( - ui: &mut ::ui::Ui, - ui_id: ::ui::UiId, - new_state: ::widget::Kind, - pos: ::point::Point, - dim: ::dimensions::Dimensions - ) { - match *get_widget(ui, ui_id) { - ref mut state => { - if !state.matches(&new_state) { - panic!("The Kind variant returned by Ui is different to that which \ - was requested (Check that there are no UiId conflicts)."); - } - *state = new_state; - } - } - ui.set_place(ui_id, pos, dim); - } - ) ); diff --git a/src/mouse.rs b/src/mouse.rs index 303c46a53..cc800dc57 100644 --- a/src/mouse.rs +++ b/src/mouse.rs @@ -1,28 +1,35 @@ -use point::Point; +use position::Point; /// Represents the current state of a mouse button. -#[derive(Debug, Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub enum ButtonState { Up, Down, } /// Represents the current state of the Mouse. -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] pub struct Mouse { - pub pos: Point, + pub xy: Point, pub left: ButtonState, pub middle: ButtonState, pub right: ButtonState, } impl Mouse { + /// Constructor for a Mouse struct. - pub fn new(pos: Point, + pub fn new(xy: Point, left: ButtonState, middle: ButtonState, right: ButtonState) -> Mouse { - Mouse { pos: pos, left: left, middle: middle, right: right } + Mouse { xy: xy, left: left, middle: middle, right: right } + } + + /// Return the mouse state with its position relative to the given position. + pub fn relative_to(self, xy: Point) -> Mouse { + Mouse { xy: ::vecmath::vec2_sub(self.xy, xy), ..self } } + } diff --git a/src/point.rs b/src/point.rs deleted file mode 100644 index 8dce7fd22..000000000 --- a/src/point.rs +++ /dev/null @@ -1,5 +0,0 @@ - -use graphics::math::Scalar; - -/// General use 2D spatial point. -pub type Point = [Scalar; 2]; diff --git a/src/position.rs b/src/position.rs index 1298bf266..bd013a18b 100644 --- a/src/position.rs +++ b/src/position.rs @@ -1,64 +1,247 @@ -use point::Point; -use ui::{UiId, Ui}; +use graphics::math::Scalar; +use ui::UiId; + +/// The depth at which the widget will be rendered. This determines the order of rendering where +/// widgets with a greater depth will be rendered first. 0.0 is the default depth. +pub type Depth = f32; + +/// General use 2D spatial dimensions. +pub type Dimensions = [Scalar; 2]; + +/// General use 2D spatial point. +pub type Point = [Scalar; 2]; + +/// A cached widget's position for rendering. +#[derive(Copy, Clone, Debug, RustcEncodable, RustcDecodable)] +pub enum Position { + /// A specific position. + Absolute(Scalar, Scalar), + /// A position relative to some other widget. + Relative(Scalar, Scalar, Option), + /// A direction relative to some other widget. + Direction(Direction, Option), +} + +impl Position { + /// The default widget Position. + pub fn default() -> Position{ + Position::Direction(Direction::Down(20.0), None) + } +} + +/// Directionally positioned, relative to another widget. +#[derive(Copy, Clone, Debug, RustcEncodable, RustcDecodable)] +pub enum Direction { + /// Positioned above. + Up(Scalar), + /// Positioned below. + Down(Scalar), + /// Positioned to the left. + Left(Scalar), + /// Positioned to the right. + Right(Scalar), +} + +/// The horizontal alignment of a widget positioned relatively to another widget on the y axis. +#[derive(Copy, Clone, Debug, RustcEncodable, RustcDecodable, PartialEq)] +pub enum HorizontalAlign { + /// Align the left edges of the widgets. + Left, + /// Align the centres of the widgets' closest parallel edges. + Middle, + /// Align the right edges of the relative widgets. + Right, +} + +/// The vertical alignment of a widget positioned relatively to another widget on the x axis. +#[derive(Copy, Clone, Debug, RustcEncodable, RustcDecodable, PartialEq)] +pub enum VerticalAlign { + /// Align the top edges of the widgets. + Top, + /// Align the centres of the widgets' closest parallel edges. + Middle, + /// Align the bottom edges of the widgets. + Bottom, +} /// Widgets that are positionable. pub trait Positionable: Sized { + /// Set the Position. + fn position(self, pos: Position) -> Self; + /// Set the position with some Point. - fn point(self, pos: Point) -> Self; + fn point(self, point: Point) -> Self { + self.position(Position::Absolute(point[0], point[1])) + } /// Set the position with XY co-ords. - fn position(self, x: f64, y: f64) -> Self { - self.point([x, y]) + fn xy(self, x: Scalar, y: Scalar) -> Self { + self.position(Position::Absolute(x, y)) + } + + /// Set the point relative to the previous widget. + fn relative(self, point: Point) -> Self { + self.position(Position::Relative(point[0], point[1], None)) + } + + /// Set the xy relative to the previous widget. + fn relative_xy(self, x: Scalar, y: Scalar) -> Self { + self.position(Position::Relative(x, y, None)) + } + + /// Set the position relative to the widget with the given UiId. + fn relative_to(self, ui_id: UiId, point: Point) -> Self { + self.position(Position::Relative(point[0], point[1], Some(ui_id))) + } + + /// Set the position relative to the widget with the given UiId. + fn relative_xy_to(self, ui_id: UiId, x: Scalar, y: Scalar) -> Self { + self.position(Position::Relative(x, y, Some(ui_id))) } /// Set the position as below the previous widget. - fn down(self, padding: f64, ui: &Ui) -> Self { - let (x, y) = ui.get_placing(ui.get_prev_uiid()).down(padding); - self.point([x, y]) + fn down(self, pixels: Scalar) -> Self { + self.position(Position::Direction(Direction::Down(pixels), None)) } /// Set the position as above the previous widget. - fn up(self, padding: f64, ui: &Ui) -> Self { - let (x, y) = ui.get_placing(ui.get_prev_uiid()).up(padding); - self.point([x, y]) + fn up(self, pixels: Scalar) -> Self { + self.position(Position::Direction(Direction::Up(pixels), None)) } /// Set the position to the left of the previous widget. - fn left(self, padding: f64, ui: &Ui) -> Self { - let (x, y) = ui.get_placing(ui.get_prev_uiid()).left(padding); - self.point([x, y]) + fn left(self, pixels: Scalar) -> Self { + self.position(Position::Direction(Direction::Left(pixels), None)) } /// Set the position to the right of the previous widget. - fn right(self, padding: f64, ui: &Ui) -> Self { - let (x, y) = ui.get_placing(ui.get_prev_uiid()).right(padding); - self.point([x, y]) + fn right(self, pixels: Scalar) -> Self { + self.position(Position::Direction(Direction::Right(pixels), None)) } /// Set the position as below the widget with the given UiId. - fn down_from(self, ui_id: UiId, padding: f64, ui: &Ui) -> Self { - let (x, y) = ui.get_placing(ui_id).down(padding); - self.point([x, y]) + fn down_from(self, ui_id: UiId, pixels: Scalar) -> Self { + self.position(Position::Direction(Direction::Down(pixels), Some(ui_id))) } /// Set the position as above the widget with the given UiId. - fn up_from(self, ui_id: UiId, padding: f64, ui: &Ui) -> Self { - let (x, y) = ui.get_placing(ui_id).up(padding); - self.point([x, y]) + fn up_from(self, ui_id: UiId, pixels: Scalar) -> Self { + self.position(Position::Direction(Direction::Up(pixels), Some(ui_id))) } /// Set the position to the left of the widget with the given UiId. - fn left_from(self, ui_id: UiId, padding: f64, ui: &Ui) -> Self { - let (x, y) = ui.get_placing(ui_id).left(padding); - self.point([x, y]) + fn left_from(self, ui_id: UiId, pixels: Scalar) -> Self { + self.position(Position::Direction(Direction::Left(pixels), Some(ui_id))) } /// Set the position to the right of the widget with the given UiId. - fn right_from(self, ui_id: UiId, padding: f64, ui: &Ui) -> Self { - let (x, y) = ui.get_placing(ui_id).right(padding); - self.point([x, y]) + fn right_from(self, ui_id: UiId, pixels: Scalar) -> Self { + self.position(Position::Direction(Direction::Right(pixels), Some(ui_id))) + } + + ///// Alignment methods. ///// + + /// Align the position horizontally (only effective for Up or Down `Direction`s). + fn horizontal_align(self, align: HorizontalAlign) -> Self; + + /// Align the position vertically (only effective for Left or Right `Direction`s). + fn vertical_align(self, align: VerticalAlign) -> Self; + + /// Align the position to the left (only effective for Up or Down `Direction`s). + fn align_left(self) -> Self { + self.horizontal_align(HorizontalAlign::Left) + } + + /// Align the position to the middle (only effective for Up or Down `Direction`s). + fn align_middle_x(self) -> Self { + self.horizontal_align(HorizontalAlign::Middle) } + /// Align the position to the right (only effective for Up or Down `Direction`s). + fn align_right(self) -> Self { + self.horizontal_align(HorizontalAlign::Right) + } + + /// Align the position to the top (only effective for Left or Right `Direction`s). + fn align_top(self) -> Self { + self.vertical_align(VerticalAlign::Top) + } + + /// Align the position to the middle (only effective for Left or Right `Direction`s). + fn align_middle_y(self) -> Self { + self.vertical_align(VerticalAlign::Middle) + } + + /// Align the position to the bottom (only effective for Left or Right `Direction`s). + fn align_bottom(self) -> Self { + self.vertical_align(VerticalAlign::Bottom) + } + +} + +/// Widgets that support different dimensions. +pub trait Sizeable: Sized { + + /// Set the width for the widget. + fn width(self, width: Scalar) -> Self; + + /// Set the height for the widget. + #[inline] + fn height(self, height: Scalar) -> Self; + + /// Set the dimensions for the widget. + #[inline] + fn dim(self, dim: Dimensions) -> Self { + self.width(dim[0]).height(dim[1]) + } + + /// Set the width and height for the widget. + #[inline] + fn dimensions(self, width: Scalar, height: Scalar) -> Self { + self.dim([width, height]) + } + +} + +/// A corner of a rectangle. +#[derive(Copy, Clone)] +pub enum Corner { + TopLeft, + TopRight, + BottomLeft, + BottomRight, +} + +/// Return which corner of the rectangle the given Point is within. +pub fn corner(xy: Point, dim: Dimensions) -> Corner { + use utils::map_range; + let x_perc = map_range(xy[0], -dim[0] / 2.0, dim[0] / 2.0, -1.0, 1.0); + let y_perc = map_range(xy[1], -dim[1] / 2.0, dim[1] / 2.0, -1.0, 1.0); + if x_perc <= 0.0 && y_perc <= 0.0 { Corner::BottomLeft } + else if x_perc > 0.0 && y_perc <= 0.0 { Corner::BottomRight } + else if x_perc <= 0.0 && y_perc > 0.0 { Corner::TopLeft } + else { Corner::TopRight } +} + +/// The x offset required to align an element with `width` to the left of a target element. +pub fn align_left_of(target_width: Scalar, width: Scalar) -> Scalar { + width / 2.0 - target_width / 2.0 } + +/// The x offset required to align an element with `width` to the right of a target element. +pub fn align_right_of(target_width: Scalar, width: Scalar) -> Scalar { + target_width / 2.0 - width / 2.0 +} + +/// The y offset required to align an element with `height` to the bottom of a target element. +pub fn align_bottom_of(target_height: Scalar, height: Scalar) -> Scalar { + height / 2.0 - target_height / 2.0 +} + +/// The y offset required to align an element with `height` to the top of a target element. +pub fn align_top_of(target_height: Scalar, height: Scalar) -> Scalar { + target_height / 2.0 - height / 2.0 +} + diff --git a/src/rectangle.rs b/src/rectangle.rs deleted file mode 100644 index e83634a9f..000000000 --- a/src/rectangle.rs +++ /dev/null @@ -1,148 +0,0 @@ - -use color::Color; -use dimensions::Dimensions; -use graphics::{self, DrawState, Graphics}; -use graphics::math::Matrix2d; -use graphics::character::CharacterCache; -use label::{self, FontSize}; -use point::Point; -use ui::Ui; -use utils::map_range; - -/// Represents the state of the Button widget. -#[derive(PartialEq, Debug, Clone, Copy)] -pub enum State { - Normal, - Highlighted, - Clicked, -} - -/// Draw a basic rectangle. The primary purpose -/// of this is to be used as a building block for -/// other widgets. -pub fn draw( - win_w: f64, - win_h: f64, - graphics: &mut B, - state: State, - pos: Point, - dim: Dimensions, - maybe_frame: Option<(f64, Color)>, - color: Color -) { - let draw_state = graphics::default_draw_state(); - let transform = graphics::abs_transform(win_w, win_h); - if let Some((_, f_color)) = maybe_frame { - draw_frame(draw_state, transform, graphics, pos, dim, f_color) - } - let f_width = if let Some((f_width, _)) = maybe_frame { f_width } else { 0.0 }; - draw_normal(draw_state, transform, graphics, state, pos, dim, f_width, color); -} - -/// Draw the button border. -fn draw_frame( - draw_state: &DrawState, - transform: Matrix2d, - graphics: &mut B, - pos: Point, - dim: Dimensions, - color: Color -) { - graphics::Rectangle::new(color.to_fsa()) - .draw( - [pos[0], pos[1], dim[0], dim[1]], - draw_state, - transform, - graphics - ); -} - -/// Draw the rectangle while considering frame -/// width for position and dimensions. -fn draw_normal( - draw_state: &DrawState, - transform: Matrix2d, - graphics: &mut B, - state: State, - pos: Point, - dim: Dimensions, - frame_width: f64, - color: Color -) { - let color = match state { - State::Normal => color, - State::Highlighted => color.highlighted(), - State::Clicked => color.clicked(), - }; - graphics::Rectangle::new(color.to_fsa()) - .draw([pos[0] + frame_width, - pos[1] + frame_width, - dim[0] - frame_width * 2.0, - dim[1] - frame_width * 2.0], - draw_state, - transform, - graphics); -} - -/// Return whether or not the widget has been hit by a mouse_press. -#[inline] -pub fn is_over(pos: Point, - mouse_pos: Point, - dim: Dimensions) -> bool { - if mouse_pos[0] > pos[0] - && mouse_pos[1] > pos[1] - && mouse_pos[0] < pos[0] + dim[0] - && mouse_pos[1] < pos[1] + dim[1] { true } - else { false } -} - -/// Draw a label centered within a rect of given position and dimensions. -pub fn draw_with_centered_label( - win_w: f64, - win_h: f64, - graphics: &mut B, - ui: &mut Ui, - state: State, - pos: Point, - dim: Dimensions, - maybe_frame: Option<(f64, Color)>, - color: Color, - text: &str, - font_size: FontSize, - text_color: Color -) - where - B: Graphics::Texture>, - C: CharacterCache -{ - let draw_state = graphics::default_draw_state(); - let transform = graphics::abs_transform(win_w, win_h); - if let Some((_, f_color)) = maybe_frame { - draw_frame(draw_state, transform, graphics, pos, dim, f_color) - } - let f_width = if let Some((f_width, _)) = maybe_frame { f_width } else { 0.0 }; - draw_normal(draw_state, transform, graphics, state, pos, dim, f_width, color); - let text_w = label::width(ui, font_size, text); - let l_pos = [pos[0] + (dim[0] - text_w) / 2.0, pos[1] + (dim[1] - font_size as f64) / 2.0]; - ui.draw_text(graphics, l_pos, font_size, text_color, text); -} - -#[derive(Copy, Clone)] -pub enum Corner { - TopLeft, - TopRight, - BottomLeft, - BottomRight, -} - -/// Return which corner of the rectangle the given Point is within. -pub fn corner(rect_p: Point, p: Point, dim: Dimensions) -> Corner { - let x_temp = p[0] - rect_p[0]; - let y_temp = p[1] - rect_p[1]; - let x_perc = map_range(x_temp, 0.0, dim[0], 0f64, 1.0); - let y_perc = map_range(y_temp, dim[1], 0.0, 0f64, 1.0); - if x_perc <= 0.5 && y_perc <= 0.5 { Corner::BottomLeft } - else if x_perc > 0.5 && y_perc <= 0.5 { Corner::BottomRight } - else if x_perc <= 0.5 && y_perc > 0.5 { Corner::TopLeft } - else { Corner::TopRight } -} diff --git a/src/shape.rs b/src/shape.rs deleted file mode 100644 index b9c1a0c2e..000000000 --- a/src/shape.rs +++ /dev/null @@ -1,30 +0,0 @@ - -use dimensions::Dimensions; - -/// Widgets that have some shape and dimension. -pub trait Shapeable: Sized { - - /// Return the dimensions of the widget. - fn get_dim(&self) -> Dimensions; - - /// Set the dimensions for the widget. - fn dim(self, dim: Dimensions) -> Self; - - /// Set the width and height for the widget. - fn dimensions(self, width: f64, height: f64) -> Self { - self.dim([width, height]) - } - - /// Set the width for the widget. - fn width(self, width: f64) -> Self { - let size = self.get_dim(); - self.dim([width, size[1]]) - } - - /// Set the height for the widget. - fn height(self, height: f64) -> Self { - let size = self.get_dim(); - self.dim([size[0], height]) - } - -} diff --git a/src/theme.rs b/src/theme.rs index 87bef82e9..e624adad0 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,5 +1,6 @@ use color::{Color, black, white}; +use position::{Position, HorizontalAlign, VerticalAlign}; use rustc_serialize::{json, Encodable, Decodable}; use std::borrow::ToOwned; use std::error::Error; @@ -28,6 +29,12 @@ pub struct Theme { pub font_size_medium: u32, /// A default "small" font size. pub font_size_small: u32, + /// A default widget position. + pub position: Position, + /// A default horizontal alignment for widgets. + pub h_align: HorizontalAlign, + /// A default vertical alignment for widgets. + pub v_align: VerticalAlign, //TODO: Add unique theme-ing for each widget. //i.e. maybe_slider: Option, etc } @@ -46,6 +53,9 @@ impl Theme { font_size_large: 26, font_size_medium: 18, font_size_small: 12, + position: Position::default(), + h_align: HorizontalAlign::Left, + v_align: VerticalAlign::Top, } } diff --git a/src/ui.rs b/src/ui.rs index 77b0c83f8..b36d7c36e 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,7 +1,5 @@ -use color::Color; -use dimensions::Dimensions; -use graphics::{self, Graphics}; +use graphics::Graphics; use graphics::character::{Character, CharacterCache}; use label::FontSize; use mouse::{ButtonState, Mouse}; @@ -14,15 +12,16 @@ use piston::event::{ RenderEvent, TextEvent, }; -use point::Point; +use position::{Dimensions, HorizontalAlign, Point, Position, VerticalAlign}; use std::iter::repeat; use theme::Theme; use widget::Kind as WidgetKind; -use widget::{Placing, Widget}; +use widget::Widget; /// User interface identifier. Each widget must use a unique `UiId` so that it's state can be -/// cached within the `Ui` type. -pub type UiId = u64; +/// cached within the `Ui` type. The reason we use a usize is because widgets are cached within +/// a `Vec`, which is limited to a size of `usize` elements. +pub type UiId = usize; /// `Ui` is the most important type within Conrod and is necessary for rendering and maintaining /// widget state. @@ -52,13 +51,10 @@ pub struct Ui { /// Window height. pub win_h: f64, /// The UiId of the previously drawn Widget. - prev_uiid: u64, + maybe_prev_ui_id: Option, } -impl Ui - where - C: CharacterCache -{ +impl Ui { /// Constructor for a UiContext. pub fn new(character_cache: C, theme: Theme) -> Ui { @@ -73,7 +69,7 @@ impl Ui prev_event_was_render: false, win_w: 0.0, win_h: 0.0, - prev_uiid: 0, + maybe_prev_ui_id: None, } } @@ -81,16 +77,21 @@ impl Ui pub fn handle_event(&mut self, event: &E) { if self.prev_event_was_render { self.flush_input(); + self.maybe_prev_ui_id = None; self.prev_event_was_render = false; } + event.render(|args| { self.win_w = args.width as f64; self.win_h = args.height as f64; self.prev_event_was_render = true; }); + event.mouse_cursor(|x, y| { - self.mouse.pos = [x, y]; + // Convert mouse coords to (0, 0) origin. + self.mouse.xy = [x - self.win_w / 2.0, -(y - self.win_h / 2.0)]; }); + event.press(|button_type| { use piston::input::Button; use piston::input::MouseButton::Left; @@ -106,10 +107,10 @@ impl Ui Button::Keyboard(key) => self.keys_just_pressed.push(key), } }); + event.release(|button_type| { use piston::input::Button; use piston::input::MouseButton::Left; - match button_type { Button::Mouse(button) => { *match button { @@ -121,6 +122,7 @@ impl Ui Button::Keyboard(key) => self.keys_just_released.push(key), } }); + event.text(|text| { self.text_just_entered.push(text.to_string()) }); @@ -129,12 +131,18 @@ impl Ui /// Return a reference to a `Character` from the GlyphCache. pub fn get_character(&mut self, size: FontSize, - ch: char) -> &Character<::Texture> { + ch: char) -> &Character + where + C: CharacterCache + { self.character_cache.character(size, ch) } /// Return the width of a 'Character'. - pub fn get_character_w(&mut self, size: FontSize, ch: char) -> f64 { + pub fn get_character_w(&mut self, size: FontSize, ch: char) -> f64 + where + C: CharacterCache + { self.get_character(size, ch).width() } @@ -145,36 +153,6 @@ impl Ui self.text_just_entered.clear(); } - /// Draws text - pub fn draw_text(&mut self, - graphics: &mut B, - pos: Point, - size: FontSize, - color: Color, - text: &str) - where - B: Graphics::Texture> - { - use graphics::text::Text; - use graphics::Transformed; - use num::Float; - - let draw_state = graphics::default_draw_state(); - let transform = graphics::abs_transform(self.win_w, self.win_h) - .trans(pos[0].ceil(), pos[1].ceil() + size as f64); - Text::colored(color.to_fsa(), size).draw( - text, - &mut self.character_cache, - draw_state, - transform, - graphics - ); - } - -} - -impl Ui { - /// Return the current mouse state. pub fn get_mouse_state(&self) -> Mouse { self.mouse @@ -215,19 +193,118 @@ impl Ui { } } - /// Set the Placing for a particular widget. - pub fn set_place(&mut self, ui_id: UiId, pos: Point, dim: Dimensions) { - self.widget_cache[ui_id as usize].placing = Placing::Place(pos[0], pos[1], dim[0], dim[1]); - self.prev_uiid = ui_id; + /// Set the given widget at the given UiId. + pub fn set_widget(&mut self, ui_id: UiId, widget: Widget) { + if self.widget_cache[ui_id].kind.matches(&widget.kind) + || self.widget_cache[ui_id].kind.matches(&WidgetKind::NoWidget) { + self.widget_cache[ui_id] = widget; + self.maybe_prev_ui_id = Some(ui_id); + } else { + panic!("A widget of a different kind already exists at the given UiId ({:?}). + You tried to insert a {:?}, however the existing widget is a {:?}. + Check your widgets' `UiId`s for errors.", + ui_id, &widget.kind, &self.widget_cache[ui_id].kind); + } } - /// Get the UiId of the previous widget. - pub fn get_prev_uiid(&self) -> UiId { self.prev_uiid } + /// Get the centred xy coords for some given `Dimension`s, `Position` and alignment. + pub fn get_xy(&self, + position: Position, + dim: Dimensions, + h_align: HorizontalAlign, + v_align: VerticalAlign) -> Point { + match position { + Position::Absolute(x, y) => [x, y], + Position::Relative(x, y, maybe_ui_id) => { + match maybe_ui_id.or(self.maybe_prev_ui_id) { + None => [0.0, 0.0], + Some(rel_ui_id) => ::vecmath::vec2_add(self.widget_cache[rel_ui_id].xy, [x, y]), + } + }, + Position::Direction(direction, maybe_ui_id) => { + use position::{align_left_of, align_right_of, align_top_of, align_bottom_of}; + match maybe_ui_id.or(self.maybe_prev_ui_id) { + None => [0.0, 0.0], + Some(rel_ui_id) => { + use position::Direction; + let rel_xy = self.widget_cache[rel_ui_id].xy; + if let Some(ref element) = self.widget_cache[rel_ui_id].element { + let (rel_w, rel_h) = element.get_size(); + let (rel_w, rel_h) = (rel_w as f64, rel_h as f64); + match direction { + + Direction::Up(px) => { + let x = rel_xy[0] + match h_align { + HorizontalAlign::Middle => 0.0, + HorizontalAlign::Left => align_left_of(rel_w, dim[0]), + HorizontalAlign::Right => align_right_of(rel_w, dim[0]), + }; + let y = rel_xy[1] + rel_h / 2.0 + dim[1] / 2.0 + px; + [x, y] + }, + + Direction::Down(px) => { + let x = rel_xy[0] + match h_align { + HorizontalAlign::Middle => 0.0, + HorizontalAlign::Left => align_left_of(rel_w, dim[0]), + HorizontalAlign::Right => align_right_of(rel_w, dim[0]), + }; + let y = rel_xy[1] - rel_h / 2.0 - dim[1] / 2.0 - px; + [x, y] + }, + + Direction::Left(px) => { + let y = rel_xy[1] + match v_align { + VerticalAlign::Middle => 0.0, + VerticalAlign::Bottom => align_bottom_of(rel_h, dim[1]), + VerticalAlign::Top => align_top_of(rel_h, dim[1]), + }; + let x = rel_xy[0] - rel_w / 2.0 - dim[0] / 2.0 - px; + [x, y] + }, - /// Get the Placing for a particular widget. - pub fn get_placing(&self, ui_id: UiId) -> Placing { - if ui_id as usize >= self.widget_cache.len() { Placing::NoPlace } - else { self.widget_cache[ui_id as usize].placing } + Direction::Right(px) => { + let y = rel_xy[1] + match v_align { + VerticalAlign::Middle => 0.0, + VerticalAlign::Bottom => align_bottom_of(rel_h, dim[1]), + VerticalAlign::Top => align_top_of(rel_h, dim[1]), + }; + let x = rel_xy[0] + rel_w / 2.0 + dim[0] / 2.0 + px; + [x, y] + }, + + } + } else { + rel_xy + } + }, + } + }, + } + } + + /// Draw the `Ui` in it's current state. + /// - Sort widgets by render depth (depth first). + /// - Construct the elmesque `Renderer` for rendering the elm `Element`s. + /// - Render all widgets. + pub fn draw(&mut self, graphics: &mut G) + where + C: CharacterCache, + G: Graphics, + { + use elmesque::Renderer; + use std::cmp::Ordering; + let Ui { ref mut widget_cache, ref win_w, ref win_h, ref mut character_cache, .. } = *self; + let mut widgets: Vec<_> = widget_cache.iter_mut().collect(); + widgets.sort_by(|a, b| if a.depth < b.depth { Ordering::Greater } + else if a.depth > b.depth { Ordering::Less } + else { Ordering::Equal }); + let mut renderer = Renderer::new(*win_w, *win_h, graphics).character_cache(character_cache); + for widget in widgets.into_iter() { + if let Some(element) = widget.element.take() { + element.draw(&mut renderer); + } + } } } diff --git a/src/utils.rs b/src/utils.rs index eb9bad36d..0434de892 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,60 +1,18 @@ -use std::cmp::Ordering::{self, Less, Equal, Greater}; -use std::f32::consts::PI; use num::{Float, NumCast, PrimInt, ToPrimitive}; +use position::{Dimensions, Point}; +use vecmath::vec2_sub; /// Clamp a value between a given min and max. pub fn clamp(n: T, min: T, max: T) -> T { if n < min { min } else if n > max { max } else { n } } -/// Clamp a f32 between 0f32 and 1f32. -pub fn clampf32(f: f32) -> f32 { - if f < 0f32 { 0f32 } else if f > 1f32 { 1f32 } else { f } -} - -/// Compare two f64s and return an Ordering. -pub fn compare_f64s(a: f64, b: f64) -> Ordering { - if a > b { Greater } - else if a < b { Less } - else { Equal } -} - -/// Convert turns to radians. -pub fn turns(t: F) -> F { - let f: F = NumCast::from(2.0 * PI).unwrap(); - f * t -} - -/// Convert degrees to radians. -pub fn degrees(d: F) -> F { - d * NumCast::from(PI / 180.0).unwrap() -} - -/// The modulo function. -#[inline] -pub fn modulo(a: I, b: I) -> I { - match a % b { - r if (r > I::zero() && b < I::zero()) - || (r < I::zero() && b > I::zero()) => (r + b), - r => r, - } -} - -/// Modulo float. -pub fn fmod(f: f32, n: i32) -> f32 { - let i = f.floor() as i32; - modulo(i, n) as f32 + f - i as f32 -} - -/// Return the max between to floats. -pub fn max(a: f32, b: f32) -> f32 { - if a >= b { a } else { b } -} - -/// Return the min between to floats. -pub fn min(a: f32, b: f32) -> f32 { - if a <= b { a } else { b } +/// Return whether or not a given point is over a rectangle at a given point on a cartesian plane. +pub fn is_over_rect(rect_point: Point, mouse_point: Point, rect_dim: Dimensions) -> bool { + let point = vec2_sub(rect_point, mouse_point); + if point[0].abs() < rect_dim[0] / 2.0 && point[1].abs() < rect_dim[1] / 2.0 { true } + else { false } } /// Get value percentage between max and min. diff --git a/src/widget/button.rs b/src/widget/button.rs index ccbb40620..364fb77c0 100644 --- a/src/widget/button.rs +++ b/src/widget/button.rs @@ -1,20 +1,15 @@ use color::{Color, Colorable}; -use dimensions::Dimensions; use frame::Frameable; -use graphics::Graphics; use graphics::character::CharacterCache; use label::{FontSize, Labelable}; use mouse::Mouse; -use point::Point; -use position::Positionable; -use rectangle; -use shape::Shapeable; +use position::{Depth, Dimensions, HorizontalAlign, Position, Positionable, VerticalAlign}; use ui::{UiId, Ui}; use widget::Kind; /// Represents the state of the Button widget. -#[derive(PartialEq, Clone, Copy)] +#[derive(PartialEq, Clone, Copy, Debug)] pub enum State { Normal, Highlighted, @@ -22,12 +17,12 @@ pub enum State { } impl State { - /// Return the associated Rectangle state. - fn as_rectangle_state(&self) -> rectangle::State { - match self { - &State::Normal => rectangle::State::Normal, - &State::Highlighted => rectangle::State::Highlighted, - &State::Clicked => rectangle::State::Clicked, + /// Alter the widget color depending on the state. + fn color(&self, color: Color) -> Color { + match *self { + State::Normal => color, + State::Highlighted => color.highlighted(), + State::Clicked => color.clicked(), } } } @@ -35,9 +30,7 @@ impl State { widget_fns!(Button, State, Kind::Button(State::Normal)); /// Check the current state of the button. -fn get_new_state(is_over: bool, - prev: State, - mouse: Mouse) -> State { +fn get_new_state(is_over: bool, prev: State, mouse: Mouse) -> State { use mouse::ButtonState::{Down, Up}; use self::State::{Normal, Highlighted, Clicked}; match (is_over, prev, mouse.left) { @@ -51,9 +44,11 @@ fn get_new_state(is_over: bool, /// A pressable button widget whose callback is triggered upon release. pub struct Button<'a, F> { - ui_id: UiId, - pos: Point, + pos: Position, dim: Dimensions, + maybe_h_align: Option, + maybe_v_align: Option, + depth: Depth, maybe_color: Option, maybe_frame: Option, maybe_frame_color: Option, @@ -66,11 +61,13 @@ pub struct Button<'a, F> { impl<'a, F> Button<'a, F> { /// Create a button context to be built upon. - pub fn new(ui_id: UiId) -> Button<'a, F> { + pub fn new() -> Button<'a, F> { Button { - ui_id: ui_id, - pos: [0.0, 0.0], + pos: Position::default(), dim: [64.0, 64.0], + maybe_h_align: None, + maybe_v_align: None, + depth: 0.0, maybe_callback: None, maybe_color: None, maybe_frame: None, @@ -87,6 +84,65 @@ impl<'a, F> Button<'a, F> { self } + /// After building the Button, use this method to set its current state into the given `Ui`. + /// It will use this state for rendering the next time `ui.draw(graphics)` is called. + pub fn set(mut self, ui_id: UiId, ui: &mut Ui) + where + C: CharacterCache, + F: FnMut(), + { + use elmesque::form::{collage, rect, text}; + use utils::is_over_rect; + + let state = *get_state(ui, ui_id); + let dim = self.dim; + let h_align = self.maybe_h_align.unwrap_or(ui.theme.h_align); + let v_align = self.maybe_v_align.unwrap_or(ui.theme.v_align); + let xy = ui.get_xy(self.pos, dim, h_align, v_align); + let mouse = ui.get_mouse_state().relative_to(xy); + let is_over = is_over_rect([0.0, 0.0], mouse.xy, dim); + let new_state = get_new_state(is_over, state, mouse); + + // Callback. + if let (true, State::Clicked, State::Highlighted) = (is_over, state, new_state) { + if let Some(ref mut callback) = self.maybe_callback { callback() } + } + + // Consruct the frame and pressable Forms. + let frame_w = self.maybe_frame.unwrap_or(ui.theme.frame_width); + let frame_color = self.maybe_frame_color.unwrap_or(ui.theme.frame_color); + let frame_form = rect(dim[0], dim[1]).filled(frame_color); + let (inner_w, inner_h) = (dim[0] - frame_w * 2.0, dim[1] - frame_w * 2.0); + let color = new_state.color(self.maybe_color.unwrap_or(ui.theme.shape_color)); + let pressable_form = rect(inner_w, inner_h).filled(color); + + // Construct the label's Form. + let maybe_label_form = self.maybe_label.map(|label_text| { + use elmesque::text::Text; + let text_color = self.maybe_label_color.unwrap_or(ui.theme.label_color); + let size = self.maybe_label_font_size.unwrap_or(ui.theme.font_size_medium); + text(Text::from_string(label_text.to_string()).color(text_color).height(size as f64)) + }); + + // Construct the button's Form. + let form_chain = Some(frame_form).into_iter() + .chain(Some(pressable_form).into_iter()) + .chain(maybe_label_form.into_iter()) + .map(|form| form.shift(xy[0].floor(), xy[1].floor())); + + // Turn the form into a renderable Element. + let element = collage(dim[0] as i32, dim[1] as i32, form_chain.collect()); + + // Store the button's new state in the Ui. + ui.set_widget(ui_id, ::widget::Widget { + kind: Kind::Button(new_state), + xy: xy, + depth: self.depth, + element: Some(element), + }); + + } + } impl<'a, F> Colorable for Button<'a, F> { @@ -125,67 +181,30 @@ impl<'a, F> Labelable<'a> for Button<'a, F> { } impl<'a, F> Positionable for Button<'a, F> { - fn point(mut self, pos: Point) -> Self { + fn position(mut self, pos: Position) -> Self { self.pos = pos; self } + #[inline] + fn horizontal_align(self, h_align: HorizontalAlign) -> Self { + Button { maybe_h_align: Some(h_align), ..self } + } + #[inline] + fn vertical_align(self, v_align: VerticalAlign) -> Self { + Button { maybe_v_align: Some(v_align), ..self } + } } -impl<'a, F> Shapeable for Button<'a, F> { - fn get_dim(&self) -> Dimensions { self.dim } - fn dim(mut self, dim: Dimensions) -> Self { self.dim = dim; self } -} - -impl<'a, F> ::draw::Drawable for Button<'a, F> - where - F: FnMut() + 'a -{ - - fn draw(&mut self, ui: &mut Ui, graphics: &mut B) - where - B: Graphics::Texture>, - C: CharacterCache - { - - let state = *get_state(ui, self.ui_id); - let mouse = ui.get_mouse_state(); - let is_over = rectangle::is_over(self.pos, mouse.pos, self.dim); - let new_state = get_new_state(is_over, state, mouse); - - // Callback. - match (is_over, state, new_state) { - (true, State::Clicked, State::Highlighted) => match self.maybe_callback { - Some(ref mut callback) => (*callback)(), None => (), - }, _ => (), - } - - // Draw. - let rect_state = new_state.as_rectangle_state(); - let color = self.maybe_color.unwrap_or(ui.theme.shape_color); - let frame_w = self.maybe_frame.unwrap_or(ui.theme.frame_width); - let maybe_frame = match frame_w > 0.0 { - true => Some((frame_w, self.maybe_frame_color.unwrap_or(ui.theme.frame_color))), - false => None, - }; - match self.maybe_label { - None => { - rectangle::draw( - ui.win_w, ui.win_h, graphics, rect_state, self.pos, - self.dim, maybe_frame, color - ) - }, - Some(text) => { - let text_color = self.maybe_label_color.unwrap_or(ui.theme.label_color); - let size = self.maybe_label_font_size.unwrap_or(ui.theme.font_size_medium); - rectangle::draw_with_centered_label( - ui.win_w, ui.win_h, graphics, ui, rect_state, - self.pos, self.dim, maybe_frame, color, - text, size, text_color - ) - }, - } - - set_state(ui, self.ui_id, Kind::Button(new_state), self.pos, self.dim); - +impl<'a, F> ::position::Sizeable for Button<'a, F> { + #[inline] + fn width(self, w: f64) -> Self { + let h = self.dim[1]; + Button { dim: [w, h], ..self } + } + #[inline] + fn height(self, h: f64) -> Self { + let w = self.dim[0]; + Button { dim: [w, h], ..self } } } + diff --git a/src/widget/drop_down_list.rs b/src/widget/drop_down_list.rs index 4f1f4fcfc..8f0698615 100644 --- a/src/widget/drop_down_list.rs +++ b/src/widget/drop_down_list.rs @@ -1,17 +1,11 @@ use color::{Color, Colorable}; -use dimensions::Dimensions; use frame::Frameable; -use graphics::Graphics; use graphics::character::CharacterCache; use label::{FontSize, Labelable}; use mouse::Mouse; -use point::Point; -use position::Positionable; -use rectangle; -use shape::Shapeable; +use position::{Depth, Dimensions, HorizontalAlign, Point, Position, Positionable, VerticalAlign}; use ui::{UiId, Ui}; -use vecmath::vec2_add; use widget::Kind; /// Tuple / Callback params. @@ -19,14 +13,14 @@ pub type Idx = usize; pub type Len = usize; /// Represents the state of the menu. -#[derive(PartialEq, Clone, Copy)] +#[derive(PartialEq, Clone, Copy, Debug)] pub enum State { Closed(DrawState), Open(DrawState), } /// Represents the state of the DropDownList widget. -#[derive(PartialEq, Clone, Copy)] +#[derive(PartialEq, Clone, Copy, Debug)] pub enum DrawState { Normal, Highlighted(Idx, Len), @@ -34,21 +28,11 @@ pub enum DrawState { } impl DrawState { - /// Translate the DropDownList's DrawState to the equivalent rectangle::State. - fn as_rect_state(&self) -> rectangle::State { - match self { - &DrawState::Normal => rectangle::State::Normal, - &DrawState::Highlighted(_, _) => rectangle::State::Highlighted, - &DrawState::Clicked(_, _) => rectangle::State::Clicked, - } - } -} - -impl State { - /// Translate the DropDownList's State to the equivalent rectangle::State. - fn as_rect_state(&self) -> rectangle::State { - match self { - &State::Open(draw_state) | &State::Closed(draw_state) => draw_state.as_rect_state(), + fn color(&self, color: Color) -> Color { + match *self { + DrawState::Normal => color, + DrawState::Highlighted(_, _) => color.highlighted(), + DrawState::Clicked(_, _) => color.clicked(), } } } @@ -56,23 +40,24 @@ impl State { widget_fns!(DropDownList, State, Kind::DropDownList(State::Closed(DrawState::Normal))); /// Is the cursor currently over the widget? If so which item? -fn is_over(pos: Point, - mouse_pos: Point, +fn is_over(mouse_pos: Point, + frame_w: f64, dim: Dimensions, state: State, len: Len) -> Option { + use utils::is_over_rect; match state { - State::Closed(_) => { - match rectangle::is_over(pos, mouse_pos, dim) { - false => None, - true => Some(0), - } + State::Closed(_) => match is_over_rect([0.0, 0.0], mouse_pos, dim) { + false => None, + true => Some(0), }, State::Open(_) => { - let total_h = dim[1] * len as f64; - match rectangle::is_over(pos, mouse_pos, [dim[0], total_h]) { + let item_h = dim[1] - frame_w; + let total_h = item_h * len as f64; + let open_centre_y = -(total_h - item_h) / 2.0; + match is_over_rect([0.0, open_centre_y], mouse_pos, [dim[0], total_h]) { false => None, - true => Some((((mouse_pos[1] - pos[1]) / total_h) * len as f64) as usize), + true => Some(((mouse_pos[1] - item_h / 2.0).abs() / item_h) as usize), } }, } @@ -128,11 +113,13 @@ fn get_new_state(is_over_idx: Option, /// Displays a given `Vec` as a selectable drop down menu. It's callback is triggered upon /// selection of a list item. pub struct DropDownList<'a, F> { - ui_id: UiId, strings: &'a mut Vec, selected: &'a mut Option, - pos: Point, + pos: Position, dim: Dimensions, + maybe_h_align: Option, + maybe_v_align: Option, + depth: Depth, maybe_callback: Option, maybe_color: Option, maybe_frame: Option, @@ -145,15 +132,15 @@ pub struct DropDownList<'a, F> { impl<'a, F> DropDownList<'a, F> { /// Construct a new DropDownList. - pub fn new(ui_id: UiId, - strings: &'a mut Vec, - selected: &'a mut Option) -> DropDownList<'a, F> { + pub fn new(strings: &'a mut Vec, selected: &'a mut Option) -> DropDownList<'a, F> { DropDownList { - ui_id: ui_id, strings: strings, selected: selected, - pos: [0.0, 0.0], + pos: Position::default(), dim: [128.0, 32.0], + maybe_h_align: None, + maybe_v_align: None, + depth: 0.0, maybe_callback: None, maybe_color: None, maybe_frame: None, @@ -170,6 +157,131 @@ impl<'a, F> DropDownList<'a, F> { self } + /// After building the DropDownList, use this method to set its current state into the given + /// `Ui`. It will use this state for rendering the next time `ui.draw(graphics)` is called. + pub fn set(mut self, ui_id: UiId, ui: &mut Ui) + where + C: CharacterCache, + F: FnMut(&mut Option, Idx, String), + { + use elmesque::form::{collage, rect, text}; + use elmesque::text::Text; + + let state = *get_state(ui, ui_id); + let dim = self.dim; + let h_align = self.maybe_h_align.unwrap_or(ui.theme.h_align); + let v_align = self.maybe_v_align.unwrap_or(ui.theme.v_align); + let xy = ui.get_xy(self.pos, dim, h_align, v_align); + let mouse = ui.get_mouse_state().relative_to(xy); + let frame_w = self.maybe_frame.unwrap_or(ui.theme.frame_width); + let is_over_idx = is_over(mouse.xy, frame_w, dim, state, self.strings.len()); + let new_state = get_new_state(is_over_idx, self.strings.len(), state, mouse); + let selected = self.selected.and_then(|idx| if idx < self.strings.len() { Some(idx) } + else { None }); + + // Call the `callback` closure if mouse was released on one of the DropDownMenu items. + if let Some(ref mut callback) = self.maybe_callback { + if let (State::Open(o_d_state), State::Closed(c_d_state)) = (state, new_state) { + if let (DrawState::Clicked(idx, _), DrawState::Normal) = (o_d_state, c_d_state) { + *self.selected = selected; + callback(self.selected, idx, self.strings[idx].clone()) + } + } + } + + // Get the DropDownList's styling. + let color = self.maybe_color.unwrap_or(ui.theme.shape_color); + let t_size = self.maybe_label_font_size.unwrap_or(ui.theme.font_size_medium); + let t_color = self.maybe_label_color.unwrap_or(ui.theme.label_color); + let pad_dim = ::vecmath::vec2_sub(dim, [frame_w * 2.0; 2]); + let frame_color = self.maybe_frame_color.unwrap_or(ui.theme.frame_color); + + // Construct the DropDownList's Element. + let element = match new_state { + + State::Closed(draw_state) => { + let string = match selected { + Some(idx) => &(*self.strings)[idx][..], + None => match self.maybe_label { + Some(text) => text, + None => &(*self.strings)[0][..], + }, + }.to_string(); + let frame_form = rect(dim[0], dim[1]).filled(frame_color); + let inner_form = rect(pad_dim[0], pad_dim[1]).filled(draw_state.color(color)); + let text_form = text(Text::from_string(string) + .color(t_color) + .height(t_size as f64)); + + // Chain and shift the Forms into position. + let form_chain = Some(frame_form).into_iter() + .chain(Some(inner_form).into_iter()) + .chain(Some(text_form).into_iter()) + .map(|form| form.shift(xy[0].floor(), xy[1].floor())); + + // Collect the Form's into a renderable Element. + collage(dim[0] as i32, dim[1] as i32, form_chain.collect()) + }, + + State::Open(draw_state) => { + // Chain and shift the Forms into position. + let form_chain = self.strings.iter().enumerate().flat_map(|(i, string)| { + let color = match selected { + None => match draw_state { + DrawState::Normal => color, + DrawState::Highlighted(idx, _) => { + if i == idx { color.highlighted() } + else { color } + }, + DrawState::Clicked(idx, _) => { + if i == idx { color.clicked() } + else { color } + }, + }, + Some(sel_idx) => { + if sel_idx == i { color.clicked() } + else { + match draw_state { + DrawState::Normal => color, + DrawState::Highlighted(idx, _) => { + if i == idx { color.highlighted() } + else { color } + }, + DrawState::Clicked(idx, _) => { + if i == idx { color.clicked() } + else { color } + }, + } + } + }, + }; + let shift_amt = -(i as f64 * dim[1] - i as f64 * frame_w).floor(); + let frame_form = rect(dim[0], dim[1]).filled(frame_color); + let inner_form = rect(pad_dim[0], pad_dim[1]).filled(color); + let text_form = text(Text::from_string(string.clone()) + .color(t_color) + .height(t_size as f64)); + Some(frame_form.shift_y(shift_amt)).into_iter() + .chain(Some(inner_form.shift_y(shift_amt)).into_iter()) + .chain(Some(text_form.shift_y(shift_amt)).into_iter()) + }).map(|form| form.shift(xy[0].floor(), xy[1].floor())); + + // Collect the Form's into a renderable Element. + collage(dim[0] as i32, dim[1] as i32, form_chain.collect()) + }, + + }; + + // Store the drop down list's new state in the Ui. + ui.set_widget(ui_id, ::widget::Widget { + kind: Kind::DropDownList(new_state), + xy: xy, + depth: self.depth, + element: Some(element), + }); + + } + } impl<'a, F> Colorable for DropDownList<'a, F> { @@ -209,126 +321,30 @@ impl<'a, F> Labelable<'a> for DropDownList<'a, F> } impl<'a, F> Positionable for DropDownList<'a, F> { - fn point(mut self, pos: Point) -> Self { + fn position(mut self, pos: Position) -> Self { self.pos = pos; self } + #[inline] + fn horizontal_align(self, h_align: HorizontalAlign) -> Self { + DropDownList { maybe_h_align: Some(h_align), ..self } + } + #[inline] + fn vertical_align(self, v_align: VerticalAlign) -> Self { + DropDownList { maybe_v_align: Some(v_align), ..self } + } } -impl<'a, F> Shapeable for DropDownList<'a, F> { - fn get_dim(&self) -> Dimensions { self.dim } - fn dim(mut self, dim: Dimensions) -> Self { self.dim = dim; self } -} - -impl<'a, F> ::draw::Drawable for DropDownList<'a, F> - where - F: FnMut(&mut Option, Idx, String) + 'a -{ - - fn draw(&mut self, ui: &mut Ui, graphics: &mut B) - where - B: Graphics::Texture>, - C: CharacterCache - { - - let state = *get_state(ui, self.ui_id); - let mouse = ui.get_mouse_state(); - let is_over_idx = is_over(self.pos, mouse.pos, self.dim, state, self.strings.len()); - let new_state = get_new_state(is_over_idx, self.strings.len(), state, mouse); - - let sel = match *self.selected { - Some(idx) if idx < self.strings.len() => { Some(idx) }, - _ => None, - }; - let color = self.maybe_color.unwrap_or(ui.theme.shape_color); - let t_size = self.maybe_label_font_size.unwrap_or(ui.theme.font_size_medium); - let t_color = self.maybe_label_color.unwrap_or(ui.theme.label_color); - - // Call the `callback` closure if mouse was released - // on one of the DropDownMenu items. - match (state, new_state) { - (State::Open(o_d_state), State::Closed(c_d_state)) => { - match (o_d_state, c_d_state) { - (DrawState::Clicked(idx, _), DrawState::Normal) => { - match self.maybe_callback { - Some(ref mut callback) => (*callback)(self.selected, idx, (*self.strings)[idx].clone()), - None => (), - } - }, _ => (), - } - }, _ => (), - } - - let frame_w = self.maybe_frame.unwrap_or(ui.theme.frame_width); - let maybe_frame = match frame_w > 0.0 { - true => Some((frame_w, self.maybe_frame_color.unwrap_or(ui.theme.frame_color))), - false => None, - }; - - match new_state { - - State::Closed(_) => { - let rect_state = new_state.as_rect_state(); - let text = match sel { - Some(idx) => &(*self.strings)[idx][..], - None => match self.maybe_label { - Some(text) => text, - None => &(*self.strings)[0][..], - }, - }; - rectangle::draw_with_centered_label( - ui.win_w, ui.win_h, graphics, ui, rect_state, - self.pos, self.dim, maybe_frame, color, - text, t_size, t_color - ) - }, - - State::Open(draw_state) => { - for (i, string) in self.strings.iter().enumerate() { - let rect_state = match sel { - None => { - match draw_state { - DrawState::Normal => rectangle::State::Normal, - DrawState::Highlighted(idx, _) => { - if i == idx { rectangle::State::Highlighted } - else { rectangle::State::Normal } - }, - DrawState::Clicked(idx, _) => { - if i == idx { rectangle::State::Clicked } - else { rectangle::State::Normal } - }, - } - }, - Some(sel_idx) => { - if sel_idx == i { rectangle::State::Clicked } - else { - match draw_state { - DrawState::Normal => rectangle::State::Normal, - DrawState::Highlighted(idx, _) => { - if i == idx { rectangle::State::Highlighted } - else { rectangle::State::Normal } - }, - DrawState::Clicked(idx, _) => { - if i == idx { rectangle::State::Clicked } - else { rectangle::State::Normal } - }, - } - } - }, - }; - let idx_y = self.dim[1] * i as f64 - i as f64 * frame_w; - let idx_pos = vec2_add(self.pos, [0.0, idx_y]); - rectangle::draw_with_centered_label( - ui.win_w, ui.win_h, graphics, ui, rect_state, idx_pos, - self.dim, maybe_frame, color, &string, - t_size, t_color - ) - } - }, - - } - - set_state(ui, self.ui_id, Kind::DropDownList(new_state), self.pos, self.dim); - +impl<'a, F> ::position::Sizeable for DropDownList<'a, F> { + #[inline] + fn width(self, w: f64) -> Self { + let h = self.dim[1]; + DropDownList { dim: [w, h], ..self } + } + #[inline] + fn height(self, h: f64) -> Self { + let w = self.dim[0]; + DropDownList { dim: [w, h], ..self } } } + diff --git a/src/widget/envelope_editor.rs b/src/widget/envelope_editor.rs index 3c8cee65b..eac199676 100644 --- a/src/widget/envelope_editor.rs +++ b/src/widget/envelope_editor.rs @@ -1,21 +1,16 @@ use color::{Color, Colorable}; -use dimensions::Dimensions; use frame::Frameable; use graphics::math::Scalar; -use graphics::{self, Graphics}; use graphics::character::CharacterCache; use label::{self, FontSize, Labelable}; use mouse::Mouse; -use num::{Float, ToPrimitive, FromPrimitive}; -use point::Point; -use position::Positionable; -use rectangle::{self, Corner}; -use shape::Shapeable; +use num::{Float, NumCast}; +use position::{self, Corner, Depth, Dimensions, HorizontalAlign, Point, Position, VerticalAlign}; use std::cmp::Ordering; use ui::{UiId, Ui}; use utils::{clamp, map_range, percentage, val_to_string}; -use vecmath::{vec2_add, vec2_sub}; +use vecmath::vec2_sub; use widget::Kind; /// Represents the specific elements that the EnvelopeEditor is made up of. This is used to @@ -48,12 +43,12 @@ pub enum State { } impl State { - /// Return the associated Rectangle state. - fn as_rectangle_state(&self) -> rectangle::State { - match self { - &State::Normal => rectangle::State::Normal, - &State::Highlighted(_) => rectangle::State::Highlighted, - &State::Clicked(_, _) => rectangle::State::Clicked, + /// Alter the widget color depending on the state. + fn color(&self, color: Color) -> Color { + match *self { + State::Normal => color, + State::Highlighted(_) => color.highlighted(), + State::Clicked(_, _) => color.clicked(), } } } @@ -64,23 +59,23 @@ widget_fns!(EnvelopeEditor, State, Kind::EnvelopeEditor(State::Normal)); /// EnvelopeEditor. pub trait EnvelopePoint { /// A value on the X-axis of the envelope. - type X: Float + ToPrimitive + FromPrimitive + ToString; + type X: Float + NumCast + ToString; /// A value on the Y-axis of the envelope. - type Y: Float + ToPrimitive + FromPrimitive + ToString; + type Y: Float + NumCast + ToString; /// Return the X value. - fn get_x(&self) -> ::X; + fn get_x(&self) -> Self::X; /// Return the Y value. - fn get_y(&self) -> ::Y; + fn get_y(&self) -> Self::Y; /// Set the X value. - fn set_x(&mut self, _x: ::X); + fn set_x(&mut self, _x: Self::X); /// Set the Y value. - fn set_y(&mut self, _y: ::Y); + fn set_y(&mut self, _y: Self::Y); /// Return the bezier curve depth (-1. to 1.) for the next interpolation. fn get_curve(&self) -> f32 { 1.0 } /// Set the bezier curve depth (-1. to 1.) for the next interpolation. fn set_curve(&mut self, _curve: f32) {} /// Create a new EnvPoint. - fn new(_x: ::X, _y: ::Y) -> Self; + fn new(_x: Self::X, _y: Self::Y) -> Self; } impl EnvelopePoint for Point { @@ -100,39 +95,39 @@ impl EnvelopePoint for Point { /// Determine whether or not the cursor is over the EnvelopeEditor. If it is, return the element /// under the cursor and the closest EnvPoint to the cursor. -fn is_over_and_closest(pos: Point, - mouse_pos: Point, +fn is_over_and_closest(mouse_xy: Point, dim: Dimensions, - pad_pos: Point, pad_dim: Dimensions, - perc_env: &Vec<(f32, f32, f32)>, - pt_radius: f64) -> (Option, Option) { - match rectangle::is_over(pos, mouse_pos, dim) { - false => (None, None), - true => match rectangle::is_over(pad_pos, mouse_pos, pad_dim) { - false => (Some(Element::Rect), Some(Element::Rect)), - true => { - let mut closest_distance = ::std::f64::MAX; - let mut closest_env_point = Element::Pad; - for (i, p) in perc_env.iter().enumerate() { - let (x, y, _) = *p; - let p_pos = [map_range(x, 0.0, 1.0, pad_pos[0], pad_pos[0] + pad_dim[0]), - map_range(y, 0.0, 1.0, pad_pos[1] + pad_dim[1], pad_pos[1])]; - let distance = (mouse_pos[0] - p_pos[0]).powf(2.0) - + (mouse_pos[1] - p_pos[1]).powf(2.0); - //let distance = ::std::num::abs(mouse_pos.x - p_pos.x); - if distance <= pt_radius.powf(2.0) { - return (Some(Element::EnvPoint(i, (p_pos[0], p_pos[1]))), - Some(Element::EnvPoint(i, (p_pos[0], p_pos[1])))) - } - else if distance < closest_distance { - closest_distance = distance; - closest_env_point = Element::EnvPoint(i, (p_pos[0], p_pos[1])); - } + perc_env: &[(f32, f32, f32)], + pt_radius: Scalar) -> (Option, Option) { + use utils::is_over_rect; + if is_over_rect([0.0, 0.0], mouse_xy, dim) { + if is_over_rect([0.0, 0.0], mouse_xy, pad_dim) { + let mut closest_distance = ::std::f64::MAX; + let mut closest_env_point = Element::Pad; + for (i, p) in perc_env.iter().enumerate() { + let (x, y, _) = *p; + let half_pad_w = pad_dim[0] / 2.0; + let half_pad_h = pad_dim[1] / 2.0; + let p_xy = [map_range(x, 0.0, 1.0, -half_pad_w, half_pad_w), + map_range(y, 0.0, 1.0, -half_pad_h, half_pad_h)]; + let distance = (mouse_xy[0] - p_xy[0]).powf(2.0) + + (mouse_xy[1] - p_xy[1]).powf(2.0); + if distance <= pt_radius.powf(2.0) { + return (Some(Element::EnvPoint(i, (p_xy[0], p_xy[1]))), + Some(Element::EnvPoint(i, (p_xy[0], p_xy[1])))) } - (Some(Element::Pad), Some(closest_env_point)) - }, - }, + else if distance < closest_distance { + closest_distance = distance; + closest_env_point = Element::EnvPoint(i, (p_xy[0], p_xy[1])); + } + } + (Some(Element::Pad), Some(closest_env_point)) + } else { + (Some(Element::Rect), Some(Element::Rect)) + } + } else { + (None, None) } } @@ -151,25 +146,25 @@ fn get_new_state(is_over_elem: Option, (Some(_), Clicked(p_elem, m_button), Down, Up) | (Some(_), Clicked(p_elem, m_button), Up, Down) => { match p_elem { - EnvPoint(idx, _) => Clicked(EnvPoint(idx, (mouse.pos[0], mouse.pos[1])), m_button), + EnvPoint(idx, _) => Clicked(EnvPoint(idx, (mouse.xy[0], mouse.xy[1])), m_button), CurvePoint(idx, _) => - Clicked(CurvePoint(idx, (mouse.pos[0], mouse.pos[1])), m_button), + Clicked(CurvePoint(idx, (mouse.xy[0], mouse.xy[1])), m_button), _ => Clicked(p_elem, m_button), } }, (None, Clicked(p_elem, m_button), Down, Up) => { match (p_elem, m_button) { (EnvPoint(idx, _), Left) => - Clicked(EnvPoint(idx, (mouse.pos[0], mouse.pos[1])), Left), + Clicked(EnvPoint(idx, (mouse.xy[0], mouse.xy[1])), Left), (CurvePoint(idx, _), Left) => - Clicked(CurvePoint(idx, (mouse.pos[0], mouse.pos[1])), Left), + Clicked(CurvePoint(idx, (mouse.xy[0], mouse.xy[1])), Left), _ => Clicked(p_elem, Left), } }, (Some(_), Highlighted(p_elem), Up, Down) => { match p_elem { - EnvPoint(idx, _) => Clicked(EnvPoint(idx, (mouse.pos[0], mouse.pos[1])), Right), - CurvePoint(idx, _) => Clicked(CurvePoint(idx, (mouse.pos[0], mouse.pos[1])), Right), + EnvPoint(idx, _) => Clicked(EnvPoint(idx, (mouse.xy[0], mouse.xy[1])), Right), + CurvePoint(idx, _) => Clicked(CurvePoint(idx, (mouse.xy[0], mouse.xy[1])), Right), _ => Clicked(p_elem, Right), } }, @@ -177,38 +172,22 @@ fn get_new_state(is_over_elem: Option, } } -/// Draw a circle at the given position. -fn draw_circle( - win_w: f64, - win_h: f64, - graphics: &mut B, - pos: Point, - color: Color, - radius: f64 -) { - graphics::Ellipse::new(color.to_fsa()) - .draw( - [pos[0], pos[1], 2.0 * radius, 2.0 * radius], - &graphics::default_draw_state(), - graphics::abs_transform(win_w, win_h), - graphics - ); -} - /// Used for editing a series of 2D Points on a cartesian (X, Y) plane within some given range. /// Useful for things such as oscillator/automation envelopes or any value series represented /// periodically. pub struct EnvelopeEditor<'a, E:'a, F> where E: EnvelopePoint { - ui_id: UiId, env: &'a mut Vec, skew_y_range: f32, min_x: E::X, max_x: E::X, min_y: E::Y, max_y: E::Y, pt_radius: f64, line_width: f64, - font_size: FontSize, - pos: Point, + value_font_size: FontSize, + pos: Position, dim: Dimensions, + maybe_h_align: Option, + maybe_v_align: Option, + depth: Depth, maybe_callback: Option, maybe_color: Option, maybe_frame: Option, @@ -235,7 +214,7 @@ impl<'a, E, F> EnvelopeEditor<'a, E, F> where E: EnvelopePoint { /// Set the font size for the displayed values. #[inline] pub fn value_font_size(self, size: FontSize) -> EnvelopeEditor<'a, E, F> { - EnvelopeEditor { font_size: size, ..self } + EnvelopeEditor { value_font_size: size, ..self } } /// Set the value skewing for the envelope's y-axis. This is useful for displaying exponential @@ -246,19 +225,21 @@ impl<'a, E, F> EnvelopeEditor<'a, E, F> where E: EnvelopePoint { } /// Construct an EnvelopeEditor widget. - pub fn new(ui_id: UiId, env: &'a mut Vec, - min_x: E::X, max_x: E::X, min_y: E::Y, max_y: E::Y) -> EnvelopeEditor<'a, E, F> { + pub fn new(env: &'a mut Vec, min_x: E::X, max_x: E::X, min_y: E::Y, max_y: E::Y) + -> EnvelopeEditor<'a, E, F> { EnvelopeEditor { - ui_id: ui_id, env: env, skew_y_range: 1.0, // Default skew amount (no skew). min_x: min_x, max_x: max_x, min_y: min_y, max_y: max_y, pt_radius: 6.0, // Default envelope point radius. line_width: 2.0, // Default envelope line width. - font_size: 18u32, - pos: [0.0, 0.0], + value_font_size: 14u32, + pos: Position::default(), dim: [256.0, 128.0], + maybe_h_align: None, + maybe_v_align: None, + depth: 0.0, maybe_callback: None, maybe_color: None, maybe_frame: None, @@ -275,103 +256,38 @@ impl<'a, E, F> EnvelopeEditor<'a, E, F> where E: EnvelopePoint { self } -} - -impl<'a, E, F> Colorable for EnvelopeEditor<'a, E, F> - where - E: EnvelopePoint -{ - fn color(mut self, color: Color) -> Self { - self.maybe_color = Some(color); - self - } -} - -impl<'a, E, F> Frameable for EnvelopeEditor<'a, E, F> - where - E: EnvelopePoint -{ - fn frame(mut self, width: f64) -> Self { - self.maybe_frame = Some(width); - self - } - fn frame_color(mut self, color: Color) -> Self { - self.maybe_frame_color = Some(color); - self - } -} - -impl<'a, E, F> Labelable<'a> for EnvelopeEditor<'a, E, F> - where - E: EnvelopePoint -{ - fn label(mut self, text: &'a str) -> Self { - self.maybe_label = Some(text); - self - } - - fn label_color(mut self, color: Color) -> Self { - self.maybe_label_color = Some(color); - self - } - - fn label_font_size(mut self, size: FontSize) -> Self { - self.maybe_label_font_size = Some(size); - self - } -} - -impl<'a, E, F> Positionable for EnvelopeEditor<'a, E, F> - where - E: EnvelopePoint -{ - fn point(mut self, pos: Point) -> Self { - self.pos = pos; - self - } -} - -impl<'a, E, F> Shapeable for EnvelopeEditor<'a, E, F> - where - E: EnvelopePoint -{ - fn get_dim(&self) -> Dimensions { self.dim } - fn dim(mut self, dim: Dimensions) -> Self { self.dim = dim; self } -} - -impl<'a, E, F> ::draw::Drawable for EnvelopeEditor<'a, E, F> - where - E: EnvelopePoint, - E::X: Float, - E::Y: Float, - F: FnMut(&mut Vec, usize) + 'a -{ - #[inline] - fn draw(&mut self, ui: &mut Ui, graphics: &mut B) + /// After building the EnvelopeEditor, use this method to set its current state into the given + /// `Ui`. It will use this state for rendering the next time `ui.draw(graphics)` is called. + pub fn set(mut self, ui_id: UiId, ui: &mut Ui) where - B: Graphics::Texture>, - C: CharacterCache + C: CharacterCache, + E: EnvelopePoint, + E::X: Float, + E::Y: Float, + F: FnMut(&mut Vec, usize), { - let state = *get_state(ui, self.ui_id); - let mouse = ui.get_mouse_state(); + use elmesque::form::{circle, collage, Form, line, rect, solid, text}; + use elmesque::text::Text; + + let state = *get_state(ui, ui_id); + let dim = self.dim; + let h_align = self.maybe_h_align.unwrap_or(ui.theme.h_align); + let v_align = self.maybe_v_align.unwrap_or(ui.theme.v_align); + let xy = ui.get_xy(self.pos, dim, h_align, v_align); + let mouse = ui.get_mouse_state().relative_to(xy); let skew = self.skew_y_range; let (min_x, max_x, min_y, max_y) = (self.min_x, self.max_x, self.min_y, self.max_y); let pt_radius = self.pt_radius; - let font_size = self.font_size; - - // Rect. - let color = self.maybe_color.unwrap_or(ui.theme.shape_color); + let value_font_size = self.value_font_size; let frame_w = self.maybe_frame.unwrap_or(ui.theme.frame_width); let frame_w2 = frame_w * 2.0; - let maybe_frame = match frame_w > 0.0 { - true => Some((frame_w, self.maybe_frame_color.unwrap_or(ui.theme.frame_color))), - false => None, - }; - let pad_pos = vec2_add(self.pos, [frame_w; 2]); - let pad_dim = vec2_sub(self.dim, [frame_w2; 2]); + let pad_dim = vec2_sub(dim, [frame_w2; 2]); + let half_pad_w = pad_dim[0] / 2.0; + let half_pad_h = pad_dim[1] / 2.0; + let line_width = self.line_width; - // Create a vector with each EnvelopePoint value represented as a - // skewed percentage between 0.0 .. 1.0 . + // Create a vector with each EnvelopePoint value represented as a skewed weight + // between 0.0 and 1.0. let perc_env: Vec<(f32, f32, f32)> = self.env.iter().map(|pt| { (percentage(pt.get_x(), min_x, max_x), percentage(pt.get_y(), min_y, max_y).powf(1.0 / skew), @@ -379,145 +295,117 @@ impl<'a, E, F> ::draw::Drawable for EnvelopeEditor<'a, E, F> }).collect(); // Check for new state. - let (is_over_elem, is_closest_elem) = is_over_and_closest( - self.pos, mouse.pos, self.dim, - pad_pos, pad_dim, &perc_env, pt_radius - ); + let (is_over_elem, is_closest_elem) = + is_over_and_closest(mouse.xy, dim, pad_dim, &perc_env[..], pt_radius); let new_state = get_new_state(is_over_elem, state, mouse); - // Draw rect. - rectangle::draw(ui.win_w, ui.win_h, graphics, - new_state.as_rectangle_state(), - self.pos, self.dim, maybe_frame, color); + // Construct the frame and inner rectangle Forms. + let frame_color = self.maybe_frame_color.unwrap_or(ui.theme.frame_color); + let frame_form = rect(dim[0], dim[1]).filled(frame_color); + let color = new_state.color(self.maybe_color.unwrap_or(ui.theme.shape_color)); + let pressable_form = rect(pad_dim[0], pad_dim[1]).filled(color); - // If there's a label, draw it. - if let Some(l_text) = self.maybe_label { - let l_size = self.maybe_label_font_size.unwrap_or(ui.theme.font_size_medium); + // Construct the label Form. + let maybe_label_form = self.maybe_label.map(|l_text| { let l_color = self.maybe_label_color.unwrap_or(ui.theme.label_color); - let l_w = label::width(ui, l_size, l_text); - let l_pos = [pad_pos[0] + (pad_dim[0] - l_w) / 2.0, - pad_pos[1] + (pad_dim[1] - l_size as f64) / 2.0]; - ui.draw_text(graphics, l_pos, l_size, l_color, l_text); - }; + let l_size = self.maybe_label_font_size.unwrap_or(ui.theme.font_size_medium); + text(Text::from_string(l_text.to_string()).color(l_color).height(l_size as f64)) + }); // Draw the envelope lines. - match self.env.len() { - 0 | 1 => (), - _ => { - let color = color.plain_contrast(); - let line = graphics::Line::new_round(color.to_fsa(), 0.5 * self.line_width); - let draw_state = graphics::default_draw_state(); - let transform = graphics::abs_transform(ui.win_w, ui.win_h); - for i in 1..perc_env.len() { - let (x_a, y_a, _) = perc_env[i - 1]; - let (x_b, y_b, _) = perc_env[i]; - let p_a = [map_range(x_a, 0.0, 1.0, pad_pos[0], pad_pos[0] + pad_dim[0]), - map_range(y_a, 0.0, 1.0, pad_pos[1] + pad_dim[1], pad_pos[1])]; - let p_b = [map_range(x_b, 0.0, 1.0, pad_pos[0], pad_pos[0] + pad_dim[0]), - map_range(y_b, 0.0, 1.0, pad_pos[1] + pad_dim[1], pad_pos[1])]; - line.draw( - [p_a[0], p_a[1], p_b[0], p_b[1]], - draw_state, - transform, - graphics - ); - } - }, - } + let line_color = color.plain_contrast(); + let envelope_line_forms = perc_env.windows(2).map(|window| { + let ((x_a, y_a, _), (x_b, y_b, _)) = (window[0], window[1]); + let p_a = [map_range(x_a, 0.0, 1.0, -half_pad_w, half_pad_w), + map_range(y_a, 0.0, 1.0, -half_pad_h, half_pad_h)]; + let p_b = [map_range(x_b, 0.0, 1.0, -half_pad_w, half_pad_w), + map_range(y_b, 0.0, 1.0, -half_pad_h, half_pad_h)]; + let style = solid(line_color).width(line_width); + line(style, p_a[0], p_a[1], p_b[0], p_b[1]) + }); // Determine the left and right X bounds for a point. - let get_x_bounds = |envelope_perc: &Vec<(f32, f32, f32)>, idx: usize| -> (f32, f32) { + let get_x_bounds = |envelope_perc: &[(f32, f32, f32)], idx: usize| -> (f32, f32) { let right_bound = if envelope_perc.len() > 0 && envelope_perc.len() - 1 > idx { - (*envelope_perc)[idx + 1].0 + envelope_perc[idx + 1].0 // X value of point on right. } else { 1.0 }; let left_bound = if envelope_perc.len() > 0 && idx > 0 { - (*envelope_perc)[idx - 1].0 + envelope_perc[idx - 1].0 // X value of point on left. } else { 0.0 }; (left_bound, right_bound) }; - // Draw the (closest) envelope point and it's label and - // return the idx if it is currently clicked. - let is_clicked_env_point = match (state, new_state) { + // Draw the closest envelope point and it's label. Return the idx if it is currently clicked. + let (maybe_closest_point_form, is_clicked_env_point) = match (state, new_state) { (_, State::Clicked(elem, _)) | (_, State::Highlighted(elem)) => { - - // Draw the envelope point. - let mut draw_env_pt = |ui: &mut Ui, - envelope: &mut Vec, - idx: usize, - p_pos: Point| { - - let x_string = val_to_string( - (*envelope)[idx].get_x(), - max_x, - max_x - min_x, - pad_dim[0] as usize - ); - let y_string = val_to_string( - (*envelope)[idx].get_y(), - max_y, - max_y - min_y, - pad_dim[1] as usize - ); + use std::iter::Chain; + use std::option::IntoIter; + + // Construct a Form for an envelope point and it's value in text form. + let env_pt_form = |ui: &mut Ui, env: &[E], idx: usize, p_pos: Point| + -> Chain, IntoIter
> { + let x_range = max_x - min_x; + let y_range = max_y - min_y; + let x_px_range = pad_dim[0] as usize; + let y_px_range = pad_dim[1] as usize; + let x_string = val_to_string(env[idx].get_x(), max_x, x_range, x_px_range); + let y_string = val_to_string(env[idx].get_y(), max_y, y_range, y_px_range); let xy_string = format!("{}, {}", x_string, y_string); - let xy_string_w = label::width(ui, font_size, &xy_string); - let xy_string_pos = match rectangle::corner(pad_pos, p_pos, pad_dim) { - Corner::TopLeft => [p_pos[0], p_pos[1]], - Corner::TopRight => [p_pos[0] - xy_string_w, p_pos[1]], - Corner::BottomLeft => [p_pos[0], p_pos[1] - font_size as f64], - Corner::BottomRight => [p_pos[0] - xy_string_w, p_pos[1] - font_size as f64], + const PAD: f64 = 5.0; // Slight padding between the crosshair and the text. + let w = label::width(ui, value_font_size, &xy_string); + let h = value_font_size as f64; + let x_shift = w / 2.0 + PAD; + let y_shift = h / 2.0 + PAD; + let (text_x, text_y) = match position::corner(p_pos, pad_dim) { + Corner::TopLeft => (x_shift, -y_shift), + Corner::TopRight => (-x_shift, -y_shift), + Corner::BottomLeft => (x_shift, y_shift), + Corner::BottomRight => (-x_shift, y_shift), }; - ui.draw_text(graphics, xy_string_pos, - font_size, color.plain_contrast(), &xy_string); - draw_circle(ui.win_w, ui.win_h, graphics, - vec2_sub(p_pos, [pt_radius, pt_radius]), - color.plain_contrast(), pt_radius); + let color = color.plain_contrast(); + let circle_form = circle(pt_radius).filled(color) + .shift(p_pos[0].floor(), p_pos[1].floor()); + let text_form = text(Text::from_string(xy_string).color(color).height(h)) + .shift(p_pos[0], p_pos[1]) + .shift(text_x.floor(), text_y.floor()); + Some(circle_form).into_iter().chain(Some(text_form).into_iter()) }; match elem { // If a point is clicked, draw that point. - Element::EnvPoint(idx, p_pos) => { - let p_pos = [p_pos.0, p_pos.1]; - let pad_x_right = pad_pos[0] + pad_dim[0]; - let (left_x_bound, right_x_bound) = get_x_bounds(&perc_env, idx); - let left_pixel_bound = map_range(left_x_bound, 0.0, 1.0, pad_pos[0], pad_x_right); - let right_pixel_bound = map_range(right_x_bound, 0.0, 1.0, pad_pos[0], pad_x_right); - let p_pos_x_clamped = clamp(p_pos[0], left_pixel_bound, right_pixel_bound); - let p_pos_y_clamped = clamp(p_pos[1], pad_pos[1], pad_pos[1] + pad_dim[1]); - draw_env_pt(ui, self.env, idx, [p_pos_x_clamped, p_pos_y_clamped]); - Some(idx) + Element::EnvPoint(idx, (x, y)) => { + let (left_x_bound, right_x_bound) = get_x_bounds(&perc_env[..], idx); + let left_pixel_bound = map_range(left_x_bound, 0.0, 1.0, -half_pad_w, half_pad_w); + let right_pixel_bound = map_range(right_x_bound, 0.0, 1.0, -half_pad_w, half_pad_w); + let p_pos_x_clamped = clamp(x, left_pixel_bound, right_pixel_bound); + let p_pos_y_clamped = clamp(y, -half_pad_h, half_pad_h); + let p_pos_clamped = [p_pos_x_clamped, p_pos_y_clamped]; + let point_form = env_pt_form(ui, &self.env[..], idx, p_pos_clamped); + (Some(point_form), Some(idx)) }, - // Otherwise, draw the closest point. - Element::Pad => { - for closest_elem in is_closest_elem.iter() { - match *closest_elem { - Element::EnvPoint(closest_idx, closest_env_pt) => { - let closest_env_pt = [closest_env_pt.0, closest_env_pt.1]; - draw_env_pt(ui, self.env, closest_idx, closest_env_pt); - }, - _ => (), - } - } - None - }, _ => None, + // Otherwise, draw the closest point if there is one. + Element::Pad => match is_closest_elem { + Some(Element::EnvPoint(closest_idx, (x, y))) => { + let point_form = env_pt_form(ui, &self.env[..], closest_idx, [x, y]); + (Some(point_form), None) + }, + _ => (None, None), + }, + _ => (None, None), } - }, _ => None, + }, + _ => (None, None), }; // Determine new values. - let get_new_value = |perc_envelope: &Vec<(f32, f32, f32)>, - idx: usize, - mouse_x: f64, - mouse_y: f64| -> (E::X, E::Y) { - let mouse_x_on_pad = mouse_x - pad_pos[0]; - let mouse_y_on_pad = mouse_y - pad_pos[1]; - let mouse_x_clamped = clamp(mouse_x_on_pad, 0f64, pad_dim[0]); - let mouse_y_clamped = clamp(mouse_y_on_pad, 0.0, pad_dim[1]); - let new_x_perc = percentage(mouse_x_clamped, 0f64, pad_dim[0]); - let new_y_perc = percentage(mouse_y_clamped, pad_dim[1], 0f64).powf(skew); + let get_new_value = |perc_envelope: &[(f32, f32, f32)], idx: usize| -> (E::X, E::Y) { + let mouse_x_clamped = clamp(mouse.xy[0], -half_pad_w, half_pad_w); + let mouse_y_clamped = clamp(mouse.xy[1], -half_pad_h, half_pad_h); + let new_x_perc = percentage(mouse_x_clamped, -half_pad_w, half_pad_w); + let new_y_perc = percentage(mouse_y_clamped, -half_pad_h, half_pad_h).powf(skew); let (left_bound, right_bound) = get_x_bounds(perc_envelope, idx); (map_range(if new_x_perc > right_bound { right_bound } else if new_x_perc < left_bound { left_bound } @@ -525,8 +413,7 @@ impl<'a, E, F> ::draw::Drawable for EnvelopeEditor<'a, E, F> map_range(new_y_perc, 0.0, 1.0, min_y, max_y)) }; - // If a point is currently clicked, check for callback - // and value setting conditions. + // If a point is currently clicked, check for callback and value setting conditions. match is_clicked_env_point { Some(idx) => { @@ -534,11 +421,12 @@ impl<'a, E, F> ::draw::Drawable for EnvelopeEditor<'a, E, F> // Call the `callback` closure if mouse was released // on one of the DropDownMenu items. match (state, new_state) { - (State::Clicked(_, m_button), State::Highlighted(_)) | (State::Clicked(_, m_button), State::Normal) => { + (State::Clicked(_, m_button), State::Highlighted(_)) | + (State::Clicked(_, m_button), State::Normal) => { match m_button { MouseButton::Left => { // Adjust the point and trigger the callback. - let (new_x, new_y) = get_new_value(&perc_env, idx, mouse.pos[0], mouse.pos[1]); + let (new_x, new_y) = get_new_value(&perc_env[..], idx); self.env[idx].set_x(new_x); self.env[idx].set_y(new_y); match self.maybe_callback { @@ -560,9 +448,9 @@ impl<'a, E, F> ::draw::Drawable for EnvelopeEditor<'a, E, F> (State::Clicked(_, prev_m_button), State::Clicked(_, m_button)) => { match (prev_m_button, m_button) { (MouseButton::Left, MouseButton::Left) => { - let (new_x, new_y) = get_new_value(&perc_env, idx, mouse.pos[0], mouse.pos[1]); - let current_x = (*self.env)[idx].get_x(); - let current_y = (*self.env)[idx].get_y(); + let (new_x, new_y) = get_new_value(&perc_env[..], idx); + let current_x = self.env[idx].get_x(); + let current_y = self.env[idx].get_y(); if new_x != current_x || new_y != current_y { // Adjust the point and trigger the callback. self.env[idx].set_x(new_x); @@ -582,14 +470,13 @@ impl<'a, E, F> ::draw::Drawable for EnvelopeEditor<'a, E, F> None => { - // Check if a there are no points. If there are - // and the mouse was clicked, add a point. + // Check if a there are no points. If so and the mouse was clicked, add a point. if self.env.len() == 0 { match (state, new_state) { (State::Clicked(elem, m_button), State::Highlighted(_)) => { match (elem, m_button) { (Element::Pad, MouseButton::Left) => { - let (new_x, new_y) = get_new_value(&perc_env, 0, mouse.pos[0], mouse.pos[1]); + let (new_x, new_y) = get_new_value(&perc_env[..], 0); let new_point = EnvelopePoint::new(new_x, new_y); self.env.push(new_point); }, _ => (), @@ -605,12 +492,10 @@ impl<'a, E, F> ::draw::Drawable for EnvelopeEditor<'a, E, F> match (elem, m_button) { (Element::Pad, MouseButton::Left) => { let (new_x, new_y) = { - let mouse_x_on_pad = mouse.pos[0] - pad_pos[0]; - let mouse_y_on_pad = mouse.pos[1] - pad_pos[1]; - let mouse_x_clamped = clamp(mouse_x_on_pad, 0f64, pad_dim[0]); - let mouse_y_clamped = clamp(mouse_y_on_pad, 0.0, pad_dim[1]); - let new_x_perc = percentage(mouse_x_clamped, 0f64, pad_dim[0]); - let new_y_perc = percentage(mouse_y_clamped, pad_dim[1], 0f64).powf(skew); + let mouse_x = clamp(mouse.xy[0], -half_pad_w, half_pad_w); + let mouse_y = clamp(mouse.xy[1], -half_pad_h, half_pad_h); + let new_x_perc = percentage(mouse_x, -half_pad_w, half_pad_w); + let new_y_perc = percentage(mouse_y, -half_pad_h, half_pad_h).powf(skew); (map_range(new_x_perc, 0.0, 1.0, min_x, max_x), map_range(new_y_perc, 0.0, 1.0, min_y, max_y)) }; @@ -629,8 +514,111 @@ impl<'a, E, F> ::draw::Drawable for EnvelopeEditor<'a, E, F> } - // Set the new state. - set_state(ui, self.ui_id, Kind::EnvelopeEditor(new_state), self.pos, self.dim); + // Group the different Forms into a single form. + let form_chain = Some(frame_form).into_iter() + .chain(Some(pressable_form).into_iter()) + .chain(maybe_label_form.into_iter()) + .chain(envelope_line_forms); + let forms = match maybe_closest_point_form { + Some(closest_point_form) => form_chain + .chain(closest_point_form) + .map(|form| form.shift(xy[0].floor(), xy[1].floor())) + .collect(), + None => form_chain + .map(|form| form.shift(xy[0].floor(), xy[1].floor())) + .collect(), + }; + + // Turn the form into a renderable element. + let element = collage(dim[0] as i32, dim[1] as i32, forms); + + // Store the EnvelopeEditor's new state in the Ui. + ui.set_widget(ui_id, ::widget::Widget { + kind: Kind::EnvelopeEditor(new_state), + xy: xy, + depth: self.depth, + element: Some(element), + }); } + } + +impl<'a, E, F> Colorable for EnvelopeEditor<'a, E, F> + where + E: EnvelopePoint +{ + fn color(mut self, color: Color) -> Self { + self.maybe_color = Some(color); + self + } +} + +impl<'a, E, F> Frameable for EnvelopeEditor<'a, E, F> + where + E: EnvelopePoint +{ + fn frame(mut self, width: f64) -> Self { + self.maybe_frame = Some(width); + self + } + fn frame_color(mut self, color: Color) -> Self { + self.maybe_frame_color = Some(color); + self + } +} + +impl<'a, E, F> Labelable<'a> for EnvelopeEditor<'a, E, F> + where + E: EnvelopePoint +{ + fn label(mut self, text: &'a str) -> Self { + self.maybe_label = Some(text); + self + } + + fn label_color(mut self, color: Color) -> Self { + self.maybe_label_color = Some(color); + self + } + + fn label_font_size(mut self, size: FontSize) -> Self { + self.maybe_label_font_size = Some(size); + self + } +} + +impl<'a, E, F> position::Positionable for EnvelopeEditor<'a, E, F> + where + E: EnvelopePoint +{ + fn position(mut self, pos: Position) -> Self { + self.pos = pos; + self + } + #[inline] + fn horizontal_align(self, h_align: HorizontalAlign) -> Self { + EnvelopeEditor { maybe_h_align: Some(h_align), ..self } + } + #[inline] + fn vertical_align(self, v_align: VerticalAlign) -> Self { + EnvelopeEditor { maybe_v_align: Some(v_align), ..self } + } +} + +impl<'a, E, F> position::Sizeable for EnvelopeEditor<'a, E, F> + where + E: EnvelopePoint +{ + #[inline] + fn width(self, w: f64) -> Self { + let h = self.dim[1]; + EnvelopeEditor { dim: [w, h], ..self } + } + #[inline] + fn height(self, h: f64) -> Self { + let w = self.dim[0]; + EnvelopeEditor { dim: [w, h], ..self } + } +} + diff --git a/src/widget/label.rs b/src/widget/label.rs index 7e22f2498..a8e20d89c 100644 --- a/src/widget/label.rs +++ b/src/widget/label.rs @@ -1,41 +1,68 @@ -use color::{black, Color, Colorable}; -use graphics::Graphics; +use color::{Color, Colorable}; use graphics::character::CharacterCache; -use label::FontSize; -use point::Point; -use position::Positionable; -use ui::Ui; +use label::{self, FontSize}; +use position::{Depth, HorizontalAlign, Position, Positionable, VerticalAlign}; +use ui::{Ui, UiId}; /// Displays some given text centred within a rectangle. pub struct Label<'a> { text: &'a str, - pos: Point, + pos: Position, + maybe_h_align: Option, + maybe_v_align: Option, + depth: Depth, size: FontSize, maybe_color: Option, } -impl<'a> Label<'a> { - - /// Set the font size for the label. - pub fn size(self, size: FontSize) -> Label<'a> { - Label { size: size, ..self } - } - -} - impl<'a> Label<'a> { /// Construct a new Label widget. pub fn new(text: &'a str) -> Label<'a> { Label { text: text, - pos: [0.0, 0.0], + pos: Position::default(), + maybe_h_align: None, + maybe_v_align: None, + depth: 0.0, size: 24u32, maybe_color: None, } } + /// Set the font size for the label. + #[inline] + pub fn size(self, size: FontSize) -> Label<'a> { + Label { size: size, ..self } + } + + /// After building the Label, use this method to set its current state into the given `Ui`. It + /// will use this state for rendering the next time `ui.draw(graphics)` is called. + pub fn set(self, ui_id: UiId, ui: &mut Ui) + where + C: CharacterCache, + { + use elmesque::form::{text, collage}; + use elmesque::text::Text; + let dim = [label::width(ui, self.size, self.text), self.size as f64]; + let h_align = self.maybe_h_align.unwrap_or(ui.theme.h_align); + let v_align = self.maybe_v_align.unwrap_or(ui.theme.v_align); + let xy = ui.get_xy(self.pos, dim, h_align, v_align); + let color = self.maybe_color.unwrap_or(ui.theme.label_color); + let form = text(Text::from_string(self.text.to_string()) + .color(color) + .height(self.size as f64)).shift(xy[0].floor(), xy[1].floor()); + let element = collage(dim[0] as i32, dim[1] as i32, vec![form]); + // Store the label's new state in the Ui. + ui.set_widget(ui_id, ::widget::Widget { + kind: ::widget::Kind::Label, + xy: xy, + depth: self.depth, + element: Some(element), + }); + } + } impl<'a> Colorable for Label<'a> { @@ -46,20 +73,17 @@ impl<'a> Colorable for Label<'a> { } impl<'a> Positionable for Label<'a> { - fn point(mut self, pos: Point) -> Self { + fn position(mut self, pos: Position) -> Self { self.pos = pos; self } -} - -impl<'a> ::draw::Drawable for Label<'a> { - fn draw(&mut self, ui: &mut Ui, graphics: &mut B) - where - B: Graphics::Texture>, - C: CharacterCache - { - let color = self.maybe_color.unwrap_or(black()); - ui.draw_text(graphics, self.pos, self.size, color, self.text); + #[inline] + fn horizontal_align(self, h_align: HorizontalAlign) -> Self { + Label { maybe_h_align: Some(h_align), ..self } + } + #[inline] + fn vertical_align(self, v_align: VerticalAlign) -> Self { + Label { maybe_v_align: Some(v_align), ..self } } } diff --git a/src/widget/matrix.rs b/src/widget/matrix.rs index 90a0b7704..02583730e 100644 --- a/src/widget/matrix.rs +++ b/src/widget/matrix.rs @@ -1,8 +1,6 @@ -use dimensions::Dimensions; -use point::Point; -use position::Positionable; -use shape::Shapeable; +use position::{self, Dimensions, HorizontalAlign, Point, Position, VerticalAlign}; +use ui::Ui; /// Callback params. pub type WidgetNum = usize; @@ -20,8 +18,10 @@ pub type PosY = f64; pub struct Matrix { cols: usize, rows: usize, - pos: Point, + pos: Position, dim: Dimensions, + maybe_h_align: Option, + maybe_v_align: Option, cell_pad_w: f64, cell_pad_h: f64, } @@ -38,8 +38,10 @@ impl Matrix { Matrix { cols: cols, rows: rows, - pos: [0.0, 0.0], + pos: Position::default(), dim: [256.0, 256.0], + maybe_h_align: None, + maybe_v_align: None, cell_pad_w: 0.0, cell_pad_h: 0.0, } @@ -47,24 +49,30 @@ impl Matrix { /// The callback called for each widget in the matrix. This should be called following all /// builder methods. - pub fn each_widget(&mut self, mut callback: F) + pub fn each_widget(&mut self, ui: &mut Ui, mut callback: F) where - F: FnMut(WidgetNum, ColNum, RowNum, Point, Dimensions) + F: FnMut(&mut Ui, WidgetNum, ColNum, RowNum, Point, Dimensions) { - let widget_w = self.dim[0] / self.cols as f64; - let widget_h = self.dim[1] / self.rows as f64; + use utils::map_range; + let dim = self.dim; + let h_align = self.maybe_h_align.unwrap_or(ui.theme.h_align); + let v_align = self.maybe_v_align.unwrap_or(ui.theme.v_align); + let xy = ui.get_xy(self.pos, dim, h_align, v_align); + let (half_w, half_h) = (dim[0] / 2.0, dim[1] / 2.0); + let widget_w = dim[0] / self.cols as f64; + let widget_h = dim[1] / self.rows as f64; + let x_min = -half_w + widget_w / 2.0; + let x_max = half_w + widget_w / 2.0; + let y_min = -half_h - widget_h / 2.0; + let y_max = half_h - widget_h / 2.0; let mut widget_num = 0; for col in 0..self.cols { for row in 0..self.rows { - callback( - widget_num, - col, - row, - [self.pos[0] + (widget_w * col as f64) + self.cell_pad_w, - self.pos[1] + (widget_h * row as f64) + self.cell_pad_h], - [widget_w - self.cell_pad_w * 2.0, - widget_h - self.cell_pad_h * 2.0], - ); + let x = xy[0] + map_range(col as f64, 0.0, self.cols as f64, x_min, x_max); + let y = xy[1] + map_range(row as f64, 0.0, self.rows as f64, y_max, y_min); + let w = widget_w - self.cell_pad_w * 2.0; + let h = widget_h - self.cell_pad_h * 2.0; + callback(ui, widget_num, col, row, [x, y], [w, h]); widget_num += 1; } } @@ -77,14 +85,30 @@ impl Matrix { } -impl Positionable for Matrix { - fn point(mut self, pos: Point) -> Self { +impl position::Positionable for Matrix { + fn position(mut self, pos: Position) -> Self { self.pos = pos; self } + #[inline] + fn horizontal_align(self, h_align: HorizontalAlign) -> Self { + Matrix { maybe_h_align: Some(h_align), ..self } + } + #[inline] + fn vertical_align(self, v_align: VerticalAlign) -> Self { + Matrix { maybe_v_align: Some(v_align), ..self } + } } -impl Shapeable for Matrix { - fn get_dim(&self) -> Dimensions { self.dim } - fn dim(mut self, dim: Dimensions) -> Self { self.dim = dim; self } +impl position::Sizeable for Matrix { + #[inline] + fn width(self, w: f64) -> Self { + let h = self.dim[1]; + Matrix { dim: [w, h], ..self } + } + #[inline] + fn height(self, h: f64) -> Self { + let w = self.dim[0]; + Matrix { dim: [w, h], ..self } + } } diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 67e4ccf1d..ec46660ac 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -1,5 +1,6 @@ use elmesque::Element; +use position::{Depth, Point}; pub mod button; pub mod drop_down_list; @@ -12,12 +13,13 @@ pub mod text_box; pub mod toggle; pub mod xy_pad; -/// Represents some Widget type. -#[derive(Clone)] +/// A widget element for storage within the Ui's `widget_cache`. +#[derive(Clone, Debug)] pub struct Widget { pub kind: Kind, - pub placing: Placing, - pub maybe_element: Option, + pub xy: Point, + pub depth: Depth, + pub element: Option, } impl Widget { @@ -26,8 +28,9 @@ impl Widget { pub fn empty() -> Widget { Widget { kind: Kind::NoWidget, - placing: Placing::NoPlace, - maybe_element: None, + xy: [0.0, 0.0], + depth: 0.0, + element: None, } } @@ -35,8 +38,9 @@ impl Widget { pub fn new(kind: Kind) -> Widget { Widget { kind: kind, - placing: Placing::NoPlace, - maybe_element: None, + xy: [0.0, 0.0], + depth: 0.0, + element: None, } } @@ -44,14 +48,16 @@ impl Widget { /// Algebraic widget type for storing in ui_context /// and for ease of state-matching. -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] pub enum Kind { NoWidget, Button(button::State), DropDownList(drop_down_list::State), EnvelopeEditor(envelope_editor::State), + Label, NumberDialer(number_dialer::State), Slider(slider::State), + Spacer, TextBox(text_box::State), Toggle(toggle::State), XYPad(xy_pad::State), @@ -64,8 +70,10 @@ impl Kind { (&Kind::Button(_), &Kind::Button(_)) => true, (&Kind::DropDownList(_), &Kind::DropDownList(_)) => true, (&Kind::EnvelopeEditor(_), &Kind::EnvelopeEditor(_)) => true, + (&Kind::Label, &Kind::Label) => true, (&Kind::NumberDialer(_), &Kind::NumberDialer(_)) => true, (&Kind::Slider(_), &Kind::Slider(_)) => true, + (&Kind::Spacer, &Kind::Spacer) => true, (&Kind::TextBox(_), &Kind::TextBox(_)) => true, (&Kind::Toggle(_), &Kind::Toggle(_)) => true, (&Kind::XYPad(_), &Kind::XYPad(_)) => true, @@ -74,38 +82,3 @@ impl Kind { } } -/// Represents the placement of the widget including -/// x / y position, width and height. -#[derive(Clone, Copy)] -pub enum Placing { - Place(f64, f64, f64, f64), // (x, y, w, h) - NoPlace, -} - -impl Placing { - pub fn down(&self, padding: f64) -> (f64, f64) { - match self { - &Placing::Place(x, y, _, h) => (x, y + h + padding), - &Placing::NoPlace => (0.0, 0.0), - } - } - pub fn up(&self, padding: f64) -> (f64, f64) { - match self { - &Placing::Place(x, y, _, _) => (x, y - padding), - &Placing::NoPlace => (0.0, 0.0), - } - } - pub fn left(&self, padding: f64) -> (f64, f64) { - match self { - &Placing::Place(x, y, _, _) => (x - padding, y), - &Placing::NoPlace => (0.0, 0.0), - } - } - pub fn right(&self, padding: f64) -> (f64, f64) { - match self { - &Placing::Place(x, y, w, _) => (x + w + padding, y), - &Placing::NoPlace => (0.0, 0.0), - } - } -} - diff --git a/src/widget/number_dialer.rs b/src/widget/number_dialer.rs index ca65a0d96..435570556 100644 --- a/src/widget/number_dialer.rs +++ b/src/widget/number_dialer.rs @@ -1,39 +1,31 @@ use color::{Color, Colorable}; -use dimensions::Dimensions; use frame::Frameable; -use graphics::{self, Graphics, Transformed}; use graphics::character::CharacterCache; use label::{self, FontSize, Labelable}; use mouse::Mouse; -use num::{Float, ToPrimitive, FromPrimitive}; -use point::Point; -use position::Positionable; -use rectangle; -use shape::Shapeable; +use num::{Float, NumCast}; +use position::{self, Depth, Dimensions, HorizontalAlign, Point, Position, VerticalAlign}; use std::cmp::Ordering; use std::iter::repeat; -use utils::{clamp, compare_f64s}; +use utils::clamp; use ui::{UiId, Ui}; -use vecmath::vec2_add; +use vecmath::{vec2_add, vec2_sub}; use widget::Kind; -/// Represents the specific elements that the -/// NumberDialer is made up of. This is used to -/// specify which element is Highlighted or Clicked -/// when storing State. -#[derive(Debug, PartialEq, Clone, Copy)] +/// Represents the specific elements that the NumberDialer is made up of. This is used to specify +/// which element is Highlighted or Clicked when storing State. +#[derive(Clone, Copy, Debug, RustcEncodable, RustcDecodable, PartialEq)] pub enum Element { Rect, LabelGlyphs, - /// Represents a value glyph slot at `usize` index - /// as well as the last mouse.pos.y for comparison - /// in determining new value. + /// Represents a value glyph slot at `usize` index as well as the last mouse.xy.y for + /// comparison in determining new value. ValueGlyph(usize, f64) } /// Represents the state of the Button widget. -#[derive(PartialEq, Clone, Copy)] +#[derive(Clone, Copy, Debug, RustcEncodable, RustcDecodable, PartialEq)] pub enum State { Normal, Highlighted(Element), @@ -42,9 +34,8 @@ pub enum State { widget_fns!(NumberDialer, State, Kind::NumberDialer(State::Normal)); -/// Create the string to be drawn from the given values -/// and precision. Combine this with the label string if -/// one is given. +/// Create the string to be drawn from the given values and precision. Combine this with the label +/// string if one is given. fn create_val_string(val: T, len: usize, precision: u8) -> String { let mut val_string = val.to_string(); // First check we have the correct number of decimal places. @@ -66,12 +57,12 @@ fn create_val_string(val: T, len: usize, precision: u8) -> String { } }, } - // Now check that the total length matches. We already know that - // the decimal end of the string is correct, so if the lengths - // don't match we know we must prepend the difference as '0's. - match val_string.len().cmp(&len) { - Ordering::Less => format!("{}{}", repeat('0').take(len - val_string.len()).collect::(), val_string), - _ => val_string, + // Now check that the total length matches. We already know that the decimal end of the string + // is correct, so if the lengths don't match we know we must prepend the difference as '0's. + if val_string.len() < len { + repeat('0').take(len - val_string.len()).chain(val_string.chars()).collect() + } else { + val_string } } @@ -89,41 +80,36 @@ fn val_string_width(font_size: FontSize, val_string: &String) -> f64 { /// Determine if the cursor is over the number_dialer and if so, which element. #[inline] -fn is_over(pos: Point, - frame_w: f64, - mouse_pos: Point, +fn is_over(mouse_xy: Point, dim: Dimensions, - label_pos: Point, + pad_dim: Dimensions, + label_xy: Point, label_dim: Dimensions, - val_string_w: f64, - val_string_h: f64, + val_string_dim: Point, val_string_len: usize) -> Option { - match rectangle::is_over(pos, mouse_pos, dim) { - false => None, - true => { - match rectangle::is_over(label_pos, mouse_pos, label_dim) { - true => Some(Element::LabelGlyphs), - false => { - let frame_w2 = frame_w * 2.0; - let slot_rect_pos = [label_pos[0] + label_dim[0], pos[1] + frame_w]; - match rectangle::is_over(slot_rect_pos, mouse_pos, - [val_string_w, dim[1] - frame_w2]) { - false => Some(Element::Rect), - true => { - let slot_w = value_glyph_slot_width(val_string_h as u32); - let mut slot_pos = slot_rect_pos; - for i in 0..val_string_len { - if rectangle::is_over(slot_pos, mouse_pos, [slot_w, dim[1]]) { - return Some(Element::ValueGlyph(i, mouse_pos[1])) - } - slot_pos[0] += slot_w; - } - Some(Element::Rect) - }, + use utils::is_over_rect; + if is_over_rect([0.0, 0.0], mouse_xy, dim) { + if is_over_rect(label_xy, mouse_xy, label_dim) { + Some(Element::LabelGlyphs) + } else { + let slot_w = value_glyph_slot_width(val_string_dim[1] as u32); + let slot_rect_xy = [label_xy[0] + label_dim[0] / 2.0 + slot_w / 2.0, 0.0]; + let val_string_xy = [slot_rect_xy[0] - slot_w / 2.0 + val_string_dim[0] / 2.0, 0.0]; + if is_over_rect(val_string_xy, mouse_xy, [val_string_dim[0], pad_dim[1]]) { + let mut slot_xy = slot_rect_xy; + for i in 0..val_string_len { + if is_over_rect(slot_xy, mouse_xy, [slot_w, pad_dim[1]]) { + return Some(Element::ValueGlyph(i, mouse_xy[1])) } - }, + slot_xy[0] += slot_w; + } + Some(Element::Rect) + } else { + Some(Element::Rect) } - }, + } + } else { + None } } @@ -139,13 +125,13 @@ fn get_new_state(is_over_elem: Option, prev: State, mouse: Mouse) -> St (Some(elem), Highlighted(_), Down) => Clicked(elem), (Some(_), Clicked(p_elem), Down) => { match p_elem { - ValueGlyph(idx, _) => Clicked(ValueGlyph(idx, mouse.pos[1])), + ValueGlyph(idx, _) => Clicked(ValueGlyph(idx, mouse.xy[1])), _ => Clicked(p_elem), } }, (None, Clicked(p_elem), Down) => { match p_elem { - ValueGlyph(idx, _) => Clicked(ValueGlyph(idx, mouse.pos[1])), + ValueGlyph(idx, _) => Clicked(ValueGlyph(idx, mouse.xy[1])), _ => Clicked(p_elem), } }, @@ -153,124 +139,17 @@ fn get_new_state(is_over_elem: Option, prev: State, mouse: Mouse) -> St } } -/// Return the new value along with it's String representation. -#[inline] -fn get_new_value(val: T, min: T, max: T, idx: usize, y_ord: Ordering, val_string: &String) -> T - where - T: Float + FromPrimitive + ToPrimitive + ToString -{ - match y_ord { - Ordering::Equal => val, - _ => { - let decimal_pos = val_string.chars().position(|ch| ch == '.'); - let val_f = val.to_f64().unwrap(); - let min_f = min.to_f64().unwrap(); - let max_f = max.to_f64().unwrap(); - let new_val_f = match decimal_pos { - None => { - let power = val_string.len() - idx - 1; - match y_ord { - Ordering::Less => clamp(val_f + (10.0).powf(power as f32) as f64, min_f, max_f), - Ordering::Greater => clamp(val_f - (10.0).powf(power as f32) as f64, min_f, max_f), - _ => val_f, - } - }, - Some(dec_idx) => { - let mut power = dec_idx as isize - idx as isize - 1; - if power < -1 { power += 1; } - match y_ord { - Ordering::Less => clamp(val_f + (10.0).powf(power as f32) as f64, min_f, max_f), - Ordering::Greater => clamp(val_f - (10.0).powf(power as f32) as f64, min_f, max_f), - _ => val_f, - } - }, - }; - FromPrimitive::from_f64(new_val_f).unwrap() - }, - } - -} - -/// Draw the value string glyphs. -#[inline] -fn draw_value_string( - win_w: f64, - win_h: f64, - graphics: &mut B, - ui: &mut Ui, - state: State, - slot_y: f64, - rect_color: Color, - slot_w: f64, - pad_h: f64, - pos: Point, - size: FontSize, - font_color: Color, - string: &str -) - where - B: Graphics::Texture>, - C: CharacterCache -{ - let mut x = 0.0f64; - let y = 0.0f64; - let draw_state = graphics::default_draw_state(); - let transform = graphics::abs_transform(win_w, win_h) - .trans(pos[0], pos[1] + size as f64); - let half_slot_w = slot_w / 2.0; - let image = graphics::Image::new_colored(font_color.to_fsa()); - for (i, ch) in string.chars().enumerate() { - let character = ui.get_character(size, ch); - match state { - State::Highlighted(elem) => match elem { - Element::ValueGlyph(idx, _) => { - let context_slot_y = slot_y - (pos[1] + size as f64); - let rect_color = if idx == i { rect_color.highlighted() } - else { rect_color }; - graphics::Rectangle::new(rect_color.to_fsa()).draw( - [x as f64, context_slot_y, size as f64, pad_h], - draw_state, - transform, - graphics - ); - }, - _ => (), - }, - State::Clicked(elem) => match elem { - Element::ValueGlyph(idx, _) => { - let context_slot_y = slot_y - (pos[1] + size as f64); - let rect_color = if idx == i { rect_color.clicked() } - else { rect_color }; - graphics::Rectangle::new(rect_color.to_fsa()).draw( - [x, context_slot_y, size as f64, pad_h], - draw_state, - transform, - graphics - ); - }, - _ => (), - }, - _ => (), - }; - let x_shift = half_slot_w - 0.5 * character.width(); - let d = transform.trans( - x + character.left() + x_shift, - y - character.top() - ); - image.draw(&character.texture, draw_state, d, graphics); - x += slot_w; - } -} - /// A widget for precision control over any digit within a value. The callback is triggered when /// the value is updated or if the mouse button is released while the cursor is above the widget. pub struct NumberDialer<'a, T, F> { - ui_id: UiId, value: T, min: T, max: T, - pos: Point, + pos: Position, + maybe_h_align: Option, + maybe_v_align: Option, dim: Dimensions, + depth: Depth, precision: u8, maybe_color: Option, maybe_frame: Option, @@ -284,14 +163,16 @@ pub struct NumberDialer<'a, T, F> { impl<'a, T: Float, F> NumberDialer<'a, T, F> { /// Construct a new NumberDialer widget. - pub fn new(ui_id: UiId, value: T, min: T, max: T, precision: u8) -> NumberDialer<'a, T, F> { + pub fn new(value: T, min: T, max: T, precision: u8) -> NumberDialer<'a, T, F> { NumberDialer { - ui_id: ui_id, value: clamp(value, min, max), min: min, max: max, - pos: [0.0, 0.0], + pos: Position::default(), + maybe_h_align: None, + maybe_v_align: None, dim: [128.0, 48.0], + depth: 0.0, precision: precision, maybe_color: None, maybe_frame: None, @@ -310,6 +191,164 @@ impl<'a, T: Float, F> NumberDialer<'a, T, F> { self } + /// After building the NumberDialer, use this method to set its current state into the given + /// `Ui`. It will use this state for rendering the next time `ui.draw(graphics)` is called. + pub fn set(mut self, ui_id: UiId, ui: &mut Ui) + where + C: CharacterCache, + F: FnMut(T), + T: ToString, + { + use elmesque::form::{collage, rect, text}; + use elmesque::text::Text; + + let state = *get_state(ui, ui_id); + let dim = self.dim; + let h_align = self.maybe_h_align.unwrap_or(ui.theme.h_align); + let v_align = self.maybe_v_align.unwrap_or(ui.theme.v_align); + let xy = ui.get_xy(self.pos, dim, h_align, v_align); + let mouse = ui.get_mouse_state().relative_to(xy); + let frame_w = self.maybe_frame.unwrap_or(ui.theme.frame_width); + let frame_w2 = frame_w * 2.0; + let pad_dim = vec2_sub(dim, [frame_w2; 2]); + let font_size = self.maybe_label_font_size.unwrap_or(ui.theme.font_size_medium); + let label_string = self.maybe_label.map_or_else(|| String::new(), |text| format!("{}: ", text)); + let label_dim = [label::width(ui, font_size, &label_string), font_size as f64]; + let val_string_len = self.max.to_string().len() + if self.precision == 0 { 0 } + else { 1 + self.precision as usize }; + let mut val_string = create_val_string(self.value, val_string_len, self.precision); + let val_string_dim = [val_string_width(font_size, &val_string), font_size as f64]; + let label_x = -val_string_dim[0] / 2.0; + let label_pos = [label_x, 0.0]; + let is_over_elem = is_over(mouse.xy, dim, pad_dim, label_pos, label_dim, val_string_dim, val_string_len); + let new_state = get_new_state(is_over_elem, state, mouse); + + // Construct the frame and inner rectangle Forms. + let frame_color = self.maybe_frame_color.unwrap_or(ui.theme.frame_color); + let color = self.maybe_color.unwrap_or(ui.theme.shape_color); + let frame_form = rect(dim[0], dim[1]).filled(frame_color); + let inner_form = rect(pad_dim[0], pad_dim[1]).filled(color); + + // Construct the label form. + let val_string_color = self.maybe_label_color.unwrap_or(ui.theme.label_color); + let label_form = text(Text::from_string(label_string.clone()) + .color(val_string_color) + .height(font_size as f64)).shift_x(label_x.floor()); + + // Determine new value from the initial state and the new state. + let mut new_val = self.value; + if let (State::Clicked(elem), State::Clicked(new_elem)) = (state, new_state) { + if let (Element::ValueGlyph(idx, y), Element::ValueGlyph(_, new_y)) = (elem, new_elem) { + let ord = new_y.partial_cmp(&y).unwrap_or(Ordering::Equal); + if ord != Ordering::Equal { + let decimal_pos = val_string.chars().position(|ch| ch == '.'); + let val_f: f64 = NumCast::from(self.value).unwrap(); + let min_f: f64 = NumCast::from(self.min).unwrap(); + let max_f: f64 = NumCast::from(self.max).unwrap(); + let new_val_f = match decimal_pos { + None => { + let power = val_string.len() - idx - 1; + match ord { + Ordering::Greater => { + clamp(val_f + (10.0).powf(power as f32) as f64, min_f, max_f) + }, + Ordering::Less => { + clamp(val_f - (10.0).powf(power as f32) as f64, min_f, max_f) + }, + _ => val_f, + } + }, + Some(dec_idx) => { + let mut power = dec_idx as isize - idx as isize - 1; + if power < -1 { power += 1; } + match ord { + Ordering::Greater => { + clamp(val_f + (10.0).powf(power as f32) as f64, min_f, max_f) + }, + Ordering::Less => { + clamp(val_f - (10.0).powf(power as f32) as f64, min_f, max_f) + }, + _ => val_f, + } + }, + }; + new_val = NumCast::from(new_val_f).unwrap() + }; + } + }; + + // If the value has changed, create a new string for val_string. + if self.value != new_val { + val_string = create_val_string(new_val, val_string_len, self.precision); + } + + // Construct the value_string's Form. + let val_string_pos = vec2_add(label_pos, [label_dim[0] / 2.0, 0.0]); + let slot_w = value_glyph_slot_width(font_size); + let mut x = slot_w / 2.0; + let val_string_forms = { + val_string.chars().enumerate().flat_map(|(i, ch)| { + let maybe_rect_form = match new_state { + State::Highlighted(elem) => if let Element::ValueGlyph(idx, _) = elem { + let rect_color = if idx == i { color.highlighted() } + else { color }; + Some(rect(slot_w, pad_dim[1]).filled(rect_color) + .shift(val_string_pos[0].floor(), val_string_pos[1].floor()) + .shift_x(x.floor())) + } else { + None + }, + State::Clicked(elem) => if let Element::ValueGlyph(idx, _) = elem { + let rect_color = if idx == i { color.clicked() } + else { color }; + Some(rect(slot_w, pad_dim[1]).filled(rect_color) + .shift(val_string_pos[0].floor(), val_string_pos[1].floor()) + .shift_x(x.floor())) + } else { + None + }, + _ => None, + }; + let character_form = text(Text::from_string(ch.to_string()) + .color(val_string_color) + .height(font_size as f64)) + .shift(val_string_pos[0].floor(), val_string_pos[1].floor()) + .shift_x(x.floor()); + x += slot_w; + maybe_rect_form.into_iter().chain(Some(character_form).into_iter()) + }) + }; + + // Call the `callback` with the new value if the mouse is pressed/released on the widget + // or if the value has changed. + if self.value != new_val || match (state, new_state) { + (State::Highlighted(_), State::Clicked(_)) | + (State::Clicked(_), State::Highlighted(_)) => true, + _ => false, + } { + if let Some(ref mut callback) = self.maybe_callback { callback(new_val) } + } + + // Chain the forms and shift them into position. + let form_chain = Some(frame_form).into_iter() + .chain(Some(inner_form).into_iter()) + .chain(Some(label_form).into_iter()) + .chain(val_string_forms) + .map(|form| form.shift(xy[0].floor(), xy[1].floor())); + + // Collect the Forms into a renderable Element. + let element = collage(dim[0] as i32, dim[1] as i32, form_chain.collect()); + + // Store the button's new state in the Ui. + ui.set_widget(ui_id, ::widget::Widget { + kind: Kind::NumberDialer(new_state), + xy: xy, + depth: self.depth, + element: Some(element), + }); + + } + } impl<'a, T, F> Colorable for NumberDialer<'a, T, F> { @@ -348,115 +387,31 @@ impl<'a, T, F> Labelable<'a> for NumberDialer<'a, T, F> } } -impl<'a, T, F> Positionable for NumberDialer<'a, T, F> { - fn point(mut self, pos: Point) -> Self { +impl<'a, T, F> position::Positionable for NumberDialer<'a, T, F> { + fn position(mut self, pos: Position) -> Self { self.pos = pos; self } + #[inline] + fn horizontal_align(self, h_align: HorizontalAlign) -> Self { + NumberDialer { maybe_h_align: Some(h_align), ..self } + } + #[inline] + fn vertical_align(self, v_align: VerticalAlign) -> Self { + NumberDialer { maybe_v_align: Some(v_align), ..self } + } } -impl<'a, T, F> Shapeable for NumberDialer<'a, T, F> { - fn get_dim(&self) -> Dimensions { self.dim } - fn dim(mut self, dim: Dimensions) -> Self { self.dim = dim; self } -} - -impl<'a, T, F> ::draw::Drawable for NumberDialer<'a, T, F> - where - T: Float + FromPrimitive + ToPrimitive + ToString, - F: FnMut(T) + 'a -{ +impl<'a, T, F> position::Sizeable for NumberDialer<'a, T, F> { #[inline] - /// Draw the number_dialer. When successfully pressed, - /// or if the value is changed, the given `callback` - /// function will be called. - fn draw(&mut self, ui: &mut Ui, graphics: &mut B) - where - B: Graphics::Texture>, - C: CharacterCache - { - - let state = *get_state(ui, self.ui_id); - let mouse = ui.get_mouse_state(); - let frame_w = self.maybe_frame.unwrap_or(ui.theme.frame_width); - let frame_w2 = frame_w * 2.0; - let maybe_frame = match frame_w > 0.0 { - true => Some((frame_w, self.maybe_frame_color.unwrap_or(ui.theme.frame_color))), - false => None, - }; - let pad_h = self.dim[1] - frame_w2; - let font_size = self.maybe_label_font_size.unwrap_or(ui.theme.font_size_medium); - let label_string = match self.maybe_label { - Some(text) => format!("{}: ", text), - None => String::new(), - }; - let label_dim = match label_string.len() { - 0 => [0.0, 0.0], - _ => [label::width(ui, font_size, &label_string), font_size as f64], - }; - let val_string_len = self.max.to_string().len() + if self.precision == 0 { 0 } - else { 1 + self.precision as usize }; - let mut val_string = create_val_string(self.value, val_string_len, self.precision); - let (val_string_w, val_string_h) = (val_string_width(font_size, &val_string), font_size as f64); - let label_x = self.pos[0] + (self.dim[0] - (label_dim[0] + val_string_w)) / 2.0; - let label_y = self.pos[1] + (self.dim[1] - font_size as f64) / 2.0; - let label_pos = [label_x, label_y]; - let is_over_elem = is_over(self.pos, frame_w, mouse.pos, self.dim, - label_pos, label_dim, val_string_w, val_string_h, - val_string.len()); - let new_state = get_new_state(is_over_elem, state, mouse); - let color = self.maybe_color.unwrap_or(ui.theme.shape_color); - - // Draw the widget rectangle. - rectangle::draw(ui.win_w, ui.win_h, graphics, rectangle::State::Normal, - self.pos, self.dim, maybe_frame, color); - - // If there's a label, draw it. - let val_string_color = self.maybe_label_color.unwrap_or(ui.theme.label_color); - if self.maybe_label.is_some() { - ui.draw_text(graphics, label_pos, font_size, val_string_color, &label_string); - }; - - // Determine new value from the initial state and the new state. - let new_val = match (state, new_state) { - (State::Clicked(elem), State::Clicked(new_elem)) => { - match (elem, new_elem) { - (Element::ValueGlyph(idx, y), Element::ValueGlyph(_, new_y)) => { - get_new_value(self.value, self.min, self.max, idx, - compare_f64s(new_y, y), &val_string) - }, _ => self.value, - } - }, _ => self.value, - }; - - // If the value has changed, create a new string for val_string. - if self.value != new_val { - val_string = create_val_string(new_val, val_string_len, self.precision) - } - - // Draw the value string. - let val_string_pos = vec2_add(label_pos, [label_dim[0], 0.0]); - draw_value_string(ui.win_w, ui.win_h, graphics, ui, new_state, - self.pos[1] + frame_w, color, - value_glyph_slot_width(font_size), pad_h, - val_string_pos, - font_size, - val_string_color, - &val_string); - - // Call the `callback` with the new value if the mouse is pressed/released - // on the widget or if the value has changed. - if self.value != new_val || match (state, new_state) { - (State::Highlighted(_), State::Clicked(_)) | (State::Clicked(_), State::Highlighted(_)) => true, - _ => false, - } { - match self.maybe_callback { - Some(ref mut callback) => (*callback)(new_val), - None => () - } - } - - set_state(ui, self.ui_id, Kind::NumberDialer(new_state), self.pos, self.dim); - + fn width(self, w: f64) -> Self { + let h = self.dim[1]; + NumberDialer { dim: [w, h], ..self } + } + #[inline] + fn height(self, h: f64) -> Self { + let w = self.dim[0]; + NumberDialer { dim: [w, h], ..self } } - } + diff --git a/src/widget/slider.rs b/src/widget/slider.rs index 233b17322..4143827e4 100644 --- a/src/widget/slider.rs +++ b/src/widget/slider.rs @@ -1,23 +1,17 @@ use color::{Color, Colorable}; -use dimensions::Dimensions; use frame::Frameable; -use label::{self, FontSize, Labelable}; -use mouse::Mouse; -use graphics::Graphics; use graphics::character::CharacterCache; -use num::{Float, ToPrimitive, FromPrimitive}; -use point::Point; -use position::Positionable; -use rectangle; -use shape::Shapeable; +use label::{FontSize, Labelable}; +use mouse::Mouse; +use num::{Float, NumCast, ToPrimitive}; +use position::{self, Depth, Dimensions, HorizontalAlign, Position, VerticalAlign}; use ui::{UiId, Ui}; use utils::{clamp, percentage, value_from_perc}; -use vecmath::vec2_add; use widget::Kind; /// Represents the state of the Button widget. -#[derive(PartialEq, Clone, Copy)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum State { Normal, Highlighted, @@ -25,12 +19,12 @@ pub enum State { } impl State { - /// Return the associated Rectangle state. - fn as_rectangle_state(&self) -> rectangle::State { + /// Return the color associated with the state. + fn color(&self, color: Color) -> Color { match self { - &State::Normal => rectangle::State::Normal, - &State::Highlighted => rectangle::State::Highlighted, - &State::Clicked => rectangle::State::Clicked, + &State::Normal => color, + &State::Highlighted => color.highlighted(), + &State::Clicked => color.clicked(), } } } @@ -57,12 +51,14 @@ fn get_new_state(is_over: bool, /// is triggered if the value is updated or if the mouse button is released while the cursor is /// above the rectangle. pub struct Slider<'a, T, F> { - ui_id: UiId, value: T, min: T, max: T, - pos: Point, + pos: Position, + maybe_h_align: Option, + maybe_v_align: Option, dim: Dimensions, + depth: Depth, maybe_callback: Option, maybe_color: Option, maybe_frame: Option, @@ -75,14 +71,16 @@ pub struct Slider<'a, T, F> { impl<'a, T, F> Slider<'a, T, F> { /// Construct a new Slider widget. - pub fn new(ui_id: UiId, value: T, min: T, max: T) -> Slider<'a, T, F> { + pub fn new(value: T, min: T, max: T) -> Slider<'a, T, F> { Slider { - ui_id: ui_id, value: value, min: min, max: max, - pos: [0.0, 0.0], + pos: Position::default(), + maybe_h_align: None, + maybe_v_align: None, dim: [192.0, 48.0], + depth: 0.0, maybe_callback: None, maybe_color: None, maybe_frame: None, @@ -100,6 +98,126 @@ impl<'a, T, F> Slider<'a, T, F> { self } + /// After building the Button, use this method to set its current state into the given `Ui`. + /// It will use this state for rendering the next time `ui.draw(graphics)` is called. + pub fn set(mut self, ui_id: UiId, ui: &mut Ui) + where + C: CharacterCache, + F: FnMut(T), + T: Float + NumCast + ToPrimitive, + { + use elmesque::form::{collage, rect, text}; + use utils::{is_over_rect, map_range}; + + let state = *get_state(ui, ui_id); + let dim = self.dim; + let h_align = self.maybe_h_align.unwrap_or(ui.theme.h_align); + let v_align = self.maybe_v_align.unwrap_or(ui.theme.v_align); + let xy = ui.get_xy(self.pos, dim, h_align, v_align); + let mouse = ui.get_mouse_state().relative_to(xy); + let is_over = is_over_rect([0.0, 0.0], mouse.xy, dim); + let new_state = get_new_state(is_over, state, mouse); + + let frame_w = self.maybe_frame.unwrap_or(ui.theme.frame_width); + let frame_w2 = frame_w * 2.0; + let (inner_w, inner_h) = (dim[0] - frame_w2, dim[1] - frame_w2); + let (half_inner_w, half_inner_h) = (inner_w / 2.0, inner_h / 2.0); + + let is_horizontal = dim[0] > dim[1]; + let (new_value, pad_rel_xy, pad_dim) = if is_horizontal { + // Horizontal. + let w = match (is_over, state, new_state) { + (true, State::Highlighted, State::Clicked) | (_, State::Clicked, State::Clicked) => { + let w = map_range(mouse.xy[0], -half_inner_w, half_inner_w, 0.0, inner_w); + clamp(w, 0.0, inner_w) + }, + _ => { + let value_percentage = percentage(self.value, self.min, self.max); + clamp(value_percentage as f64 * inner_w, 0.0, inner_w) + }, + }; + let new_value = value_from_perc((w / inner_w) as f32, self.min, self.max); + let p = [-(inner_w - w) / 2.0, 0.0]; + (new_value, p, [w, inner_h]) + } else { + // Vertical. + let h = match (is_over, state, new_state) { + (true, State::Highlighted, State::Clicked) | (_, State::Clicked, State::Clicked) => { + let h = map_range(mouse.xy[1], -half_inner_h, half_inner_h, 0.0, inner_h); + clamp(h, 0.0, inner_h) + }, + _ => { + let value_percentage = percentage(self.value, self.min, self.max); + clamp(value_percentage as f64 * inner_h, 0.0, inner_h) + }, + }; + let new_value = value_from_perc((h / inner_h) as f32, self.min, self.max); + let rel_xy = [0.0, -(inner_h - h) / 2.0]; + (new_value, rel_xy, [inner_w, h]) + }; + + // Callback. + match self.maybe_callback { + Some(ref mut callback) => { + if self.value != new_value || match (state, new_state) { + (State::Highlighted, State::Clicked) | (State::Clicked, State::Highlighted) => true, + _ => false, + } { callback(new_value) } + }, None => (), + } + + // Draw. + let frame_color = new_state.color(self.maybe_frame_color.unwrap_or(ui.theme.frame_color)); + let color = new_state.color(self.maybe_color.unwrap_or(ui.theme.shape_color)); + + // Rectangle frame / backdrop Form. + let frame_form = rect(dim[0], dim[1]) + .filled(frame_color); + // Slider rectangle Form. + let pad_form = rect(pad_dim[0], pad_dim[1]) + .filled(color) + .shift(pad_rel_xy[0], pad_rel_xy[1]); + + // Label Form. + let maybe_label_form = self.maybe_label.map(|label_text| { + use elmesque::text::Text; + use label; + const TEXT_PADDING: f64 = 10.0; + let text_color = self.maybe_label_color.unwrap_or(ui.theme.label_color); + let size = self.maybe_label_font_size.unwrap_or(ui.theme.font_size_medium); + let label_w = label::width(ui, size, &label_text); + let is_horizontal = self.dim[0] > self.dim[1]; + let l_pos = if is_horizontal { + let x = position::align_left_of(dim[0], label_w) + TEXT_PADDING; + [x, 0.0] + } else { + let y = position::align_bottom_of(dim[1], size as f64) + TEXT_PADDING; + [0.0, y] + }; + text(Text::from_string(label_text.to_string()).color(text_color).height(size as f64)) + .shift(l_pos[0].floor(), l_pos[1].floor()) + .shift(xy[0].floor(), xy[1].floor()) + }); + + // Chain the Forms and shift them into position. + let form_chain = Some(frame_form).into_iter() + .chain(Some(pad_form).into_iter()) + .map(|form| form.shift(xy[0], xy[1])) + .chain(maybe_label_form.into_iter()); + + // Collect the Forms into a renderable Element. + let element = collage(dim[0] as i32, dim[1] as i32, form_chain.collect()); + + // Store the slider's state in the `Ui`. + ui.set_widget(ui_id, ::widget::Widget { + kind: Kind::Slider(new_state), + xy: xy, + depth: self.depth, + element: Some(element), + }); + + } + } impl<'a, T, F> Colorable for Slider<'a, T, F> { @@ -138,115 +256,31 @@ impl<'a, T, F> Labelable<'a> for Slider<'a, T, F> } } -impl<'a, T, F> Positionable for Slider<'a, T, F> { - fn point(mut self, pos: Point) -> Self { +impl<'a, T, F> position::Positionable for Slider<'a, T, F> { + #[inline] + fn horizontal_align(self, h_align: HorizontalAlign) -> Self { + Slider { maybe_h_align: Some(h_align), ..self } + } + #[inline] + fn vertical_align(self, v_align: VerticalAlign) -> Self { + Slider { maybe_v_align: Some(v_align), ..self } + } + fn position(mut self, pos: Position) -> Self { self.pos = pos; self } } -impl<'a, T, F> Shapeable for Slider<'a, T, F> { - fn get_dim(&self) -> Dimensions { self.dim } - fn dim(mut self, dim: Dimensions) -> Self { self.dim = dim; self } -} - -impl<'a, T, F> ::draw::Drawable for Slider<'a, T, F> - where - T: Float + FromPrimitive + ToPrimitive, - F: FnMut(T) + 'a -{ - - fn draw(&mut self, ui: &mut Ui, graphics: &mut B) - where - B: Graphics::Texture>, - C: CharacterCache - { - - let state = *get_state(ui, self.ui_id); - let mouse = ui.get_mouse_state(); - let is_over = rectangle::is_over(self.pos, mouse.pos, self.dim); - let new_state = get_new_state(is_over, state, mouse); - - let frame_w = self.maybe_frame.unwrap_or(ui.theme.frame_width); - let frame_w2 = frame_w * 2.0; - let frame_color = self.maybe_frame_color.unwrap_or(ui.theme.frame_color); - - let is_horizontal = self.dim[0] > self.dim[1]; - let (new_value, pad_pos, pad_dim) = if is_horizontal { - // Horizontal. - let p = vec2_add(self.pos, [frame_w, frame_w]); - let max_w = self.dim[0] - frame_w2; - let w = match (is_over, state, new_state) { - (true, State::Highlighted, State::Clicked) | (_, State::Clicked, State::Clicked) => - clamp(mouse.pos[0] - p[0], 0f64, max_w), - _ => clamp(percentage(self.value, self.min, self.max) as f64 * max_w, 0f64, max_w), - }; - let h = self.dim[1] - frame_w2; - let new_value = value_from_perc((w / max_w) as f32, self.min, self.max); - (new_value, p, [w, h]) - } else { - // Vertical. - let max_h = self.dim[1] - frame_w2; - let corner = vec2_add(self.pos, [frame_w, frame_w]); - let y_max = corner[1] + max_h; - let (h, p) = match (is_over, state, new_state) { - (true, State::Highlighted, State::Clicked) | (_, State::Clicked, State::Clicked) => { - let p = [corner[0], clamp(mouse.pos[1], corner[1], y_max)]; - let h = clamp(max_h - (p[1] - corner[1]), 0.0, max_h); - (h, p) - }, - _ => { - let h = clamp(percentage(self.value, self.min, self.max) as f64 * max_h, 0.0, max_h); - let p = [corner[0], corner[1] + max_h - h]; - (h, p) - }, - }; - let w = self.dim[0] - frame_w2; - let new_value = value_from_perc((h / max_h) as f32, self.min, self.max); - (new_value, p, [w, h]) - }; - - // Callback. - match self.maybe_callback { - Some(ref mut callback) => { - if self.value != new_value || match (state, new_state) { - (State::Highlighted, State::Clicked) | (State::Clicked, State::Highlighted) => true, - _ => false, - } { (*callback)(new_value) } - }, None => (), - } - - // Draw. - let rect_state = new_state.as_rectangle_state(); - let color = self.maybe_color.unwrap_or(ui.theme.shape_color); - - // Rectangle frame / backdrop. - rectangle::draw(ui.win_w, ui.win_h, graphics, rect_state, - self.pos, self.dim, None, frame_color); - // Slider rectangle. - rectangle::draw(ui.win_w, ui.win_h, graphics, rect_state, - pad_pos, pad_dim, None, color); - - // If there's a label, draw it. - if let Some(text) = self.maybe_label { - let text_color = self.maybe_label_color.unwrap_or(ui.theme.label_color); - let size = self.maybe_label_font_size.unwrap_or(ui.theme.font_size_medium); - let is_horizontal = self.dim[0] > self.dim[1]; - let l_pos = if is_horizontal { - let x = pad_pos[0] + (pad_dim[1] - size as f64) / 2.0; - let y = pad_pos[1] + (pad_dim[1] - size as f64) / 2.0; - [x, y] - } else { - let label_w = label::width(ui, size, &text); - let x = pad_pos[0] + (pad_dim[0] - label_w) / 2.0; - let y = pad_pos[1] + pad_dim[1] - pad_dim[0] - frame_w; - [x, y] - }; - // Draw the label. - ui.draw_text(graphics, l_pos, size, text_color, &text); - } - - set_state(ui, self.ui_id, Kind::Slider(new_state), self.pos, self.dim); - +impl<'a, T, F> position::Sizeable for Slider<'a, T, F> { + #[inline] + fn width(self, w: f64) -> Self { + let h = self.dim[1]; + Slider { dim: [w, h], ..self } + } + #[inline] + fn height(self, h: f64) -> Self { + let w = self.dim[0]; + Slider { dim: [w, h], ..self } } } + diff --git a/src/widget/text_box.rs b/src/widget/text_box.rs index 2bd5f915e..0f76de779 100644 --- a/src/widget/text_box.rs +++ b/src/widget/text_box.rs @@ -1,20 +1,15 @@ use clock_ticks::precise_time_s; use color::{Color, Colorable}; -use dimensions::Dimensions; use frame::Frameable; -use graphics::{self, Graphics}; use graphics::character::CharacterCache; use label::{self, FontSize}; use mouse::Mouse; use num::Float; use piston::input::keyboard::Key::{Backspace, Left, Right, Return}; -use point::Point; -use position::Positionable; -use rectangle; -use shape::Shapeable; +use position::{self, Depth, Dimensions, HorizontalAlign, Point, Position, VerticalAlign}; use ui::{UiId, Ui}; -use vecmath::{vec2_add, vec2_sub}; +use vecmath::vec2_sub; use widget::Kind; pub type Idx = usize; @@ -27,6 +22,19 @@ pub enum State { Uncaptured(Uncaptured), } +impl State { + /// Return the color associated with the current state. + fn color(&self, color: Color) -> Color { + match *self { + State::Capturing(_) => color, + State::Uncaptured(state) => match state { + Uncaptured::Highlighted => color.highlighted(), + Uncaptured::Normal => color, + } + } + } +} + #[derive(Debug, PartialEq, Clone, Copy)] pub struct View { cursor: Cursor, @@ -93,6 +101,7 @@ pub enum Anchor { End, } +/// The TextBox's state if it is uncaptured. #[derive(Debug, PartialEq, Clone, Copy)] pub enum Uncaptured { Highlighted, @@ -107,85 +116,71 @@ pub enum Element { Rect, } -impl State { - /// Return the associated Rectangle state. - fn as_rectangle_state(&self) -> rectangle::State { - match *self { - State::Capturing(_) => rectangle::State::Normal, - State::Uncaptured(state) => match state { - Uncaptured::Highlighted => rectangle::State::Highlighted, - Uncaptured::Normal => rectangle::State::Normal, - } - } - } -} - widget_fns!(TextBox, State, Kind::TextBox(State::Uncaptured(Uncaptured::Normal))); -static TEXT_PADDING: f64 = 5f64; +static TEXT_PADDING: f64 = 5.0; /// Find the position of a character in a text box. fn cursor_position(ui: &mut Ui, - idx: usize, - mut text_x: f64, - font_size: FontSize, - text: &str) -> CursorX { + idx: usize, + mut text_start_x: f64, + font_size: FontSize, + text: &str) -> CursorX { assert!(idx <= text.len()); if idx == 0 { - return text_x; + return text_start_x; } for (i, ch) in text.chars().enumerate() { if i >= idx { break; } - text_x += ui.get_character(font_size, ch).width(); + text_start_x += ui.get_character(font_size, ch).width(); } - text_x + text_start_x } /// Check if cursor is over the pad and if so, which fn over_elem(ui: &mut Ui, - pos: Point, - mouse_pos: Point, - rect_dim: Dimensions, - pad_pos: Point, - pad_dim: Dimensions, - text_pos: Point, - text_w: f64, - font_size: FontSize, - text: &str) -> Element { - match rectangle::is_over(pos, mouse_pos, rect_dim) { - false => Element::Nill, - true => match rectangle::is_over(pad_pos, mouse_pos, pad_dim) { - false => Element::Rect, - true => { - let (idx, _) = closest_idx(ui, mouse_pos, text_pos[0], text_w, font_size, text); - Element::Char(idx) - }, - }, + mouse_xy: Point, + dim: Dimensions, + pad_dim: Dimensions, + text_start_x: f64, + text_w: f64, + font_size: FontSize, + text: &str) -> Element { + use utils::is_over_rect; + if is_over_rect([0.0, 0.0], mouse_xy, dim) { + if is_over_rect([0.0, 0.0], mouse_xy, pad_dim) { + let (idx, _) = closest_idx(ui, mouse_xy, text_start_x, text_w, font_size, text); + Element::Char(idx) + } else { + Element::Rect + } + } else { + Element::Nill } } /// Check which character is closest to the mouse cursor. fn closest_idx(ui: &mut Ui, - mouse_pos: Point, - text_x: f64, - text_w: f64, - font_size: FontSize, - text: &str) -> (Idx, f64) { - if mouse_pos[0] <= text_x { return (0, text_x) } - let mut x = text_x; + mouse_xy: Point, + text_start_x: f64, + text_w: f64, + font_size: FontSize, + text: &str) -> (Idx, f64) { + if mouse_xy[0] <= text_start_x { return (0, text_start_x) } + let mut left_x = text_start_x; + let mut x = left_x; let mut prev_x = x; - let mut left_x = text_x; for (i, ch) in text.chars().enumerate() { let character = ui.get_character(font_size, ch); let char_w = character.width(); x += char_w; let right_x = prev_x + char_w / 2.0; - if mouse_pos[0] > left_x && mouse_pos[0] <= right_x { return (i, prev_x) } + if mouse_xy[0] > left_x && mouse_xy[0] <= right_x { return (i, prev_x) } prev_x = x; left_x = right_x; } - (text.len(), text_x + text_w) + (text.len(), text_start_x + text_w) } /// Check and return the current state of the TextBox. @@ -210,9 +205,9 @@ fn get_new_state(over_elem: Element, prev_state: State, mouse: Mouse) -> State { }, Element::Char(idx) => { match prev.cursor.anchor { - Anchor::None => prev.cursor = Cursor::from_index(idx), + Anchor::None => prev.cursor = Cursor::from_index(idx), Anchor::Start => prev.cursor = Cursor::from_range(prev.cursor.start, idx), - Anchor::End => prev.cursor = Cursor::from_range(prev.cursor.end, idx), + Anchor::End => prev.cursor = Cursor::from_range(prev.cursor.end, idx), } Capturing(prev) }, @@ -252,37 +247,16 @@ fn get_new_state(over_elem: Element, prev_state: State, mouse: Mouse) -> State { } } -/// Draw the text cursor. -fn draw_cursor( - win_w: f64, - win_h: f64, - graphics: &mut B, - color: Color, - cursor_x: f64, - pad_pos_y: f64, - pad_h: f64 -) { - use color::Rgba; - let draw_state = graphics::default_draw_state(); - let transform = graphics::abs_transform(win_w, win_h); - let Rgba(r, g, b, a) = color.plain_contrast().to_rgb(); - graphics::Line::new_round([r, g, b, (a * (precise_time_s() * 2.5).sin() as f32).abs()], 0.5f64) - .draw( - [cursor_x, pad_pos_y, cursor_x, pad_pos_y + pad_h], - draw_state, - transform, - graphics - ); -} - /// A widget for displaying and mutating a given one-line text `String`. It's callback is /// triggered upon pressing of the `Enter`/`Return` key. pub struct TextBox<'a, F> { - ui_id: UiId, text: &'a mut String, font_size: u32, - pos: Point, + pos: Position, + maybe_h_align: Option, + maybe_v_align: Option, dim: Dimensions, + depth: Depth, maybe_callback: Option, maybe_color: Option, maybe_frame: Option, @@ -292,13 +266,15 @@ pub struct TextBox<'a, F> { impl<'a, F> TextBox<'a, F> { /// Construct a TextBox widget. - pub fn new(ui_id: UiId, text: &'a mut String) -> TextBox<'a, F> { + pub fn new(text: &'a mut String) -> TextBox<'a, F> { TextBox { - ui_id: ui_id, text: text, font_size: 24, // Default font_size. - pos: [0.0, 0.0], + pos: Position::default(), + maybe_h_align: None, + maybe_v_align: None, dim: [192.0, 48.0], + depth: 0.0, maybe_callback: None, maybe_color: None, maybe_frame: None, @@ -318,84 +294,42 @@ impl<'a, F> TextBox<'a, F> { self } - fn cursor_rect - (&self, ui: &mut Ui, text_x: f64, start: Idx, end: Idx) -> - (Point, Dimensions) { - let pos = cursor_position(ui, start, text_x, self.font_size, &self.text); - let htext: String = self.text.chars().skip(start).take(end - start).collect(); - let htext_w = label::width(ui, self.font_size, &htext); - ([pos, self.pos[1]], [htext_w, self.dim[1]]) - } - -} - -impl<'a, F> Colorable for TextBox<'a, F> { - fn color(mut self, color: Color) -> Self { - self.maybe_color = Some(color); - self - } -} - -impl<'a, F> Frameable for TextBox<'a, F> { - fn frame(mut self, width: f64) -> Self { - self.maybe_frame = Some(width); - self - } - fn frame_color(mut self, color: Color) -> Self { - self.maybe_frame_color = Some(color); - self - } -} - -impl<'a, F> Positionable for TextBox<'a, F> { - fn point(mut self, pos: Point) -> Self { - self.pos = pos; - self - } -} - -impl<'a, F> Shapeable for TextBox<'a, F> { - fn get_dim(&self) -> Dimensions { self.dim } - fn dim(mut self, dim: Dimensions) -> Self { self.dim = dim; self } -} - -impl<'a, F> ::draw::Drawable for TextBox<'a, F> - where - F: FnMut(&mut String) + 'a -{ - - #[inline] - fn draw(&mut self, ui: &mut Ui, graphics: &mut B) + /// After building the TextBox, use this method to set its current state into the given `Ui`. + /// It will use this state for rendering the next time `ui.draw(graphics)` is called. + pub fn set(mut self, ui_id: UiId, ui: &mut Ui) where - B: Graphics::Texture>, - C: CharacterCache + C: CharacterCache, + F: FnMut(&mut String), { - let mouse = ui.get_mouse_state(); - let state = *get_state(ui, self.ui_id); - - // Rect. - let color = self.maybe_color.unwrap_or(ui.theme.shape_color); + use elmesque::form::{collage, line, rect, solid, text}; + use elmesque::text::Text; + + let dim = self.dim; + let h_align = self.maybe_h_align.unwrap_or(ui.theme.h_align); + let v_align = self.maybe_v_align.unwrap_or(ui.theme.v_align); + let xy = ui.get_xy(self.pos, dim, h_align, v_align); + let mouse = ui.get_mouse_state().relative_to(xy); + let state = *get_state(ui, ui_id); let frame_w = self.maybe_frame.unwrap_or(ui.theme.frame_width); let frame_w2 = frame_w * 2.0; - let maybe_frame = match frame_w > 0.0 { - true => Some((frame_w, self.maybe_frame_color.unwrap_or(ui.theme.frame_color))), - false => None, - }; - let pad_pos = vec2_add(self.pos, [frame_w; 2]); - let pad_dim = vec2_sub(self.dim, [frame_w2; 2]); - let text_x = pad_pos[0] + TEXT_PADDING; - let text_y = pad_pos[1] + (pad_dim[1] - self.font_size as f64) / 2.0; - let text_pos = [text_x, text_y]; - let text_w = label::width(ui, self.font_size, &self.text); - let over_elem = over_elem(ui, self.pos, mouse.pos, self.dim, - pad_pos, pad_dim, text_pos, text_w, - self.font_size, &self.text); + let font_size = self.font_size; + let pad_dim = vec2_sub(dim, [frame_w2; 2]); + let half_pad_h = pad_dim[1] / 2.0; + let text_w = label::width(ui, font_size, &self.text); + let text_x = position::align_left_of(pad_dim[0], text_w) + TEXT_PADDING; + let text_start_x = text_x - text_w / 2.0; + let over_elem = over_elem(ui, mouse.xy, dim, pad_dim, text_start_x, text_w, font_size, &self.text); let mut new_state = get_new_state(over_elem, state, mouse); - rectangle::draw(ui.win_w, ui.win_h, graphics, new_state.as_rectangle_state(), - self.pos, self.dim, maybe_frame, color); + // Construct the frame and inner rectangle Forms. + let color = new_state.color(self.maybe_color.unwrap_or(ui.theme.shape_color)); + let frame_color = self.maybe_frame_color.unwrap_or(ui.theme.frame_color); + let frame_form = rect(dim[0], dim[1]).filled(frame_color); + let inner_form = rect(pad_dim[0], pad_dim[1]).filled(color); - if let State::Capturing(view) = new_state { + // - Check cursor validity (and update new_state if necessary). + // - Construct the cursor and text `Form`s. + let (maybe_cursor_form, text_form) = if let State::Capturing(view) = new_state { let mut cursor = view.cursor; let mut v_offset = view.offset; @@ -407,11 +341,11 @@ impl<'a, F> ::draw::Drawable for TextBox<'a, F> Anchor::End => cursor.start, Anchor::Start | Anchor::None => cursor.end, }; - let cursor_x = cursor_position(ui, cursor_idx, text_x, self.font_size, &self.text); + let cursor_x = cursor_position(ui, cursor_idx, text_start_x, self.font_size, &self.text); if cursor.is_cursor() || cursor.anchor != Anchor::None { let cursor_x_view = cursor_x - v_offset; - let text_right = self.pos[0] + self.dim[0] - TEXT_PADDING - frame_w; + let text_right = self.dim[0] - TEXT_PADDING - frame_w; if cursor_x_view < text_x { v_offset += cursor_x_view - text_x; @@ -420,23 +354,43 @@ impl<'a, F> ::draw::Drawable for TextBox<'a, F> } } - // Draw the cursor. - if cursor.is_cursor() { - draw_cursor(ui.win_w, ui.win_h, graphics, color, cursor_x - v_offset, pad_pos[1], pad_dim[1]); + // Set the updated state. + new_state = State::Capturing(View { cursor: cursor, offset: v_offset }); + + // Construct the Cursor's Form. + let cursor_alpha = (precise_time_s() * 2.5).sin() as f32 * 0.5 + 0.5; + let cursor_form = if cursor.is_cursor() { + line(solid(color.plain_contrast()), 0.0, half_pad_h, 0.0, -half_pad_h) + .alpha(cursor_alpha) + .shift_x(cursor_x) } else { - let (pos, dim) = self.cursor_rect(ui, text_x - v_offset, cursor.start, cursor.end); - rectangle::draw(ui.win_w, ui.win_h, graphics, new_state.as_rectangle_state(), - [pos[0], pos[1] + frame_w], [dim[0], dim[1] - frame_w2], - None, color.highlighted()); - } + let (block_xy, dim) = { + let (start, end) = (cursor.start, cursor.end); + let cursor_x = cursor_position(ui, start, text_start_x, font_size, &self.text); + let htext: String = self.text.chars().skip(start).take(end - start).collect(); + let htext_w = label::width(ui, font_size, &htext); + ([cursor_x + htext_w / 2.0, 0.0], [htext_w, dim[1]]) + }; + rect(dim[0], dim[1]).filled(color.highlighted()) + .alpha(cursor_alpha) + .shift(block_xy[0], block_xy[1]) + }; - ui.draw_text(graphics, [text_pos[0] - v_offset, text_pos[1]], self.font_size, color.plain_contrast(), &self.text); + // Construct the text's Form. + let text_form = text(Text::from_string(self.text.clone()) + .color(color.plain_contrast()) + .height(font_size as f64)).shift_x(text_x.floor()); - new_state = State::Capturing(View { cursor: cursor, offset: v_offset }); + (Some(cursor_form), text_form) } else { - ui.draw_text(graphics, text_pos, self.font_size, color.plain_contrast(), &self.text); - } + // Construct the text's Form. + let text_form = text(Text::from_string(self.text.clone()) + .color(color.plain_contrast()) + .height(font_size as f64)).shift_x(text_x.floor()); + (None, text_form) + }; + // If TextBox is captured, check for recent input and update the text accordingly. if let State::Capturing(captured) = new_state { let mut cursor = captured.cursor; @@ -444,6 +398,9 @@ impl<'a, F> ::draw::Drawable for TextBox<'a, F> for text in ui.get_entered_text().iter() { if text.len() == 0 { continue; } + let max_w = pad_dim[0] - TEXT_PADDING * 2.0; + if text_w + label::width(ui, font_size, &text) > max_w { continue; } + let end: String = self.text.chars().skip(cursor.end).collect(); self.text.truncate(cursor.start); self.text.push_str(&text); @@ -455,47 +412,103 @@ impl<'a, F> ::draw::Drawable for TextBox<'a, F> let pressed_keys = ui.get_pressed_keys(); for key in pressed_keys.iter() { match *key { - Backspace => { - if cursor.is_cursor() { - if cursor.start > 0 { - let end: String = self.text.chars().skip(cursor.end).collect(); - self.text.truncate(cursor.start - 1); - self.text.push_str(&end); - cursor.shift(-1); - } - } else { + Backspace => if cursor.is_cursor() { + if cursor.start > 0 { let end: String = self.text.chars().skip(cursor.end).collect(); - self.text.truncate(cursor.start); + self.text.truncate(cursor.start - 1); self.text.push_str(&end); - cursor.end = cursor.start; - } - }, - Left => { - if cursor.is_cursor() { cursor.shift(-1); } + } else { + let end: String = self.text.chars().skip(cursor.end).collect(); + self.text.truncate(cursor.start); + self.text.push_str(&end); + cursor.end = cursor.start; }, - Right => { - if cursor.is_cursor() && self.text.len() > cursor.end { - cursor.shift(1); - } + Left => if cursor.is_cursor() { + cursor.shift(-1); + }, + Right => if cursor.is_cursor() && self.text.len() > cursor.end { + cursor.shift(1); }, Return => if self.text.len() > 0 { - let TextBox { // borrowck - ref mut maybe_callback, - ref mut text, - .. - } = *self; - match *maybe_callback { - Some(ref mut callback) => (*callback)(*text), - None => (), + let TextBox { ref mut maybe_callback, ref mut text, .. } = self; + if let Some(ref mut callback) = *maybe_callback { + callback(*text); } }, _ => (), } } + new_state = State::Capturing(View { cursor: cursor, .. captured }); } - set_state(ui, self.ui_id, Kind::TextBox(new_state), self.pos, self.dim); + + // Chain the Forms and shift them into position. + let form_chain = Some(frame_form).into_iter() + .chain(Some(inner_form).into_iter()) + .chain(Some(text_form).into_iter()) + .chain(maybe_cursor_form.into_iter()) + .map(|form| form.shift(xy[0], xy[1])); + + // Collect the Forms into a renderable `Element`. + let element = collage(dim[0] as i32, dim[1] as i32, form_chain.collect()); + + // Store the TextBox's new state in the Ui. + ui.set_widget(ui_id, ::widget::Widget { + kind: Kind::TextBox(new_state), + xy: xy, + depth: self.depth, + element: Some(element), + }); + } + } + +impl<'a, F> Colorable for TextBox<'a, F> { + fn color(mut self, color: Color) -> Self { + self.maybe_color = Some(color); + self + } +} + +impl<'a, F> Frameable for TextBox<'a, F> { + fn frame(mut self, width: f64) -> Self { + self.maybe_frame = Some(width); + self + } + fn frame_color(mut self, color: Color) -> Self { + self.maybe_frame_color = Some(color); + self + } +} + +impl<'a, F> position::Positionable for TextBox<'a, F> { + fn position(mut self, pos: Position) -> Self { + self.pos = pos; + self + } + #[inline] + fn horizontal_align(self, h_align: HorizontalAlign) -> Self { + TextBox { maybe_h_align: Some(h_align), ..self } + } + #[inline] + fn vertical_align(self, v_align: VerticalAlign) -> Self { + TextBox { maybe_v_align: Some(v_align), ..self } + } +} + +impl<'a, F> position::Sizeable for TextBox<'a, F> { + #[inline] + fn width(self, w: f64) -> Self { + let h = self.dim[1]; + TextBox { dim: [w, h], ..self } + } + #[inline] + fn height(self, h: f64) -> Self { + let w = self.dim[0]; + TextBox { dim: [w, h], ..self } + } +} + diff --git a/src/widget/toggle.rs b/src/widget/toggle.rs index dfd08bfdf..13cb4bd2e 100644 --- a/src/widget/toggle.rs +++ b/src/widget/toggle.rs @@ -1,20 +1,14 @@ use color::{Color, Colorable}; -use dimensions::Dimensions; use frame::Frameable; -use graphics::Graphics; -use graphics::character::CharacterCache; use label::{FontSize, Labelable}; use mouse::Mouse; -use point::Point; -use position::Positionable; -use rectangle; -use shape::Shapeable; +use position::{self, Depth, Dimensions, HorizontalAlign, Position, VerticalAlign}; use ui::{UiId, Ui}; use widget::Kind; /// Represents the state of the Toggle widget. -#[derive(PartialEq, Clone, Copy)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum State { Normal, Highlighted, @@ -22,12 +16,12 @@ pub enum State { } impl State { - /// Return the associated Rectangle state. - fn as_rectangle_state(&self) -> rectangle::State { - match self { - &State::Normal => rectangle::State::Normal, - &State::Highlighted => rectangle::State::Highlighted, - &State::Clicked => rectangle::State::Clicked, + /// Alter the widget color depending on the state. + fn color(&self, color: Color) -> Color { + match *self { + State::Normal => color, + State::Highlighted => color.highlighted(), + State::Clicked => color.clicked(), } } } @@ -53,9 +47,11 @@ fn get_new_state(is_over: bool, /// triggered upon release and will return the new bool state. Note that the toggle will not /// mutate the bool for you, you should do this yourself within the callback closure. pub struct Toggle<'a, F> { - ui_id: UiId, - pos: Point, + pos: Position, dim: Dimensions, + maybe_h_align: Option, + maybe_v_align: Option, + depth: Depth, maybe_callback: Option, maybe_color: Option, maybe_frame: Option, @@ -69,11 +65,13 @@ pub struct Toggle<'a, F> { impl<'a, F> Toggle<'a, F> { /// Construct a new Toggle widget. - pub fn new(ui_id: UiId, value: bool) -> Toggle<'a, F> { + pub fn new(value: bool) -> Toggle<'a, F> { Toggle { - ui_id: ui_id, - pos: [0.0, 0.0], + pos: Position::default(), dim: [64.0, 64.0], + maybe_h_align: None, + maybe_v_align: None, + depth: 0.0, maybe_callback: None, maybe_color: None, maybe_frame: None, @@ -91,6 +89,66 @@ impl<'a, F> Toggle<'a, F> { self } + /// After building the Toggle, use this method to set its current state into the given `Ui`. + /// It will use this state for rendering the next time `ui.draw(graphics)` is called. + pub fn set(mut self, ui_id: UiId, ui: &mut Ui) + where + F: FnMut(bool), + { + use elmesque::form::{collage, rect, text}; + use utils::is_over_rect; + + let state = *get_state(ui, ui_id); + let dim = self.dim; + let h_align = self.maybe_h_align.unwrap_or(ui.theme.h_align); + let v_align = self.maybe_v_align.unwrap_or(ui.theme.v_align); + let xy = ui.get_xy(self.pos, self.dim, h_align, v_align); + let mouse = ui.get_mouse_state(); + let is_over = is_over_rect(xy, mouse.xy, self.dim); + let new_state = get_new_state(is_over, state, mouse); + + // Callback. + if let (true, State::Clicked, State::Highlighted) = (is_over, state, new_state) { + if let Some(ref mut callback) = self.maybe_callback { callback(!self.value) } + } + + // Construct the frame and pressable forms. + let frame_w = self.maybe_frame.unwrap_or(ui.theme.frame_width); + let frame_color = self.maybe_frame_color.unwrap_or(ui.theme.frame_color); + let (inner_w, inner_h) = (dim[0] - frame_w * 2.0, dim[1] - frame_w * 2.0); + let frame_form = rect(dim[0], dim[1]).filled(frame_color); + let color = self.maybe_color.unwrap_or(ui.theme.shape_color); + let color = new_state.color(if self.value { color } else { color.with_luminance(0.1) }); + let pressable_form = rect(inner_w, inner_h).filled(color); + + // Construct the label's Form. + let maybe_label_form = self.maybe_label.map(|label_text| { + use elmesque::text::Text; + let text_color = self.maybe_label_color.unwrap_or(ui.theme.label_color); + let size = self.maybe_label_font_size.unwrap_or(ui.theme.font_size_medium); + text(Text::from_string(label_text.to_string()).color(text_color).height(size as f64)) + .shift(xy[0].floor(), xy[1].floor()) + }); + + // Chain the Forms and shift them into position. + let form_chain = Some(frame_form).into_iter() + .chain(Some(pressable_form).into_iter()) + .map(|form| form.shift(xy[0], xy[1])) + .chain(maybe_label_form.into_iter()); + + // Collect the Forms into a renderable Element. + let element = collage(dim[0] as i32, dim[1] as i32, form_chain.collect()); + + // Store the button's new state in the Ui. + ui.set_widget(ui_id, ::widget::Widget { + kind: Kind::Toggle(new_state), + xy: xy, + depth: self.depth, + element: Some(element), + }); + + } + } impl<'a, F> Colorable for Toggle<'a, F> { @@ -111,8 +169,7 @@ impl<'a, F> Frameable for Toggle<'a, F> { } } -impl<'a, F> Labelable<'a> for Toggle<'a, F> -{ +impl<'a, F> Labelable<'a> for Toggle<'a, F> { fn label(mut self, text: &'a str) -> Self { self.maybe_label = Some(text); self @@ -129,67 +186,31 @@ impl<'a, F> Labelable<'a> for Toggle<'a, F> } } -impl<'a, F> Positionable for Toggle<'a, F> { - fn point(mut self, pos: Point) -> Self { +impl<'a, F> position::Positionable for Toggle<'a, F> { + fn position(mut self, pos: Position) -> Self { self.pos = pos; self } + #[inline] + fn horizontal_align(self, h_align: HorizontalAlign) -> Self { + Toggle { maybe_h_align: Some(h_align), ..self } + } + #[inline] + fn vertical_align(self, v_align: VerticalAlign) -> Self { + Toggle { maybe_v_align: Some(v_align), ..self } + } } -impl<'a, F> Shapeable for Toggle<'a, F> { - fn get_dim(&self) -> Dimensions { self.dim } - fn dim(mut self, dim: Dimensions) -> Self { self.dim = dim; self } -} - -impl<'a, F> ::draw::Drawable for Toggle<'a, F> where F: FnMut(bool) + 'a { - fn draw(&mut self, ui: &mut Ui, graphics: &mut B) - where - B: Graphics::Texture>, - C: CharacterCache - { - let color = self.maybe_color.unwrap_or(ui.theme.shape_color); - let color = match self.value { - true => color, - false => color.with_luminance(0.1), - }; - let state = *get_state(ui, self.ui_id); - let mouse = ui.get_mouse_state(); - let is_over = rectangle::is_over(self.pos, mouse.pos, self.dim); - let new_state = get_new_state(is_over, state, mouse); - let rect_state = new_state.as_rectangle_state(); - match self.maybe_callback { - Some(ref mut callback) => { - match (is_over, state, new_state) { - (true, State::Clicked, State::Highlighted) => - (*callback)(match self.value { true => false, false => true }), - _ => (), - } - }, None => (), - } - let frame_w = self.maybe_frame.unwrap_or(ui.theme.frame_width); - let maybe_frame = match frame_w > 0.0 { - true => Some((frame_w, self.maybe_frame_color.unwrap_or(ui.theme.frame_color))), - false => None, - }; - match self.maybe_label { - None => { - rectangle::draw( - ui.win_w, ui.win_h, graphics, rect_state, self.pos, - self.dim, maybe_frame, color - ) - }, - Some(text) => { - let text_color = self.maybe_label_color.unwrap_or(ui.theme.label_color); - let size = self.maybe_label_font_size.unwrap_or(ui.theme.font_size_medium); - rectangle::draw_with_centered_label( - ui.win_w, ui.win_h, graphics, ui, rect_state, - self.pos, self.dim, maybe_frame, color, - text, size, text_color - ) - }, - } - - set_state(ui, self.ui_id, Kind::Toggle(new_state), self.pos, self.dim); - +impl<'a, F> position::Sizeable for Toggle<'a, F> { + #[inline] + fn width(self, w: f64) -> Self { + let h = self.dim[1]; + Toggle { dim: [w, h], ..self } + } + #[inline] + fn height(self, h: f64) -> Self { + let w = self.dim[0]; + Toggle { dim: [w, h], ..self } } } + diff --git a/src/widget/xy_pad.rs b/src/widget/xy_pad.rs index 95cfb339a..f73faf5ec 100644 --- a/src/widget/xy_pad.rs +++ b/src/widget/xy_pad.rs @@ -1,19 +1,14 @@ use color::{Color, Colorable}; -use dimensions::Dimensions; use frame::Frameable; -use graphics::{self, Graphics}; use graphics::character::CharacterCache; use label::{self, FontSize, Labelable}; use mouse::Mouse; -use num::{Float, ToPrimitive, FromPrimitive}; -use point::Point; -use position::Positionable; -use rectangle::{self, Corner}; -use shape::Shapeable; +use num::{Float, NumCast}; +use position::{self, Corner, Depth, Dimensions, HorizontalAlign, Position, VerticalAlign}; use ui::{UiId, Ui}; use utils::{clamp, map_range, val_to_string}; -use vecmath::{vec2_add, vec2_sub}; +use vecmath::vec2_sub; use widget::Kind; /// Represents the state of the xy_pad widget. @@ -25,12 +20,12 @@ pub enum State { } impl State { - /// Return the associated Rectangle state. - fn as_rectangle_state(&self) -> rectangle::State { + /// The color associated with the current state. + fn color(&self, color: Color) -> Color { match self { - &State::Normal => rectangle::State::Normal, - &State::Highlighted => rectangle::State::Highlighted, - &State::Clicked => rectangle::State::Clicked, + &State::Normal => color, + &State::Highlighted => color.highlighted(), + &State::Clicked => color.clicked(), } } } @@ -52,46 +47,19 @@ fn get_new_state(is_over: bool, } } -/// Draw the crosshair. -fn draw_crosshair( - win_w: f64, - win_h: f64, - graphics: &mut B, - pos: Point, - line_width: f64, - vert_x: f64, hori_y: f64, - pad_dim: Dimensions, - color: Color -) { - let draw_state = graphics::default_draw_state(); - let transform = graphics::abs_transform(win_w, win_h); - let line = graphics::Line::new(color.to_fsa(), 0.5 * line_width); - line.draw( - [vert_x, pos[1], vert_x, pos[1] + pad_dim[1]], - draw_state, - transform, - graphics - ); - line.draw( - [pos[0], hori_y, pos[0] + pad_dim[0], hori_y], - draw_state, - transform, - graphics - ); -} - - /// Used for displaying and controlling a 2D point on a cartesian plane within a given range. /// Its callback is triggered when the value is updated or if the mouse button is released while /// the cursor is above the rectangle. pub struct XYPad<'a, X, Y, F> { - ui_id: UiId, x: X, min_x: X, max_x: X, y: Y, min_y: Y, max_y: Y, line_width: f64, - font_size: FontSize, - pos: Point, + value_font_size: FontSize, + pos: Position, dim: Dimensions, + maybe_h_align: Option, + maybe_v_align: Option, + depth: Depth, maybe_callback: Option, maybe_color: Option, maybe_frame: Option, @@ -104,17 +72,17 @@ pub struct XYPad<'a, X, Y, F> { impl<'a, X, Y, F> XYPad<'a, X, Y, F> { /// Construct a new XYPad widget. - pub fn new(ui_id: UiId, - x_val: X, min_x: X, max_x: X, - y_val: Y, min_y: Y, max_y: Y) -> XYPad<'a, X, Y, F> { + pub fn new(x_val: X, min_x: X, max_x: X, y_val: Y, min_y: Y, max_y: Y) -> XYPad<'a, X, Y, F> { XYPad { - ui_id: ui_id, x: x_val, min_x: min_x, max_x: max_x, y: y_val, min_y: min_y, max_y: max_y, line_width: 1.0, - font_size: 18u32, - pos: [0.0, 0.0], + value_font_size: 14u32, + pos: Position::default(), dim: [128.0, 128.0], + maybe_h_align: None, + maybe_v_align: None, + depth: 0.0, maybe_callback: None, maybe_color: None, maybe_frame: None, @@ -134,7 +102,7 @@ impl<'a, X, Y, F> XYPad<'a, X, Y, F> { /// Set the font size for the displayed crosshair value. #[inline] pub fn value_font_size(self, size: FontSize) -> XYPad<'a, X, Y, F> { - XYPad { font_size: size, ..self } + XYPad { value_font_size: size, ..self } } /// Set the callback for the XYPad. It will be triggered when the value is updated or if the @@ -144,6 +112,125 @@ impl<'a, X, Y, F> XYPad<'a, X, Y, F> { self } + /// After building the Button, use this method to set its current state into the given `Ui`. + /// It will use this state for rendering the next time `ui.draw(graphics)` is called. + pub fn set(mut self, ui_id: UiId, ui: &mut Ui) + where + C: CharacterCache, + F: FnMut(X, Y), + X: Float + NumCast + ToString, + Y: Float + NumCast + ToString, + { + use elmesque::form::{collage, line, rect, solid, text}; + use elmesque::text::Text; + use utils::is_over_rect; + + let state = *get_state(ui, ui_id); + let dim = self.dim; + let h_align = self.maybe_h_align.unwrap_or(ui.theme.h_align); + let v_align = self.maybe_v_align.unwrap_or(ui.theme.v_align); + let xy = ui.get_xy(self.pos, dim, h_align, v_align); + let mouse = ui.get_mouse_state().relative_to(xy); + let frame_w = self.maybe_frame.unwrap_or(ui.theme.frame_width); + let frame_w2 = frame_w * 2.0; + let pad_dim = vec2_sub(dim, [frame_w2; 2]); + let is_over_pad = is_over_rect([0.0, 0.0], mouse.xy, pad_dim); + let new_state = get_new_state(is_over_pad, state, mouse); + let half_pad_w = pad_dim[0] / 2.0; + let half_pad_h = pad_dim[1] / 2.0; + + // Determine new values from the mouse position over the pad. + let (new_x, new_y) = match (is_over_pad, new_state) { + (_, State::Normal) | (_, State::Highlighted) => (self.x, self.y), + (_, State::Clicked) => { + let temp_x = clamp(mouse.xy[0], -half_pad_w, half_pad_w); + let temp_y = clamp(mouse.xy[1], -half_pad_h, half_pad_h); + (map_range(temp_x, -half_pad_w, half_pad_w, self.min_x, self.max_x), + map_range(temp_y, -half_pad_h, half_pad_h, self.min_y, self.max_y)) + } + }; + + // Callback if value is changed or the pad is clicked/released. + if let Some(ref mut callback) = self.maybe_callback { + if self.x != new_x || self.y != new_y { callback(new_x, new_y) } + else { + match (state, new_state) { + (State::Highlighted, State::Clicked) | + (State::Clicked, State::Highlighted) => callback(new_x, new_y), + _ => (), + } + } + } + + // Construct the frame and inner rectangle Forms. + let color = new_state.color(self.maybe_color.unwrap_or(ui.theme.shape_color)); + let frame_color = self.maybe_frame_color.unwrap_or(ui.theme.frame_color); + let frame_form = rect(dim[0], dim[1]).filled(frame_color); + let pressable_form = rect(pad_dim[0], pad_dim[1]).filled(color); + + // Construct the label Form. + let maybe_label_form = self.maybe_label.map(|l_text| { + let l_color = self.maybe_label_color.unwrap_or(ui.theme.label_color); + let l_size = self.maybe_label_font_size.unwrap_or(ui.theme.font_size_medium); + text(Text::from_string(l_text.to_string()).color(l_color).height(l_size as f64)) + }); + + // Construct the crosshair line Forms. + let (ch_x, ch_y) = match (is_over_pad, new_state) { + (_, State::Normal) | (_, State::Highlighted) => + (map_range(new_x, self.min_x, self.max_x, -half_pad_w, half_pad_w).floor(), + map_range(new_y, self.min_y, self.max_y, -half_pad_h, half_pad_h).floor()), + (_, State::Clicked) => + (clamp(mouse.xy[0], -half_pad_w, half_pad_w).floor(), + clamp(mouse.xy[1], -half_pad_h, half_pad_h).floor()), + }; + let line_style = solid(color.plain_contrast()).width(self.line_width); + let vert_form = line(line_style.clone(), 0.0, -half_pad_h, 0.0, half_pad_h).shift_x(ch_x); + let hori_form = line(line_style, -half_pad_w, 0.0, half_pad_w, 0.0).shift_y(ch_y); + + // Construct the value string Form. + let x_string = val_to_string(self.x, self.max_x, self.max_x - self.min_x, dim[0] as usize); + let y_string = val_to_string(self.y, self.max_y, self.max_y - self.min_y, dim[1] as usize); + let value_string = format!("{}, {}", x_string, y_string); + let value_text_form = { + const PAD: f64 = 5.0; // Slight padding between the crosshair and the text. + let w = label::width(ui, self.value_font_size, &value_string); + let h = self.value_font_size as f64; + let x_shift = w / 2.0 + PAD; + let y_shift = h / 2.0 + PAD; + let (value_text_x, value_text_y) = match position::corner([ch_x, ch_y], pad_dim) { + Corner::TopLeft => (x_shift, -y_shift), + Corner::TopRight => (-x_shift, -y_shift), + Corner::BottomLeft => (x_shift, y_shift), + Corner::BottomRight => (-x_shift, y_shift), + }; + text(Text::from_string(value_string).color(color.plain_contrast()).height(h)) + .shift(ch_x, ch_y) + .shift(value_text_x.floor(), value_text_y.floor()) + }; + + // Chain the Forms and shift them into position. + let form_chain = Some(frame_form).into_iter() + .chain(Some(pressable_form).into_iter()) + .chain(maybe_label_form.into_iter()) + .chain(Some(vert_form).into_iter()) + .chain(Some(hori_form).into_iter()) + .chain(Some(value_text_form).into_iter()) + .map(|form| form.shift(xy[0].round(), xy[1].round())); + + // Turn the form into a renderable Element. + let element = collage(dim[0] as i32, dim[1] as i32, form_chain.collect()); + + // Store the XYPad's new state in the Ui. + ui.set_widget(ui_id, ::widget::Widget { + kind: Kind::XYPad(new_state), + xy: xy, + depth: self.depth, + element: Some(element), + }); + + } + } impl<'a, X, Y, F> Colorable for XYPad<'a, X, Y, F> { @@ -182,116 +269,31 @@ impl<'a, X, Y, F> Labelable<'a> for XYPad<'a, X, Y, F> } } -impl<'a, X, Y, F> Positionable for XYPad<'a, X, Y, F> { - fn point(mut self, pos: Point) -> Self { +impl<'a, X, Y, F> position::Positionable for XYPad<'a, X, Y, F> { + fn position(mut self, pos: Position) -> Self { self.pos = pos; self } + #[inline] + fn horizontal_align(self, h_align: HorizontalAlign) -> Self { + XYPad { maybe_h_align: Some(h_align), ..self } + } + #[inline] + fn vertical_align(self, v_align: VerticalAlign) -> Self { + XYPad { maybe_v_align: Some(v_align), ..self } + } } -impl<'a, X, Y, F> Shapeable for XYPad<'a, X, Y, F> { - fn get_dim(&self) -> Dimensions { self.dim } - fn dim(mut self, dim: Dimensions) -> Self { self.dim = dim; self } -} - -impl<'a, X, Y, F> ::draw::Drawable for XYPad<'a, X, Y, F> - where - X: Float + ToPrimitive + FromPrimitive + ToString, - Y: Float + ToPrimitive + FromPrimitive + ToString, - F: FnMut(X, Y) + 'a -{ - - fn draw(&mut self, ui: &mut Ui, graphics: &mut B) - where - B: Graphics::Texture>, - C: CharacterCache - { - - // Init. - let state = *get_state(ui, self.ui_id); - let mouse = ui.get_mouse_state(); - let frame_w = self.maybe_frame.unwrap_or(ui.theme.frame_width); - let frame_w2 = frame_w * 2.0; - let maybe_frame = match frame_w > 0.0 { - true => Some((frame_w, self.maybe_frame_color.unwrap_or(ui.theme.frame_color))), - false => None, - }; - let pad_dim = vec2_sub(self.dim, [frame_w2; 2]); - let pad_pos = vec2_add(self.pos, [frame_w, frame_w]); - let is_over_pad = rectangle::is_over(pad_pos, mouse.pos, pad_dim); - let new_state = get_new_state(is_over_pad, state, mouse); - - // Determine new values. - let (new_x, new_y) = match (is_over_pad, new_state) { - (_, State::Normal) | (_, State::Highlighted) => (self.x, self.y), - (_, State::Clicked) => { - let temp_x = clamp(mouse.pos[0], pad_pos[0], pad_pos[0] + pad_dim[0]); - let temp_y = clamp(mouse.pos[1], pad_pos[1], pad_pos[1] + pad_dim[1]); - (map_range(temp_x - self.pos[0], pad_dim[0], 0.0, self.min_x, self.max_x), - map_range(temp_y - self.pos[1], pad_dim[1], 0.0, self.min_y, self.max_y)) - } - }; - - // Callback if value is changed or the pad is clicked/released. - match self.maybe_callback { - Some(ref mut callback) => { - if self.x != new_x || self.y != new_y { (*callback)(new_x, new_y) } - else { - match (state, new_state) { - (State::Highlighted, State::Clicked) - | (State::Clicked, State::Highlighted) => (*callback)(new_x, new_y), - _ => (), - } - } - }, - None => (), - } - - // Draw. - let rect_state = new_state.as_rectangle_state(); - let color = self.maybe_color.unwrap_or(ui.theme.shape_color); - rectangle::draw(ui.win_w, ui.win_h, graphics, rect_state, self.pos, - self.dim, maybe_frame, color); - let (vert_x, hori_y) = match (is_over_pad, new_state) { - (_, State::Normal) | (_, State::Highlighted) => - (pad_pos[0] + map_range(new_x, self.min_x, self.max_x, pad_dim[0], 0.0), - pad_pos[1] + map_range(new_y, self.min_y, self.max_y, pad_dim[1], 0.0)), - (_, State::Clicked) => - (clamp(mouse.pos[0], pad_pos[0], pad_pos[0] + pad_dim[0]), - clamp(mouse.pos[1], pad_pos[1], pad_pos[1] + pad_dim[1])), - }; - // Crosshair. - draw_crosshair(ui.win_w, ui.win_h, graphics, pad_pos, self.line_width, - vert_x, hori_y, pad_dim, color.plain_contrast()); - // Label. - if let Some(l_text) = self.maybe_label { - let l_color = self.maybe_label_color.unwrap_or(ui.theme.label_color); - let l_size = self.maybe_label_font_size.unwrap_or(ui.theme.font_size_medium); - let l_w = label::width(ui, l_size, l_text); - let l_x = pad_pos[0] + (pad_dim[0] - l_w) / 2.0; - let l_y = pad_pos[1] + (pad_dim[1] - l_size as f64) / 2.0; - let l_pos = [l_x, l_y]; - ui.draw_text(graphics, l_pos, l_size, l_color, l_text); - } - // xy value string. - let x_string = val_to_string(self.x, self.max_x, - self.max_x - self.min_x, self.dim[0] as usize); - let y_string = val_to_string(self.y, self.max_y, - self.max_y - self.min_y, self.dim[1] as usize); - let xy_string = format!("{}, {}", x_string, y_string); - let xy_string_w = label::width(ui, self.font_size, &xy_string); - let xy_string_pos = { - match rectangle::corner(pad_pos, [vert_x, hori_y], pad_dim) { - Corner::TopLeft => [vert_x, hori_y], - Corner::TopRight => [vert_x - xy_string_w, hori_y], - Corner::BottomLeft => [vert_x, hori_y - self.font_size as f64], - Corner::BottomRight => [vert_x - xy_string_w, hori_y - self.font_size as f64], - } - }; - ui.draw_text(graphics, xy_string_pos, self.font_size, - color.plain_contrast(), &xy_string); - - set_state(ui, self.ui_id, Kind::XYPad(new_state), self.pos, self.dim); - +impl<'a, X, Y, F> position::Sizeable for XYPad<'a, X, Y, F> { + #[inline] + fn width(self, w: f64) -> Self { + let h = self.dim[1]; + XYPad { dim: [w, h], ..self } + } + #[inline] + fn height(self, h: f64) -> Self { + let w = self.dim[0]; + XYPad { dim: [w, h], ..self } } } +