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)]
20pub struct Line {
21 pub x1: f64,
23 pub y1: f64,
25 pub x2: f64,
27 pub y2: f64,
29 pub color: Color,
31}
32
33impl Line {
34 pub const fn new(x1: f64, y1: f64, x2: f64, y2: f64, color: Color) -> Self {
36 Self {
37 x1,
38 y1,
39 x2,
40 y2,
41 color,
42 }
43 }
44}
45
46impl Shape for Line {
47 #[expect(clippy::similar_names)]
48 fn draw(&self, painter: &mut Painter) {
49 let (x_bounds, y_bounds) = painter.bounds();
50 let Some((world_x1, world_y1, world_x2, world_y2)) =
51 clip_line(x_bounds, y_bounds, self.x1, self.y1, self.x2, self.y2)
52 else {
53 return;
54 };
55 let Some((x1, y1)) = painter.get_point(world_x1, world_y1) else {
56 return;
57 };
58 let Some((x2, y2)) = painter.get_point(world_x2, world_y2) else {
59 return;
60 };
61
62 draw_line(painter, x1, y1, x2, y2, self.color);
63 }
64}
65
66fn clip_line(
67 &[xmin, xmax]: &[f64; 2],
68 &[ymin, ymax]: &[f64; 2],
69 x1: f64,
70 y1: f64,
71 x2: f64,
72 y2: f64,
73) -> Option<(f64, f64, f64, f64)> {
74 if let Some(LineSegment {
75 p1: Point { x: x1, y: y1 },
76 p2: Point { x: x2, y: y2 },
77 }) = cohen_sutherland::clip_line(
78 LineSegment::new(Point::new(x1, y1), Point::new(x2, y2)),
79 Window::new(xmin, xmax, ymin, ymax),
80 ) {
81 Some((x1, y1, x2, y2))
82 } else {
83 None
84 }
85}
86
87pub(super) fn draw_line(
88 painter: &mut Painter,
89 x1: usize,
90 y1: usize,
91 x2: usize,
92 y2: usize,
93 color: Color,
94) {
95 for_each_line_point(x1, y1, x2, y2, |x, y| painter.paint(x, y, color));
96}
97
98pub(super) fn for_each_line_point<F>(x1: usize, y1: usize, x2: usize, y2: usize, mut f: F)
100where
101 F: FnMut(usize, usize),
102{
103 let dx = x2.abs_diff(x1);
104 let dy = y2.abs_diff(y1);
105
106 if dx == 0 {
107 for y in y1.min(y2)..=y1.max(y2) {
108 f(x1, y);
109 }
110 } else if dy == 0 {
111 for x in x1.min(x2)..=x1.max(x2) {
112 f(x, y1);
113 }
114 } else if dy < dx {
115 if x1 > x2 {
116 for_each_line_point_low(x2, y2, x1, y1, f);
117 } else {
118 for_each_line_point_low(x1, y1, x2, y2, f);
119 }
120 } else if y1 > y2 {
121 for_each_line_point_high(x2, y2, x1, y1, f);
122 } else {
123 for_each_line_point_high(x1, y1, x2, y2, f);
124 }
125}
126
127fn for_each_line_point_low<F>(x1: usize, y1: usize, x2: usize, y2: usize, mut f: F)
128where
129 F: FnMut(usize, usize),
130{
131 let dx = (x2 - x1) as isize;
132 let dy = (y2 as isize - y1 as isize).abs();
133 let mut d = 2 * dy - dx;
134 let mut y = y1;
135 for x in x1..=x2 {
136 f(x, y);
137 if d > 0 {
138 y = if y1 > y2 {
139 y.saturating_sub(1)
140 } else {
141 y.saturating_add(1)
142 };
143 d -= 2 * dx;
144 }
145 d += 2 * dy;
146 }
147}
148
149fn for_each_line_point_high<F>(x1: usize, y1: usize, x2: usize, y2: usize, mut f: F)
150where
151 F: FnMut(usize, usize),
152{
153 let dx = (x2 as isize - x1 as isize).abs();
154 let dy = (y2 - y1) as isize;
155 let mut d = 2 * dx - dy;
156 let mut x = x1;
157 for y in y1..=y2 {
158 f(x, y);
159 if d > 0 {
160 x = if x1 > x2 {
161 x.saturating_sub(1)
162 } else {
163 x.saturating_add(1)
164 };
165 d -= 2 * dy;
166 }
167 d += 2 * dx;
168 }
169}
170
171#[derive(Debug, Clone, PartialEq)]
176pub struct FilledLine {
177 pub x1: f64,
179 pub y1: f64,
181 pub x2: f64,
183 pub y2: f64,
185 pub fill_to_y: f64,
187 pub color: Color,
189}
190
191impl FilledLine {
192 pub const fn new(x1: f64, y1: f64, x2: f64, y2: f64, fill_to_y: f64, color: Color) -> Self {
194 Self {
195 x1,
196 y1,
197 x2,
198 y2,
199 fill_to_y,
200 color,
201 }
202 }
203}
204
205impl Shape for FilledLine {
206 #[expect(clippy::similar_names)]
207 fn draw(&self, painter: &mut Painter) {
208 let (x_bounds, y_bounds) = painter.bounds();
209 let Some((world_x1, world_y1, world_x2, world_y2)) =
210 clip_line(x_bounds, y_bounds, self.x1, self.y1, self.x2, self.y2)
211 else {
212 return;
213 };
214 let Some((x1, y1)) = painter.get_point(world_x1, world_y1) else {
215 return;
216 };
217 let Some((x2, y2)) = painter.get_point(world_x2, world_y2) else {
218 return;
219 };
220
221 let y_fill = self.fill_to_y.clamp(y_bounds[0], y_bounds[1]);
222 let Some((_, y_fill)) = painter.get_point(world_x1, y_fill) else {
223 return;
224 };
225
226 for_each_line_point(x1, y1, x2, y2, |x, y| {
227 let start = y.min(y_fill);
228 let end = y.max(y_fill);
229 for y in start..=end {
230 painter.paint(x, y, self.color);
231 }
232 });
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use ratatui_core::buffer::Buffer;
239 use ratatui_core::layout::Rect;
240 use ratatui_core::style::Style;
241 use ratatui_core::symbols::Marker;
242 use ratatui_core::widgets::Widget;
243 use rstest::rstest;
244
245 use super::*;
246 use crate::canvas::Canvas;
247
248 #[rstest]
249 #[case::off_grid1(&Line::new(-1.0, 0.0, -1.0, 10.0, Color::Red), [" "; 10])]
250 #[case::off_grid2(&Line::new(0.0, -1.0, 10.0, -1.0, Color::Red), [" "; 10])]
251 #[case::off_grid3(&Line::new(-10.0, 5.0, -1.0, 5.0, Color::Red), [" "; 10])]
252 #[case::off_grid4(&Line::new(5.0, 11.0, 5.0, 20.0, Color::Red), [" "; 10])]
253 #[case::off_grid5(&Line::new(-10.0, 0.0, 5.0, 0.0, Color::Red), [
254 " ",
255 " ",
256 " ",
257 " ",
258 " ",
259 " ",
260 " ",
261 " ",
262 " ",
263 "•••••• ",
264 ])]
265 #[case::off_grid6(&Line::new(-1.0, -1.0, 10.0, 10.0, Color::Red), [
266 " •",
267 " • ",
268 " • ",
269 " • ",
270 " • ",
271 " • ",
272 " • ",
273 " • ",
274 " • ",
275 "• ",
276 ])]
277 #[case::off_grid7(&Line::new(0.0, 0.0, 11.0, 11.0, Color::Red), [
278 " •",
279 " • ",
280 " • ",
281 " • ",
282 " • ",
283 " • ",
284 " • ",
285 " • ",
286 " • ",
287 "• ",
288 ])]
289 #[case::off_grid8(&Line::new(-1.0, -1.0, 11.0, 11.0, Color::Red), [
290 " •",
291 " • ",
292 " • ",
293 " • ",
294 " • ",
295 " • ",
296 " • ",
297 " • ",
298 " • ",
299 "• ",
300 ])]
301 #[case::horizontal1(&Line::new(0.0, 0.0, 10.0, 0.0, Color::Red), [
302 " ",
303 " ",
304 " ",
305 " ",
306 " ",
307 " ",
308 " ",
309 " ",
310 " ",
311 "••••••••••",
312 ])]
313 #[case::horizontal2(&Line::new(10.0, 10.0, 0.0, 10.0, Color::Red), [
314 "••••••••••",
315 " ",
316 " ",
317 " ",
318 " ",
319 " ",
320 " ",
321 " ",
322 " ",
323 " ",
324 ])]
325 #[case::vertical1(&Line::new(0.0, 0.0, 0.0, 10.0, Color::Red), ["• "; 10])]
326 #[case::vertical2(&Line::new(10.0, 10.0, 10.0, 0.0, Color::Red), [" •"; 10])]
327 #[case::diagonal1(&Line::new(0.0, 0.0, 10.0, 5.0, Color::Red), [
329 " ",
330 " ",
331 " ",
332 " ",
333 " ",
334 " ••",
335 " •• ",
336 " •• ",
337 " •• ",
338 "•• ",
339 ])]
340 #[case::diagonal2(&Line::new(10.0, 0.0, 0.0, 5.0, Color::Red), [
342 " ",
343 " ",
344 " ",
345 " ",
346 " ",
347 "•• ",
348 " •• ",
349 " •• ",
350 " •• ",
351 " ••",
352 ])]
353 #[case::diagonal3(&Line::new(0.0, 0.0, 5.0, 10.0, Color::Red), [
355 " • ",
356 " • ",
357 " • ",
358 " • ",
359 " • ",
360 " • ",
361 " • ",
362 " • ",
363 " • ",
364 "• ",
365 ])]
366 #[case::diagonal4(&Line::new(0.0, 10.0, 5.0, 0.0, Color::Red), [
368 "• ",
369 " • ",
370 " • ",
371 " • ",
372 " • ",
373 " • ",
374 " • ",
375 " • ",
376 " • ",
377 " • ",
378 ])]
379 fn tests<'expected_line, ExpectedLines>(#[case] line: &Line, #[case] expected: ExpectedLines)
380 where
381 ExpectedLines: IntoIterator,
382 ExpectedLines::Item: Into<ratatui_core::text::Line<'expected_line>>,
383 {
384 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
385 let canvas = Canvas::default()
386 .marker(Marker::Dot)
387 .x_bounds([0.0, 10.0])
388 .y_bounds([0.0, 10.0])
389 .paint(|context| context.draw(line));
390 canvas.render(buffer.area, &mut buffer);
391
392 let mut expected = Buffer::with_lines(expected);
393 for cell in &mut expected.content {
394 if cell.symbol() == "•" {
395 cell.set_style(Style::new().red());
396 }
397 }
398 assert_eq!(buffer, expected);
399 }
400
401 #[rstest]
402 #[case::filled_off_grid1(&FilledLine::new(-1.0, 0.0, -1.0, 10.0, 0.0, Color::Red), [" "; 10])]
403 #[case::filled_off_grid2(&FilledLine::new(0.0, -1.0, 10.0, -1.0, 0.0, Color::Red), [" "; 10])]
404 #[case::filled_off_grid3(&FilledLine::new(-10.0, 5.0, -1.0, 5.0, 0.0, Color::Red), [" "; 10])]
405 #[case::filled_off_grid4(&FilledLine::new(5.0, 11.0, 5.0, 20.0, 0.0, Color::Red), [" "; 10])]
406 #[case::filled_off_grid5(&FilledLine::new(-10.0, 0.0, 5.0, 0.0, -10.0, Color::Red), [
407 " ",
408 " ",
409 " ",
410 " ",
411 " ",
412 " ",
413 " ",
414 " ",
415 " ",
416 "•••••• ",
417 ])]
418 #[case::filled_off_grid6(&FilledLine::new(0.0, 0.0, 10.0, 10.0, 0.0, Color::Red), [
419 " •",
420 " ••",
421 " •••",
422 " ••••",
423 " •••••",
424 " ••••••",
425 " •••••••",
426 " ••••••••",
427 " •••••••••",
428 "••••••••••",
429 ])]
430 #[case::filled_off_grid7(&FilledLine::new(0.0, 0.0, 11.0, 11.0, 0.0, Color::Red), [
431 " •",
432 " ••",
433 " •••",
434 " ••••",
435 " •••••",
436 " ••••••",
437 " •••••••",
438 " ••••••••",
439 " •••••••••",
440 "••••••••••",
441 ])]
442 #[case::filled_off_grid8(&FilledLine::new(-1.0, -1.0, 11.0, 11.0, 0.0, Color::Red), [
443 " •",
444 " ••",
445 " •••",
446 " ••••",
447 " •••••",
448 " ••••••",
449 " •••••••",
450 " ••••••••",
451 " •••••••••",
452 "••••••••••",
453 ])]
454 #[case::filled_horizontal1(&FilledLine::new(0.0, 0.0, 10.0, 0.0, 0.0, Color::Red), [
455 " ",
456 " ",
457 " ",
458 " ",
459 " ",
460 " ",
461 " ",
462 " ",
463 " ",
464 "••••••••••",
465 ])]
466 #[case::filled_horizontal2(&FilledLine::new(0.0, 0.0, 10.0, 0.0, 10.0, Color::Red), [
467 "••••••••••",
468 "••••••••••",
469 "••••••••••",
470 "••••••••••",
471 "••••••••••",
472 "••••••••••",
473 "••••••••••",
474 "••••••••••",
475 "••••••••••",
476 "••••••••••",
477 ])]
478 #[case::filled_horizontal3(&FilledLine::new(10.0, 10.0, 0.0, 10.0, 10.0, Color::Red), [
479 "••••••••••",
480 " ",
481 " ",
482 " ",
483 " ",
484 " ",
485 " ",
486 " ",
487 " ",
488 " ",
489 ])]
490 #[case::filled_horizontal4(&FilledLine::new(10.0, 10.0, 0.0, 10.0, 0.0, Color::Red), [
491 "••••••••••",
492 "••••••••••",
493 "••••••••••",
494 "••••••••••",
495 "••••••••••",
496 "••••••••••",
497 "••••••••••",
498 "••••••••••",
499 "••••••••••",
500 "••••••••••",
501 ])]
502 #[case::filled_vertical1(&FilledLine::new(0.0, 0.0, 0.0, 10.0, 0.0, Color::Red), ["• "; 10])]
503 #[case::filled_vertical2(&FilledLine::new(10.0, 10.0, 10.0, 0.0, 0.0, Color::Red), [" •"; 10])]
504 #[case::filled_diagonal1(&FilledLine::new(0.0, 0.0, 10.0, 5.0, 0.0, Color::Red), [
506 " ",
507 " ",
508 " ",
509 " ",
510 " ",
511 " ••",
512 " ••••",
513 " ••••••",
514 " ••••••••",
515 "••••••••••",
516 ])]
517 #[case::filled_diagonal2(&FilledLine::new(10.0, 0.0, 0.0, 5.0, 0.0, Color::Red), [
519 " ",
520 " ",
521 " ",
522 " ",
523 " ",
524 "•• ",
525 "•••• ",
526 "•••••• ",
527 "•••••••• ",
528 "••••••••••",
529 ])]
530 #[case::filled_diagonal3(&FilledLine::new(0.0, 0.0, 5.0, 10.0, 0.0, Color::Red), [
532 " • ",
533 " •• ",
534 " •• ",
535 " ••• ",
536 " ••• ",
537 " •••• ",
538 " •••• ",
539 " ••••• ",
540 " ••••• ",
541 "•••••• ",
542 ])]
543 #[case::filled_diagonal4(&FilledLine::new(0.0, 10.0, 5.0, 0.0, 0.0, Color::Red), [
545 "• ",
546 "•• ",
547 "•• ",
548 "••• ",
549 "••• ",
550 "•••• ",
551 "•••• ",
552 "••••• ",
553 "••••• ",
554 "•••••• ",
555 ])]
556 #[case::filled_split1(&FilledLine::new(0.0, 0.0, 10.0, 10.0, 5.0, Color::Red), [
557 " •",
558 " ••",
559 " •••",
560 " ••••",
561 " •••••",
562 "••••••••••",
563 "•••• ",
564 "••• ",
565 "•• ",
566 "• ",
567 ])]
568 #[case::filled_split2(&FilledLine::new(0.0, 0.0, 10.0, 10.0, 7.0, Color::Red), [
569 " •",
570 " ••",
571 " •••",
572 "••••••••••",
573 "•••••• ",
574 "••••• ",
575 "•••• ",
576 "••• ",
577 "•• ",
578 "• ",
579 ])]
580 fn tests_filled<'expected_line, ExpectedLines>(
581 #[case] line: &FilledLine,
582 #[case] expected: ExpectedLines,
583 ) where
584 ExpectedLines: IntoIterator,
585 ExpectedLines::Item: Into<ratatui_core::text::Line<'expected_line>>,
586 {
587 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
588 let canvas = Canvas::default()
589 .marker(Marker::Dot)
590 .x_bounds([0.0, 10.0])
591 .y_bounds([0.0, 10.0])
592 .paint(|context| context.draw(line));
593 canvas.render(buffer.area, &mut buffer);
594
595 let mut expected = Buffer::with_lines(expected);
596 for cell in &mut expected.content {
597 if cell.symbol() == "•" {
598 cell.set_style(Style::new().red());
599 }
600 }
601 assert_eq!(buffer, expected);
602 }
603}