1use line_clipping::{LineSegment, Point, Window, cohen_sutherland};
2use ratatui_core::style::Color;
3
4use crate::canvas::{Painter, Shape};
5
6#[derive(Debug, Default, Clone, PartialEq)]
8pub struct Line {
9 pub x1: f64,
11 pub y1: f64,
13 pub x2: f64,
15 pub y2: f64,
17 pub color: Color,
19}
20
21impl Line {
22 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 #[expect(clippy::similar_names)]
36 fn draw(&self, painter: &mut Painter) {
37 let (x_bounds, y_bounds) = painter.bounds();
38 let Some((world_x1, world_y1, world_x2, world_y2)) =
39 clip_line(x_bounds, y_bounds, self.x1, self.y1, self.x2, self.y2)
40 else {
41 return;
42 };
43 let Some((x1, y1)) = painter.get_point(world_x1, world_y1) else {
44 return;
45 };
46 let Some((x2, y2)) = painter.get_point(world_x2, world_y2) else {
47 return;
48 };
49
50 let (dx, x_range) = if x2 >= x1 {
51 (x2 - x1, x1..=x2)
52 } else {
53 (x1 - x2, x2..=x1)
54 };
55 let (dy, y_range) = if y2 >= y1 {
56 (y2 - y1, y1..=y2)
57 } else {
58 (y1 - y2, y2..=y1)
59 };
60
61 if dx == 0 {
62 for y in y_range {
63 painter.paint(x1, y, self.color);
64 }
65 } else if dy == 0 {
66 for x in x_range {
67 painter.paint(x, y1, self.color);
68 }
69 } else if dy < dx {
70 if x1 > x2 {
71 draw_line_low(painter, x2, y2, x1, y1, self.color);
72 } else {
73 draw_line_low(painter, x1, y1, x2, y2, self.color);
74 }
75 } else if y1 > y2 {
76 draw_line_high(painter, x2, y2, x1, y1, self.color);
77 } else {
78 draw_line_high(painter, x1, y1, x2, y2, self.color);
79 }
80 }
81}
82
83fn clip_line(
84 &[xmin, xmax]: &[f64; 2],
85 &[ymin, ymax]: &[f64; 2],
86 x1: f64,
87 y1: f64,
88 x2: f64,
89 y2: f64,
90) -> Option<(f64, f64, f64, f64)> {
91 if let Some(LineSegment {
92 p1: Point { x: x1, y: y1 },
93 p2: Point { x: x2, y: y2 },
94 }) = cohen_sutherland::clip_line(
95 LineSegment::new(Point::new(x1, y1), Point::new(x2, y2)),
96 Window::new(xmin, xmax, ymin, ymax),
97 ) {
98 Some((x1, y1, x2, y2))
99 } else {
100 None
101 }
102}
103
104fn draw_line_low(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: usize, color: Color) {
105 let dx = (x2 - x1) as isize;
106 let dy = (y2 as isize - y1 as isize).abs();
107 let mut d = 2 * dy - dx;
108 let mut y = y1;
109 for x in x1..=x2 {
110 painter.paint(x, y, color);
111 if d > 0 {
112 y = if y1 > y2 {
113 y.saturating_sub(1)
114 } else {
115 y.saturating_add(1)
116 };
117 d -= 2 * dx;
118 }
119 d += 2 * dy;
120 }
121}
122
123fn draw_line_high(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: usize, color: Color) {
124 let dx = (x2 as isize - x1 as isize).abs();
125 let dy = (y2 - y1) as isize;
126 let mut d = 2 * dx - dy;
127 let mut x = x1;
128 for y in y1..=y2 {
129 painter.paint(x, y, color);
130 if d > 0 {
131 x = if x1 > x2 {
132 x.saturating_sub(1)
133 } else {
134 x.saturating_add(1)
135 };
136 d -= 2 * dy;
137 }
138 d += 2 * dx;
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use ratatui_core::buffer::Buffer;
145 use ratatui_core::layout::Rect;
146 use ratatui_core::style::Style;
147 use ratatui_core::symbols::Marker;
148 use ratatui_core::widgets::Widget;
149 use rstest::rstest;
150
151 use super::*;
152 use crate::canvas::Canvas;
153
154 #[rstest]
155 #[case::off_grid1(&Line::new(-1.0, 0.0, -1.0, 10.0, Color::Red), [" "; 10])]
156 #[case::off_grid2(&Line::new(0.0, -1.0, 10.0, -1.0, Color::Red), [" "; 10])]
157 #[case::off_grid3(&Line::new(-10.0, 5.0, -1.0, 5.0, Color::Red), [" "; 10])]
158 #[case::off_grid4(&Line::new(5.0, 11.0, 5.0, 20.0, Color::Red), [" "; 10])]
159 #[case::off_grid5(&Line::new(-10.0, 0.0, 5.0, 0.0, Color::Red), [
160 " ",
161 " ",
162 " ",
163 " ",
164 " ",
165 " ",
166 " ",
167 " ",
168 " ",
169 "•••••• ",
170 ])]
171 #[case::off_grid6(&Line::new(-1.0, -1.0, 10.0, 10.0, Color::Red), [
172 " •",
173 " • ",
174 " • ",
175 " • ",
176 " • ",
177 " • ",
178 " • ",
179 " • ",
180 " • ",
181 "• ",
182 ])]
183 #[case::off_grid7(&Line::new(0.0, 0.0, 11.0, 11.0, Color::Red), [
184 " •",
185 " • ",
186 " • ",
187 " • ",
188 " • ",
189 " • ",
190 " • ",
191 " • ",
192 " • ",
193 "• ",
194 ])]
195 #[case::off_grid8(&Line::new(-1.0, -1.0, 11.0, 11.0, Color::Red), [
196 " •",
197 " • ",
198 " • ",
199 " • ",
200 " • ",
201 " • ",
202 " • ",
203 " • ",
204 " • ",
205 "• ",
206 ])]
207 #[case::horizontal1(&Line::new(0.0, 0.0, 10.0, 0.0, Color::Red), [
208 " ",
209 " ",
210 " ",
211 " ",
212 " ",
213 " ",
214 " ",
215 " ",
216 " ",
217 "••••••••••",
218 ])]
219 #[case::horizontal2(&Line::new(10.0, 10.0, 0.0, 10.0, Color::Red), [
220 "••••••••••",
221 " ",
222 " ",
223 " ",
224 " ",
225 " ",
226 " ",
227 " ",
228 " ",
229 " ",
230 ])]
231 #[case::vertical1(&Line::new(0.0, 0.0, 0.0, 10.0, Color::Red), ["• "; 10])]
232 #[case::vertical2(&Line::new(10.0, 10.0, 10.0, 0.0, Color::Red), [" •"; 10])]
233 #[case::diagonal1(&Line::new(0.0, 0.0, 10.0, 5.0, Color::Red), [
235 " ",
236 " ",
237 " ",
238 " ",
239 " ",
240 " ••",
241 " •• ",
242 " •• ",
243 " •• ",
244 "•• ",
245 ])]
246 #[case::diagonal2(&Line::new(10.0, 0.0, 0.0, 5.0, Color::Red), [
248 " ",
249 " ",
250 " ",
251 " ",
252 " ",
253 "•• ",
254 " •• ",
255 " •• ",
256 " •• ",
257 " ••",
258 ])]
259 #[case::diagonal3(&Line::new(0.0, 0.0, 5.0, 10.0, Color::Red), [
261 " • ",
262 " • ",
263 " • ",
264 " • ",
265 " • ",
266 " • ",
267 " • ",
268 " • ",
269 " • ",
270 "• ",
271 ])]
272 #[case::diagonal4(&Line::new(0.0, 10.0, 5.0, 0.0, Color::Red), [
274 "• ",
275 " • ",
276 " • ",
277 " • ",
278 " • ",
279 " • ",
280 " • ",
281 " • ",
282 " • ",
283 " • ",
284 ])]
285 fn tests<'expected_line, ExpectedLines>(#[case] line: &Line, #[case] expected: ExpectedLines)
286 where
287 ExpectedLines: IntoIterator,
288 ExpectedLines::Item: Into<ratatui_core::text::Line<'expected_line>>,
289 {
290 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
291 let canvas = Canvas::default()
292 .marker(Marker::Dot)
293 .x_bounds([0.0, 10.0])
294 .y_bounds([0.0, 10.0])
295 .paint(|context| context.draw(line));
296 canvas.render(buffer.area, &mut buffer);
297
298 let mut expected = Buffer::with_lines(expected);
299 for cell in &mut expected.content {
300 if cell.symbol() == "•" {
301 cell.set_style(Style::new().red());
302 }
303 }
304 assert_eq!(buffer, expected);
305 }
306}