Skip to main content

rlvgl_widgets/
line.rs

1//! Borrowed-slice polyline widget.
2//!
3//! The line widget draws adjacent point pairs as anti-aliased strokes. Points
4//! are concrete pixel offsets relative to the widget bounds; percent
5//! coordinates are intentionally deferred by LPAR-11.
6
7use 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/// Pixel point relative to a [`Line`] widget's bounds.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct Point {
16    /// Horizontal offset from the widget bounds' left edge.
17    pub x: i32,
18    /// Vertical offset from the widget bounds' top edge.
19    pub y: i32,
20}
21
22/// Connected line-segment widget backed by a borrowed point slice.
23///
24/// The lifetime parameter ties the widget to its point storage. The widget
25/// never stores raw pointers and never copies the point slice.
26#[derive(Debug)]
27pub struct Line<'a> {
28    bounds: Rect,
29    points: &'a [Point],
30    y_invert: bool,
31    /// Style used for the line stroke.
32    ///
33    /// `border_color` supplies the stroke color, `border_width` supplies the
34    /// stroke width in pixels, and `alpha` modulates the final stroke alpha.
35    pub style: Style,
36}
37
38impl<'a> Line<'a> {
39    /// Create a line widget with borrowed point storage.
40    ///
41    /// The default stroke is one pixel wide using [`Style::default`]'s border
42    /// color so a newly created line is visible without additional styling.
43    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    /// Return the currently borrowed point slice.
57    pub fn points(&self) -> &'a [Point] {
58        self.points
59    }
60
61    /// Replace the borrowed point slice.
62    pub fn set_points(&mut self, points: &'a [Point]) {
63        self.points = points;
64    }
65
66    /// Enable or disable bottom-origin y coordinate mapping.
67    pub fn set_y_invert(&mut self, y_invert: bool) {
68        self.y_invert = y_invert;
69    }
70
71    /// Return whether bottom-origin y coordinate mapping is enabled.
72    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}