Skip to main content

gpui_base/plot/
element.rs

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