Skip to main content

gpui_base/plot/
element.rs

1//! The element behind every [`Plot`]: layout, appear and hover tracking and
2//! the overlay the plot returns from [`Plot::tooltip`].
3use std::{cell::Cell, rc::Rc};
4
5use gpui::{
6    AnyElement, App, Bounds, Element, ElementId, GlobalElementId, Hitbox, HitboxBehavior,
7    InspectorElementId, IntoElement, LayoutId, LongPressEvent, MouseMoveEvent, Pixels, Point, Size,
8    Style, TouchPhase, Window,
9};
10
11use super::{Plot, PlotAppear, appear::track_appear, hover::track_hover};
12
13/// Paints a [`Plot`] filling its container, with appear tracking when the plot
14/// has an [`Plot::id`], and hover tracking and the plot's tooltip overlay when
15/// it is also [`Plot::interactive`].
16///
17/// A plot becomes an element through this type:
18///
19/// ```ignore
20/// impl IntoElement for Sparkline {
21///     type Element = PlotElement<Self>;
22///
23///     fn into_element(self) -> Self::Element {
24///         PlotElement::new(self)
25///     }
26/// }
27/// ```
28///
29/// GPUI Component's `#[derive(IntoPlot)]` writes that impl.
30pub struct PlotElement<P>(P);
31
32impl<P: Plot + 'static> PlotElement<P> {
33    pub fn new(plot: P) -> Self {
34        Self(plot)
35    }
36
37    /// The last cursor position (plot-relative), shared by `prepaint` and `paint`.
38    fn tooltip_cursor(
39        global_id: &GlobalElementId,
40        window: &mut Window,
41    ) -> Rc<Cell<Option<Point<Pixels>>>> {
42        window.with_element_state(global_id, |prev, _| {
43            let cell: Rc<Cell<Option<Point<Pixels>>>> = prev.unwrap_or_default();
44            (cell.clone(), cell)
45        })
46    }
47}
48
49impl<P: Plot + 'static> IntoElement for PlotElement<P> {
50    type Element = Self;
51
52    fn into_element(self) -> Self::Element {
53        self
54    }
55}
56
57impl<P: Plot + 'static> Element for PlotElement<P> {
58    type RequestLayoutState = ();
59    // The occlusion-aware hitbox, the plot's prepainted children and the
60    // prepainted tooltip overlay, carried from `prepaint` to `paint`.
61    type PrepaintState = (Option<Hitbox>, Vec<AnyElement>, Option<AnyElement>);
62
63    fn id(&self) -> Option<ElementId> {
64        // `Some` gives the plot element state: appear motion and, when
65        // interactive, tooltips.
66        self.0.id()
67    }
68
69    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
70        None
71    }
72
73    fn request_layout(
74        &mut self,
75        _: Option<&GlobalElementId>,
76        _: Option<&InspectorElementId>,
77        window: &mut Window,
78        cx: &mut App,
79    ) -> (LayoutId, Self::RequestLayoutState) {
80        let style = Style {
81            size: Size::full(),
82            ..Default::default()
83        };
84        (window.request_layout(style, None, cx), ())
85    }
86
87    fn prepaint(
88        &mut self,
89        global_id: Option<&GlobalElementId>,
90        _: Option<&InspectorElementId>,
91        bounds: Bounds<Pixels>,
92        _: &mut Self::RequestLayoutState,
93        window: &mut Window,
94        cx: &mut App,
95    ) -> Self::PrepaintState {
96        // Children are laid out here, where `layout_as_root` / `prepaint_at` are legal,
97        // and before the early return so plots without an id still get them.
98        let children = self.0.prepaint(bounds, window, cx);
99
100        let Some(global_id) = global_id else {
101            return (None, children, None);
102        };
103
104        let appear = match self.0.appear_generation() {
105            Some(generation) => track_appear(global_id, generation, window, cx),
106            None => PlotAppear::complete(),
107        };
108        let appearing = appear.is_appearing();
109        self.0.appear(appear, window, cx);
110
111        if !self.0.interactive() {
112            return (None, children, None);
113        }
114
115        // `Hitbox::is_hovered` is false while an open popup or modal covers the plot.
116        let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
117
118        // The cell only gates visibility; the position comes from the live mouse and
119        // this frame's bounds so the tooltip does not lag a frame while scrolling.
120        let cursor = Self::tooltip_cursor(global_id, window)
121            .get()
122            .map(|_| window.mouse_position())
123            .filter(|mouse| bounds.contains(mouse))
124            .map(|mouse| mouse - bounds.origin);
125        // No tooltip while the marks draw in: its dots would land on data not
126        // painted yet.
127        let live = cursor
128            .filter(|_| !appearing)
129            .and_then(|position| self.0.tooltip_state(position, bounds, cx));
130
131        // The datum under the cursor, or the last one while its hover fades out.
132        let hover = track_hover(live, cursor, window, cx);
133        self.0
134            .hover(hover.as_ref().map(|(hover, _)| hover), window, cx);
135
136        let overlay = hover.and_then(|(hover, cursor)| {
137            let mut overlay = self.0.tooltip(hover.state(), cursor, bounds, window, cx)?;
138            overlay.prepaint_as_root(bounds.origin, bounds.size.into(), window, cx);
139            Some(overlay)
140        });
141
142        (Some(hitbox), children, overlay)
143    }
144
145    fn paint(
146        &mut self,
147        global_id: Option<&GlobalElementId>,
148        _: Option<&InspectorElementId>,
149        bounds: Bounds<Pixels>,
150        _: &mut Self::RequestLayoutState,
151        prepaint: &mut Self::PrepaintState,
152        window: &mut Window,
153        cx: &mut App,
154    ) {
155        self.0.paint(bounds, window, cx);
156
157        let (hitbox, children, overlay) = prepaint;
158        for child in children.iter_mut() {
159            child.paint(window, cx);
160        }
161
162        if let (Some(global_id), Some(hitbox)) = (global_id, hitbox.as_ref()) {
163            let cell = Self::tooltip_cursor(global_id, window);
164            let hitbox = hitbox.clone();
165            // Notify only the view painting this plot: `window.refresh()` would drop
166            // every sibling view's cache on each pixel of mouse movement.
167            let view = window.current_view();
168
169            if cfg!(any(target_os = "ios", target_os = "android")) {
170                // A finger has no hover: only a long press opens the tooltip, drags
171                // the crosshair, and closes it on lift.
172                window.on_mouse_event(
173                    move |e: &LongPressEvent, phase, window: &mut Window, cx: &mut App| {
174                        if !phase.bubble() {
175                            return;
176                        }
177                        let next = match e.phase {
178                            TouchPhase::Started => {
179                                if window.default_prevented() || !hitbox.is_hovered(window) {
180                                    return;
181                                }
182                                window.prevent_default();
183                                Some(e.start_position - bounds.origin)
184                            }
185                            TouchPhase::Moved => {
186                                if cell.get().is_none() {
187                                    return;
188                                }
189                                Some(e.position - bounds.origin)
190                            }
191                            TouchPhase::Ended | TouchPhase::Cancelled => None,
192                        };
193                        if cell.get() != next {
194                            cell.set(next);
195                            cx.notify(view);
196                        }
197                    },
198                );
199            } else {
200                // Relayout can move the plot under a still cursor without a move
201                // event, so re-derive hover each frame; only a visibility flip needs
202                // a corrective frame.
203                let next = hitbox
204                    .is_hovered(window)
205                    .then(|| window.mouse_position() - bounds.origin);
206                if cell.get() != next {
207                    let visibility_changed = cell.get().is_some() != next.is_some();
208                    cell.set(next);
209                    if visibility_changed {
210                        window.request_animation_frame();
211                    }
212                }
213
214                window.on_mouse_event(
215                    move |e: &MouseMoveEvent, _, window: &mut Window, cx: &mut App| {
216                        let next = hitbox
217                            .is_hovered(window)
218                            .then(|| e.position - bounds.origin);
219                        if cell.get() != next {
220                            cell.set(next);
221                            cx.notify(view);
222                        }
223                    },
224                );
225            }
226        }
227
228        // Crosshair and dots paint above the plot; the deferred box paints above everything.
229        if let Some(overlay) = overlay.as_mut() {
230            overlay.paint(window, cx);
231        }
232    }
233}