1use rlvgl_core::event::Event;
8use rlvgl_core::raster::PointF;
9use rlvgl_core::renderer::Renderer;
10use rlvgl_core::style::Style;
11use rlvgl_core::widget::{Rect, Widget};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct Point {
16 pub x: i32,
18 pub y: i32,
20}
21
22#[derive(Debug)]
27pub struct Line<'a> {
28 bounds: Rect,
29 points: &'a [Point],
30 y_invert: bool,
31 pub style: Style,
36}
37
38impl<'a> Line<'a> {
39 pub fn new(bounds: Rect, points: &'a [Point]) -> Self {
44 let style = Style {
45 border_width: 1,
46 ..Style::default()
47 };
48 Self {
49 bounds,
50 points,
51 y_invert: false,
52 style,
53 }
54 }
55
56 pub fn points(&self) -> &'a [Point] {
58 self.points
59 }
60
61 pub fn set_points(&mut self, points: &'a [Point]) {
63 self.points = points;
64 }
65
66 pub fn set_y_invert(&mut self, y_invert: bool) {
68 self.y_invert = y_invert;
69 }
70
71 pub fn y_invert(&self) -> bool {
73 self.y_invert
74 }
75
76 fn point_to_framebuffer(&self, point: Point) -> PointF {
77 let y = if self.y_invert {
78 self.bounds.height - point.y
79 } else {
80 point.y
81 };
82 PointF::new((self.bounds.x + point.x) as f32, (self.bounds.y + y) as f32)
83 }
84}
85
86impl Widget for Line<'_> {
87 fn bounds(&self) -> Rect {
88 self.bounds
89 }
90
91 fn draw(&self, renderer: &mut dyn Renderer) {
92 if self.style.border_width == 0 || self.style.alpha == 0 {
93 return;
94 }
95
96 let color = self.style.border_color.with_alpha(self.style.alpha);
97 let width = self.style.border_width as f32;
98 for pair in self.points.windows(2) {
99 renderer.stroke_line_aa(
100 self.point_to_framebuffer(pair[0]),
101 self.point_to_framebuffer(pair[1]),
102 width,
103 color,
104 );
105 }
106 }
107
108 fn handle_event(&mut self, _event: &Event) -> bool {
109 false
110 }
111
112 fn set_bounds(&mut self, bounds: Rect) {
113 self.bounds = bounds;
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use alloc::vec::Vec;
121 use rlvgl_core::font::ShapedText;
122 use rlvgl_core::widget::Color;
123
124 #[derive(Debug, Clone, Copy, PartialEq)]
125 struct Stroke {
126 a: PointF,
127 b: PointF,
128 width: f32,
129 color: Color,
130 }
131
132 struct RecordingRenderer {
133 strokes: Vec<Stroke>,
134 }
135
136 impl RecordingRenderer {
137 fn new() -> Self {
138 Self {
139 strokes: Vec::new(),
140 }
141 }
142 }
143
144 impl Renderer for RecordingRenderer {
145 fn fill_rect(&mut self, _rect: Rect, _color: Color) {}
146
147 fn draw_text(&mut self, _position: (i32, i32), _text: &str, _color: Color) {}
148
149 fn draw_text_shaped(
150 &mut self,
151 _shaped: &ShapedText<'_>,
152 _origin: (i32, i32),
153 _color: Color,
154 ) {
155 }
156
157 fn stroke_line_aa(&mut self, a: PointF, b: PointF, width: f32, color: Color) {
158 self.strokes.push(Stroke { a, b, width, color });
159 }
160 }
161
162 #[test]
163 fn draws_adjacent_segments_with_style_stroke() {
164 let points = [
165 Point { x: 1, y: 2 },
166 Point { x: 11, y: 12 },
167 Point { x: 21, y: 2 },
168 ];
169 let mut line = Line::new(
170 Rect {
171 x: 10,
172 y: 20,
173 width: 40,
174 height: 30,
175 },
176 &points,
177 );
178 line.style.border_color = Color(10, 20, 30, 200);
179 line.style.border_width = 3;
180 line.style.alpha = 128;
181
182 let mut renderer = RecordingRenderer::new();
183 line.draw(&mut renderer);
184
185 assert_eq!(
186 renderer.strokes,
187 alloc::vec![
188 Stroke {
189 a: PointF::new(11.0, 22.0),
190 b: PointF::new(21.0, 32.0),
191 width: 3.0,
192 color: Color(10, 20, 30, 200).with_alpha(128),
193 },
194 Stroke {
195 a: PointF::new(21.0, 32.0),
196 b: PointF::new(31.0, 22.0),
197 width: 3.0,
198 color: Color(10, 20, 30, 200).with_alpha(128),
199 },
200 ]
201 );
202 }
203
204 #[test]
205 fn y_invert_maps_against_bounds_height() {
206 let points = [Point { x: 0, y: 0 }, Point { x: 10, y: 7 }];
207 let mut line = Line::new(
208 Rect {
209 x: 4,
210 y: 5,
211 width: 30,
212 height: 20,
213 },
214 &points,
215 );
216 line.set_y_invert(true);
217
218 let mut renderer = RecordingRenderer::new();
219 line.draw(&mut renderer);
220
221 assert_eq!(
222 renderer.strokes,
223 alloc::vec![Stroke {
224 a: PointF::new(4.0, 25.0),
225 b: PointF::new(14.0, 18.0),
226 width: 1.0,
227 color: line.style.border_color.with_alpha(line.style.alpha),
228 }]
229 );
230 }
231
232 #[test]
233 fn set_points_and_set_bounds_update_draw_inputs() {
234 let old_points = [Point { x: 0, y: 0 }, Point { x: 1, y: 1 }];
235 let new_points = [Point { x: 2, y: 3 }, Point { x: 4, y: 5 }];
236 let mut line = Line::new(
237 Rect {
238 x: 0,
239 y: 0,
240 width: 10,
241 height: 10,
242 },
243 &old_points,
244 );
245
246 line.set_points(&new_points);
247 line.set_bounds(Rect {
248 x: 7,
249 y: 11,
250 width: 20,
251 height: 30,
252 });
253
254 assert_eq!(line.points(), &new_points);
255 assert_eq!(
256 line.bounds(),
257 Rect {
258 x: 7,
259 y: 11,
260 width: 20,
261 height: 30,
262 }
263 );
264
265 let mut renderer = RecordingRenderer::new();
266 line.draw(&mut renderer);
267
268 assert_eq!(
269 renderer.strokes,
270 alloc::vec![Stroke {
271 a: PointF::new(9.0, 14.0),
272 b: PointF::new(11.0, 16.0),
273 width: 1.0,
274 color: line.style.border_color.with_alpha(line.style.alpha),
275 }]
276 );
277 }
278
279 #[test]
280 fn zero_width_or_short_point_lists_do_not_stroke() {
281 let points = [Point { x: 0, y: 0 }];
282 let mut line = Line::new(
283 Rect {
284 x: 0,
285 y: 0,
286 width: 10,
287 height: 10,
288 },
289 &points,
290 );
291 let mut renderer = RecordingRenderer::new();
292 line.draw(&mut renderer);
293 assert!(renderer.strokes.is_empty());
294
295 let points = [Point { x: 0, y: 0 }, Point { x: 5, y: 5 }];
296 line.set_points(&points);
297 line.style.border_width = 0;
298 line.draw(&mut renderer);
299 assert!(renderer.strokes.is_empty());
300 }
301}