Skip to main content

gpui_component/plot/
axis.rs

1use gpui::{
2    App, Bounds, FontWeight, Hsla, PathBuilder, Pixels, Point, SharedString, TextAlign, Window,
3    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
11pub const AXIS_GAP: f32 = 18.;
12
13/// Which side of an axis line the tick labels render on.
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
15pub enum AxisLabelSide {
16    /// X-axis: labels below the line. Y-axis: labels right of the line. (Default.)
17    #[default]
18    End,
19    /// X-axis: labels above the line. Y-axis: labels left of the line.
20    Start,
21}
22
23pub struct AxisText {
24    pub text: SharedString,
25    pub tick: Pixels,
26    pub color: Hsla,
27    pub font_size: Pixels,
28    pub align: TextAlign,
29}
30
31impl AxisText {
32    pub fn new(text: impl Into<SharedString>, tick: impl Into<Pixels>, color: Hsla) -> Self {
33        Self {
34            text: text.into(),
35            tick: tick.into(),
36            color,
37            font_size: TEXT_SIZE.into(),
38            align: TextAlign::Left,
39        }
40    }
41
42    pub fn font_size(mut self, font_size: impl Into<Pixels>) -> Self {
43        self.font_size = font_size.into();
44        self
45    }
46
47    pub fn align(mut self, align: TextAlign) -> Self {
48        self.align = align;
49        self
50    }
51}
52
53#[derive(Default)]
54pub struct PlotAxis {
55    x: Option<Pixels>,
56    x_label: PlotLabel,
57    x_axis: bool,
58    x_label_side: AxisLabelSide,
59    y: Option<Pixels>,
60    y_label: PlotLabel,
61    y_axis: bool,
62    y_label_side: AxisLabelSide,
63    stroke: Hsla,
64}
65
66impl PlotAxis {
67    pub fn new() -> Self {
68        Self {
69            x_axis: true,
70            ..Default::default()
71        }
72    }
73
74    /// Set the x-axis of the Axis.
75    pub fn x(mut self, x: impl Into<Pixels>) -> Self {
76        self.x = Some(x.into());
77        self
78    }
79
80    /// Show or hide the x-axis of the Axis.
81    ///
82    /// Default is true.
83    pub fn x_axis(mut self, x_axis: bool) -> Self {
84        self.x_axis = x_axis;
85        self
86    }
87
88    /// Set the x-label of the Axis.
89    pub fn x_label(mut self, label: impl IntoIterator<Item = AxisText>) -> Self {
90        if let Some(x) = self.x {
91            let side = self.x_label_side;
92            self.x_label = label
93                .into_iter()
94                .map(|t| {
95                    let y = match side {
96                        AxisLabelSide::End => x + px(TEXT_GAP * 3.),
97                        AxisLabelSide::Start => x - px(TEXT_GAP + TEXT_HEIGHT),
98                    };
99                    Text {
100                        text: t.text,
101                        origin: point(t.tick, y),
102                        color: t.color,
103                        font_size: t.font_size,
104                        font_weight: FontWeight::NORMAL,
105                        align: t.align,
106                    }
107                })
108                .into();
109        }
110        self
111    }
112
113    /// Set which side of the x-axis line tick labels render on.
114    pub fn x_label_side(mut self, side: AxisLabelSide) -> Self {
115        self.x_label_side = side;
116        self
117    }
118
119    /// Set the y-axis of the Axis.
120    pub fn y(mut self, y: impl Into<Pixels>) -> Self {
121        self.y = Some(y.into());
122        self
123    }
124
125    /// Show or hide the y-axis of the Axis.
126    ///
127    /// Default is true.
128    pub fn y_axis(mut self, y_axis: bool) -> Self {
129        self.y_axis = y_axis;
130        self
131    }
132
133    /// Set the y-label of the Axis.
134    pub fn y_label(mut self, label: impl IntoIterator<Item = AxisText>) -> Self {
135        if let Some(y) = self.y {
136            let side = self.y_label_side;
137            self.y_label = label
138                .into_iter()
139                .map(|t| {
140                    let x = match side {
141                        AxisLabelSide::End => y + px(TEXT_GAP),
142                        AxisLabelSide::Start => y - px(TEXT_GAP),
143                    };
144                    Text {
145                        text: t.text,
146                        origin: point(x, t.tick - px(TEXT_SIZE / 2.)),
147                        color: t.color,
148                        font_size: t.font_size,
149                        font_weight: FontWeight::NORMAL,
150                        align: t.align,
151                    }
152                })
153                .into();
154        }
155        self
156    }
157
158    /// Set which side of the y-axis line tick labels render on.
159    pub fn y_label_side(mut self, side: AxisLabelSide) -> Self {
160        self.y_label_side = side;
161        self
162    }
163
164    /// Set the stroke color of the Axis.
165    pub fn stroke(mut self, stroke: impl Into<Hsla>) -> Self {
166        self.stroke = stroke.into();
167        self
168    }
169
170    fn draw_axis(&self, start_point: Point<Pixels>, end_point: Point<Pixels>, window: &mut Window) {
171        let mut builder = PathBuilder::stroke(px(1.));
172        builder.move_to(start_point);
173        builder.line_to(end_point);
174        if let Ok(path) = builder.build() {
175            window.paint_path(path, self.stroke);
176        }
177    }
178
179    /// Paint the Axis.
180    pub fn paint(&self, bounds: &Bounds<Pixels>, window: &mut Window, cx: &mut App) {
181        let origin = bounds.origin;
182
183        // X axis
184        if let Some(x) = self.x {
185            if self.x_axis {
186                self.draw_axis(
187                    origin_point(px(0.), x, origin),
188                    origin_point(bounds.size.width, x, origin),
189                    window,
190                );
191            }
192        }
193        self.x_label.paint(bounds, window, cx);
194
195        // Y axis
196        if let Some(y) = self.y {
197            if self.y_axis {
198                self.draw_axis(
199                    origin_point(y, px(0.), origin),
200                    origin_point(y, bounds.size.height, origin),
201                    window,
202                );
203            }
204        }
205        self.y_label.paint(bounds, window, cx);
206    }
207}