diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 455e51bab3..9921b4425c 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -333,7 +333,7 @@ impl Hud { debug_info: DebugInfo, ) -> Vec { let mut events = Vec::new(); - let ref mut ui_widgets = self.ui.set_widgets(); + let ref mut ui_widgets = self.ui.set_widgets().0; let version = format!("{}-{}", env!("CARGO_PKG_VERSION"), common::util::GIT_HASH); diff --git a/voxygen/src/hud/skillbar.rs b/voxygen/src/hud/skillbar.rs index dc3018f829..6c76b787a1 100644 --- a/voxygen/src/hud/skillbar.rs +++ b/voxygen/src/hud/skillbar.rs @@ -9,6 +9,7 @@ widget_ids! { struct Ids { health_bar, health_bar_color, + health_tooltip, l_click, level_text, mana_bar, diff --git a/voxygen/src/menu/char_selection/ui.rs b/voxygen/src/menu/char_selection/ui.rs index 8594187df5..225e96e504 100644 --- a/voxygen/src/menu/char_selection/ui.rs +++ b/voxygen/src/menu/char_selection/ui.rs @@ -242,7 +242,7 @@ impl CharSelectionUi { // TODO: Split this into multiple modules or functions. fn update_layout(&mut self, client: &Client) -> Vec { let mut events = Vec::new(); - let ref mut ui_widgets = self.ui.set_widgets(); + let ref mut ui_widgets = self.ui.set_widgets().0; let version = env!("CARGO_PKG_VERSION"); // Character Selection ///////////////// diff --git a/voxygen/src/menu/main/ui.rs b/voxygen/src/menu/main/ui.rs index 3527ed7bdd..0af167effa 100644 --- a/voxygen/src/menu/main/ui.rs +++ b/voxygen/src/menu/main/ui.rs @@ -3,7 +3,7 @@ use crate::{ ui::{ self, img_ids::{BlankGraphic, ImageGraphic, VoxelGraphic}, - Ui, + Tooltip, Tooltipable, Ui, }, GlobalState, }; @@ -139,7 +139,7 @@ impl MainMenuUi { fn update_layout(&mut self, global_state: &mut GlobalState) -> Vec { let mut events = Vec::new(); - let ref mut ui_widgets = self.ui.set_widgets(); + let (ref mut ui_widgets, ref mut tooltip_manager) = self.ui.set_widgets(); let version = env!("CARGO_PKG_VERSION"); const TEXT_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0); const TEXT_COLOR_2: Color = Color::Rgba(1.0, 1.0, 1.0, 0.2); @@ -427,6 +427,14 @@ impl MainMenuUi { .label_color(TEXT_COLOR) .label_font_size(24) .label_y(Relative::Scalar(5.0)) + .with_tooltip( + tooltip_manager, + Tooltip::new("Login", "Click to login with the entered details") + .title_font_size(15) + .desc_font_size(10) + .title_text_color(TEXT_COLOR) + .desc_text_color(TEXT_COLOR_2), + ) .set(self.ids.login_button, ui_widgets) .was_clicked() { diff --git a/voxygen/src/ui/mod.rs b/voxygen/src/ui/mod.rs index 25a9e90a04..017d4894e2 100644 --- a/voxygen/src/ui/mod.rs +++ b/voxygen/src/ui/mod.rs @@ -16,6 +16,7 @@ pub use widgets::{ image_slider::ImageSlider, ingame::{Ingame, IngameAnchor, Ingameable}, toggle_button::ToggleButton, + tooltip::{Tooltip, Tooltipable}, }; use crate::{ @@ -44,9 +45,11 @@ use std::{ io::{BufReader, Read}, ops::Range, sync::Arc, + time::Duration, }; use util::{linear_to_srgb, srgb_to_linear}; use vek::*; +use widgets::tooltip::TooltipManager; #[derive(Debug)] pub enum UiError { @@ -106,6 +109,8 @@ pub struct Ui { need_cache_resize: bool, // Scaling of the ui scale: Scale, + // Tooltips + tooltip_manager: TooltipManager, } impl Ui { @@ -115,8 +120,16 @@ impl Ui { let renderer = window.renderer_mut(); + let mut ui = UiBuilder::new(win_dims).build(); + let tooltip_manager = TooltipManager::new( + ui.widget_id_generator(), + Duration::from_millis(1000), + Duration::from_millis(1000), + scale.scale_factor_logical(), + ); + Ok(Self { - ui: UiBuilder::new(win_dims).build(), + ui, image_map: Map::new(), cache: Cache::new(renderer)?, draw_commands: vec![], @@ -127,6 +140,7 @@ impl Ui { window_resized: None, need_cache_resize: false, scale, + tooltip_manager, }) } @@ -157,8 +171,8 @@ impl Ui { self.ui.widget_id_generator() } - pub fn set_widgets(&mut self) -> UiCell { - self.ui.set_widgets() + pub fn set_widgets(&mut self) -> (UiCell, &mut TooltipManager) { + (self.ui.set_widgets(), &mut self.tooltip_manager) } // Accepts Option so widget can be unfocused. @@ -221,6 +235,10 @@ impl Ui { } pub fn maintain(&mut self, renderer: &mut Renderer, cam_params: Option<(Mat4, f32)>) { + // Maintain tooltip manager + self.tooltip_manager + .maintain(self.ui.global_input(), self.scale.scale_factor_logical()); + // Regenerate draw commands and associated models only if the ui changed let mut primitives = match self.ui.draw_if_changed() { Some(primitives) => primitives, diff --git a/voxygen/src/ui/widgets/mod.rs b/voxygen/src/ui/widgets/mod.rs index 26420fc870..a3987f1a94 100644 --- a/voxygen/src/ui/widgets/mod.rs +++ b/voxygen/src/ui/widgets/mod.rs @@ -1,3 +1,4 @@ pub mod image_slider; pub mod ingame; pub mod toggle_button; +pub mod tooltip; diff --git a/voxygen/src/ui/widgets/tooltip.rs b/voxygen/src/ui/widgets/tooltip.rs new file mode 100644 index 0000000000..81a7240b77 --- /dev/null +++ b/voxygen/src/ui/widgets/tooltip.rs @@ -0,0 +1,308 @@ +use conrod_core::{ + builder_method, builder_methods, input::global::Global, text, widget, widget_ids, Color, + Colorable, FontSize, Positionable, Sizeable, UiCell, Widget, WidgetCommon, WidgetStyle, +}; +use std::time::{Duration, Instant}; + +#[derive(Copy, Clone)] +struct Hover(widget::Id, [f64; 2]); +#[derive(Copy, Clone)] +enum HoverState { + Hovering(Hover), + Fading(Instant, Hover, Option<(Instant, widget::Id)>), + Start(Instant, widget::Id), + None, +} + +// Spacing between the tooltip and mouse +const MOUSE_PAD_Y: f64 = 15.0; + +pub struct TooltipManager { + tooltip_id: widget::Id, + state: HoverState, + // How long before a tooltip is displayed when hovering + hover_dur: Duration, + // How long it takes a tooltip to disappear + fade_dur: Duration, + // Current scaling of the ui + logical_scale_factor: f64, +} +impl TooltipManager { + pub fn new( + mut generator: widget::id::Generator, + hover_dur: Duration, + fade_dur: Duration, + logical_scale_factor: f64, + ) -> Self { + Self { + tooltip_id: generator.next(), + state: HoverState::None, + hover_dur, + fade_dur, + logical_scale_factor, + } + } + pub fn maintain(&mut self, input: &Global, logical_scale_factor: f64) { + self.logical_scale_factor = logical_scale_factor; + + let current = &input.current; + + if let Some(um_id) = current.widget_under_mouse { + match self.state { + HoverState::Hovering(hover) if um_id == hover.0 || um_id == self.tooltip_id => (), + HoverState::Hovering(hover) => { + self.state = + HoverState::Fading(Instant::now(), hover, Some((Instant::now(), um_id))) + } + HoverState::Fading(_, _, Some((_, id))) + if um_id == id || um_id == self.tooltip_id => {} + HoverState::Fading(start, hover, _) => { + self.state = HoverState::Fading(start, hover, Some((Instant::now(), um_id))) + } + HoverState::Start(_, id) if um_id == id || um_id == self.tooltip_id => (), + HoverState::Start(_, _) | HoverState::None => { + self.state = HoverState::Start(Instant::now(), um_id) + } + } + } else { + match self.state { + HoverState::Hovering(hover) => { + self.state = HoverState::Fading(Instant::now(), hover, None) + } + HoverState::Fading(start, hover, Some((_, _))) => { + self.state = HoverState::Fading(start, hover, None) + } + HoverState::Start(_, _) => self.state = HoverState::None, + HoverState::Fading(_, _, None) | HoverState::None => (), + } + } + + // Handle fade timing + if let HoverState::Fading(start, _, maybe_hover) = self.state { + if start.elapsed() > self.fade_dur { + self.state = match maybe_hover { + Some((start, hover)) => HoverState::Start(start, hover), + None => HoverState::None, + }; + } + } + } + fn set_tooltip(&mut self, tooltip: Tooltip, src_id: widget::Id, ui: &mut UiCell) { + let tooltip_id = self.tooltip_id; + let mp_h = MOUSE_PAD_Y / self.logical_scale_factor; + + let tooltip = |transparency, mouse_pos: [f64; 2], ui: &mut UiCell| { + let [t_w, t_h] = tooltip.get_wh(ui).unwrap_or([0.0, 0.0]); + let [m_x, m_y] = mouse_pos; + let (w_w, w_h) = (ui.win_w, ui.win_h); + + // Determine position based on size and mouse position + // Flow to the bottom right of the mouse + let x = (m_x + t_w / 2.0).min(w_w / 2.0 - t_w / 2.0); + let y = (m_y - mp_h - t_h / 2.0).max(-w_h / 2.0 + t_h / 2.0); + tooltip + .floating(true) + .transparency(transparency) + .x_y(x, y) + .set(tooltip_id, ui); + }; + + match self.state { + HoverState::Hovering(hover) => tooltip(1.0, hover.1, ui), + HoverState::Fading(start, hover, _) => tooltip( + (1.0f32 - start.elapsed().as_millis() as f32 / self.hover_dur.as_millis() as f32) + .max(0.0), + hover.1, + ui, + ), + HoverState::Start(start, id) if id == src_id && start.elapsed() > self.hover_dur => { + let xy = ui.global_input().current.mouse.xy; + self.state = HoverState::Hovering(Hover(id, xy)); + tooltip(1.0, xy, ui); + } + HoverState::Start(_, _) | HoverState::None => (), + } + } +} + +pub struct Tooltipped<'a, W> { + inner: W, + tooltip_manager: &'a mut TooltipManager, + tooltip: Tooltip<'a>, +} +impl<'a, W: Widget> Tooltipped<'a, W> { + pub fn set(self, id: widget::Id, ui: &mut UiCell) -> W::Event { + let event = self.inner.set(id, ui); + self.tooltip_manager.set_tooltip(self.tooltip, id, ui); + event + } +} + +pub trait Tooltipable { + // If `Tooltip` is expensive to construct accept a closure here instead. + fn with_tooltip<'a>( + self, + tooltip_manager: &'a mut TooltipManager, + tooltip: Tooltip<'a>, + ) -> Tooltipped<'a, Self> + where + Self: std::marker::Sized; +} +impl Tooltipable for W { + fn with_tooltip<'a>( + self, + tooltip_manager: &'a mut TooltipManager, + tooltip: Tooltip<'a>, + ) -> Tooltipped<'a, W> { + Tooltipped { + inner: self, + tooltip_manager, + tooltip, + } + } +} + +/// A widget for displaying tooltips +#[derive(Clone, WidgetCommon)] +pub struct Tooltip<'a> { + #[conrod(common_builder)] + common: widget::CommonBuilder, + title_text: &'a str, + desc_text: &'a str, + style: Style, + transparency: f32, +} + +#[derive(Clone, Debug, Default, PartialEq, WidgetStyle)] +pub struct Style { + #[conrod(default = "theme.background_color")] + pub color: Option, + title: widget::text::Style, + desc: widget::text::Style, + // add background imgs here +} + +widget_ids! { + struct Ids { + title, + desc, + back_rect, + } +} + +pub struct State { + ids: Ids, +} + +impl<'a> Tooltip<'a> { + pub fn new(title: &'a str, desc: &'a str) -> Self { + Tooltip { + common: widget::CommonBuilder::default(), + style: Style::default(), + title_text: title, + desc_text: desc, + transparency: 1.0, + } + } + + /// Align the text to the left of its bounding **Rect**'s *x* axis range. + //pub fn left_justify(self) -> Self { + // self.justify(text::Justify::Left) + //} + + /// Align the text to the middle of its bounding **Rect**'s *x* axis range. + //pub fn center_justify(self) -> Self { + // self.justify(text::Justify::Center) + //} + + /// Align the text to the right of its bounding **Rect**'s *x* axis range. + //pub fn right_justify(self) -> Self { + // self.justify(text::Justify::Right) + //} + + // TODO: add method(s) to make children widgets and use that to determine height in height function (and in update to draw the widgets) + + /// Specify the font used for displaying the text. + pub fn font_id(mut self, font_id: text::font::Id) -> Self { + self.style.title.font_id = Some(Some(font_id)); + self.style.desc.font_id = Some(Some(font_id)); + self + } + + builder_methods! { + pub title_text_color { style.title.color = Some(Color) } + pub desc_text_color { style.desc.color = Some(Color) } + pub title_font_size { style.title.font_size = Some(FontSize) } + pub desc_font_size { style.desc.font_size = Some(FontSize) } + pub title_justify { style.title.justify = Some(text::Justify) } + pub desc_justify { style.desc.justify = Some(text::Justify) } + transparency { transparency = f32 } + } +} + +impl<'a> Widget for Tooltip<'a> { + type State = State; + type Style = Style; + type Event = (); + + fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { + State { + ids: Ids::new(id_gen), + } + } + + fn style(&self) -> Self::Style { + self.style.clone() + } + + fn update(self, args: widget::UpdateArgs) { + let widget::UpdateArgs { + id, + state, + rect, + style, + ui, + .. + } = args; + + // Apply transparency + let color = style.color(ui.theme()).alpha(self.transparency); + + // Background rectangle + widget::Rectangle::fill(rect.dim()) + .xy(rect.xy()) + .graphics_for(id) + .parent(id) + .color(color) + //.floating(true) + .set(state.ids.back_rect, ui); + + // Title of tooltip + widget::Text::new(self.title_text) + .w(rect.w()) + .graphics_for(id) + .parent(id) + .top_left_with_margins_on(state.ids.back_rect, 5.0, 5.0) + .with_style(self.style.title) + // Apply transparency + .color(style.title.color(ui.theme()).alpha(self.transparency)) + //.floating(true) + .set(state.ids.title, ui); + + // Description of tooltip + widget::Text::new(self.desc_text) + .w(rect.w()) + .graphics_for(id) + .parent(id) + .down_from(state.ids.title, 10.0) + .with_style(self.style.desc) + // Apply transparency + .color(style.desc.color(ui.theme()).alpha(self.transparency)) + // .floating(true) + .set(state.ids.desc, ui); + } +} + +impl<'a> Colorable for Tooltip<'a> { + builder_method!(color { style.color = Some(Color) }); +}