diff --git a/examples/canvas.rs b/examples/canvas.rs index 4ea8f48c..12d99502 100644 --- a/examples/canvas.rs +++ b/examples/canvas.rs @@ -1,28 +1,29 @@ use std::{ - error::Error, - io, + io::{self, stdout, Stdout}, time::{Duration, Instant}, }; use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, - execute, + event::{self, Event, KeyCode}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + ExecutableCommand, }; use ratatui::{ prelude::*, widgets::{canvas::*, *}, }; +fn main() -> io::Result<()> { + App::run() +} + struct App { x: f64, y: f64, - ball: Rectangle, + ball: Circle, playground: Rect, vx: f64, vy: f64, - dir_x: bool, - dir_y: bool, tick_count: u64, marker: Marker, } @@ -32,155 +33,166 @@ impl App { App { x: 0.0, y: 0.0, - ball: Rectangle { - x: 10.0, - y: 30.0, - width: 10.0, - height: 10.0, + ball: Circle { + x: 20.0, + y: 40.0, + radius: 10.0, color: Color::Yellow, }, - playground: Rect::new(10, 10, 100, 100), + playground: Rect::new(10, 10, 200, 100), vx: 1.0, vy: 1.0, - dir_x: true, - dir_y: true, tick_count: 0, marker: Marker::Dot, } } - fn on_tick(&mut self) { - self.tick_count += 1; - // only change marker every 4 ticks (1s) to avoid stroboscopic effect - if (self.tick_count % 4) == 0 { - self.marker = match self.marker { - Marker::Dot => Marker::Block, - Marker::Block => Marker::Bar, - Marker::Bar => Marker::Braille, - Marker::Braille => Marker::Dot, - }; - } - if self.ball.x < self.playground.left() as f64 - || self.ball.x + self.ball.width > self.playground.right() as f64 - { - self.dir_x = !self.dir_x; - } - if self.ball.y < self.playground.top() as f64 - || self.ball.y + self.ball.height > self.playground.bottom() as f64 - { - self.dir_y = !self.dir_y; - } - - if self.dir_x { - self.ball.x += self.vx; - } else { - self.ball.x -= self.vx; - } - - if self.dir_y { - self.ball.y += self.vy; - } else { - self.ball.y -= self.vy - } - } -} - -fn main() -> Result<(), Box> { - // setup terminal - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - // create app and run it - let tick_rate = Duration::from_millis(250); - let app = App::new(); - let res = run_app(&mut terminal, app, tick_rate); - - // restore terminal - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - terminal.show_cursor()?; - - if let Err(err) = res { - println!("{err:?}"); - } - - Ok(()) -} - -fn run_app( - terminal: &mut Terminal, - mut app: App, - tick_rate: Duration, -) -> io::Result<()> { - let mut last_tick = Instant::now(); - loop { - terminal.draw(|f| ui(f, &app))?; - - let timeout = tick_rate - .checked_sub(last_tick.elapsed()) - .unwrap_or_else(|| Duration::from_secs(0)); - if event::poll(timeout)? { - if let Event::Key(key) = event::read()? { - match key.code { - KeyCode::Char('q') => { - return Ok(()); + pub fn run() -> io::Result<()> { + let mut terminal = init_terminal()?; + let mut app = App::new(); + let mut last_tick = Instant::now(); + let tick_rate = Duration::from_millis(16); + loop { + let _ = terminal.draw(|frame| app.ui(frame)); + let timeout = tick_rate.saturating_sub(last_tick.elapsed()); + if event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => break, + KeyCode::Down => app.y += 1.0, + KeyCode::Up => app.y -= 1.0, + KeyCode::Right => app.x += 1.0, + KeyCode::Left => app.x -= 1.0, + _ => {} } - KeyCode::Down => { - app.y += 1.0; - } - KeyCode::Up => { - app.y -= 1.0; - } - KeyCode::Right => { - app.x += 1.0; - } - KeyCode::Left => { - app.x -= 1.0; - } - _ => {} } } + + if last_tick.elapsed() >= tick_rate { + app.on_tick(); + last_tick = Instant::now(); + } + } + restore_terminal() + } + + fn on_tick(&mut self) { + self.tick_count += 1; + // only change marker every 180 ticks (3s) to avoid stroboscopic effect + if (self.tick_count % 180) == 0 { + self.marker = match self.marker { + Marker::Dot => Marker::Braille, + Marker::Braille => Marker::Block, + Marker::Block => Marker::HalfBlock, + Marker::HalfBlock => Marker::Bar, + Marker::Bar => Marker::Dot, + }; + } + // bounce the ball by flipping the velocity vector + let ball = &self.ball; + let playground = self.playground; + if ball.x - ball.radius < playground.left() as f64 + || ball.x + ball.radius > playground.right() as f64 + { + self.vx = -self.vx; + } + if ball.y - ball.radius < playground.top() as f64 + || ball.y + ball.radius > playground.bottom() as f64 + { + self.vy = -self.vy; } - if last_tick.elapsed() >= tick_rate { - app.on_tick(); - last_tick = Instant::now(); - } + self.ball.x += self.vx; + self.ball.y += self.vy; + } + + fn ui(&self, frame: &mut Frame) { + let main_layout = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(frame.size()); + + let right_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(main_layout[1]); + + frame.render_widget(self.map_canvas(), main_layout[0]); + frame.render_widget(self.pong_canvas(), right_layout[0]); + frame.render_widget(self.boxes_canvas(right_layout[1]), right_layout[1]); + } + + fn map_canvas(&self) -> impl Widget + '_ { + Canvas::default() + .block(Block::default().borders(Borders::ALL).title("World")) + .marker(self.marker) + .paint(|ctx| { + ctx.draw(&Map { + color: Color::Green, + resolution: MapResolution::High, + }); + ctx.print(self.x, -self.y, "You are here".yellow()); + }) + .x_bounds([-180.0, 180.0]) + .y_bounds([-90.0, 90.0]) + } + + fn pong_canvas(&self) -> impl Widget + '_ { + Canvas::default() + .block(Block::default().borders(Borders::ALL).title("Pong")) + .marker(self.marker) + .paint(|ctx| { + ctx.draw(&self.ball); + }) + .x_bounds([10.0, 210.0]) + .y_bounds([10.0, 110.0]) + } + + fn boxes_canvas(&self, area: Rect) -> impl Widget { + let (left, right, bottom, top) = + (0.0, area.width as f64, 0.0, area.height as f64 * 2.0 - 4.0); + Canvas::default() + .block(Block::default().borders(Borders::ALL).title("Rects")) + .marker(self.marker) + .x_bounds([left, right]) + .y_bounds([bottom, top]) + .paint(|ctx| { + for i in 0..=11 { + ctx.draw(&Rectangle { + x: (i * i + 3 * i) as f64 / 2.0 + 2.0, + y: 2.0, + width: i as f64, + height: i as f64, + color: Color::Red, + }); + ctx.draw(&Rectangle { + x: (i * i + 3 * i) as f64 / 2.0 + 2.0, + y: 21.0, + width: i as f64, + height: i as f64, + color: Color::Blue, + }); + } + for i in 0..100 { + if i % 10 != 0 { + ctx.print(i as f64 + 1.0, 0.0, format!("{i}", i = i % 10)); + } + if i % 2 == 0 && i % 10 != 0 { + ctx.print(0.0, i as f64, format!("{i}", i = i % 10)); + } + } + }) } } -fn ui(f: &mut Frame, app: &App) { - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(f.size()); - let canvas = Canvas::default() - .block(Block::default().borders(Borders::ALL).title("World")) - .marker(app.marker) - .paint(|ctx| { - ctx.draw(&Map { - color: Color::White, - resolution: MapResolution::High, - }); - ctx.print(app.x, -app.y, "You are here".yellow()); - }) - .x_bounds([-180.0, 180.0]) - .y_bounds([-90.0, 90.0]); - f.render_widget(canvas, chunks[0]); - let canvas = Canvas::default() - .block(Block::default().borders(Borders::ALL).title("Pong")) - .marker(app.marker) - .paint(|ctx| { - ctx.draw(&app.ball); - }) - .x_bounds([10.0, 110.0]) - .y_bounds([10.0, 110.0]); - f.render_widget(canvas, chunks[1]); +fn init_terminal() -> io::Result>> { + enable_raw_mode()?; + stdout().execute(EnterAlternateScreen)?; + Terminal::new(CrosstermBackend::new(stdout())) +} + +fn restore_terminal() -> io::Result<()> { + disable_raw_mode()?; + stdout().execute(LeaveAlternateScreen)?; + Ok(()) } diff --git a/examples/canvas.tape b/examples/canvas.tape index 55ab9382..5e9b62ef 100644 --- a/examples/canvas.tape +++ b/examples/canvas.tape @@ -2,11 +2,12 @@ # To run this script, install vhs and run `vhs ./examples/canvas.tape` Output "target/canvas.gif" Set Theme "OceanicMaterial" +Set FontSize 12 Set Width 1200 Set Height 800 Hide Type "cargo run --example=canvas --features=crossterm" Enter -Sleep 1s +Sleep 2s Show -Sleep 5s +Sleep 30s diff --git a/examples/demo2/tabs/traceroute.rs b/examples/demo2/tabs/traceroute.rs index cb1867bd..dc78d5f7 100644 --- a/examples/demo2/tabs/traceroute.rs +++ b/examples/demo2/tabs/traceroute.rs @@ -111,11 +111,11 @@ fn render_map(selected_row: usize, area: Rect, buf: &mut Buffer) { .padding(Padding::new(1, 0, 1, 0)) .style(theme.style), ) - .marker(Marker::Dot) + .marker(Marker::HalfBlock) // picked to show Australia for the demo as it's the most interesting part of the map // (and the only part with hops ;)) - .x_bounds([113.0, 154.0]) - .y_bounds([-42.0, -11.0]) + .x_bounds([112.0, 155.0]) + .y_bounds([-46.0, -11.0]) .paint(|context| { context.draw(&map); if let Some(path) = path { diff --git a/src/symbols.rs b/src/symbols.rs index 815cdbb6..86470797 100644 --- a/src/symbols.rs +++ b/src/symbols.rs @@ -54,6 +54,12 @@ pub mod block { }; } +pub mod half_block { + pub const UPPER: char = '▀'; + pub const LOWER: char = '▄'; + pub const FULL: char = '█'; +} + pub mod bar { pub const FULL: &str = "█"; pub const SEVEN_EIGHTHS: &str = "▇"; @@ -408,6 +414,10 @@ pub enum Marker { /// Braille Patterns. If your terminal does not support this, you will see unicode replacement /// characters (�) instead of Braille dots. Braille, + /// Use the unicode block and half block characters ("█", "▄", and "▀") to represent points in + /// a grid that is double the resolution of the terminal. Because each terminal cell is + /// generally about twice as tall as it is wide, this allows for a square grid of pixels. + HalfBlock, } pub mod scrollbar { diff --git a/src/widgets/canvas.rs b/src/widgets/canvas.rs index 5df91932..e9ab5a5f 100644 --- a/src/widgets/canvas.rs +++ b/src/widgets/canvas.rs @@ -5,7 +5,9 @@ mod points; mod rectangle; mod world; -use std::fmt::Debug; +use std::{fmt::Debug, iter::zip}; + +use itertools::Itertools; pub use self::{ circle::Circle, @@ -36,36 +38,78 @@ pub struct Label<'a> { line: TextLine<'a>, } +/// A single layer of the canvas. +/// +/// This allows the canvas to be drawn in multiple layers. This is useful if you want to draw +/// multiple shapes on the canvas in specific order. #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] struct Layer { + // a string of characters representing the grid. This will be wrapped to the width of the grid + // when rendering string: String, - colors: Vec, + // colors for foreground and background + colors: Vec<(Color, Color)>, } +/// A grid of cells that can be painted on. +/// +/// The grid represents a particular screen region measured in rows and columns. The underlying +/// resolution of the grid might exceed the number of rows and columns. For example, a grid of +/// Braille patterns will have a resolution of 2x4 dots per cell. This means that a grid of 10x10 +/// cells will have a resolution of 20x40 dots. trait Grid: Debug { + /// Get the width of the grid in number of terminal columns fn width(&self) -> u16; + /// Get the height of the grid in number of terminal rows fn height(&self) -> u16; + /// Get the resolution of the grid in number of dots. This doesn't have to be the same as the + /// number of rows and columns of the grid. For example, a grid of Braille patterns will have a + /// resolution of 2x4 dots per cell. This means that a grid of 10x10 cells will have a + /// resolution of 20x40 dots. fn resolution(&self) -> (f64, f64); + /// Paint a point of the grid. The point is expressed in number of dots starting at the origin + /// of the grid in the top left corner. Note that this is not the same as the (x, y) coordinates + /// of the canvas. fn paint(&mut self, x: usize, y: usize, color: Color); + /// Save the current state of the grid as a layer to be rendered fn save(&self) -> Layer; + /// Reset the grid to its initial state fn reset(&mut self); } +/// The BrailleGrid is a grid made up of cells each containing a Braille pattern. +/// +/// This makes it possible to draw shapes with a resolution of 2x4 dots per cell. This is useful +/// when you want to draw shapes with a high resolution. Font support for Braille patterns is +/// required to see the dots. If your terminal or font does not support this unicode block, you +/// will see unicode replacement characters (�) instead of braille dots. +/// +/// This grid type only supports a single foreground color for each 2x4 dots cell. There is no way +/// to set the individual color of each dot in the braille pattern. #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] struct BrailleGrid { + /// width of the grid in number of terminal columns width: u16, + /// height of the grid in number of terminal rows height: u16, - cells: Vec, + /// represents the unicode braille patterns. Will take a value between 0x2800 and 0x28FF + /// this is converted to a utf16 string when converting to a layer. See + /// for more info. + utf16_code_points: Vec, + /// The color of each cell only supports foreground colors for now as there's no way to + /// individually set the background color of each dot in the braille pattern. colors: Vec, } impl BrailleGrid { + /// Create a new BrailleGrid with the given width and height measured in terminal columns and + /// rows respectively. fn new(width: u16, height: u16) -> BrailleGrid { let length = usize::from(width * height); BrailleGrid { width, height, - cells: vec![symbols::braille::BLANK; length], + utf16_code_points: vec![symbols::braille::BLANK; length], colors: vec![Color::Reset; length], } } @@ -81,31 +125,26 @@ impl Grid for BrailleGrid { } fn resolution(&self) -> (f64, f64) { - ( - f64::from(self.width) * 2.0 - 1.0, - f64::from(self.height) * 4.0 - 1.0, - ) + (f64::from(self.width) * 2.0, f64::from(self.height) * 4.0) } fn save(&self) -> Layer { - Layer { - string: String::from_utf16(&self.cells).unwrap(), - colors: self.colors.clone(), - } + let string = String::from_utf16(&self.utf16_code_points).unwrap(); + // the background color is always reset for braille patterns + let colors = self.colors.iter().map(|c| (*c, Color::Reset)).collect(); + Layer { string, colors } } fn reset(&mut self) { - for c in &mut self.cells { - *c = symbols::braille::BLANK; - } - for c in &mut self.colors { - *c = Color::Reset; - } + self.utf16_code_points.fill(symbols::braille::BLANK); + self.colors.fill(Color::Reset); } fn paint(&mut self, x: usize, y: usize, color: Color) { let index = y / 4 * self.width as usize + x / 2; - if let Some(c) = self.cells.get_mut(index) { + // using get_mut here because we are indexing the vector with usize values + // and we want to make sure we don't panic if the index is out of bounds + if let Some(c) = self.utf16_code_points.get_mut(index) { *c |= symbols::braille::DOTS[y % 4][x % 2]; } if let Some(c) = self.colors.get_mut(index) { @@ -114,16 +153,27 @@ impl Grid for BrailleGrid { } } +/// The CharGrid is a grid made up of cells each containing a single character. +/// +/// This makes it possible to draw shapes with a resolution of 1x1 dots per cell. This is useful +/// when you want to draw shapes with a low resolution. #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] struct CharGrid { + /// width of the grid in number of terminal columns width: u16, + /// height of the grid in number of terminal rows height: u16, + /// represents a single character for each cell cells: Vec, + /// The color of each cell colors: Vec, + /// The character to use for every cell - e.g. a block, dot, etc. cell_char: char, } impl CharGrid { + /// Create a new CharGrid with the given width and height measured in terminal columns and + /// rows respectively. fn new(width: u16, height: u16, cell_char: char) -> CharGrid { let length = usize::from(width * height); CharGrid { @@ -146,27 +196,25 @@ impl Grid for CharGrid { } fn resolution(&self) -> (f64, f64) { - (f64::from(self.width) - 1.0, f64::from(self.height) - 1.0) + (f64::from(self.width), f64::from(self.height)) } fn save(&self) -> Layer { Layer { string: self.cells.iter().collect(), - colors: self.colors.clone(), + colors: self.colors.iter().map(|c| (*c, Color::Reset)).collect(), } } fn reset(&mut self) { - for c in &mut self.cells { - *c = ' '; - } - for c in &mut self.colors { - *c = Color::Reset; - } + self.cells.fill(' '); + self.colors.fill(Color::Reset); } fn paint(&mut self, x: usize, y: usize, color: Color) { let index = y * self.width as usize + x; + // using get_mut here because we are indexing the vector with usize values + // and we want to make sure we don't panic if the index is out of bounds if let Some(c) = self.cells.get_mut(index) { *c = self.cell_char; } @@ -176,6 +224,128 @@ impl Grid for CharGrid { } } +/// The HalfBlockGrid is a grid made up of cells each containing a half block character. +/// +/// In terminals, each character is usually twice as tall as it is wide. Unicode has a couple of +/// vertical half block characters, the upper half block '▀' and lower half block '▄' which take up +/// half the height of a normal character but the full width. Together with an empty space ' ' and a +/// full block '█', we can effectively double the resolution of a single cell. In addition, because +/// each character can have a foreground and background color, we can control the color of the upper +/// and lower half of each cell. This allows us to draw shapes with a resolution of 1x2 "pixels" per +/// cell. +/// +/// This allows for more flexibility than the BrailleGrid which only supports a single +/// foreground color for each 2x4 dots cell, and the CharGrid which only supports a single +/// character for each cell. +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +struct HalfBlockGrid { + /// width of the grid in number of terminal columns + width: u16, + /// height of the grid in number of terminal rows + height: u16, + /// represents a single color for each "pixel" arranged in column, row order + pixels: Vec>, +} + +impl HalfBlockGrid { + /// Create a new HalfBlockGrid with the given width and height measured in terminal columns and + /// rows respectively. + fn new(width: u16, height: u16) -> HalfBlockGrid { + HalfBlockGrid { + width, + height, + pixels: vec![vec![Color::Reset; width as usize]; height as usize * 2], + } + } +} + +impl Grid for HalfBlockGrid { + fn width(&self) -> u16 { + self.width + } + + fn height(&self) -> u16 { + self.height + } + + fn resolution(&self) -> (f64, f64) { + (f64::from(self.width), f64::from(self.height) * 2.0) + } + + fn save(&self) -> Layer { + // Given that we store the pixels in a grid, and that we want to use 2 pixels arranged + // vertically to form a single terminal cell, which can be either empty, upper half block, + // lower half block or full block, we need examine the pixels in vertical pairs to decide + // what character to print in each cell. So these are the 4 states we use to represent each + // cell: + // + // 1. upper: reset, lower: reset => ' ' fg: reset / bg: reset + // 2. upper: reset, lower: color => '▄' fg: lower color / bg: reset + // 3. upper: color, lower: reset => '▀' fg: upper color / bg: reset + // 4. upper: color, lower: color => '▀' fg: upper color / bg: lower color + // + // Note that because the foreground reset color (i.e. default foreground color) is usually + // not the same as the background reset color (i.e. default background color), we need to + // swap around the colors for that state (2 reset/color). + // + // When the upper and lower colors are the same, we could continue to use an upper half + // block, but we choose to use a full block instead. This allows us to write unit tests that + // treat the cell as a single character instead of two half block characters. + + // first we join each adjacent row together to get an iterator that contains vertical pairs + // of pixels, with the lower row being the first element in the pair + let vertical_color_pairs = self + .pixels + .iter() + .tuples() + .flat_map(|(upper_row, lower_row)| zip(upper_row, lower_row)); + + // then we work out what character to print for each pair of pixels + let string = vertical_color_pairs + .clone() + .map(|(upper, lower)| match (upper, lower) { + (Color::Reset, Color::Reset) => ' ', + (Color::Reset, _) => symbols::half_block::LOWER, + (_, Color::Reset) => symbols::half_block::UPPER, + (&lower, &upper) => { + if lower == upper { + symbols::half_block::FULL + } else { + symbols::half_block::UPPER + } + } + }) + .collect(); + + // then we convert these each vertical pair of pixels into a foreground and background color + let colors = vertical_color_pairs + .map(|(upper, lower)| { + let (fg, bg) = match (upper, lower) { + (Color::Reset, Color::Reset) => (Color::Reset, Color::Reset), + (Color::Reset, &lower) => (lower, Color::Reset), + (&upper, Color::Reset) => (upper, Color::Reset), + (&upper, &lower) => (upper, lower), + }; + (fg, bg) + }) + .collect(); + + Layer { string, colors } + } + + fn reset(&mut self) { + self.pixels.fill(vec![Color::Reset; self.width as usize]); + } + + fn paint(&mut self, x: usize, y: usize, color: Color) { + self.pixels[y][x] = color; + } +} + +/// Painter is an abstraction over the [`Context`] that allows to draw shapes on the grid. +/// +/// It is used by the [`Shape`] trait to draw shapes on the grid. It can be useful to think of this +/// as similar to the [`Buffer`] struct that is used to draw widgets on the terminal. #[derive(Debug)] pub struct Painter<'a, 'b> { context: &'a mut Context<'b>, @@ -185,6 +355,17 @@ pub struct Painter<'a, 'b> { impl<'a, 'b> Painter<'a, 'b> { /// Convert the (x, y) coordinates to location of a point on the grid /// + /// (x, y) coordinates are expressed in the coordinate system of the canvas. The origin is in + /// the lower left corner of the canvas (unlike most other coordinates in Ratatui where the + /// origin is the upper left corner). The x and y bounds of the canvas define the specific area + /// of some coordinate system that will be drawn on the canvas. The resolution of the grid is + /// used to convert the (x, y) coordinates to the location of a point on the grid. + /// + /// The grid coordinates are expressed in the coordinate system of the grid. The origin is in + /// the top left corner of the grid. The x and y bounds of the grid are always [0, width - 1] + /// and [0, height - 1] respectively. The resolution of the grid is used to convert the (x, y) + /// coordinates to the location of a point on the grid. + /// /// # Examples: /// ``` /// use ratatui::{prelude::*, widgets::canvas::*}; @@ -215,8 +396,8 @@ impl<'a, 'b> Painter<'a, 'b> { if width == 0.0 || height == 0.0 { return None; } - let x = ((x - left) * self.resolution.0 / width) as usize; - let y = ((top - y) * self.resolution.1 / height) as usize; + let x = ((x - left) * (self.resolution.0 - 1.0) / width) as usize; + let y = ((top - y) * (self.resolution.1 - 1.0) / height) as usize; Some((x, y)) } @@ -246,6 +427,11 @@ impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> { } /// Holds the state of the Canvas when painting to it. +/// +/// This is used by the [`Canvas`] widget to draw shapes on the grid. It can be useful to think of +/// this as similar to the [`Frame`] struct that is used to draw widgets on the terminal. +/// +/// [`Frame`]: crate::prelude::Frame #[derive(Debug)] pub struct Context<'a> { x_bounds: [f64; 2], @@ -257,6 +443,22 @@ pub struct Context<'a> { } impl<'a> Context<'a> { + /// Create a new Context with the given width and height measured in terminal columns and rows + /// respectively. The x and y bounds define the specific area of some coordinate system that + /// will be drawn on the canvas. The marker defines the type of points used to draw the shapes. + /// + /// Applications should not use this directly but rather use the [`Canvas`] widget. This will be + /// created by the [`Canvas::paint`] moethod and passed to the closure that is used to draw on + /// the canvas. + /// + /// The x and y bounds should be specified as left/right and bottom/top respectively. For + /// example, if you want to draw a map of the world, you might want to use the following bounds: + /// + /// ``` + /// use ratatui::{prelude::*, widgets::canvas::*}; + /// + /// let ctx = Context::new(100, 100, [-180.0, 180.0], [-90.0, 90.0], symbols::Marker::Braille); + /// ``` pub fn new( width: u16, height: u16, @@ -272,6 +474,7 @@ impl<'a> Context<'a> { symbols::Marker::Block => Box::new(CharGrid::new(width, height, block)), symbols::Marker::Bar => Box::new(CharGrid::new(width, height, bar)), symbols::Marker::Braille => Box::new(BrailleGrid::new(width, height)), + symbols::Marker::HalfBlock => Box::new(HalfBlockGrid::new(width, height)), }; Context { x_bounds, @@ -293,14 +496,16 @@ impl<'a> Context<'a> { shape.draw(&mut painter); } - /// Go one layer above in the canvas. + /// Save the existing state of the grid as a layer to be rendered and reset the grid to its + /// initial state for the next layer. pub fn layer(&mut self) { self.layers.push(self.grid.save()); self.grid.reset(); self.dirty = false; } - /// Print a string on the canvas at the given position + /// Print a string on the canvas at the given position. Note that the text is always printed + /// on top of the canvas and is not affected by the layers. pub fn print(&mut self, x: f64, y: f64, line: T) where T: Into>, @@ -330,6 +535,22 @@ impl<'a> Context<'a> { /// /// See [Unicode Braille Patterns](https://en.wikipedia.org/wiki/Braille_Patterns) for more info. /// +/// The HalfBlock marker is useful when you want to draw shapes with a higher resolution than a +/// CharGrid but lower than a BrailleGrid. This grid type supports a foreground and background color +/// for each terminal cell. This allows for more flexibility than the BrailleGrid which only +/// supports a single foreground color for each 2x4 dots cell. +/// +/// The Canvas widget is used by calling the [`Canvas::paint`] method and passing a closure that +/// will be used to draw on the canvas. The closure will be passed a [`Context`] object that can be +/// used to draw shapes on the canvas. +/// +/// The [`Context`] object provides a [`Context::draw`] method that can be used to draw shapes on +/// the canvas. The [`Context::layer`] method can be used to save the current state of the canvas +/// and start a new layer. This is useful if you want to draw multiple shapes on the canvas in +/// specific order. The [`Context`] object also provides a [`Context::print`] method that can be +/// used to print text on the canvas. Note that the text is always printed on top of the canvas and +/// is not affected by the layers. +/// /// # Examples /// /// ``` @@ -371,7 +592,7 @@ where block: Option>, x_bounds: [f64; 2], y_bounds: [f64; 2], - painter: Option, + paint_func: Option, background_color: Color, marker: symbols::Marker, } @@ -385,7 +606,7 @@ where block: None, x_bounds: [0.0, 0.0], y_bounds: [0.0, 0.0], - painter: None, + paint_func: None, background_color: Color::Reset, marker: symbols::Marker::Braille, } @@ -396,6 +617,7 @@ impl<'a, F> Canvas<'a, F> where F: Fn(&mut Context), { + /// Set the block that will be rendered around the canvas pub fn block(mut self, block: Block<'a>) -> Canvas<'a, F> { self.block = Some(block); self @@ -420,10 +642,11 @@ where /// Store the closure that will be used to draw to the Canvas pub fn paint(mut self, f: F) -> Canvas<'a, F> { - self.painter = Some(f); + self.paint_func = Some(f); self } + /// Change the background color of the canvas pub fn background_color(mut self, color: Color) -> Canvas<'a, F> { self.background_color = color; self @@ -433,12 +656,18 @@ where /// as they provide a more fine grained result but you might want to use the simple dot or /// block instead if the targeted terminal does not support those symbols. /// + /// The HalfBlock marker is useful when you want to draw shapes with a higher resolution than a + /// CharGrid but lower than a BrailleGrid. This grid type supports a foreground and background + /// color for each terminal cell. This allows for more flexibility than the BrailleGrid which + /// only supports a single foreground color for each 2x4 dots cell. + /// /// # Examples /// /// ``` /// use ratatui::{prelude::*, widgets::canvas::*}; /// /// Canvas::default().marker(symbols::Marker::Braille).paint(|ctx| {}); + /// Canvas::default().marker(symbols::Marker::HalfBlock).paint(|ctx| {}); /// Canvas::default().marker(symbols::Marker::Dot).paint(|ctx| {}); /// Canvas::default().marker(symbols::Marker::Block).paint(|ctx| {}); /// ``` @@ -466,7 +695,7 @@ where let width = canvas_area.width as usize; - let Some(ref painter) = self.painter else { + let Some(ref painter) = self.paint_func else { return; }; @@ -484,17 +713,19 @@ where // Retrieve painted points for each layer for layer in ctx.layers { - for (i, (ch, color)) in layer - .string - .chars() - .zip(layer.colors.into_iter()) - .enumerate() - { + for (index, (ch, colors)) in layer.string.chars().zip(layer.colors).enumerate() { if ch != ' ' && ch != '\u{2800}' { - let (x, y) = (i % width, i / width); - buf.get_mut(x as u16 + canvas_area.left(), y as u16 + canvas_area.top()) - .set_char(ch) - .set_fg(color); + let (x, y) = ( + (index % width) as u16 + canvas_area.left(), + (index / width) as u16 + canvas_area.top(), + ); + let cell = buf.get_mut(x, y).set_char(ch); + if colors.0 != Color::Reset { + cell.set_fg(colors.0); + } + if colors.1 != Color::Reset { + cell.set_bg(colors.1); + } } } } diff --git a/src/widgets/canvas/rectangle.rs b/src/widgets/canvas/rectangle.rs index e3628013..542ae820 100644 --- a/src/widgets/canvas/rectangle.rs +++ b/src/widgets/canvas/rectangle.rs @@ -94,6 +94,41 @@ mod tests { assert_buffer_eq!(buffer, expected); } + #[test] + fn draw_half_block_lines() { + let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10)); + let canvas = Canvas::default() + .marker(Marker::HalfBlock) + .x_bounds([0.0, 10.0]) + .y_bounds([0.0, 10.0]) + .paint(|context| { + context.draw(&Rectangle { + x: 0.0, + y: 0.0, + width: 10.0, + height: 10.0, + color: Color::Red, + }); + }); + canvas.render(buffer.area, &mut buffer); + let mut expected = Buffer::with_lines(vec![ + "█▀▀▀▀▀▀▀▀█", + "█ █", + "█ █", + "█ █", + "█ █", + "█ █", + "█ █", + "█ █", + "█ █", + "█▄▄▄▄▄▄▄▄█", + ]); + expected.set_style(buffer.area, Style::new().red().on_red()); + expected.set_style(buffer.area.inner(&Margin::new(1, 0)), Style::reset().red()); + expected.set_style(buffer.area.inner(&Margin::new(1, 1)), Style::reset()); + assert_buffer_eq!(buffer, expected); + } + #[test] fn draw_braille_lines() { let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));