use super::image_frame::ImageFrame; use conrod_core::{ builder_method, builder_methods, image, input::global::Global, position::Dimension, text, widget, widget_ids, Color, Colorable, FontSize, Positionable, Sizeable, Ui, 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; const TEXT_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0); // Default text color 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 => (), HoverState::Hovering(hover) => { self.state = HoverState::Fading(Instant::now(), hover, Some((Instant::now(), um_id))) }, HoverState::Fading(_, _, Some((_, id))) if um_id == id => {}, HoverState::Fading(start, hover, _) => { self.state = HoverState::Fading(start, hover, Some((Instant::now(), um_id))) }, HoverState::Start(_, id) if um_id == 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, }; } } } // return true if visible #[allow(clippy::too_many_arguments)] // TODO: Pending review in #587 fn set_tooltip( &mut self, tooltip: &Tooltip, title_text: &str, desc_text: &str, title_col: Color, img_id: Option, image_dims: Option<(f64, f64)>, src_id: widget::Id, ui: &mut UiCell, ) -> bool { 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| { // Fill in text and the potential image beforehand to get an accurate size for // spacing let tooltip = tooltip .clone() .title(title_text) .desc(desc_text) .title_col(title_col) .image(img_id) .image_dims(image_dims); let [t_w, t_h] = tooltip.get_wh(ui).unwrap_or([0.0, 0.0]); let [m_x, m_y] = [mouse_pos[0], mouse_pos[1]]; let (w_w, w_h) = (ui.win_w, ui.win_h); // Determine position based on size and mouse position // Flow to the top left of the mouse when there is space let x = if (m_x + w_w / 2.0) > t_w { m_x - t_w / 2.0 } else { m_x + t_w / 2.0 }; let y = if w_h - (m_y + w_h / 2.0) > t_h + mp_h { m_y + mp_h + t_h / 2.0 } else { m_y - mp_h - t_h / 2.0 }; tooltip .floating(true) .transparency(transparency) .x_y(x, y) .set(tooltip_id, ui); }; match self.state { HoverState::Hovering(Hover(id, xy)) if id == src_id => tooltip(1.0, xy, ui), HoverState::Fading(start, Hover(id, xy), _) if id == src_id => tooltip( (0.1f32 - start.elapsed().as_millis() as f32 / self.hover_dur.as_millis() as f32) .max(0.0), xy, 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); }, _ => (), } matches!(self.state, HoverState::Hovering(Hover(id, _)) if id == src_id) } } pub struct Tooltipped<'a, W> { inner: W, tooltip_manager: &'a mut TooltipManager, title_text: &'a str, desc_text: &'a str, img_id: Option, image_dims: Option<(f64, f64)>, tooltip: &'a Tooltip<'a>, title_col: Color, } impl<'a, W: Widget> Tooltipped<'a, W> { pub fn tooltip_image(mut self, img_id: image::Id) -> Self { self.img_id = Some(img_id); self } pub fn tooltip_image_dims(mut self, dims: (f64, f64)) -> Self { self.image_dims = Some(dims); self } pub fn set_ext(self, id: widget::Id, ui: &mut UiCell) -> (W::Event, bool) { let event = self.inner.set(id, ui); let visible = self.tooltip_manager.set_tooltip( self.tooltip, self.title_text, self.desc_text, self.title_col, self.img_id, self.image_dims, id, ui, ); (event, visible) } pub fn set(self, id: widget::Id, ui: &mut UiCell) -> W::Event { self.set_ext(id, ui).0 } } pub trait Tooltipable { // If `Tooltip` is expensive to construct accept a closure here instead. fn with_tooltip<'a>( self, tooltip_manager: &'a mut TooltipManager, title_text: &'a str, desc_text: &'a str, tooltip: &'a Tooltip<'a>, title_col: Color, ) -> Tooltipped<'a, Self> where Self: std::marker::Sized; } impl Tooltipable for W { fn with_tooltip<'a>( self, tooltip_manager: &'a mut TooltipManager, title_text: &'a str, desc_text: &'a str, tooltip: &'a Tooltip<'a>, title_col: Color, ) -> Tooltipped<'a, W> { Tooltipped { inner: self, tooltip_manager, title_text, desc_text, img_id: None, image_dims: None, tooltip, title_col, } } } /// Vertical spacing between elements of the tooltip const V_PAD: f64 = 10.0; /// Horizontal spacing between elements of the tooltip const H_PAD: f64 = 10.0; /// Default portion of inner width that goes to an image const IMAGE_W_FRAC: f64 = 0.3; /// Default width multiplied by the description font size const DEFAULT_CHAR_W: f64 = 30.0; /// Text vertical spacing factor to account for overhanging text const TEXT_SPACE_FACTOR: f64 = 0.35; /// 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, title_col: Color, image: Option, image_dims: Option<(f64, f64)>, style: Style, transparency: f32, image_frame: ImageFrame, } #[derive(Clone, Debug, Default, PartialEq, WidgetStyle)] pub struct Style { #[conrod(default = "Color::Rgba(1.0, 1.0, 1.0, 1.0)")] pub color: Option, title: widget::text::Style, desc: widget::text::Style, // add background imgs here } widget_ids! { struct Ids { title, desc, image_frame, image, } } pub struct State { ids: Ids, } impl<'a> Tooltip<'a> { builder_methods! { 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) } image { image = Option } title { title_text = &'a str } desc { desc_text = &'a str } image_dims { image_dims = Option<(f64, f64)> } transparency { transparency = f32 } title_col { title_col = Color} } pub fn new(image_frame: ImageFrame) -> Self { Tooltip { common: widget::CommonBuilder::default(), style: Style::default(), title_text: "", desc_text: "", transparency: 1.0, image_frame, image: None, image_dims: None, title_col: TEXT_COLOR, } } /// 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) //} fn text_image_width(&self, total_width: f64) -> (f64, f64) { let inner_width = (total_width - H_PAD * 2.0).max(0.0); // Image defaults to 30% of the width let image_w = if self.image.is_some() { match self.image_dims { Some((w, _)) => w, None => (inner_width - H_PAD).max(0.0) * IMAGE_W_FRAC, } } else { 0.0 }; // Text gets the remaining width let text_w = (inner_width - if self.image.is_some() { image_w + H_PAD } else { 0.0 }) .max(0.0); (text_w, image_w) } /// 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 } } impl<'a> Widget for Tooltip<'a> { type Event = (); type State = State; type Style = Style; 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; // Widths let (text_w, image_w) = self.text_image_width(rect.w()); // Apply transparency let color = style.color(ui.theme()).alpha(self.transparency); // Background image frame self.image_frame .wh(rect.dim()) .xy(rect.xy()) .graphics_for(id) .parent(id) .color(color) .set(state.ids.image_frame, ui); // Image if let Some(img_id) = self.image { widget::Image::new(img_id) .w_h(image_w, self.image_dims.map_or(image_w, |(_, h)| h)) .graphics_for(id) .parent(id) .color(Some(color)) .top_left_with_margins_on(state.ids.image_frame, V_PAD, H_PAD) .set(state.ids.image, ui); } // Spacing for overhanging text let title_space = self.style.title.font_size(&ui.theme) as f64 * TEXT_SPACE_FACTOR; // Title of tooltip if !self.title_text.is_empty() { let title = widget::Text::new(self.title_text) .w(text_w) .graphics_for(id) .parent(id) .with_style(self.style.title) // Apply transparency .color(self.title_col); if self.image.is_some() { title .right_from(state.ids.image, H_PAD) .align_top_of(state.ids.image) } else { title.top_left_with_margins_on(state.ids.image_frame, V_PAD, H_PAD) } .set(state.ids.title, ui); } // Description of tooltip let desc = widget::Text::new(self.desc_text) .w(text_w) .graphics_for(id) .parent(id) // Apply transparency .color(style.desc.color(ui.theme()).alpha(self.transparency)) .with_style(self.style.desc); if !self.title_text.is_empty() { desc.down_from(state.ids.title, V_PAD * 0.5 + title_space) .align_left_of(state.ids.title) } else if self.image.is_some() { desc.right_from(state.ids.image, H_PAD) .align_top_of(state.ids.image) } else { desc.top_left_with_margins_on(state.ids.image_frame, V_PAD, H_PAD) } .set(state.ids.desc, ui); } /// Default width is based on the description font size unless the text is /// small enough to fit on a single line fn default_x_dimension(&self, ui: &Ui) -> Dimension { let single_line_title_w = widget::Text::new(self.title_text) .with_style(self.style.title) .get_w(ui) .unwrap_or(0.0); let single_line_desc_w = widget::Text::new(self.desc_text) .with_style(self.style.desc) .get_w(ui) .unwrap_or(0.0); let text_w = single_line_title_w.max(single_line_desc_w); let inner_w = if self.image.is_some() { match self.image_dims { Some((w, _)) => w + text_w + H_PAD, None => text_w / (1.0 - IMAGE_W_FRAC) + H_PAD, } } else { text_w }; let width = inner_w.min(self.style.desc.font_size(&ui.theme) as f64 * DEFAULT_CHAR_W) + 2.0 * H_PAD; Dimension::Absolute(width) } fn default_y_dimension(&self, ui: &Ui) -> Dimension { let (text_w, image_w) = self.text_image_width(self.get_w(ui).unwrap_or(0.0)); let title_h = if self.title_text.is_empty() { 0.0 } else { widget::Text::new(self.title_text) .with_style(self.style.title) .w(text_w) .get_h(ui) .unwrap_or(0.0) + self.style.title.font_size(&ui.theme) as f64 * TEXT_SPACE_FACTOR + 0.5 * V_PAD }; let desc_h = if self.desc_text.is_empty() { 0.0 } else { widget::Text::new(self.desc_text) .with_style(self.style.desc) .w(text_w) .get_h(ui) .unwrap_or(0.0) + self.style.desc.font_size(&ui.theme) as f64 * TEXT_SPACE_FACTOR }; // Image defaults to square shape let image_h = self.image_dims.map_or(image_w, |(_, h)| h); // Title height + desc height + padding/spacing let height = (title_h + desc_h).max(image_h) + 2.0 * V_PAD; Dimension::Absolute(height) } } impl<'a> Colorable for Tooltip<'a> { builder_method!(color { style.color = Some(Color) }); }