Skip to main content

gpui_base/plot/
hover.rs

1//! Hover tracking shared by every [`Plot`]: which datum the cursor is on, how
2//! far its hover has faded in, and where a pointer following it has glided to.
3//!
4//! This is behavior only. A styled layer draws the crosshair, dots and tooltip
5//! box, and projects its timing through [`PlotMotion`](crate::PlotMotion).
6use gpui::{App, Pixels, Point, Window};
7
8use crate::{
9    Spring, Theme,
10    motion::{TransitionId, spring, transition},
11};
12
13/// The datum the cursor resolved to, returned from
14/// [`Plot::tooltip_state`](super::Plot::tooltip_state).
15///
16/// Positions are relative to the plot's origin.
17#[derive(Clone, Debug)]
18#[non_exhaustive]
19pub struct TooltipState {
20    /// The hovered datum's index in the plot's data.
21    pub index: usize,
22    /// Where a crosshair marking the datum sits.
23    pub cross_line: Point<Pixels>,
24    /// The data points to mark, one per series at the hovered datum.
25    pub dots: Vec<Point<Pixels>>,
26}
27
28impl TooltipState {
29    pub fn new(index: usize, cross_line: Point<Pixels>, dots: Vec<Point<Pixels>>) -> Self {
30        Self {
31            index,
32            cross_line,
33            dots,
34        }
35    }
36}
37
38/// The datum a plot has under the pointer this frame, handed to
39/// [`Plot::hover`](super::Plot::hover).
40///
41/// Carries the [`TooltipState`] the cursor resolved to and how far the hover
42/// has faded in. After the cursor leaves, the state lingers here while the
43/// progress eases back to zero, so a hover-driven presentation can fade out
44/// over the last datum instead of vanishing.
45#[derive(Clone)]
46pub struct PlotHover {
47    state: TooltipState,
48    progress: f32,
49    hovered: bool,
50}
51
52impl PlotHover {
53    /// The hovered datum: the one under the cursor, or the last one while the
54    /// hover fades out.
55    pub fn state(&self) -> &TooltipState {
56        &self.state
57    }
58
59    /// How far the hover has faded in, from `0` to `1`.
60    ///
61    /// Rises over the active [`PlotMotion`](crate::PlotMotion)'s enter when the
62    /// cursor lands on a datum and falls back over its exit after it leaves,
63    /// during which [`Self::is_hovered`] is false.
64    pub fn progress(&self) -> f32 {
65        self.progress
66    }
67
68    #[deprecated(since = "0.7.0", note = "use `progress`")]
69    pub fn focus(&self) -> f32 {
70        self.progress()
71    }
72
73    /// Whether the cursor is on the datum, as opposed to the state lingering
74    /// while its hover fades out.
75    pub fn is_hovered(&self) -> bool {
76        self.hovered
77    }
78
79    /// Whether this is the first frame the cursor is on a datum: the hover has
80    /// not started fading in yet. A position that follows the hovered datum
81    /// adopts it here instead of travelling from where the last hover ended.
82    pub fn is_entering(&self) -> bool {
83        self.hovered && self.progress == 0.
84    }
85
86    /// Follow `target` on the [pointer spring](pointer_spring), adopting the
87    /// target on the entering frame instead of travelling from where the last
88    /// hover ended.
89    ///
90    /// For a position a plot paints with, such as the center of a highlighted
91    /// band or the crosshair a styled tooltip draws.
92    pub fn glide(
93        &self,
94        id: impl Into<TransitionId>,
95        target: Pixels,
96        window: &mut Window,
97        cx: &mut App,
98    ) -> Pixels {
99        let policy = pointer_spring(cx).with_travel(!self.is_entering());
100        spring(id, target, policy, window, cx)
101    }
102}
103
104/// The spring a hover pointer — the crosshair, highlight band or hover dot —
105/// follows the hovered datum with: the active [`PlotMotion`](crate::PlotMotion)'s
106/// pointer, which snaps unless a styled layer projects one.
107pub fn pointer_spring(cx: &App) -> Spring {
108    Theme::global(cx).plot.motion().pointer()
109}
110
111/// The last datum the cursor resolved to, where the cursor was and how far the
112/// hover has faded in, kept in element state so the hover can fade out over it
113/// after the cursor leaves and so an overlay can read the fade without being
114/// handed it; see [`hover_progress`].
115struct HoverMemory {
116    state: Option<TooltipState>,
117    cursor: Point<Pixels>,
118    progress: f32,
119    /// Whether this frame is the first the cursor is on a datum; see
120    /// [`PlotHover::is_entering`].
121    entering: bool,
122}
123
124impl Default for HoverMemory {
125    fn default() -> Self {
126        Self {
127            state: None,
128            cursor: Point::default(),
129            // An overlay rendered outside a plot's tracking is fully opaque.
130            progress: 1.,
131            entering: false,
132        }
133    }
134}
135
136/// The element-state key of a plot's [`HoverMemory`], within the plot's scope.
137const HOVER_MEMORY: &str = "__plot-hover";
138
139/// Resolve the datum a plot shows this frame from the `live` state the cursor
140/// resolved to.
141///
142/// While `live` is `Some` it is shown as is. After the cursor leaves, the last
143/// state lingers with its progress easing to zero over the active
144/// [`PlotMotion`](crate::PlotMotion)'s exit, then is dropped. Called by
145/// [`PlotElement`](super::PlotElement) within the plot's element scope; the
146/// returned cursor is the live one, or the last one while the state lingers.
147pub(super) fn track_hover(
148    live: Option<TooltipState>,
149    cursor: Option<Point<Pixels>>,
150    window: &mut Window,
151    cx: &mut App,
152) -> Option<(PlotHover, Point<Pixels>)> {
153    let hovered = live.is_some();
154    let memory = window.use_keyed_state(HOVER_MEMORY, cx, |_, _| HoverMemory::default());
155
156    let theme = Theme::global(cx);
157    let motion = theme.plot.motion();
158    let policy = if hovered {
159        motion.enter().clone()
160    } else {
161        motion.exit().clone()
162    };
163    let progress = transition(
164        (HOVER_MEMORY, "progress"),
165        if hovered { 1. } else { 0. },
166        policy,
167        window,
168        cx,
169    );
170
171    memory.update(cx, |memory, _| {
172        if let (Some(live), Some(cursor)) = (live, cursor) {
173            memory.state = Some(live);
174            memory.cursor = cursor;
175        }
176        memory.progress = progress;
177        memory.entering = hovered && progress == 0.;
178        if !hovered && progress <= 0. {
179            memory.state = None;
180        }
181    });
182
183    let memory = memory.read(cx);
184    let state = memory.state.clone()?;
185    Some((
186        PlotHover {
187            state,
188            progress,
189            hovered,
190        },
191        memory.cursor,
192    ))
193}
194
195/// How far the enclosing plot's hover has faded in this frame, from `0` to `1`;
196/// see [`PlotHover::progress`].
197///
198/// For an overlay a plot returns from [`Plot::tooltip`](super::Plot::tooltip),
199/// which renders within the plot's element scope and fades with its hover
200/// without being handed the progress.
201///
202/// This reads the hover the enclosing [`PlotElement`](super::PlotElement)
203/// tracked in its element scope, so it is only meaningful while that plot is
204/// rendering its overlay. Anywhere else it reads no tracked hover and returns
205/// `1`.
206pub fn hover_progress(window: &mut Window, cx: &mut App) -> f32 {
207    window
208        .use_keyed_state(HOVER_MEMORY, cx, |_, _| HoverMemory::default())
209        .read(cx)
210        .progress
211}
212
213/// Whether this frame is the first the enclosing plot's cursor is on a datum;
214/// see [`PlotHover::is_entering`].
215///
216/// Like [`hover_progress`], this reads the enclosing plot's element scope and
217/// is only meaningful while that plot is rendering its overlay. Anywhere else
218/// it returns `false`.
219pub fn is_hover_entering(window: &mut Window, cx: &mut App) -> bool {
220    window
221        .use_keyed_state(HOVER_MEMORY, cx, |_, _| HoverMemory::default())
222        .read(cx)
223        .entering
224}
225
226#[cfg(test)]
227mod tests {
228    use gpui::{point, px};
229
230    use super::*;
231
232    #[test]
233    fn test_plot_hover_readers() {
234        let state = TooltipState::new(2, point(px(10.), px(20.)), vec![]);
235        let hover = PlotHover {
236            state,
237            progress: 1.,
238            hovered: true,
239        };
240        assert_eq!(hover.state().index, 2);
241        assert!(hover.is_hovered());
242        // Fully faded in: a pointer keeps travelling rather than snapping.
243        assert!(!hover.is_entering());
244
245        // The first hovered frame, before the fade has started.
246        let entering = PlotHover {
247            progress: 0.,
248            ..hover.clone()
249        };
250        assert!(entering.is_entering());
251
252        // Fading out after the cursor left: neither hovered nor entering.
253        let lingering = PlotHover {
254            progress: 0.4,
255            hovered: false,
256            ..hover
257        };
258        assert!(!lingering.is_hovered());
259        assert!(!lingering.is_entering());
260    }
261}