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