1use super::image_frame::ImageFrame;
2use conrod_core::{
3 Color, Colorable, FontSize, Positionable, Sizeable, Ui, UiCell, Widget, WidgetCommon,
4 WidgetStyle, builder_method, builder_methods, image, input::global::Global,
5 position::Dimension, text, widget, widget_ids,
6};
7use std::time::{Duration, Instant};
8#[derive(Copy, Clone)]
9struct Hover(widget::Id, [f64; 2]);
10#[derive(Copy, Clone)]
11enum HoverState {
12 Hovering(Hover),
13 Fading(Instant, Hover, Option<(Instant, widget::Id)>),
14 Start(Instant, widget::Id),
15 None,
16}
17
18const MOUSE_PAD_Y: f64 = 15.0;
20const TEXT_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0); pub struct TooltipManager {
23 tooltip_id: widget::Id,
24 state: HoverState,
25 hover_dur: Duration,
27 fade_dur: Duration,
29 logical_scale_factor: f64,
31}
32impl TooltipManager {
33 pub fn new(
34 mut generator: widget::id::Generator,
35 hover_dur: Duration,
36 fade_dur: Duration,
37 logical_scale_factor: f64,
38 ) -> Self {
39 Self {
40 tooltip_id: generator.next(),
41 state: HoverState::None,
42 hover_dur,
43 fade_dur,
44 logical_scale_factor,
45 }
46 }
47
48 pub fn maintain(&mut self, input: &Global, logical_scale_factor: f64) {
49 self.logical_scale_factor = logical_scale_factor;
50
51 let current = &input.current;
52
53 if let Some(um_id) = current.widget_under_mouse {
54 match self.state {
55 HoverState::Hovering(hover) if um_id == hover.0 => (),
56 HoverState::Hovering(hover) => {
57 self.state =
58 HoverState::Fading(Instant::now(), hover, Some((Instant::now(), um_id)))
59 },
60 HoverState::Fading(_, _, Some((_, id))) if um_id == id => {},
61 HoverState::Fading(start, hover, _) => {
62 self.state = HoverState::Fading(start, hover, Some((Instant::now(), um_id)))
63 },
64 HoverState::Start(_, id) if um_id == id => (),
65 HoverState::Start(_, _) | HoverState::None => {
66 self.state = HoverState::Start(Instant::now(), um_id)
67 },
68 }
69 } else {
70 match self.state {
71 HoverState::Hovering(hover) => {
72 self.state = HoverState::Fading(Instant::now(), hover, None)
73 },
74 HoverState::Fading(start, hover, Some((_, _))) => {
75 self.state = HoverState::Fading(start, hover, None)
76 },
77 HoverState::Start(_, _) => self.state = HoverState::None,
78 HoverState::Fading(_, _, None) | HoverState::None => (),
79 }
80 }
81
82 if let HoverState::Fading(start, _, maybe_hover) = self.state {
84 if start.elapsed() > self.fade_dur {
85 self.state = match maybe_hover {
86 Some((start, hover)) => HoverState::Start(start, hover),
87 None => HoverState::None,
88 };
89 }
90 }
91 }
92
93 fn set_tooltip(
95 &mut self,
96 tooltip: &Tooltip,
97 title_text: &str,
98 desc_text: &str,
99 title_col: Color,
100 img_id: Option<image::Id>,
101 image_dims: Option<(f64, f64)>,
102 src_id: widget::Id,
103 ui: &mut UiCell,
104 ) -> bool {
105 let tooltip_id = self.tooltip_id;
106 let mp_h = MOUSE_PAD_Y / self.logical_scale_factor;
107
108 let tooltip = |transparency, mouse_pos: [f64; 2], ui: &mut UiCell| {
109 let tooltip = tooltip
112 .clone()
113 .title(title_text)
114 .desc(desc_text)
115 .title_col(title_col)
116 .image(img_id)
117 .image_dims(image_dims);
118
119 let [t_w, t_h] = tooltip.get_wh(ui).unwrap_or([0.0, 0.0]);
120 let [m_x, m_y] = [mouse_pos[0], mouse_pos[1]];
121 let (w_w, w_h) = (ui.win_w, ui.win_h);
122
123 let x = if (m_x + w_w / 2.0) > t_w {
126 m_x - t_w / 2.0
127 } else {
128 m_x + t_w / 2.0
129 };
130 let y = if w_h - (m_y + w_h / 2.0) > t_h + mp_h {
131 m_y + mp_h + t_h / 2.0
132 } else {
133 m_y - mp_h - t_h / 2.0
134 };
135 tooltip
136 .floating(true)
137 .transparency(transparency)
138 .x_y(x, y)
139 .set(tooltip_id, ui);
140 };
141
142 match self.state {
143 HoverState::Hovering(Hover(id, xy)) if id == src_id => tooltip(1.0, xy, ui),
144 HoverState::Fading(start, Hover(id, xy), _) if id == src_id => tooltip(
145 (0.1f32 - start.elapsed().as_millis() as f32 / self.hover_dur.as_millis() as f32)
146 .max(0.0),
147 xy,
148 ui,
149 ),
150 HoverState::Start(start, id) if id == src_id && start.elapsed() > self.hover_dur => {
151 let xy = ui.global_input().current.mouse.xy;
152 self.state = HoverState::Hovering(Hover(id, xy));
153 tooltip(1.0, xy, ui);
154 },
155 _ => (),
156 }
157 matches!(self.state, HoverState::Hovering(Hover(id, _)) if id == src_id)
158 }
159}
160
161pub struct Tooltipped<'a, W> {
162 inner: W,
163 tooltip_manager: &'a mut TooltipManager,
164 title_text: &'a str,
165 desc_text: &'a str,
166 img_id: Option<image::Id>,
167 image_dims: Option<(f64, f64)>,
168 tooltip: &'a Tooltip<'a>,
169 title_col: Color,
170}
171impl<W: Widget> Tooltipped<'_, W> {
172 pub fn tooltip_image(mut self, img_id: image::Id) -> Self {
173 self.img_id = Some(img_id);
174 self
175 }
176
177 pub fn tooltip_image_dims(mut self, dims: (f64, f64)) -> Self {
178 self.image_dims = Some(dims);
179 self
180 }
181
182 pub fn set_ext(self, id: widget::Id, ui: &mut UiCell) -> (W::Event, bool) {
183 let event = self.inner.set(id, ui);
184 let visible = self.tooltip_manager.set_tooltip(
185 self.tooltip,
186 self.title_text,
187 self.desc_text,
188 self.title_col,
189 self.img_id,
190 self.image_dims,
191 id,
192 ui,
193 );
194 (event, visible)
195 }
196
197 pub fn set(self, id: widget::Id, ui: &mut UiCell) -> W::Event { self.set_ext(id, ui).0 }
198}
199
200pub trait Tooltipable {
201 fn with_tooltip<'a>(
203 self,
204 tooltip_manager: &'a mut TooltipManager,
205 title_text: &'a str,
206 desc_text: &'a str,
207 tooltip: &'a Tooltip<'a>,
208 title_col: Color,
209 ) -> Tooltipped<'a, Self>
210 where
211 Self: Sized;
212}
213impl<W: Widget> Tooltipable for W {
214 fn with_tooltip<'a>(
215 self,
216 tooltip_manager: &'a mut TooltipManager,
217 title_text: &'a str,
218 desc_text: &'a str,
219 tooltip: &'a Tooltip<'a>,
220 title_col: Color,
221 ) -> Tooltipped<'a, W> {
222 Tooltipped {
223 inner: self,
224 tooltip_manager,
225 title_text,
226 desc_text,
227 img_id: None,
228 image_dims: None,
229 tooltip,
230 title_col,
231 }
232 }
233}
234
235const V_PAD: f64 = 10.0;
237const H_PAD: f64 = 10.0;
239const IMAGE_W_FRAC: f64 = 0.3;
241const DEFAULT_CHAR_W: f64 = 30.0;
243const TEXT_SPACE_FACTOR: f64 = 0.35;
245
246#[derive(Clone, WidgetCommon)]
248pub struct Tooltip<'a> {
249 #[conrod(common_builder)]
250 common: widget::CommonBuilder,
251 title_text: &'a str,
252 desc_text: &'a str,
253 title_col: Color,
254 image: Option<image::Id>,
255 image_dims: Option<(f64, f64)>,
256 style: Style,
257 transparency: f32,
258 image_frame: ImageFrame,
259}
260
261#[derive(Clone, Debug, Default, PartialEq, WidgetStyle)]
262pub struct Style {
263 #[conrod(default = "Color::Rgba(1.0, 1.0, 1.0, 1.0)")]
264 pub color: Option<Color>,
265 title: widget::text::Style,
266 desc: widget::text::Style,
267 }
269
270widget_ids! {
271 struct Ids {
272 title,
273 desc,
274 image_frame,
275 image,
276 }
277}
278
279pub struct State {
280 ids: Ids,
281}
282
283impl<'a> Tooltip<'a> {
284 builder_methods! {
285 pub desc_text_color { style.desc.color = Some(Color) }
286 pub title_font_size { style.title.font_size = Some(FontSize) }
287 pub desc_font_size { style.desc.font_size = Some(FontSize) }
288 pub title_justify { style.title.justify = Some(text::Justify) }
289 pub desc_justify { style.desc.justify = Some(text::Justify) }
290 image { image = Option<image::Id> }
291 title { title_text = &'a str }
292 desc { desc_text = &'a str }
293 image_dims { image_dims = Option<(f64, f64)> }
294 transparency { transparency = f32 }
295 title_col { title_col = Color}
296 }
297
298 pub fn new(image_frame: ImageFrame) -> Self {
299 Tooltip {
300 common: widget::CommonBuilder::default(),
301 style: Style::default(),
302 title_text: "",
303 desc_text: "",
304 transparency: 1.0,
305 image_frame,
306 image: None,
307 image_dims: None,
308 title_col: TEXT_COLOR,
309 }
310 }
311
312 fn text_image_width(&self, total_width: f64) -> (f64, f64) {
328 let inner_width = (total_width - H_PAD * 2.0).max(0.0);
329 let image_w = if self.image.is_some() {
331 match self.image_dims {
332 Some((w, _)) => w,
333 None => (inner_width - H_PAD).max(0.0) * IMAGE_W_FRAC,
334 }
335 } else {
336 0.0
337 };
338 let text_w = (inner_width
340 - if self.image.is_some() {
341 image_w + H_PAD
342 } else {
343 0.0
344 })
345 .max(0.0);
346
347 (text_w, image_w)
348 }
349
350 #[must_use]
352 pub fn font_id(mut self, font_id: text::font::Id) -> Self {
353 self.style.title.font_id = Some(Some(font_id));
354 self.style.desc.font_id = Some(Some(font_id));
355 self
356 }
357}
358
359impl Widget for Tooltip<'_> {
360 type Event = ();
361 type State = State;
362 type Style = Style;
363
364 fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
365 State {
366 ids: Ids::new(id_gen),
367 }
368 }
369
370 fn style(&self) -> Self::Style { self.style.clone() }
371
372 fn update(self, args: widget::UpdateArgs<Self>) {
373 let widget::UpdateArgs {
374 id,
375 state,
376 rect,
377 style,
378 ui,
379 ..
380 } = args;
381
382 let (text_w, image_w) = self.text_image_width(rect.w());
384
385 let color = style.color(ui.theme()).alpha(self.transparency);
387
388 self.image_frame
390 .wh(rect.dim())
391 .xy(rect.xy())
392 .graphics_for(id)
393 .parent(id)
394 .color(color)
395 .set(state.ids.image_frame, ui);
396
397 if let Some(img_id) = self.image {
399 widget::Image::new(img_id)
400 .w_h(image_w, self.image_dims.map_or(image_w, |(_, h)| h))
401 .graphics_for(id)
402 .parent(id)
403 .color(Some(color))
404 .top_left_with_margins_on(state.ids.image_frame, V_PAD, H_PAD)
405 .set(state.ids.image, ui);
406 }
407
408 let title_space = self.style.title.font_size(&ui.theme) as f64 * TEXT_SPACE_FACTOR;
410
411 if !self.title_text.is_empty() {
413 let title = widget::Text::new(self.title_text)
414 .w(text_w)
415 .graphics_for(id)
416 .parent(id)
417 .with_style(self.style.title)
418 .color(self.title_col);
420
421 if self.image.is_some() {
422 title
423 .right_from(state.ids.image, H_PAD)
424 .align_top_of(state.ids.image)
425 } else {
426 title.top_left_with_margins_on(state.ids.image_frame, V_PAD, H_PAD)
427 }
428 .set(state.ids.title, ui);
429 }
430
431 let desc = widget::Text::new(self.desc_text)
433 .w(text_w)
434 .graphics_for(id)
435 .parent(id)
436 .color(style.desc.color(ui.theme()).alpha(self.transparency))
438 .with_style(self.style.desc);
439
440 if !self.title_text.is_empty() {
441 desc.down_from(state.ids.title, V_PAD * 0.5 + title_space)
442 .align_left_of(state.ids.title)
443 } else if self.image.is_some() {
444 desc.right_from(state.ids.image, H_PAD)
445 .align_top_of(state.ids.image)
446 } else {
447 desc.top_left_with_margins_on(state.ids.image_frame, V_PAD, H_PAD)
448 }
449 .set(state.ids.desc, ui);
450 }
451
452 fn default_x_dimension(&self, ui: &Ui) -> Dimension {
455 let single_line_title_w = widget::Text::new(self.title_text)
456 .with_style(self.style.title)
457 .get_w(ui)
458 .unwrap_or(0.0);
459 let single_line_desc_w = widget::Text::new(self.desc_text)
460 .with_style(self.style.desc)
461 .get_w(ui)
462 .unwrap_or(0.0);
463
464 let text_w = single_line_title_w.max(single_line_desc_w);
465 let inner_w = if self.image.is_some() {
466 match self.image_dims {
467 Some((w, _)) => w + text_w + H_PAD,
468 None => text_w / (1.0 - IMAGE_W_FRAC) + H_PAD,
469 }
470 } else {
471 text_w
472 };
473
474 let width =
475 inner_w.min(self.style.desc.font_size(&ui.theme) as f64 * DEFAULT_CHAR_W) + 2.0 * H_PAD;
476 Dimension::Absolute(width)
477 }
478
479 fn default_y_dimension(&self, ui: &Ui) -> Dimension {
480 let (text_w, image_w) = self.text_image_width(self.get_w(ui).unwrap_or(0.0));
481 let title_h = if self.title_text.is_empty() {
482 0.0
483 } else {
484 widget::Text::new(self.title_text)
485 .with_style(self.style.title)
486 .w(text_w)
487 .get_h(ui)
488 .unwrap_or(0.0)
489 + self.style.title.font_size(&ui.theme) as f64 * TEXT_SPACE_FACTOR
490 + 0.5 * V_PAD
491 };
492 let desc_h = if self.desc_text.is_empty() {
493 0.0
494 } else {
495 widget::Text::new(self.desc_text)
496 .with_style(self.style.desc)
497 .w(text_w)
498 .get_h(ui)
499 .unwrap_or(0.0)
500 + self.style.desc.font_size(&ui.theme) as f64 * TEXT_SPACE_FACTOR
501 };
502 let image_h = self.image_dims.map_or(image_w, |(_, h)| h);
504 let height = (title_h + desc_h).max(image_h) + 2.0 * V_PAD;
506 Dimension::Absolute(height)
507 }
508}
509
510impl Colorable for Tooltip<'_> {
511 builder_method!(color { style.color = Some(Color) });
512}