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}