diff --git a/Cargo.lock b/Cargo.lock index f4b3a1431b..ef697517e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2368,6 +2368,7 @@ dependencies = [ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "msgbox 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "num 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "portpicker 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/voxygen/Cargo.toml b/voxygen/Cargo.toml index 1170b93dde..b622a16a06 100644 --- a/voxygen/Cargo.toml +++ b/voxygen/Cargo.toml @@ -47,3 +47,4 @@ simplelog = "0.5" msgbox = "0.1" directories = "1.0" portpicker = "0.1" +num = "0.2" diff --git a/voxygen/src/ui/mod.rs b/voxygen/src/ui/mod.rs index f151142e53..9af2f75d3e 100644 --- a/voxygen/src/ui/mod.rs +++ b/voxygen/src/ui/mod.rs @@ -12,7 +12,7 @@ mod font_ids; pub use event::Event; pub use graphic::Graphic; pub use scale::ScaleMode; -pub use widgets::toggle_button::ToggleButton; +pub use widgets::{image_slider::ImageSlider, toggle_button::ToggleButton}; use crate::{ render::{ diff --git a/voxygen/src/ui/widgets/image_slider.rs b/voxygen/src/ui/widgets/image_slider.rs new file mode 100644 index 0000000000..fde8f1de74 --- /dev/null +++ b/voxygen/src/ui/widgets/image_slider.rs @@ -0,0 +1,322 @@ +//! A widget for selecting a single value along some linear range. +use conrod_core::{ + builder_methods, image, + position::Range, + utils, + widget::{self, Image}, + widget_ids, Color, Colorable, Positionable, Rect, Sizeable, Widget, WidgetCommon, +}; +use num::{Float, Integer, Num, NumCast}; + +pub enum Discrete {} +pub enum Continuous {} + +pub trait ValueFromPercent { + fn value_from_percent(percent: f32, min: T, max: T) -> T; +} + +/// Linear value selection. +/// +/// If the slider's width is greater than it's height, it will automatically become a horizontal +/// slider, otherwise it will be a vertical slider. +/// +/// Its reaction is triggered if the value is updated or if the mouse button is released while +/// the cursor is above the rectangle. +#[derive(WidgetCommon)] +pub struct ImageSlider { + #[conrod(common_builder)] + common: widget::CommonBuilder, + value: T, + min: T, + max: T, + /// The amount in which the slider's display should be skewed. + /// + /// Higher skew amounts (above 1.0) will weight lower values. + /// + /// Lower skew amounts (below 1.0) will weight heigher values. + /// + /// All skew amounts should be greater than 0.0. + skew: f32, + track: Track, + slider: Slider, + kind: std::marker::PhantomData, +} + +struct Track { + image_id: image::Id, + color: Option, + src_rect: Option, + breadth: Option, + // Padding on the ends of the track constraining the slider to a smaller area + padding: (f32, f32), +} + +struct Slider { + image_id: image::Id, + hover_image_id: Option, + press_image_id: Option, + color: Option, + src_rect: Option, + length: Option, +} + +widget_ids! { + struct Ids { + track, + slider, + } +} + +/// Represents the state of the Slider widget. +pub struct State { + ids: Ids, +} + +impl ImageSlider { + fn new( + value: T, + min: T, + max: T, + slider_image_id: image::Id, + track_image_id: image::Id, + ) -> Self { + Self { + common: widget::CommonBuilder::default(), + value, + min, + max, + skew: 1.0, + track: Track { + image_id: track_image_id, + color: None, + src_rect: None, + breadth: None, + padding: (0.0, 0.0), + }, + slider: Slider { + image_id: slider_image_id, + hover_image_id: None, + press_image_id: None, + color: None, + src_rect: None, + length: None, + }, + kind: std::marker::PhantomData, + } + } + + builder_methods! { + pub skew { skew = f32 } + pub pad_track { track.padding = (f32, f32) } + pub hover_image { slider.hover_image_id = Some(image::Id) } + pub press_image { slider.press_image_id = Some(image::Id) } + pub track_breadth { track.breadth = Some(f32) } + pub slider_length { slider.length = Some(f32) } + pub track_color { track.color = Some(Color) } + pub slider_color { slider.color = Some(Color) } + pub track_src_rect { track.src_rect = Some(Rect) } + pub slider_src_rect { slider.src_rect = Some(Rect) } + } +} + +impl ImageSlider { + pub fn continuous( + value: T, + min: T, + max: T, + slider_image_id: image::Id, + track_image_id: image::Id, + ) -> Self { + ImageSlider::new(value, min, max, slider_image_id, track_image_id) + } +} + +impl ImageSlider { + pub fn discrete( + value: T, + min: T, + max: T, + slider_image_id: image::Id, + track_image_id: image::Id, + ) -> Self { + ImageSlider::new(value, min, max, slider_image_id, track_image_id) + } +} + +impl ValueFromPercent for Continuous { + fn value_from_percent(percent: f32, min: T, max: T) -> T { + utils::value_from_perc(percent, min, max) + } +} +impl ValueFromPercent for Discrete { + fn value_from_percent(percent: f32, min: T, max: T) -> T { + NumCast::from( + utils::value_from_perc(percent, min.to_f32().unwrap(), max.to_f32().unwrap()).round(), + ) + .unwrap() + } +} + +impl Widget for ImageSlider +where + T: NumCast + Num + Copy + PartialOrd, + K: ValueFromPercent, +{ + type State = State; + type Style = (); + type Event = Option; + + fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { + State { + ids: Ids::new(id_gen), + } + } + + fn style(&self) -> Self::Style { + () + } + + /// Update the state of the Slider. + fn update(self, args: widget::UpdateArgs) -> Self::Event { + let widget::UpdateArgs { + id, + state, + rect, + ui, + .. + } = args; + let ImageSlider { + value, + min, + max, + skew, + track, + slider, + .. + } = self; + let (start_pad, end_pad) = (track.padding.0 as f64, track.padding.1 as f64); + + let is_horizontal = rect.w() > rect.h(); + + let new_value = if let Some(mouse) = ui.widget_input(id).mouse() { + if mouse.buttons.left().is_down() { + let mouse_abs_xy = mouse.abs_xy(); + let (mouse_offset, track_length) = if is_horizontal { + // Horizontal. + ( + mouse_abs_xy[0] - rect.x.start - start_pad, + rect.w() - start_pad - end_pad, + ) + } else { + // Vertical. + ( + mouse_abs_xy[1] - rect.y.start - start_pad, + rect.h() - start_pad - end_pad, + ) + }; + let perc = utils::clamp(mouse_offset, 0.0, track_length) / track_length; + let skewed_perc = (perc).powf(skew as f64); + K::value_from_percent(skewed_perc as f32, min, max) + } else { + value + } + } else { + value + }; + + // Track + let track_rect = if is_horizontal { + let h = slider.length.map_or(rect.h() / 3.0, |h| h as f64); + Rect { + y: Range::from_pos_and_len(rect.y(), h), + ..rect + } + } else { + let w = slider.length.map_or(rect.w() / 3.0, |w| w as f64); + Rect { + x: Range::from_pos_and_len(rect.x(), w), + ..rect + } + }; + + let (x, y, w, h) = track_rect.x_y_w_h(); + Image::new(track.image_id) + .x_y(x, y) + .w_h(w, h) + .parent(id) + .graphics_for(id) + .color(track.color) + .set(state.ids.track, ui); + + // Slider + let slider_image = ui + .widget_input(id) + .mouse() + .map(|mouse| { + if mouse.buttons.left().is_down() { + slider + .press_image_id + .or(slider.hover_image_id) + .unwrap_or(slider.image_id) + } else { + slider.hover_image_id.unwrap_or(slider.image_id) + } + }) + .unwrap_or(slider.image_id); + + // The rectangle for positioning and sizing the slider. + let value_perc = utils::map_range(new_value, min, max, 0.0, 1.0); + let unskewed_perc = value_perc.powf(1.0 / skew as f64); + let slider_rect = if is_horizontal { + let pos = utils::map_range( + unskewed_perc, + 0.0, + 1.0, + rect.x.start + start_pad, + rect.x.end - end_pad, + ); + let w = slider.length.map_or(rect.w() / 10.0, |w| w as f64); + Rect { + x: Range::from_pos_and_len(pos, w), + ..rect + } + } else { + let pos = utils::map_range( + unskewed_perc, + 0.0, + 1.0, + rect.y.start + start_pad, + rect.y.end - end_pad, + ); + let h = slider.length.map_or(rect.h() / 10.0, |h| h as f64); + Rect { + y: Range::from_pos_and_len(pos, h), + ..rect + } + }; + + let (x, y, w, h) = slider_rect.x_y_w_h(); + Image::new(slider_image) + .x_y(x, y) + .w_h(w, h) + .parent(id) + .graphics_for(id) + .color(slider.color) + .set(state.ids.slider, ui); + + // If the value has just changed, return the new value. + if value != new_value { + Some(new_value) + } else { + None + } + } +} + +impl Colorable for ImageSlider { + fn color(mut self, color: Color) -> Self { + self.slider.color = Some(color); + self.track.color = Some(color); + self + } +} diff --git a/voxygen/src/ui/widgets/mod.rs b/voxygen/src/ui/widgets/mod.rs index 393608eee1..21f1c2656d 100644 --- a/voxygen/src/ui/widgets/mod.rs +++ b/voxygen/src/ui/widgets/mod.rs @@ -1 +1,2 @@ +pub mod image_slider; pub mod toggle_button;