mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-25 14:10:31 +00:00
Improve Chart Widget, safer buffer and unicode width
This commit is contained in:
parent
fde0ba95dd
commit
07ff2b08eb
9 changed files with 170 additions and 79 deletions
|
@ -9,6 +9,7 @@ bitflags = "0.7"
|
|||
cassowary = "0.2.0"
|
||||
log = "0.3"
|
||||
unicode-segmentation = "0.1.2"
|
||||
unicode-width = "0.1.3"
|
||||
|
||||
[dev-dependencies]
|
||||
log4rs = "*"
|
||||
|
|
|
@ -22,7 +22,7 @@ use log4rs::encode::pattern::PatternEncoder;
|
|||
use log4rs::config::{Appender, Config, Logger, Root};
|
||||
|
||||
use tui::Terminal;
|
||||
use tui::widgets::{Widget, Block, List, Gauge, Sparkline, Text, border, Chart};
|
||||
use tui::widgets::{Widget, Block, List, Gauge, Sparkline, Text, border, Chart, Axis, Dataset};
|
||||
use tui::layout::{Group, Direction, Alignment, Size};
|
||||
use tui::style::Color;
|
||||
|
||||
|
@ -66,10 +66,10 @@ impl SinSignal {
|
|||
}
|
||||
|
||||
impl Iterator for SinSignal {
|
||||
type Item = f64;
|
||||
fn next(&mut self) -> Option<f64> {
|
||||
type Item = (f64, f64);
|
||||
fn next(&mut self) -> Option<(f64, f64)> {
|
||||
self.x += 1.0;
|
||||
Some(((self.x * 1.0 / self.period).sin() + 1.0) * self.scale)
|
||||
Some((self.x, ((self.x * 1.0 / self.period).sin() + 1.0) * self.scale))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,7 +81,8 @@ struct App {
|
|||
show_chart: bool,
|
||||
progress: u16,
|
||||
data: Vec<u64>,
|
||||
data2: Vec<u64>,
|
||||
data2: Vec<(f64, f64)>,
|
||||
window: [f64; 2],
|
||||
colors: [Color; 2],
|
||||
color_index: usize,
|
||||
}
|
||||
|
@ -117,7 +118,8 @@ fn main() {
|
|||
show_chart: true,
|
||||
progress: 0,
|
||||
data: rand_signal.clone().take(100).collect(),
|
||||
data2: sin_signal.clone().take(100).map(|i| i as u64).collect(),
|
||||
data2: sin_signal.clone().take(100).collect(),
|
||||
window: [0.0, 100.0],
|
||||
colors: [Color::Magenta, Color::Red],
|
||||
color_index: 0,
|
||||
};
|
||||
|
@ -181,7 +183,9 @@ fn main() {
|
|||
app.data.insert(0, rand_signal.next().unwrap());
|
||||
app.data.pop();
|
||||
app.data2.remove(0);
|
||||
app.data2.push(sin_signal.next().unwrap() as u64);
|
||||
app.data2.push(sin_signal.next().unwrap());
|
||||
app.window[0] += 1.0;
|
||||
app.window[1] += 1.0;
|
||||
app.selected += 1;
|
||||
if app.selected >= app.items.len() {
|
||||
app.selected = 0;
|
||||
|
@ -211,12 +215,12 @@ fn draw(t: &mut Terminal, app: &App) {
|
|||
.chunks(&[Size::Fixed(2), Size::Fixed(3)])
|
||||
.render(&chunks[0], |chunks| {
|
||||
Gauge::default()
|
||||
.block(*Block::default().title("Gauge:"))
|
||||
.block(Block::default().title("Gauge:"))
|
||||
.bg(Color::Yellow)
|
||||
.percent(app.progress)
|
||||
.render(&chunks[0], t);
|
||||
Sparkline::default()
|
||||
.block(*Block::default().title("Sparkline:"))
|
||||
.block(Block::default().title("Sparkline:"))
|
||||
.fg(Color::Green)
|
||||
.data(&app.data)
|
||||
.render(&chunks[1], t);
|
||||
|
@ -232,21 +236,21 @@ fn draw(t: &mut Terminal, app: &App) {
|
|||
.chunks(&sizes)
|
||||
.render(&chunks[1], |chunks| {
|
||||
List::default()
|
||||
.block(*Block::default().borders(border::ALL).title("List"))
|
||||
.block(Block::default().borders(border::ALL).title("List"))
|
||||
.render(&chunks[0], t);
|
||||
if app.show_chart {
|
||||
Chart::default()
|
||||
.block(*Block::default()
|
||||
.block(Block::default()
|
||||
.borders(border::ALL)
|
||||
.title("Chart"))
|
||||
.fg(Color::Cyan)
|
||||
.axis([0, 40])
|
||||
.data(&app.data2)
|
||||
.x_axis(Axis::default().title("X").bounds(app.window))
|
||||
.y_axis(Axis::default().title("Y").bounds([0.0, 40.0]))
|
||||
.datasets(&[Dataset::default().color(Color::Cyan).data(&app.data2)])
|
||||
.render(&chunks[1], t);
|
||||
}
|
||||
});
|
||||
Text::default()
|
||||
.block(*Block::default().borders(border::ALL).title("Footer"))
|
||||
.block(Block::default().borders(border::ALL).title("Footer"))
|
||||
.fg(app.colors[app.color_index])
|
||||
.text("This żółw is a footer")
|
||||
.render(&chunks[2], t);
|
||||
|
|
|
@ -61,15 +61,21 @@ impl<'a> Buffer<'a> {
|
|||
&self.area
|
||||
}
|
||||
|
||||
pub fn index_of(&self, x: u16, y: u16) -> usize {
|
||||
pub fn index_of(&self, x: u16, y: u16) -> Option<usize> {
|
||||
let index = (y * self.area.width + x) as usize;
|
||||
debug_assert!(index < self.content.len());
|
||||
index
|
||||
if index < self.content.len() {
|
||||
Some(index)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pos_of(&self, i: usize) -> (u16, u16) {
|
||||
debug_assert!(self.area.width != 0);
|
||||
(i as u16 % self.area.width, i as u16 / self.area.width)
|
||||
pub fn pos_of(&self, i: usize) -> Option<(u16, u16)> {
|
||||
if self.area.width > 0 {
|
||||
Some((i as u16 % self.area.width, i as u16 / self.area.width))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_pos(&self, x: u16, y: u16) -> Option<(u16, u16)> {
|
||||
|
@ -86,49 +92,51 @@ impl<'a> Buffer<'a> {
|
|||
}
|
||||
|
||||
pub fn set(&mut self, x: u16, y: u16, cell: Cell<'a>) {
|
||||
let i = self.index_of(x, y);
|
||||
if let Some(i) = self.index_of(x, y) {
|
||||
self.content[i] = cell;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_symbol(&mut self, x: u16, y: u16, symbol: &'a str) {
|
||||
let i = self.index_of(x, y);
|
||||
if let Some(i) = self.index_of(x, y) {
|
||||
self.content[i].symbol = symbol;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_fg(&mut self, x: u16, y: u16, color: Color) {
|
||||
let i = self.index_of(x, y);
|
||||
if let Some(i) = self.index_of(x, y) {
|
||||
self.content[i].fg = color;
|
||||
}
|
||||
}
|
||||
pub fn set_bg(&mut self, x: u16, y: u16, color: Color) {
|
||||
let i = self.index_of(x, y);
|
||||
if let Some(i) = self.index_of(x, y) {
|
||||
self.content[i].bg = color;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_string(&mut self, x: u16, y: u16, string: &'a str, fg: Color, bg: Color) {
|
||||
let mut cursor = (x, y);
|
||||
for s in UnicodeSegmentation::graphemes(string, true).collect::<Vec<&str>>() {
|
||||
info!("{}", s);
|
||||
let index = self.index_of(cursor.0, cursor.1);
|
||||
let index = self.index_of(x, y);
|
||||
if index.is_none() {
|
||||
return;
|
||||
}
|
||||
let mut index = index.unwrap();
|
||||
let graphemes = UnicodeSegmentation::graphemes(string, true).collect::<Vec<&str>>();
|
||||
let max_index = (self.area.width - x) as usize;
|
||||
for s in graphemes.iter().take(max_index) {
|
||||
self.content[index].symbol = s;
|
||||
self.content[index].fg = fg;
|
||||
self.content[index].bg = bg;
|
||||
match self.next_pos(cursor.0, cursor.1) {
|
||||
Some(c) => {
|
||||
cursor = c;
|
||||
}
|
||||
None => {
|
||||
warn!("Failed to set all string");
|
||||
}
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_cell<F>(&mut self, x: u16, y: u16, f: F)
|
||||
where F: Fn(&mut Cell)
|
||||
{
|
||||
let i = self.index_of(x, y);
|
||||
if let Some(i) = self.index_of(x, y) {
|
||||
f(&mut self.content[i]);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn merge(&'a mut self, other: Buffer<'a>) {
|
||||
let area = self.area.union(&other.area);
|
||||
|
@ -140,7 +148,7 @@ impl<'a> Buffer<'a> {
|
|||
let offset_y = self.area.y - area.y;
|
||||
let size = self.area.area() as usize;
|
||||
for i in (0..size).rev() {
|
||||
let (x, y) = self.pos_of(i);
|
||||
let (x, y) = self.pos_of(i).unwrap();
|
||||
// New index in content
|
||||
let k = ((y + offset_y) * area.width + (x + offset_x)) as usize;
|
||||
self.content[k] = self.content[i].clone();
|
||||
|
@ -155,7 +163,7 @@ impl<'a> Buffer<'a> {
|
|||
let offset_y = other.area.y - area.y;
|
||||
let size = other.area.area() as usize;
|
||||
for i in 0..size {
|
||||
let (x, y) = other.pos_of(i);
|
||||
let (x, y) = other.pos_of(i).unwrap();
|
||||
// New index in content
|
||||
let k = ((y + offset_y) * area.width + (x + offset_x)) as usize;
|
||||
self.content[k] = other.content[i].clone();
|
||||
|
|
|
@ -5,6 +5,7 @@ extern crate bitflags;
|
|||
extern crate log;
|
||||
extern crate cassowary;
|
||||
extern crate unicode_segmentation;
|
||||
extern crate unicode_width;
|
||||
|
||||
mod buffer;
|
||||
mod util;
|
||||
|
|
|
@ -32,7 +32,7 @@ impl Terminal {
|
|||
|
||||
pub fn render_buffer(&mut self, buffer: Buffer) {
|
||||
for (i, cell) in buffer.content().iter().enumerate() {
|
||||
let (lx, ly) = buffer.pos_of(i);
|
||||
let (lx, ly) = buffer.pos_of(i).unwrap();
|
||||
let (x, y) = (lx + buffer.area().x, ly + buffer.area().y);
|
||||
if cell.symbol != "" {
|
||||
write!(self.stdout,
|
||||
|
|
|
@ -29,33 +29,33 @@ impl<'a> Default for Block<'a> {
|
|||
}
|
||||
|
||||
impl<'a> Block<'a> {
|
||||
pub fn title(&mut self, title: &'a str) -> &mut Block<'a> {
|
||||
pub fn title(mut self, title: &'a str) -> Block<'a> {
|
||||
self.title = Some(title);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn title_fg(&mut self, color: Color) -> &mut Block<'a> {
|
||||
pub fn title_fg(mut self, color: Color) -> Block<'a> {
|
||||
self.title_fg = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn title_bg(&mut self, color: Color) -> &mut Block<'a> {
|
||||
pub fn title_bg(mut self, color: Color) -> Block<'a> {
|
||||
self.title_bg = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn border_fg(&mut self, color: Color) -> &mut Block<'a> {
|
||||
pub fn border_fg(mut self, color: Color) -> Block<'a> {
|
||||
self.border_fg = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn border_bg(&mut self, color: Color) -> &mut Block<'a> {
|
||||
pub fn border_bg(mut self, color: Color) -> Block<'a> {
|
||||
self.border_bg = color;
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
pub fn borders(&mut self, flag: border::Flags) -> &mut Block<'a> {
|
||||
pub fn borders(mut self, flag: border::Flags) -> Block<'a> {
|
||||
self.borders = flag;
|
||||
self
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
use std::cmp::min;
|
||||
use std::cmp::{min, max};
|
||||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use widgets::{Widget, Block};
|
||||
use buffer::Buffer;
|
||||
|
@ -7,22 +9,95 @@ use style::Color;
|
|||
use util::hash;
|
||||
use symbols;
|
||||
|
||||
pub struct Axis<'a> {
|
||||
title: Option<&'a str>,
|
||||
bounds: [f64; 2],
|
||||
labels: Option<&'a [&'a str]>,
|
||||
}
|
||||
|
||||
impl<'a> Default for Axis<'a> {
|
||||
fn default() -> Axis<'a> {
|
||||
Axis {
|
||||
title: None,
|
||||
bounds: [0.0, 0.0],
|
||||
labels: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Axis<'a> {
|
||||
pub fn title(mut self, title: &'a str) -> Axis<'a> {
|
||||
self.title = Some(title);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bounds(mut self, bounds: [f64; 2]) -> Axis<'a> {
|
||||
self.bounds = bounds;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn labels(mut self, labels: &'a [&'a str]) -> Axis<'a> {
|
||||
self.labels = Some(labels);
|
||||
self
|
||||
}
|
||||
|
||||
fn title_width(&self) -> u16 {
|
||||
match self.title {
|
||||
Some(title) => title.width() as u16,
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn max_label_width(&self) -> u16 {
|
||||
match self.labels {
|
||||
Some(labels) => labels.iter().fold(0, |acc, l| max(l.width(), acc)) as u16,
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Dataset<'a> {
|
||||
data: &'a [(f64, f64)],
|
||||
color: Color,
|
||||
}
|
||||
|
||||
impl<'a> Default for Dataset<'a> {
|
||||
fn default() -> Dataset<'a> {
|
||||
Dataset {
|
||||
data: &[],
|
||||
color: Color::White,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Dataset<'a> {
|
||||
pub fn data(mut self, data: &'a [(f64, f64)]) -> Dataset<'a> {
|
||||
self.data = data;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Color) -> Dataset<'a> {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Chart<'a> {
|
||||
block: Option<Block<'a>>,
|
||||
fg: Color,
|
||||
x_axis: Axis<'a>,
|
||||
y_axis: Axis<'a>,
|
||||
datasets: &'a [Dataset<'a>],
|
||||
bg: Color,
|
||||
axis: [u64; 2],
|
||||
data: &'a [u64],
|
||||
}
|
||||
|
||||
impl<'a> Default for Chart<'a> {
|
||||
fn default() -> Chart<'a> {
|
||||
Chart {
|
||||
block: None,
|
||||
fg: Color::White,
|
||||
x_axis: Axis::default(),
|
||||
y_axis: Axis::default(),
|
||||
bg: Color::Black,
|
||||
axis: [0, 1],
|
||||
data: &[],
|
||||
datasets: &[],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,19 +113,18 @@ impl<'a> Chart<'a> {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn fg(&mut self, fg: Color) -> &mut Chart<'a> {
|
||||
self.fg = fg;
|
||||
pub fn x_axis(&mut self, axis: Axis<'a>) -> &mut Chart<'a> {
|
||||
self.x_axis = axis;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn axis(&mut self, axis: [u64; 2]) -> &mut Chart<'a> {
|
||||
debug_assert!(self.axis[0] <= self.axis[1]);
|
||||
self.axis = axis;
|
||||
pub fn y_axis(&mut self, axis: Axis<'a>) -> &mut Chart<'a> {
|
||||
self.y_axis = axis;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn data(&mut self, data: &'a [u64]) -> &mut Chart<'a> {
|
||||
self.data = data;
|
||||
pub fn datasets(&mut self, datasets: &'a [Dataset<'a>]) -> &mut Chart<'a> {
|
||||
self.datasets = datasets;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -62,20 +136,24 @@ impl<'a> Widget<'a> for Chart<'a> {
|
|||
None => (Buffer::empty(*area), *area),
|
||||
};
|
||||
|
||||
if self.axis[1] == 0 {
|
||||
return buf;
|
||||
}
|
||||
|
||||
let margin_x = chart_area.x - area.x;
|
||||
let margin_y = chart_area.y - area.y;
|
||||
let max_index = min(chart_area.width as usize, self.data.len());
|
||||
for (i, &y) in self.data.iter().take(max_index).enumerate() {
|
||||
if y < self.axis[1] {
|
||||
let dy = (self.axis[1] - y) * (chart_area.height - 1) as u64 /
|
||||
(self.axis[1] - self.axis[0]);
|
||||
buf.update_cell(i as u16 + margin_x, dy as u16 + margin_y, |c| {
|
||||
// info!("{:?}", self.datasets[0].data[0]);
|
||||
|
||||
for dataset in self.datasets {
|
||||
for &(x, y) in dataset.data.iter() {
|
||||
if x <= self.x_axis.bounds[0] || x > self.x_axis.bounds[1] ||
|
||||
y <= self.y_axis.bounds[0] || y > self.y_axis.bounds[1] {
|
||||
continue;
|
||||
}
|
||||
let dy = (self.y_axis.bounds[1] - y) * (chart_area.height - 1) as f64 /
|
||||
(self.y_axis.bounds[1] - self.y_axis.bounds[0]);
|
||||
let dx = (self.x_axis.bounds[1] - x) * (chart_area.width - 1) as f64 /
|
||||
(self.x_axis.bounds[1] - self.x_axis.bounds[0]);
|
||||
info!("{} {}", dx, dy);
|
||||
buf.update_cell(dx as u16 + margin_x, dy as u16 + margin_y, |c| {
|
||||
c.symbol = symbols::DOT;
|
||||
c.fg = self.fg;
|
||||
c.fg = dataset.color;
|
||||
c.bg = self.bg;
|
||||
})
|
||||
}
|
||||
|
|
|
@ -74,7 +74,6 @@ impl<'a> Widget<'a> for Gauge<'a> {
|
|||
let margin_y = gauge_area.y - area.y;
|
||||
// Gauge
|
||||
let width = (gauge_area.width * self.percent) / 100;
|
||||
info!("{}", width);
|
||||
// Label
|
||||
let len = self.percent_string.len() as u16;
|
||||
let middle = gauge_area.width / 2 - len / 2;
|
||||
|
|
|
@ -10,7 +10,7 @@ pub use self::text::Text;
|
|||
pub use self::list::List;
|
||||
pub use self::gauge::Gauge;
|
||||
pub use self::sparkline::Sparkline;
|
||||
pub use self::chart::Chart;
|
||||
pub use self::chart::{Chart, Axis, Dataset};
|
||||
|
||||
use std::hash::Hash;
|
||||
|
||||
|
|
Loading…
Reference in a new issue