ratatui/widgets/canvas/
line.rs

1use crate::{
2    style::Color,
3    widgets::canvas::{Painter, Shape},
4};
5
6/// A line from `(x1, y1)` to `(x2, y2)` with the given color
7#[derive(Debug, Default, Clone, PartialEq)]
8pub struct Line {
9    /// `x` of the starting point
10    pub x1: f64,
11    /// `y` of the starting point
12    pub y1: f64,
13    /// `x` of the ending point
14    pub x2: f64,
15    /// `y` of the ending point
16    pub y2: f64,
17    /// Color of the line
18    pub color: Color,
19}
20
21impl Line {
22    /// Create a new line from `(x1, y1)` to `(x2, y2)` with the given color
23    pub const fn new(x1: f64, y1: f64, x2: f64, y2: f64, color: Color) -> Self {
24        Self {
25            x1,
26            y1,
27            x2,
28            y2,
29            color,
30        }
31    }
32}
33
34impl Shape for Line {
35    fn draw(&self, painter: &mut Painter) {
36        let Some((x1, y1)) = painter.get_point(self.x1, self.y1) else {
37            return;
38        };
39        let Some((x2, y2)) = painter.get_point(self.x2, self.y2) else {
40            return;
41        };
42        let (dx, x_range) = if x2 >= x1 {
43            (x2 - x1, x1..=x2)
44        } else {
45            (x1 - x2, x2..=x1)
46        };
47        let (dy, y_range) = if y2 >= y1 {
48            (y2 - y1, y1..=y2)
49        } else {
50            (y1 - y2, y2..=y1)
51        };
52
53        if dx == 0 {
54            for y in y_range {
55                painter.paint(x1, y, self.color);
56            }
57        } else if dy == 0 {
58            for x in x_range {
59                painter.paint(x, y1, self.color);
60            }
61        } else if dy < dx {
62            if x1 > x2 {
63                draw_line_low(painter, x2, y2, x1, y1, self.color);
64            } else {
65                draw_line_low(painter, x1, y1, x2, y2, self.color);
66            }
67        } else if y1 > y2 {
68            draw_line_high(painter, x2, y2, x1, y1, self.color);
69        } else {
70            draw_line_high(painter, x1, y1, x2, y2, self.color);
71        }
72    }
73}
74
75fn draw_line_low(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: usize, color: Color) {
76    let dx = (x2 - x1) as isize;
77    let dy = (y2 as isize - y1 as isize).abs();
78    let mut d = 2 * dy - dx;
79    let mut y = y1;
80    for x in x1..=x2 {
81        painter.paint(x, y, color);
82        if d > 0 {
83            y = if y1 > y2 {
84                y.saturating_sub(1)
85            } else {
86                y.saturating_add(1)
87            };
88            d -= 2 * dx;
89        }
90        d += 2 * dy;
91    }
92}
93
94fn draw_line_high(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: usize, color: Color) {
95    let dx = (x2 as isize - x1 as isize).abs();
96    let dy = (y2 - y1) as isize;
97    let mut d = 2 * dx - dy;
98    let mut x = x1;
99    for y in y1..=y2 {
100        painter.paint(x, y, color);
101        if d > 0 {
102            x = if x1 > x2 {
103                x.saturating_sub(1)
104            } else {
105                x.saturating_add(1)
106            };
107            d -= 2 * dy;
108        }
109        d += 2 * dx;
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use rstest::rstest;
116
117    use super::*;
118    use crate::{
119        buffer::Buffer,
120        layout::Rect,
121        style::{Style, Stylize},
122        symbols::Marker,
123        widgets::{canvas::Canvas, Widget},
124    };
125
126    #[rstest]
127    #[case::off_grid(&Line::new(-1.0, -1.0, 10.0, 10.0, Color::Red), ["          "; 10])]
128    #[case::off_grid(&Line::new(0.0, 0.0, 11.0, 11.0, Color::Red), ["          "; 10])]
129    #[case::horizontal(&Line::new(0.0, 0.0, 10.0, 0.0, Color::Red), [
130        "          ",
131        "          ",
132        "          ",
133        "          ",
134        "          ",
135        "          ",
136        "          ",
137        "          ",
138        "          ",
139        "••••••••••",
140    ])]
141    #[case::horizontal(&Line::new(10.0, 10.0, 0.0, 10.0, Color::Red), [
142        "••••••••••",
143        "          ",
144        "          ",
145        "          ",
146        "          ",
147        "          ",
148        "          ",
149        "          ",
150        "          ",
151        "          ",
152    ])]
153    #[case::vertical(&Line::new(0.0, 0.0, 0.0, 10.0, Color::Red), ["•         "; 10])]
154    #[case::vertical(&Line::new(10.0, 10.0, 10.0, 0.0, Color::Red), ["         •"; 10])]
155    // dy < dx, x1 < x2
156    #[case::diagonal(&Line::new(0.0, 0.0, 10.0, 5.0, Color::Red), [
157        "          ",
158        "          ",
159        "          ",
160        "          ",
161        "         •",
162        "       •• ",
163        "     ••   ",
164        "   ••     ",
165        " ••       ",
166        "•         ",
167    ])]
168    // dy < dx, x1 > x2
169    #[case::diagonal(&Line::new(10.0, 0.0, 0.0, 5.0, Color::Red), [
170        "          ",
171        "          ",
172        "          ",
173        "          ",
174        "•         ",
175        " ••       ",
176        "   ••     ",
177        "     ••   ",
178        "       •• ",
179        "         •",
180    ])]
181    // dy > dx, y1 < y2
182    #[case::diagonal(&Line::new(0.0, 0.0, 5.0, 10.0, Color::Red), [
183        "    •     ",
184        "    •     ",
185        "   •      ",
186        "   •      ",
187        "  •       ",
188        "  •       ",
189        " •        ",
190        " •        ",
191        "•         ",
192        "•         ",
193    ])]
194    // dy > dx, y1 > y2
195    #[case::diagonal(&Line::new(0.0, 10.0, 5.0, 0.0, Color::Red), [
196        "•         ",
197        "•         ",
198        " •        ",
199        " •        ",
200        "  •       ",
201        "  •       ",
202        "   •      ",
203        "   •      ",
204        "    •     ",
205        "    •     ",
206    ])]
207    fn tests<'expected_line, ExpectedLines>(#[case] line: &Line, #[case] expected: ExpectedLines)
208    where
209        ExpectedLines: IntoIterator,
210        ExpectedLines::Item: Into<crate::text::Line<'expected_line>>,
211    {
212        let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
213        let canvas = Canvas::default()
214            .marker(Marker::Dot)
215            .x_bounds([0.0, 10.0])
216            .y_bounds([0.0, 10.0])
217            .paint(|context| context.draw(line));
218        canvas.render(buffer.area, &mut buffer);
219
220        let mut expected = Buffer::with_lines(expected);
221        for cell in &mut expected.content {
222            if cell.symbol() == "•" {
223                cell.set_style(Style::new().red());
224            }
225        }
226        assert_eq!(buffer, expected);
227    }
228}