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",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.14"
|
||||
|
@ -2200,6 +2209,7 @@ dependencies = [
|
|||
"indoc",
|
||||
"instability",
|
||||
"itertools 0.13.0",
|
||||
"line-clipping",
|
||||
"pretty_assertions",
|
||||
"ratatui",
|
||||
"ratatui-core",
|
||||
|
|
|
@ -53,6 +53,7 @@ unicode-segmentation.workspace = true
|
|||
unicode-width.workspace = true
|
||||
serde = { workspace = true, optional = true }
|
||||
document-features = { workspace = true, optional = true }
|
||||
line-clipping = "0.2.1"
|
||||
|
||||
[dev-dependencies]
|
||||
rstest.workspace = true
|
||||
|
|
|
@ -424,6 +424,25 @@ impl<'a, 'b> Painter<'a, 'b> {
|
|||
pub fn paint(&mut self, x: usize, y: usize, color: 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> {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use line_clipping::{cohen_sutherland, LineSegment, Point, Window};
|
||||
use ratatui_core::style::Color;
|
||||
|
||||
use crate::canvas::{Painter, Shape};
|
||||
|
@ -31,13 +32,21 @@ impl Line {
|
|||
}
|
||||
|
||||
impl Shape for Line {
|
||||
#[allow(clippy::similar_names)]
|
||||
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;
|
||||
};
|
||||
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;
|
||||
};
|
||||
let Some((x2, y2)) = painter.get_point(world_x2, world_y2) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let (dx, x_range) = if x2 >= x1 {
|
||||
(x2 - x1, x1..=x2)
|
||||
} 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) {
|
||||
let dx = (x2 - x1) as isize;
|
||||
let dy = (y2 as isize - y1 as isize).abs();
|
||||
|
@ -124,9 +154,59 @@ mod tests {
|
|||
use crate::canvas::Canvas;
|
||||
|
||||
#[rstest]
|
||||
#[case::off_grid(&Line::new(-1.0, -1.0, 10.0, 10.0, Color::Red), [" "; 10])]
|
||||
#[case::off_grid(&Line::new(0.0, 0.0, 11.0, 11.0, Color::Red), [" "; 10])]
|
||||
#[case::horizontal(&Line::new(0.0, 0.0, 10.0, 0.0, Color::Red), [
|
||||
#[case::off_grid1(&Line::new(-1.0, 0.0, -1.0, 10.0, Color::Red), [" "; 10])]
|
||||
#[case::off_grid2(&Line::new(0.0, -1.0, 10.0, -1.0, Color::Red), [" "; 10])]
|
||||
#[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::vertical(&Line::new(10.0, 10.0, 10.0, 0.0, Color::Red), [" •"; 10])]
|
||||
#[case::vertical1(&Line::new(0.0, 0.0, 0.0, 10.0, Color::Red), ["• "; 10])]
|
||||
#[case::vertical2(&Line::new(10.0, 10.0, 10.0, 0.0, Color::Red), [" •"; 10])]
|
||||
// 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
|
||||
#[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
|
||||
#[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
|
||||
#[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