veloren_voxygen/ui/ice/widget/
tooltip.rs

1use iced::{
2    Clipboard, Element, Event, Hasher, Layout, Length, Point, Rectangle, Size, Widget, layout,
3};
4use std::{
5    hash::Hash,
6    sync::Mutex,
7    time::{Duration, Instant},
8};
9use vek::*;
10
11#[derive(Copy, Clone, Debug)]
12struct Hover {
13    start: Instant,
14    aabr: Aabr<i32>,
15}
16
17impl Hover {
18    fn start(aabr: Aabr<i32>) -> Self {
19        Self {
20            start: Instant::now(),
21            aabr,
22        }
23    }
24}
25
26#[derive(Copy, Clone, Debug)]
27struct Show {
28    hover_pos: Vec2<i32>,
29    aabr: Aabr<i32>,
30}
31
32#[derive(Copy, Clone, Debug)]
33enum State {
34    Idle,
35    Start(Hover),
36    Showing(Show),
37    Fading(Instant, Show, Option<Hover>),
38}
39
40// Reports which widget the mouse is over
41#[derive(Copy, Clone, Debug)]
42struct Update((Aabr<i32>, Vec2<i32>));
43
44#[derive(Debug)]
45// TODO: consider moving all this state into the Renderer
46pub struct TooltipManager {
47    state: State,
48    update: Mutex<Option<Update>>,
49    hover_pos: Vec2<i32>,
50    // How long before a tooltip is displayed when hovering
51    hover_dur: Duration,
52    // How long it takes a tooltip to disappear
53    fade_dur: Duration,
54}
55
56impl TooltipManager {
57    pub fn new(hover_dur: Duration, fade_dur: Duration) -> Self {
58        Self {
59            state: State::Idle,
60            update: Mutex::new(None),
61            hover_pos: Default::default(),
62            hover_dur,
63            fade_dur,
64        }
65    }
66
67    /// Call this at the top of your view function or for minimum latency at the
68    /// end of message handling
69    /// that there is no tooltipped widget currently being hovered
70    pub fn maintain(&mut self) {
71        let update = self.update.get_mut().unwrap().take();
72        // Handle changes based on pointer moving
73        self.state = if let Some(Update((aabr, hover_pos))) = update {
74            self.hover_pos = hover_pos;
75            match self.state {
76                State::Idle => State::Start(Hover::start(aabr)),
77                State::Start(hover) if hover.aabr != aabr => State::Start(Hover::start(aabr)),
78                State::Start(hover) => State::Start(hover),
79                State::Showing(show) if show.aabr != aabr => {
80                    State::Fading(Instant::now(), show, Some(Hover::start(aabr)))
81                },
82                State::Showing(show) => State::Showing(show),
83                State::Fading(start, show, Some(hover)) if hover.aabr == aabr => {
84                    State::Fading(start, show, Some(hover))
85                },
86                State::Fading(start, show, _) => {
87                    State::Fading(start, show, Some(Hover::start(aabr)))
88                },
89            }
90        } else {
91            match self.state {
92                State::Idle | State::Start(_) => State::Idle,
93                State::Showing(show) => State::Fading(Instant::now(), show, None),
94                State::Fading(start, show, _) => State::Fading(start, show, None),
95            }
96        };
97
98        // Handle temporal changes
99        self.state = match self.state {
100            State::Start(Hover { start, aabr })
101            | State::Fading(_, _, Some(Hover { start, aabr }))
102                if start.elapsed() >= self.hover_dur =>
103            {
104                State::Showing(Show {
105                    aabr,
106                    hover_pos: self.hover_pos,
107                })
108            },
109            State::Fading(start, _, hover) if start.elapsed() >= self.fade_dur => match hover {
110                Some(hover) => State::Start(hover),
111                None => State::Idle,
112            },
113            state @ State::Idle
114            | state @ State::Start(_)
115            | state @ State::Showing(_)
116            | state @ State::Fading(_, _, _) => state,
117        };
118    }
119
120    fn update(&self, update: Update) { *self.update.lock().unwrap() = Some(update); }
121
122    /// Returns an options with the position of the cursor when the tooltip
123    /// started being show and the transparency if it is fading
124    fn showing(&self, aabr: Aabr<i32>) -> Option<(Point, f32)> {
125        match self.state {
126            State::Idle | State::Start(_) => None,
127            State::Showing(show) => (show.aabr == aabr).then_some({
128                (
129                    Point {
130                        x: show.hover_pos.x as f32,
131                        y: show.hover_pos.y as f32,
132                    },
133                    1.0,
134                )
135            }),
136            State::Fading(start, show, _) => (show.aabr == aabr)
137                .then(|| {
138                    (
139                        Point {
140                            x: show.hover_pos.x as f32,
141                            y: show.hover_pos.y as f32,
142                        },
143                        1.0 - start.elapsed().as_secs_f32() / self.fade_dur.as_secs_f32(),
144                    )
145                })
146                .filter(|(_, fade)| *fade > 0.0),
147        }
148    }
149}
150
151/// A widget used to display tooltips when the content element is hovered
152pub struct Tooltip<'a, M, R: Renderer> {
153    content: Element<'a, M, R>,
154    hover_content: Box<dyn 'a + FnMut() -> Element<'a, M, R>>,
155    manager: &'a TooltipManager,
156}
157
158impl<'a, M, R> Tooltip<'a, M, R>
159where
160    R: Renderer,
161{
162    pub fn new<C, H>(content: C, hover_content: H, manager: &'a TooltipManager) -> Self
163    where
164        C: Into<Element<'a, M, R>>,
165        H: 'a + FnMut() -> Element<'a, M, R>,
166    {
167        Self {
168            content: content.into(),
169            hover_content: Box::new(hover_content),
170            manager,
171        }
172    }
173}
174
175impl<M, R> Widget<M, R> for Tooltip<'_, M, R>
176where
177    R: Renderer,
178{
179    fn width(&self) -> Length { self.content.width() }
180
181    fn height(&self) -> Length { self.content.height() }
182
183    fn layout(&self, renderer: &R, limits: &layout::Limits) -> layout::Node {
184        self.content.layout(renderer, limits)
185    }
186
187    fn draw(
188        &self,
189        renderer: &mut R,
190        defaults: &R::Defaults,
191        layout: Layout<'_>,
192        cursor_position: Point,
193        viewport: &Rectangle,
194    ) -> R::Output {
195        let bounds = layout.bounds();
196        if bounds.contains(cursor_position) {
197            // TODO: these bounds aren't actually global (for example see how the Scrollable
198            // widget handles its content) so it's not actually a good key to
199            // use here
200            let aabr = aabr_from_bounds(bounds);
201            let m_pos = Vec2::new(
202                cursor_position.x.trunc() as i32,
203                cursor_position.y.trunc() as i32,
204            );
205            self.manager.update(Update((aabr, m_pos)));
206        }
207
208        self.content
209            .draw(renderer, defaults, layout, cursor_position, viewport)
210    }
211
212    fn hash_layout(&self, state: &mut Hasher) {
213        struct Marker;
214        std::any::TypeId::of::<Marker>().hash(state);
215        self.content.hash_layout(state);
216    }
217
218    fn on_event(
219        &mut self,
220        event: Event,
221        layout: Layout<'_>,
222        cursor_position: Point,
223        renderer: &R,
224        clipboard: &mut dyn Clipboard,
225        messages: &mut Vec<M>,
226    ) -> iced::event::Status {
227        self.content.on_event(
228            event,
229            layout,
230            cursor_position,
231            renderer,
232            clipboard,
233            messages,
234        )
235    }
236
237    fn overlay(&mut self, layout: Layout<'_>) -> Option<iced::overlay::Element<'_, M, R>> {
238        let bounds = layout.bounds();
239        let aabr = aabr_from_bounds(bounds);
240
241        self.manager.showing(aabr).map(|(cursor_pos, alpha)| {
242            iced::overlay::Element::new(
243                Point::ORIGIN,
244                Box::new(Overlay::new(
245                    (self.hover_content)(),
246                    cursor_pos,
247                    bounds,
248                    alpha,
249                )),
250            )
251        })
252    }
253}
254
255impl<'a, M, R> From<Tooltip<'a, M, R>> for Element<'a, M, R>
256where
257    R: 'a + Renderer,
258    M: 'a,
259{
260    fn from(tooltip: Tooltip<'a, M, R>) -> Element<'a, M, R> { Element::new(tooltip) }
261}
262
263fn aabr_from_bounds(bounds: Rectangle) -> Aabr<i32> {
264    let min = Vec2::new(bounds.x.trunc() as i32, bounds.y.trunc() as i32);
265    let max = min + Vec2::new(bounds.width.trunc() as i32, bounds.height.trunc() as i32);
266    Aabr { min, max }
267}
268
269struct Overlay<'a, M, R: Renderer> {
270    content: Element<'a, M, R>,
271    /// Cursor position
272    cursor_position: Point,
273    /// Area to avoid overlapping with
274    avoid: Rectangle,
275    /// Alpha for fading out
276    alpha: f32,
277}
278
279impl<'a, M, R: Renderer> Overlay<'a, M, R> {
280    pub fn new(
281        content: Element<'a, M, R>,
282        cursor_position: Point,
283        avoid: Rectangle,
284        alpha: f32,
285    ) -> Self {
286        Self {
287            content,
288            cursor_position,
289            avoid,
290            alpha,
291        }
292    }
293}
294
295impl<M, R> iced::Overlay<M, R> for Overlay<'_, M, R>
296where
297    R: Renderer,
298{
299    fn layout(&self, renderer: &R, bounds: Size, position: Point) -> layout::Node {
300        let avoid = Rectangle {
301            x: self.avoid.x + position.x,
302            y: self.avoid.y + position.y,
303            ..self.avoid
304        };
305        let cursor_position = Point {
306            x: self.cursor_position.x + position.x,
307            y: self.cursor_position.y + position.y,
308        };
309
310        const PAD: f32 = 8.0; // TODO: allow configuration
311        let space_above = (avoid.y - PAD).max(0.0);
312        let space_below = (bounds.height - avoid.y - avoid.height - PAD).max(0.0);
313
314        let limits = layout::Limits::new(
315            Size::ZERO,
316            Size::new(bounds.width, space_above.max(space_below)),
317        );
318
319        let mut node = self.content.layout(renderer, &limits);
320
321        let size = node.size();
322
323        node.move_to(Point {
324            x: (bounds.width - size.width).min(cursor_position.x),
325            y: if space_above >= space_below {
326                avoid.y - size.height - PAD
327            } else {
328                avoid.y + avoid.height + PAD
329            },
330        });
331
332        node
333    }
334
335    fn draw(
336        &self,
337        renderer: &mut R,
338        defaults: &R::Defaults,
339        layout: Layout<'_>,
340        cursor_position: Point,
341    ) -> R::Output {
342        renderer.draw(
343            self.alpha,
344            defaults,
345            cursor_position,
346            &layout.bounds(),
347            &self.content,
348            layout,
349        )
350    }
351
352    fn hash_layout(&self, state: &mut Hasher, position: Point) {
353        struct Marker;
354        std::any::TypeId::of::<Marker>().hash(state);
355
356        (position.x as u32).hash(state);
357        (position.y as u32).hash(state);
358        (self.cursor_position.x as u32).hash(state);
359        (self.avoid.x as u32).hash(state);
360        (self.avoid.y as u32).hash(state);
361        (self.avoid.height as u32).hash(state);
362        (self.avoid.width as u32).hash(state);
363        self.content.hash_layout(state);
364    }
365}
366
367pub trait Renderer: iced::Renderer {
368    fn draw<M>(
369        &mut self,
370        alpha: f32,
371        defaults: &Self::Defaults,
372        cursor_position: Point,
373        viewport: &Rectangle,
374        content: &Element<'_, M, Self>,
375        content_layout: Layout<'_>,
376    ) -> Self::Output;
377}