1use core::{fmt::Write, ops::Range};
2use heapless::String;
3
4use embedded_graphics::{
5 prelude::*,
6 primitives::{Line, PrimitiveStyle},
7 text::Text,
8 text::TextStyle,
9};
10
11use crate::range_conv::Scalable;
12use embedded_graphics::mono_font::ascii::FONT_5X8;
13use embedded_graphics::mono_font::MonoTextStyle;
14use embedded_graphics::text::{Alignment, Baseline, TextStyleBuilder};
15
16pub enum Placement {
18 X { x1: i32, x2: i32, y: i32 },
19 Y { y1: i32, y2: i32, x: i32 },
20}
21
22#[derive(Clone, Copy)]
24pub enum Scale {
25 Fixed(usize),
28 RangeFraction(usize),
31}
32
33impl Default for Scale {
34 fn default() -> Self {
35 Scale::RangeFraction(5)
36 }
37}
38
39pub struct Axis<'a> {
41 range: Range<i32>,
43 title: Option<&'a str>,
45 scale: Option<Scale>,
47}
48
49impl<'a> Axis<'a> {
51 pub fn new(range: Range<i32>) -> Axis<'a> {
53 Axis {
54 range,
55 title: None,
56 scale: None,
57 }
58 }
59
60 pub fn set_scale(mut self, scale: Scale) -> Axis<'a> {
62 self.scale = Some(scale);
63 self
64 }
65
66 pub fn set_title(mut self, title: &'a str) -> Axis<'a> {
68 self.title = Some(title);
69 self
70 }
71
72 pub fn into_drawable_axis<C>(self, placement: Placement) -> DrawableAxis<'a, C>
74 where
75 C: PixelColor + Default,
76 TextStyle: Clone + Default,
77 {
78 DrawableAxis {
79 axis: self,
80 placement,
81 color: None,
82 text_style: None,
83 tick_size: None,
84 thickness: None,
85 }
86 }
87}
88
89pub struct DrawableAxis<'a, C>
91where
92 C: PixelColor,
93 TextStyle: Clone + Default,
94{
95 axis: Axis<'a>,
96 placement: Placement,
97 color: Option<C>,
98 text_style: Option<MonoTextStyle<'a, C>>,
99 tick_size: Option<usize>,
100 thickness: Option<usize>,
101}
102
103impl<'a, C> DrawableAxis<'a, C>
104where
105 C: PixelColor + Default,
106 TextStyle: Clone + Default,
107{
108 pub fn set_color(mut self, val: C) -> DrawableAxis<'a, C> {
109 self.color = Some(val);
110 self
111 }
112 pub fn set_text_style(mut self, val: MonoTextStyle<'a, C>) -> DrawableAxis<'a, C> {
113 self.text_style = Some(val);
114 self
115 }
116
117 pub fn set_tick_size(mut self, val: usize) -> DrawableAxis<'a, C> {
119 self.tick_size = Some(val);
120 self
121 }
122
123 pub fn set_thickness(mut self, val: usize) -> DrawableAxis<'a, C> {
125 self.thickness = Some(val);
126 self
127 }
128}
129
130impl<'a, C> Drawable for DrawableAxis<'a, C>
131where
132 C: PixelColor + Default,
133 TextStyle: Clone + Default,
134{
135 type Color = C;
136 type Output = ();
137
138 fn draw<D: DrawTarget<Color = C>>(&self, display: &mut D) -> Result<(), D::Error> {
140 let color = self.color.unwrap_or_default();
141 let thickness = self.thickness.unwrap_or(1);
142 let tick_size = self.tick_size.unwrap_or(2);
143
144 let character_style = MonoTextStyle::new(&FONT_5X8, color);
145
146 let scale_marks = match self.axis.scale.unwrap_or_default() {
147 Scale::Fixed(interval) => self.axis.range.clone().into_iter().step_by(interval),
148 Scale::RangeFraction(fraction) => {
149 let len = self.axis.range.len();
150 self.axis.range.clone().into_iter().step_by(len / fraction)
151 }
152 };
153 match self.placement {
154 Placement::X { x1, x2, y } => {
155 let title_text_style = TextStyleBuilder::new()
156 .alignment(Alignment::Center)
157 .baseline(Baseline::Top)
158 .build();
159 let tick_text_style = TextStyleBuilder::new()
160 .alignment(Alignment::Left)
161 .baseline(Baseline::Top)
162 .build();
163 Line {
164 start: Point { x: x1, y },
165 end: Point { x: x2, y },
166 }
167 .into_styled(PrimitiveStyle::with_stroke(color, thickness as u32))
168 .draw(display)?;
169 if let Some(title) = self.axis.title {
170 Text::with_text_style(
171 title,
172 Point {
173 x: x1 + (x2 - x1) / 2,
174 y: y + 10,
175 },
176 character_style,
177 title_text_style,
178 )
179 .draw(display)?;
180 }
181 for mark in scale_marks {
182 let x = mark.scale_between_ranges(&self.axis.range, &(x1..x2));
183 Line {
184 start: Point {
185 x,
186 y: y - tick_size as i32,
187 },
188 end: Point {
189 x,
190 y: y + tick_size as i32,
191 },
192 }
193 .into_styled(PrimitiveStyle::with_stroke(color, thickness as u32))
194 .draw(display)?;
195 let mut buf: String<8> = String::new();
196 write!(buf, "{}", mark).unwrap();
197 Text::with_text_style(
198 &buf,
199 Point { x: x + 2, y: y + 2 },
200 character_style,
201 tick_text_style,
202 )
203 .draw(display)?;
204 }
205 }
206 Placement::Y { y1, y2, x } => {
207 let title_text_style = TextStyleBuilder::new()
208 .alignment(Alignment::Right)
209 .baseline(Baseline::Middle)
210 .build();
211 let tick_text_style = TextStyleBuilder::new()
212 .alignment(Alignment::Right)
213 .baseline(Baseline::Top)
214 .build();
215 Line {
216 start: Point { x, y: y1 },
217 end: Point { x, y: y2 },
218 }
219 .into_styled(PrimitiveStyle::with_stroke(color, thickness as u32))
220 .draw(display)?;
221
222 let mut tick_text_left_pos_bound = i32::MAX;
223 for mark in scale_marks {
224 let y = mark.scale_between_ranges(&self.axis.range, &(y2..y1));
225 Line {
226 start: Point {
227 x: x - tick_size as i32,
228 y,
229 },
230 end: Point {
231 x: x + tick_size as i32,
232 y,
233 },
234 }
235 .into_styled(PrimitiveStyle::with_stroke(color, thickness as u32))
236 .draw(display)?;
237 let mut buf: String<8> = String::new();
238 write!(buf, "{}", mark).unwrap();
239 let tick_val = Text::with_text_style(
240 &buf,
241 Point { x, y },
242 character_style,
243 tick_text_style,
244 );
245 if tick_val.bounding_box().top_left.x < tick_text_left_pos_bound {
246 tick_text_left_pos_bound = tick_val.bounding_box().top_left.x
247 };
248 tick_val.draw(display)?;
249 }
250 if let Some(title) = self.axis.title {
251 Text::with_text_style(
252 title,
253 Point {
254 x: tick_text_left_pos_bound - 1,
255 y: y1 + (y2 - y1) / 2,
256 },
257 character_style,
258 title_text_style,
259 )
260 .draw(display)?;
261 }
262 }
263 }
264 Ok(())
265 }
266}