mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-23 13:13:33 +00:00
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: https://github.com/ratatui/ratatui/issues/1489
This commit is contained in:
parent
8f282473b2
commit
afd1ce179b
4 changed files with 122 additions and 12 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -1377,6 +1377,15 @@ dependencies = [
|
||||||
"redox_syscall",
|
"redox_syscall",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "line-clipping"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76364bf78d7ed059f98564fc5fb94c5a6eb2b6c9edd621cb1528febe1e918b29"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.6.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
|
@ -2200,6 +2209,7 @@ dependencies = [
|
||||||
"indoc",
|
"indoc",
|
||||||
"instability",
|
"instability",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
|
"line-clipping",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"ratatui-core",
|
"ratatui-core",
|
||||||
|
|
|
@ -53,6 +53,7 @@ unicode-segmentation.workspace = true
|
||||||
unicode-width.workspace = true
|
unicode-width.workspace = true
|
||||||
serde = { workspace = true, optional = true }
|
serde = { workspace = true, optional = true }
|
||||||
document-features = { workspace = true, optional = true }
|
document-features = { workspace = true, optional = true }
|
||||||
|
line-clipping = "0.2.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rstest.workspace = true
|
rstest.workspace = true
|
||||||
|
|
|
@ -424,6 +424,25 @@ impl<'a, 'b> Painter<'a, 'b> {
|
||||||
pub fn paint(&mut self, x: usize, y: usize, color: Color) {
|
pub fn paint(&mut self, x: usize, y: usize, color: Color) {
|
||||||
self.context.grid.paint(x, y, color);
|
self.context.grid.paint(x, y, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Canvas context bounds by axis.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use ratatui::{
|
||||||
|
/// style::Color,
|
||||||
|
/// symbols,
|
||||||
|
/// widgets::canvas::{Context, Painter},
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
|
||||||
|
/// let mut painter = Painter::from(&mut ctx);
|
||||||
|
/// assert_eq!(painter.bounds(), (&[0.0, 2.0], &[0.0, 2.0]));
|
||||||
|
/// ```
|
||||||
|
pub fn bounds(&self) -> (&[f64; 2], &[f64; 2]) {
|
||||||
|
(&self.context.x_bounds, &self.context.y_bounds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> {
|
impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use line_clipping::{cohen_sutherland, LineSegment, Point, Window};
|
||||||
use ratatui_core::style::Color;
|
use ratatui_core::style::Color;
|
||||||
|
|
||||||
use crate::canvas::{Painter, Shape};
|
use crate::canvas::{Painter, Shape};
|
||||||
|
@ -31,13 +32,21 @@ impl Line {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Shape for Line {
|
impl Shape for Line {
|
||||||
|
#[allow(clippy::similar_names)]
|
||||||
fn draw(&self, painter: &mut Painter) {
|
fn draw(&self, painter: &mut Painter) {
|
||||||
let Some((x1, y1)) = painter.get_point(self.x1, self.y1) else {
|
let (x_bounds, y_bounds) = painter.bounds();
|
||||||
|
let Some((world_x1, world_y1, world_x2, world_y2)) =
|
||||||
|
clip_line(x_bounds, y_bounds, self.x1, self.y1, self.x2, self.y2)
|
||||||
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some((x2, y2)) = painter.get_point(self.x2, self.y2) else {
|
let Some((x1, y1)) = painter.get_point(world_x1, world_y1) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let Some((x2, y2)) = painter.get_point(world_x2, world_y2) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let (dx, x_range) = if x2 >= x1 {
|
let (dx, x_range) = if x2 >= x1 {
|
||||||
(x2 - x1, x1..=x2)
|
(x2 - x1, x1..=x2)
|
||||||
} else {
|
} else {
|
||||||
|
@ -71,6 +80,27 @@ impl Shape for Line {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clip_line(
|
||||||
|
&[xmin, xmax]: &[f64; 2],
|
||||||
|
&[ymin, ymax]: &[f64; 2],
|
||||||
|
x1: f64,
|
||||||
|
y1: f64,
|
||||||
|
x2: f64,
|
||||||
|
y2: f64,
|
||||||
|
) -> Option<(f64, f64, f64, f64)> {
|
||||||
|
if let Some(LineSegment {
|
||||||
|
p1: Point { x: x1, y: y1 },
|
||||||
|
p2: Point { x: x2, y: y2 },
|
||||||
|
}) = cohen_sutherland::clip_line(
|
||||||
|
LineSegment::new(Point::new(x1, y1), Point::new(x2, y2)),
|
||||||
|
Window::new(xmin, xmax, ymin, ymax),
|
||||||
|
) {
|
||||||
|
Some((x1, y1, x2, y2))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn draw_line_low(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: usize, color: Color) {
|
fn draw_line_low(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: usize, color: Color) {
|
||||||
let dx = (x2 - x1) as isize;
|
let dx = (x2 - x1) as isize;
|
||||||
let dy = (y2 as isize - y1 as isize).abs();
|
let dy = (y2 as isize - y1 as isize).abs();
|
||||||
|
@ -124,9 +154,59 @@ mod tests {
|
||||||
use crate::canvas::Canvas;
|
use crate::canvas::Canvas;
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case::off_grid(&Line::new(-1.0, -1.0, 10.0, 10.0, Color::Red), [" "; 10])]
|
#[case::off_grid1(&Line::new(-1.0, 0.0, -1.0, 10.0, Color::Red), [" "; 10])]
|
||||||
#[case::off_grid(&Line::new(0.0, 0.0, 11.0, 11.0, Color::Red), [" "; 10])]
|
#[case::off_grid2(&Line::new(0.0, -1.0, 10.0, -1.0, Color::Red), [" "; 10])]
|
||||||
#[case::horizontal(&Line::new(0.0, 0.0, 10.0, 0.0, Color::Red), [
|
#[case::off_grid3(&Line::new(-10.0, 5.0, -1.0, 5.0, Color::Red), [" "; 10])]
|
||||||
|
#[case::off_grid4(&Line::new(5.0, 11.0, 5.0, 20.0, Color::Red), [" "; 10])]
|
||||||
|
#[case::off_grid5(&Line::new(-10.0, 0.0, 5.0, 0.0, Color::Red), [
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
"••••• ",
|
||||||
|
])]
|
||||||
|
#[case::off_grid6(&Line::new(-1.0, -1.0, 10.0, 10.0, Color::Red), [
|
||||||
|
" •",
|
||||||
|
" • ",
|
||||||
|
" • ",
|
||||||
|
" • ",
|
||||||
|
" • ",
|
||||||
|
" • ",
|
||||||
|
" • ",
|
||||||
|
" • ",
|
||||||
|
" • ",
|
||||||
|
"• ",
|
||||||
|
])]
|
||||||
|
#[case::off_grid7(&Line::new(0.0, 0.0, 11.0, 11.0, Color::Red), [
|
||||||
|
" •",
|
||||||
|
" • ",
|
||||||
|
" • ",
|
||||||
|
" • ",
|
||||||
|
" • ",
|
||||||
|
" • ",
|
||||||
|
" • ",
|
||||||
|
" • ",
|
||||||
|
" • ",
|
||||||
|
"• ",
|
||||||
|
])]
|
||||||
|
#[case::off_grid8(&Line::new(-1.0, -1.0, 11.0, 11.0, Color::Red), [
|
||||||
|
" •",
|
||||||
|
" • ",
|
||||||
|
" • ",
|
||||||
|
" • ",
|
||||||
|
" • ",
|
||||||
|
" • ",
|
||||||
|
" • ",
|
||||||
|
" • ",
|
||||||
|
" • ",
|
||||||
|
"• ",
|
||||||
|
])]
|
||||||
|
#[case::horizontal1(&Line::new(0.0, 0.0, 10.0, 0.0, Color::Red), [
|
||||||
" ",
|
" ",
|
||||||
" ",
|
" ",
|
||||||
" ",
|
" ",
|
||||||
|
@ -138,7 +218,7 @@ mod tests {
|
||||||
" ",
|
" ",
|
||||||
"••••••••••",
|
"••••••••••",
|
||||||
])]
|
])]
|
||||||
#[case::horizontal(&Line::new(10.0, 10.0, 0.0, 10.0, Color::Red), [
|
#[case::horizontal2(&Line::new(10.0, 10.0, 0.0, 10.0, Color::Red), [
|
||||||
"••••••••••",
|
"••••••••••",
|
||||||
" ",
|
" ",
|
||||||
" ",
|
" ",
|
||||||
|
@ -150,10 +230,10 @@ mod tests {
|
||||||
" ",
|
" ",
|
||||||
" ",
|
" ",
|
||||||
])]
|
])]
|
||||||
#[case::vertical(&Line::new(0.0, 0.0, 0.0, 10.0, Color::Red), ["• "; 10])]
|
#[case::vertical1(&Line::new(0.0, 0.0, 0.0, 10.0, Color::Red), ["• "; 10])]
|
||||||
#[case::vertical(&Line::new(10.0, 10.0, 10.0, 0.0, Color::Red), [" •"; 10])]
|
#[case::vertical2(&Line::new(10.0, 10.0, 10.0, 0.0, Color::Red), [" •"; 10])]
|
||||||
// dy < dx, x1 < x2
|
// dy < dx, x1 < x2
|
||||||
#[case::diagonal(&Line::new(0.0, 0.0, 10.0, 5.0, Color::Red), [
|
#[case::diagonal1(&Line::new(0.0, 0.0, 10.0, 5.0, Color::Red), [
|
||||||
" ",
|
" ",
|
||||||
" ",
|
" ",
|
||||||
" ",
|
" ",
|
||||||
|
@ -166,7 +246,7 @@ mod tests {
|
||||||
"• ",
|
"• ",
|
||||||
])]
|
])]
|
||||||
// dy < dx, x1 > x2
|
// dy < dx, x1 > x2
|
||||||
#[case::diagonal(&Line::new(10.0, 0.0, 0.0, 5.0, Color::Red), [
|
#[case::diagonal2(&Line::new(10.0, 0.0, 0.0, 5.0, Color::Red), [
|
||||||
" ",
|
" ",
|
||||||
" ",
|
" ",
|
||||||
" ",
|
" ",
|
||||||
|
@ -179,7 +259,7 @@ mod tests {
|
||||||
" •",
|
" •",
|
||||||
])]
|
])]
|
||||||
// dy > dx, y1 < y2
|
// dy > dx, y1 < y2
|
||||||
#[case::diagonal(&Line::new(0.0, 0.0, 5.0, 10.0, Color::Red), [
|
#[case::diagonal3(&Line::new(0.0, 0.0, 5.0, 10.0, Color::Red), [
|
||||||
" • ",
|
" • ",
|
||||||
" • ",
|
" • ",
|
||||||
" • ",
|
" • ",
|
||||||
|
@ -192,7 +272,7 @@ mod tests {
|
||||||
"• ",
|
"• ",
|
||||||
])]
|
])]
|
||||||
// dy > dx, y1 > y2
|
// dy > dx, y1 > y2
|
||||||
#[case::diagonal(&Line::new(0.0, 10.0, 5.0, 0.0, Color::Red), [
|
#[case::diagonal4(&Line::new(0.0, 10.0, 5.0, 0.0, Color::Red), [
|
||||||
"• ",
|
"• ",
|
||||||
"• ",
|
"• ",
|
||||||
" • ",
|
" • ",
|
||||||
|
|
Loading…
Reference in a new issue