Skip to main content

gpui_base/plot/
mod.rs

1//! Unstyled plotting: scales, shapes, axes, grids, labels, and the element and
2//! hover tracking behind every [`Plot`].
3//!
4//! Colors are always handed in by the caller. A styled layer supplies chart
5//! defaults, the tooltip overlay, and hover timing through [`PlotMotion`].
6mod axis;
7mod element;
8mod grid;
9mod hover;
10pub mod label;
11mod path_cache;
12pub mod scale;
13pub mod shape;
14
15use std::{fmt::Debug, ops::Add, time::Duration};
16
17use gpui::{
18    AnyElement, App, Bounds, ElementId, IntoElement, Path, PathBuilder, Pixels, Point, Window,
19    point, px,
20};
21
22use crate::{Spring, motion::Transition};
23
24#[allow(deprecated)]
25pub use axis::AXIS_GAP;
26pub use axis::{AxisLabelPlacement, AxisLabelSide, AxisText, PlotAxis, axis_gutter};
27pub use element::PlotElement;
28pub use grid::Grid;
29pub use hover::{PlotHover, TooltipState, hover_progress, is_hover_entering, pointer_spring};
30pub use label::PlotLabel;
31pub use path_cache::{PathCache, PathCaches, ShapeKey};
32pub use scale::PlotValue;
33
34/// The timing of a plot's hover: how its progress fades in and out, and the
35/// spring a pointer follows the hovered datum with.
36///
37/// Base installs no motion of its own: every duration defaults to zero, so the
38/// hover appears, fades and glides at once. Product timing belongs to the
39/// styled layer, which projects it through [`crate::PlotTheme`].
40#[derive(Clone)]
41pub struct PlotMotion {
42    pointer: Spring,
43    enter: Transition,
44    exit: Transition,
45}
46
47impl Default for PlotMotion {
48    fn default() -> Self {
49        Self {
50            pointer: Spring::new(Duration::ZERO),
51            enter: Transition::new(Duration::ZERO),
52            exit: Transition::new(Duration::ZERO),
53        }
54    }
55}
56
57impl PlotMotion {
58    /// The spring a crosshair, highlight band or hover dot follows the hovered
59    /// datum with.
60    pub fn with_pointer(mut self, pointer: Spring) -> Self {
61        self.pointer = pointer;
62        self
63    }
64
65    /// How the hover fades in when the cursor lands on a datum.
66    pub fn with_enter(mut self, enter: Transition) -> Self {
67        self.enter = enter;
68        self
69    }
70
71    /// How the hover fades out after the cursor leaves.
72    pub fn with_exit(mut self, exit: Transition) -> Self {
73        self.exit = exit;
74        self
75    }
76
77    pub fn pointer(&self) -> Spring {
78        self.pointer
79    }
80
81    pub fn enter(&self) -> &Transition {
82        &self.enter
83    }
84
85    pub fn exit(&self) -> &Transition {
86        &self.exit
87    }
88}
89
90pub trait Plot: IntoElement {
91    /// Lay out and place the child elements this plot hosts (e.g. element labels).
92    ///
93    /// Called during the element's prepaint phase, so implementations may use
94    /// [`AnyElement::layout_as_root`] / [`AnyElement::prepaint_at`] to measure and
95    /// position children — neither is legal from [`Plot::paint`]. The returned
96    /// elements are painted right after `paint`, below the tooltip overlay.
97    ///
98    /// Runs before [`Plot::tooltip_state`] and [`Plot::tooltip`], so anything
99    /// resolved here can be reused by them.
100    ///
101    /// The default returns no children.
102    fn prepaint(
103        &mut self,
104        _bounds: Bounds<Pixels>,
105        _window: &mut Window,
106        _cx: &mut App,
107    ) -> Vec<AnyElement> {
108        vec![]
109    }
110
111    fn paint(&mut self, bounds: Bounds<Pixels>, window: &mut Window, cx: &mut App);
112
113    /// A stable element id that enables interactive tooltip support for this plot.
114    ///
115    /// Return `Some(id)` to opt in to tooltips and hover motion; the id must be unique
116    /// among sibling elements. Returning `None` (the default for a hand-written plot)
117    /// disables all tooltip behavior, leaving the plot a pure, non-interactive element.
118    ///
119    /// The charts in GPUI Component always return `Some`: their id defaults to the
120    /// source location they were constructed at, and `id` renames it.
121    fn id(&self) -> Option<ElementId> {
122        None
123    }
124
125    /// Map the cursor to the tooltip state to display.
126    ///
127    /// `position` is the cursor position relative to the plot's top-left origin (already
128    /// origin-subtracted), and `bounds` is the painted area. Return the [`TooltipState`]
129    /// to display (highlighted index, crosshair point, dots), or `None` to show
130    /// nothing. Only called while the cursor is inside `bounds`.
131    ///
132    /// The default returns `None`.
133    fn tooltip_state(
134        &self,
135        _position: Point<Pixels>,
136        _bounds: Bounds<Pixels>,
137        _cx: &App,
138    ) -> Option<TooltipState> {
139        None
140    }
141
142    /// Receive the hovered datum this frame, before [`Plot::tooltip`] and
143    /// [`Plot::paint`] run.
144    ///
145    /// `hover` carries the [`TooltipState`] the cursor resolved to, and it
146    /// lingers after the cursor leaves while [`PlotHover::progress`] eases back to
147    /// zero, so a hover-driven presentation can fade out over the last datum
148    /// instead of vanishing. `None` means nothing is hovered and nothing is
149    /// fading.
150    ///
151    /// Called on every frame the plot has an [`Plot::id`], so this is where a
152    /// plot samples its hover motion ([`crate::motion::transition`],
153    /// [`PlotHover::glide`]) and keeps the result for the other two methods.
154    /// The default ignores the hover.
155    fn hover(&mut self, _hover: Option<&PlotHover>, _window: &mut Window, _cx: &mut App) {}
156
157    /// Render the tooltip overlay for the active [`TooltipState`].
158    ///
159    /// `cursor` is the live cursor position (relative to the plot origin) and `bounds` is the
160    /// plot's painted area, so the tooltip box can follow the cursor (pass `cursor` and
161    /// `bounds.size` to the styled layer's tooltip). Return the overlay element; it is
162    /// painted absolutely positioned above the plot graphics but below sibling content
163    /// drawn after the plot (a box that may overflow the plot should `deferred` itself).
164    /// The default returns `None`.
165    ///
166    /// Also called while the hover fades out, with the lingering `state` and the
167    /// last `cursor`; the overlay renders within the plot's element scope, so it can
168    /// fade with [`hover_progress`].
169    fn tooltip(
170        &self,
171        _state: &TooltipState,
172        _cursor: Point<Pixels>,
173        _bounds: Bounds<Pixels>,
174        _window: &mut Window,
175        _cx: &mut App,
176    ) -> Option<AnyElement> {
177        None
178    }
179}
180
181/// How a [`Line`](shape::Line) or [`Area`](shape::Area) connects its points,
182/// like d3's curve factories.
183#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
184pub enum Curve {
185    /// A smooth curve through every point (`d3.curveNatural`).
186    #[default]
187    Natural,
188    /// Straight segments between points (`d3.curveLinear`).
189    Linear,
190    /// A step that holds each value until the next point (`d3.curveStepAfter`).
191    StepAfter,
192}
193
194pub fn origin_point<T>(x: T, y: T, origin: Point<T>) -> Point<T>
195where
196    T: Default + Clone + Debug + PartialEq + Add<Output = T>,
197{
198    point(x, y) + origin
199}
200
201pub fn polygon<T>(points: &[Point<T>], bounds: &Bounds<Pixels>) -> Option<Path<Pixels>>
202where
203    T: Default + Clone + Copy + Debug + Into<f32> + PartialEq,
204{
205    let mut path = PathBuilder::stroke(px(1.));
206    let points = &points
207        .iter()
208        .map(|p| {
209            point(
210                px(p.x.into() + bounds.origin.x.as_f32()),
211                px(p.y.into() + bounds.origin.y.as_f32()),
212            )
213        })
214        .collect::<Vec<_>>();
215    path.add_polygon(points, false);
216    path.build().ok()
217}