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    // The progress fades a presentation, so an overshooting curve is clamped.
164    let progress = transition(
165        (HOVER_MEMORY, "progress"),
166        if hovered { 1. } else { 0. },
167        policy,
168        window,
169        cx,
170    )
171    .clamp(0., 1.);
172
173    memory.update(cx, |memory, _| {
174        if let (Some(live), Some(cursor)) = (live, cursor) {
175            memory.state = Some(live);
176            memory.cursor = cursor;
177        }
178        memory.progress = progress;
179        memory.entering = hovered && progress == 0.;
180        if !hovered && progress <= 0. {
181            memory.state = None;
182        }
183    });
184
185    let memory = memory.read(cx);
186    let state = memory.state.clone()?;
187    Some((
188        PlotHover {
189            state,
190            progress,
191            hovered,
192        },
193        memory.cursor,
194    ))
195}
196
197/// How far the enclosing plot's hover has faded in this frame, from `0` to `1`;
198/// see [`PlotHover::progress`].
199///
200/// For an overlay a plot returns from [`Plot::tooltip`](super::Plot::tooltip),
201/// which renders within the plot's element scope and fades with its hover
202/// without being handed the progress.
203///
204/// This reads the hover the enclosing [`PlotElement`](super::PlotElement)
205/// tracked in its element scope, so it is only meaningful while that plot is
206/// rendering its overlay. Anywhere else it reads no tracked hover and returns
207/// `1`.
208pub fn hover_progress(window: &mut Window, cx: &mut App) -> f32 {
209    window
210        .use_keyed_state(HOVER_MEMORY, cx, |_, _| HoverMemory::default())
211        .read(cx)
212        .progress
213}
214
215/// Whether this frame is the first the enclosing plot's cursor is on a datum;
216/// see [`PlotHover::is_entering`].
217///
218/// Like [`hover_progress`], this reads the enclosing plot's element scope and
219/// is only meaningful while that plot is rendering its overlay. Anywhere else
220/// it returns `false`.
221pub fn is_hover_entering(window: &mut Window, cx: &mut App) -> bool {
222    window
223        .use_keyed_state(HOVER_MEMORY, cx, |_, _| HoverMemory::default())
224        .read(cx)
225        .entering
226}
227
228#[cfg(test)]
229mod tests {
230    use gpui::{point, px};
231
232    use super::*;
233
234    #[test]
235    fn test_plot_hover_readers() {
236        let state = TooltipState::new(2, point(px(10.), px(20.)), vec![]);
237        let hover = PlotHover {
238            state,
239            progress: 1.,
240            hovered: true,
241        };
242        assert_eq!(hover.state().index, 2);
243        assert!(hover.is_hovered());
244        // Fully faded in: a pointer keeps travelling rather than snapping.
245        assert!(!hover.is_entering());
246
247        // The first hovered frame, before the fade has started.
248        let entering = PlotHover {
249            progress: 0.,
250            ..hover.clone()
251        };
252        assert!(entering.is_entering());
253
254        // Fading out after the cursor left: neither hovered nor entering.
255        let lingering = PlotHover {
256            progress: 0.4,
257            hovered: false,
258            ..hover
259        };
260        assert!(!lingering.is_hovered());
261        assert!(!lingering.is_entering());
262    }
263}