Skip to content

Commit afd1ce1

Browse files
authored
fix(canvas): Lines that start outside the visible grid are now drawn (#1501)
Previously lines with points that were outside the canvas bounds were not drawn at all. Now they are clipped to the bounds of the canvas so that the portion of the line within the canvas is draw. To facilitate this, a new `Painter::bounds()` method which returns the bounds of the canvas is added. Fixes: #1489
1 parent 8f28247 commit afd1ce1

4 files changed

Lines changed: 122 additions & 12 deletions

File tree

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ratatui-widgets/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ unicode-segmentation.workspace = true
5353
unicode-width.workspace = true
5454
serde = { workspace = true, optional = true }
5555
document-features = { workspace = true, optional = true }
56+
line-clipping = "0.2.1"
5657

5758
[dev-dependencies]
5859
rstest.workspace = true

ratatui-widgets/src/canvas.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,25 @@ impl<'a, 'b> Painter<'a, 'b> {
424424
pub fn paint(&mut self, x: usize, y: usize, color: Color) {
425425
self.context.grid.paint(x, y, color);
426426
}
427+
428+
/// Canvas context bounds by axis.
429+
///
430+
/// # Example
431+
///
432+
/// ```
433+
/// use ratatui::{
434+
/// style::Color,
435+
/// symbols,
436+
/// widgets::canvas::{Context, Painter},
437+
/// };
438+
///
439+
/// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
440+
/// let mut painter = Painter::from(&mut ctx);
441+
/// assert_eq!(painter.bounds(), (&[0.0, 2.0], &[0.0, 2.0]));
442+
/// ```
443+
pub fn bounds(&self) -> (&[f64; 2], &[f64; 2]) {
444+
(&self.context.x_bounds, &self.context.y_bounds)
445+
}
427446
}
428447

429448
impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> {

ratatui-widgets/src/canvas/line.rs

Lines changed: 92 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use line_clipping::{cohen_sutherland, LineSegment, Point, Window};
12
use ratatui_core::style::Color;
23

34
use crate::canvas::{Painter, Shape};
@@ -31,13 +32,21 @@ impl Line {
3132
}
3233

3334
impl Shape for Line {
35+
#[allow(clippy::similar_names)]
3436
fn draw(&self, painter: &mut Painter) {
35-
let Some((x1, y1)) = painter.get_point(self.x1, self.y1) else {
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 {
3641
return;
3742
};
38-
let Some((x2, y2)) = painter.get_point(self.x2, self.y2) else {
43+
let Some((x1, y1)) = painter.get_point(world_x1, world_y1) else {
3944
return;
4045
};
46+
let Some((x2, y2)) = painter.get_point(world_x2, world_y2) else {
47+
return;
48+
};
49+
4150
let (dx, x_range) = if x2 >= x1 {
4251
(x2 - x1, x1..=x2)
4352
} else {
@@ -71,6 +80,27 @@ impl Shape for Line {
7180
}
7281
}
7382

83+
fn 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+
74104
fn draw_line_low(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: usize, color: Color) {
75105
let dx = (x2 - x1) as isize;
76106
let dy = (y2 as isize - y1 as isize).abs();
@@ -124,9 +154,59 @@ mod tests {
124154
use crate::canvas::Canvas;
125155

126156
#[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), [
157+
#[case::off_grid1(&Line::new(-1.0, 0.0, -1.0, 10.0, Color::Red), [" "; 10])]
158+
#[case::off_grid2(&Line::new(0.0, -1.0, 10.0, -1.0, Color::Red), [" "; 10])]
159+
#[case::off_grid3(&Line::new(-10.0, 5.0, -1.0, 5.0, Color::Red), [" "; 10])]
160+
#[case::off_grid4(&Line::new(5.0, 11.0, 5.0, 20.0, Color::Red), [" "; 10])]
161+
#[case::off_grid5(&Line::new(-10.0, 0.0, 5.0, 0.0, Color::Red), [
162+
" ",
163+
" ",
164+
" ",
165+
" ",
166+
" ",
167+
" ",
168+
" ",
169+
" ",
170+
" ",
171+
"••••• ",
172+
])]
173+
#[case::off_grid6(&Line::new(-1.0, -1.0, 10.0, 10.0, Color::Red), [
174+
" •",
175+
" • ",
176+
" • ",
177+
" • ",
178+
" • ",
179+
" • ",
180+
" • ",
181+
" • ",
182+
" • ",
183+
"• ",
184+
])]
185+
#[case::off_grid7(&Line::new(0.0, 0.0, 11.0, 11.0, Color::Red), [
186+
" •",
187+
" • ",
188+
" • ",
189+
" • ",
190+
" • ",
191+
" • ",
192+
" • ",
193+
" • ",
194+
" • ",
195+
"• ",
196+
])]
197+
#[case::off_grid8(&Line::new(-1.0, -1.0, 11.0, 11.0, Color::Red), [
198+
" •",
199+
" • ",
200+
" • ",
201+
" • ",
202+
" • ",
203+
" • ",
204+
" • ",
205+
" • ",
206+
" • ",
207+
"• ",
208+
])]
209+
#[case::horizontal1(&Line::new(0.0, 0.0, 10.0, 0.0, Color::Red), [
130210
" ",
131211
" ",
132212
" ",
@@ -138,7 +218,7 @@ mod tests {
138218
" ",
139219
"••••••••••",
140220
])]
141-
#[case::horizontal(&Line::new(10.0, 10.0, 0.0, 10.0, Color::Red), [
221+
#[case::horizontal2(&Line::new(10.0, 10.0, 0.0, 10.0, Color::Red), [
142222
"••••••••••",
143223
" ",
144224
" ",
@@ -150,10 +230,10 @@ mod tests {
150230
" ",
151231
" ",
152232
])]
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])]
233+
#[case::vertical1(&Line::new(0.0, 0.0, 0.0, 10.0, Color::Red), ["• "; 10])]
234+
#[case::vertical2(&Line::new(10.0, 10.0, 10.0, 0.0, Color::Red), [" •"; 10])]
155235
// dy < dx, x1 < x2
156-
#[case::diagonal(&Line::new(0.0, 0.0, 10.0, 5.0, Color::Red), [
236+
#[case::diagonal1(&Line::new(0.0, 0.0, 10.0, 5.0, Color::Red), [
157237
" ",
158238
" ",
159239
" ",
@@ -166,7 +246,7 @@ mod tests {
166246
"• ",
167247
])]
168248
// dy < dx, x1 > x2
169-
#[case::diagonal(&Line::new(10.0, 0.0, 0.0, 5.0, Color::Red), [
249+
#[case::diagonal2(&Line::new(10.0, 0.0, 0.0, 5.0, Color::Red), [
170250
" ",
171251
" ",
172252
" ",
@@ -179,7 +259,7 @@ mod tests {
179259
" •",
180260
])]
181261
// dy > dx, y1 < y2
182-
#[case::diagonal(&Line::new(0.0, 0.0, 5.0, 10.0, Color::Red), [
262+
#[case::diagonal3(&Line::new(0.0, 0.0, 5.0, 10.0, Color::Red), [
183263
" • ",
184264
" • ",
185265
" • ",
@@ -192,7 +272,7 @@ mod tests {
192272
"• ",
193273
])]
194274
// dy > dx, y1 > y2
195-
#[case::diagonal(&Line::new(0.0, 10.0, 5.0, 0.0, Color::Red), [
275+
#[case::diagonal4(&Line::new(0.0, 10.0, 5.0, 0.0, Color::Red), [
196276
"• ",
197277
"• ",
198278
" • ",

0 commit comments

Comments
 (0)