1use gpui::{
2 App, Background, Bounds, FontWeight, Hsla, PathBuilder, Pixels, Point, SharedString, TextAlign,
3 Window, point, px,
4};
5
6use super::{
7 label::PlotLabel, label::TEXT_GAP, label::TEXT_HEIGHT, label::TEXT_SIZE, label::Text,
8 origin_point,
9};
10
11#[deprecated(
13 since = "0.7.0",
14 note = "use `axis_gutter` with the label font size the chart draws"
15)]
16pub const AXIS_GAP: f32 = 18.;
17
18pub fn axis_gutter(font_size: Pixels) -> f32 {
26 font_size.as_f32() + TEXT_GAP * 4.
27}
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
31pub enum AxisLabelSide {
32 #[default]
34 End,
35 Start,
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
41pub enum AxisLabelPlacement {
42 #[default]
44 Outside,
45 Inside,
48}
49
50#[non_exhaustive]
53pub struct AxisText {
54 pub text: SharedString,
55 pub tick: Pixels,
56 pub color: Hsla,
57 pub font_size: Pixels,
58 pub align: TextAlign,
59}
60
61impl AxisText {
62 pub fn new(text: impl Into<SharedString>, tick: impl Into<Pixels>, color: Hsla) -> Self {
63 Self {
64 text: text.into(),
65 tick: tick.into(),
66 color,
67 font_size: TEXT_SIZE.into(),
68 align: TextAlign::Left,
69 }
70 }
71
72 pub fn font_size(mut self, font_size: impl Into<Pixels>) -> Self {
73 self.font_size = font_size.into();
74 self
75 }
76
77 pub fn align(mut self, align: TextAlign) -> Self {
78 self.align = align;
79 self
80 }
81}
82
83pub struct PlotAxis {
89 x: Option<Pixels>,
90 x_labels: Vec<AxisText>,
91 x_axis: bool,
92 x_label_side: AxisLabelSide,
93 y: Option<Pixels>,
94 y_labels: Vec<AxisText>,
95 y_axis: bool,
96 y_label_side: AxisLabelSide,
97 stroke: Background,
98}
99
100impl Default for PlotAxis {
101 fn default() -> Self {
102 Self::new()
103 }
104}
105
106impl PlotAxis {
107 pub fn new() -> Self {
108 Self {
109 x: None,
110 x_labels: Vec::new(),
111 x_axis: true,
112 x_label_side: AxisLabelSide::default(),
113 y: None,
114 y_labels: Vec::new(),
115 y_axis: false,
116 y_label_side: AxisLabelSide::default(),
117 stroke: Hsla::default().into(),
118 }
119 }
120
121 pub fn x(mut self, position: impl Into<Pixels>) -> Self {
124 self.x = Some(position.into());
125 self
126 }
127
128 pub fn x_axis(mut self, x_axis: bool) -> Self {
132 self.x_axis = x_axis;
133 self
134 }
135
136 pub fn x_label(mut self, labels: impl IntoIterator<Item = AxisText>) -> Self {
138 self.x_labels = labels.into_iter().collect();
139 self
140 }
141
142 pub fn x_label_side(mut self, side: AxisLabelSide) -> Self {
144 self.x_label_side = side;
145 self
146 }
147
148 pub fn y(mut self, position: impl Into<Pixels>) -> Self {
151 self.y = Some(position.into());
152 self
153 }
154
155 pub fn y_axis(mut self, y_axis: bool) -> Self {
159 self.y_axis = y_axis;
160 self
161 }
162
163 pub fn y_label(mut self, labels: impl IntoIterator<Item = AxisText>) -> Self {
165 self.y_labels = labels.into_iter().collect();
166 self
167 }
168
169 pub fn y_label_side(mut self, side: AxisLabelSide) -> Self {
171 self.y_label_side = side;
172 self
173 }
174
175 pub fn stroke(mut self, stroke: impl Into<Background>) -> Self {
177 self.stroke = stroke.into();
178 self
179 }
180
181 fn x_texts(&self, x: Pixels) -> Vec<Text> {
183 self.x_labels
184 .iter()
185 .map(|t| {
186 let y = match self.x_label_side {
187 AxisLabelSide::End => x + px(TEXT_GAP * 3.),
188 AxisLabelSide::Start => x - px(TEXT_GAP + TEXT_HEIGHT),
189 };
190 axis_text(t, point(t.tick, y))
191 })
192 .collect()
193 }
194
195 fn y_texts(&self, y: Pixels) -> Vec<Text> {
197 self.y_labels
198 .iter()
199 .map(|t| {
200 let x = match self.y_label_side {
201 AxisLabelSide::End => y + px(TEXT_GAP),
202 AxisLabelSide::Start => y - px(TEXT_GAP),
203 };
204 axis_text(t, point(x, t.tick - px(TEXT_SIZE / 2.)))
205 })
206 .collect()
207 }
208
209 fn draw_axis(&self, start_point: Point<Pixels>, end_point: Point<Pixels>, window: &mut Window) {
210 let mut builder = PathBuilder::stroke(px(1.));
211 builder.move_to(start_point);
212 builder.line_to(end_point);
213 if let Ok(path) = builder.build() {
214 window.paint_path(path, self.stroke);
215 }
216 }
217
218 pub fn paint(&self, bounds: &Bounds<Pixels>, window: &mut Window, cx: &mut App) {
220 let origin = bounds.origin;
221
222 if let Some(x) = self.x {
223 if self.x_axis {
224 self.draw_axis(
225 origin_point(px(0.), x, origin),
226 origin_point(bounds.size.width, x, origin),
227 window,
228 );
229 }
230 PlotLabel::new(self.x_texts(x)).paint(bounds, window, cx);
231 }
232
233 if let Some(y) = self.y {
234 if self.y_axis {
235 self.draw_axis(
236 origin_point(y, px(0.), origin),
237 origin_point(y, bounds.size.height, origin),
238 window,
239 );
240 }
241 PlotLabel::new(self.y_texts(y)).paint(bounds, window, cx);
242 }
243 }
244}
245
246fn axis_text(label: &AxisText, origin: Point<Pixels>) -> Text {
248 Text::new(label.text.clone(), origin, label.color)
249 .font_size(label.font_size)
250 .font_weight(FontWeight::NORMAL)
251 .align(label.align)
252}
253
254#[cfg(test)]
255mod tests {
256 use gpui::{Hsla, px};
257
258 use super::*;
259
260 fn labels() -> Vec<AxisText> {
261 vec![
262 AxisText::new("a", px(10.), Hsla::default()),
263 AxisText::new("b", px(20.), Hsla::default()).align(TextAlign::Right),
264 ]
265 }
266
267 fn origins(texts: Vec<Text>) -> Vec<(SharedString, Point<Pixels>)> {
268 texts.into_iter().map(|t| (t.text, t.origin)).collect()
269 }
270
271 #[test]
272 fn builder_order_does_not_move_labels() {
273 let labels_first = PlotAxis::new()
274 .x_label(labels())
275 .x(px(50.))
276 .x_label_side(AxisLabelSide::Start)
277 .y_label(labels())
278 .y(px(30.))
279 .y_label_side(AxisLabelSide::Start);
280 let labels_last = PlotAxis::new()
281 .x_label_side(AxisLabelSide::Start)
282 .x(px(50.))
283 .x_label(labels())
284 .y_label_side(AxisLabelSide::Start)
285 .y(px(30.))
286 .y_label(labels());
287
288 assert_eq!(
289 origins(labels_first.x_texts(px(50.))),
290 origins(labels_last.x_texts(px(50.)))
291 );
292 assert_eq!(
293 origins(labels_first.y_texts(px(30.))),
294 origins(labels_last.y_texts(px(30.)))
295 );
296 assert_eq!(
298 labels_first.x_texts(px(50.))[0].origin.y,
299 px(50. - TEXT_GAP - TEXT_HEIGHT)
300 );
301 }
302
303 #[test]
304 fn axis_gutter_fits_default_labels() {
305 assert_eq!(axis_gutter(px(TEXT_SIZE)), 18.);
306 }
307}