use std::marker::PhantomData;
use ratatui::prelude::*;
use ratatui::widgets::canvas::{
Canvas as RatatuiCanvas, Circle, Line as CanvasLine, Points, Rectangle,
};
use ratatui::widgets::{Block, Borders};
use super::{Component, RenderContext};
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub enum CanvasShape {
Line {
x1: f64,
y1: f64,
x2: f64,
y2: f64,
color: Color,
},
Rectangle {
x: f64,
y: f64,
width: f64,
height: f64,
color: Color,
},
Circle {
x: f64,
y: f64,
radius: f64,
color: Color,
},
Points {
coords: Vec<(f64, f64)>,
color: Color,
},
Label {
x: f64,
y: f64,
text: String,
color: Color,
},
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub enum CanvasMarker {
Dot,
Block,
HalfBlock,
#[default]
Braille,
}
impl CanvasMarker {
fn to_ratatui(&self) -> ratatui::symbols::Marker {
match self {
CanvasMarker::Dot => ratatui::symbols::Marker::Dot,
CanvasMarker::Block => ratatui::symbols::Marker::Block,
CanvasMarker::HalfBlock => ratatui::symbols::Marker::HalfBlock,
CanvasMarker::Braille => ratatui::symbols::Marker::Braille,
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub enum CanvasMessage {
AddShape(CanvasShape),
SetShapes(Vec<CanvasShape>),
Clear,
SetBounds {
x: [f64; 2],
y: [f64; 2],
},
SetMarker(CanvasMarker),
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct CanvasState {
shapes: Vec<CanvasShape>,
x_bounds: [f64; 2],
y_bounds: [f64; 2],
title: Option<String>,
marker: CanvasMarker,
}
impl Default for CanvasState {
fn default() -> Self {
Self {
shapes: Vec::new(),
x_bounds: [0.0, 100.0],
y_bounds: [0.0, 100.0],
title: None,
marker: CanvasMarker::default(),
}
}
}
impl CanvasState {
pub fn new() -> Self {
Self::default()
}
pub fn with_shapes(mut self, shapes: Vec<CanvasShape>) -> Self {
self.shapes = shapes;
self
}
pub fn with_x_bounds(mut self, min: f64, max: f64) -> Self {
self.x_bounds = [min, max];
self
}
pub fn with_y_bounds(mut self, min: f64, max: f64) -> Self {
self.y_bounds = [min, max];
self
}
pub fn with_bounds(mut self, x_min: f64, x_max: f64, y_min: f64, y_max: f64) -> Self {
self.x_bounds = [x_min, x_max];
self.y_bounds = [y_min, y_max];
self
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_marker(mut self, marker: CanvasMarker) -> Self {
self.marker = marker;
self
}
pub fn shapes(&self) -> &[CanvasShape] {
&self.shapes
}
pub fn add_shape(&mut self, shape: CanvasShape) {
self.shapes.push(shape);
}
pub fn clear(&mut self) {
self.shapes.clear();
}
pub fn x_bounds(&self) -> [f64; 2] {
self.x_bounds
}
pub fn y_bounds(&self) -> [f64; 2] {
self.y_bounds
}
pub fn set_x_bounds(&mut self, min: f64, max: f64) {
self.x_bounds = [min, max];
}
pub fn set_y_bounds(&mut self, min: f64, max: f64) {
self.y_bounds = [min, max];
}
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
pub fn set_title(&mut self, title: Option<String>) {
self.title = title;
}
pub fn marker(&self) -> &CanvasMarker {
&self.marker
}
pub fn set_marker(&mut self, marker: CanvasMarker) {
self.marker = marker;
}
pub fn update(&mut self, msg: CanvasMessage) -> Option<()> {
Canvas::update(self, msg)
}
}
pub struct Canvas(PhantomData<()>);
impl Component for Canvas {
type State = CanvasState;
type Message = CanvasMessage;
type Output = ();
fn init() -> Self::State {
CanvasState::default()
}
fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
match msg {
CanvasMessage::AddShape(shape) => {
state.shapes.push(shape);
}
CanvasMessage::SetShapes(shapes) => {
state.shapes = shapes;
}
CanvasMessage::Clear => {
state.shapes.clear();
}
CanvasMessage::SetBounds { x, y } => {
state.x_bounds = x;
state.y_bounds = y;
}
CanvasMessage::SetMarker(marker) => {
state.marker = marker;
}
}
None
}
fn view(state: &Self::State, ctx: &mut RenderContext<'_, '_>) {
if ctx.area.height < 2 || ctx.area.width < 2 {
return;
}
crate::annotation::with_registry(|reg| {
reg.register(
ctx.area,
crate::annotation::Annotation::canvas("canvas")
.with_focus(ctx.focused)
.with_disabled(ctx.disabled),
);
});
let needs_border = state.title.is_some() || ctx.focused;
let border_style = if ctx.disabled {
ctx.theme.disabled_style()
} else if ctx.focused {
ctx.theme.focused_border_style()
} else {
ctx.theme.border_style()
};
let content_style = if ctx.disabled {
ctx.theme.disabled_style()
} else {
ctx.theme.normal_style()
};
let canvas_area = if needs_border {
let mut block = Block::default()
.borders(Borders::ALL)
.border_style(border_style);
if let Some(ref title) = state.title {
block = block.title(title.as_str());
}
let inner = block.inner(ctx.area);
ctx.frame.render_widget(block, ctx.area);
inner
} else {
ctx.area
};
if canvas_area.height == 0 || canvas_area.width == 0 {
return;
}
let marker = state.marker.to_ratatui();
let x_bounds = state.x_bounds;
let y_bounds = state.y_bounds;
let shapes = state.shapes.clone();
let is_disabled = ctx.disabled;
let disabled_style = ctx.theme.disabled_style();
let canvas = RatatuiCanvas::default()
.x_bounds(x_bounds)
.y_bounds(y_bounds)
.marker(marker)
.background_color(content_style.bg.unwrap_or(Color::Reset))
.paint(move |ctx| {
for shape in &shapes {
match shape {
CanvasShape::Line {
x1,
y1,
x2,
y2,
color,
} => {
let draw_color = if is_disabled {
disabled_style.fg.unwrap_or(Color::DarkGray)
} else {
*color
};
ctx.draw(&CanvasLine::new(*x1, *y1, *x2, *y2, draw_color));
}
CanvasShape::Rectangle {
x,
y,
width,
height,
color,
} => {
let draw_color = if is_disabled {
disabled_style.fg.unwrap_or(Color::DarkGray)
} else {
*color
};
ctx.draw(&Rectangle {
x: *x,
y: *y,
width: *width,
height: *height,
color: draw_color,
});
}
CanvasShape::Circle {
x,
y,
radius,
color,
} => {
let draw_color = if is_disabled {
disabled_style.fg.unwrap_or(Color::DarkGray)
} else {
*color
};
ctx.draw(&Circle {
x: *x,
y: *y,
radius: *radius,
color: draw_color,
});
}
CanvasShape::Points { coords, color } => {
let draw_color = if is_disabled {
disabled_style.fg.unwrap_or(Color::DarkGray)
} else {
*color
};
ctx.draw(&Points {
coords,
color: draw_color,
});
}
CanvasShape::Label { x, y, text, color } => {
let draw_color = if is_disabled {
disabled_style.fg.unwrap_or(Color::DarkGray)
} else {
*color
};
ctx.print(
*x,
*y,
Span::styled(text.clone(), Style::default().fg(draw_color)),
);
}
}
}
});
ctx.frame.render_widget(canvas, canvas_area);
}
}
#[cfg(test)]
mod tests;