Cleanup code and add chart widget

pull/3/head
Florian Dehau 8 years ago
parent 2ffb63363c
commit bd404f0238

@ -9,6 +9,9 @@ use std::thread;
use std::time;
use std::sync::mpsc;
use std::io::stdin;
use std::cmp::min;
use rand::distributions::{IndependentSample, Range};
use termion::event;
use termion::input::TermRead;
@ -19,18 +22,68 @@ use log4rs::encode::pattern::PatternEncoder;
use log4rs::config::{Appender, Config, Logger, Root};
use tui::Terminal;
use tui::widgets::{Widget, Block, List, Gauge, Sparkline, border};
use tui::widgets::{Widget, Block, List, Gauge, Sparkline, Text, border, Chart};
use tui::layout::{Group, Direction, Alignment, Size};
use tui::style::Color;
#[derive(Clone)]
struct RandomSignal {
range: Range<u64>,
rng: rand::ThreadRng,
}
impl RandomSignal {
fn new(r: Range<u64>) -> RandomSignal {
RandomSignal {
range: r,
rng: rand::thread_rng(),
}
}
}
impl Iterator for RandomSignal {
type Item = u64;
fn next(&mut self) -> Option<u64> {
Some(self.range.ind_sample(&mut self.rng))
}
}
#[derive(Clone)]
struct SinSignal {
x: f64,
period: f64,
scale: f64,
}
impl SinSignal {
fn new(period: f64, scale: f64) -> SinSignal {
SinSignal {
x: 0.0,
period: period,
scale: scale,
}
}
}
impl Iterator for SinSignal {
type Item = f64;
fn next(&mut self) -> Option<f64> {
self.x += 1.0;
Some(((self.x * 1.0 / self.period).sin() + 1.0) * self.scale)
}
}
struct App {
name: String,
fetching: bool,
items: Vec<String>,
selected: usize,
show_episodes: bool,
show_chart: bool,
progress: u16,
data: Vec<u64>,
data2: Vec<u64>,
colors: [Color; 2],
color_index: usize,
}
enum Event {
@ -53,14 +106,20 @@ fn main() {
let handle = log4rs::init_config(config).unwrap();
info!("Start");
let mut rand_signal = RandomSignal::new(Range::new(0, 100));
let mut sin_signal = SinSignal::new(4.0, 20.0);
let mut app = App {
name: String::from("Test app"),
fetching: false,
items: ["1", "2", "3"].into_iter().map(|e| String::from(*e)).collect(),
selected: 0,
show_episodes: false,
show_chart: true,
progress: 0,
data: (0..100).map(|_| rand::random::<u8>() as u64).collect(),
data: rand_signal.clone().take(100).collect(),
data2: sin_signal.clone().take(100).map(|i| i as u64).collect(),
colors: [Color::Magenta, Color::Red],
color_index: 0,
};
let (tx, rx) = mpsc::channel();
let input_tx = tx.clone();
@ -80,7 +139,7 @@ fn main() {
let tx = tx.clone();
loop {
tx.send(Event::Tick).unwrap();
thread::sleep(time::Duration::from_millis(1000));
thread::sleep(time::Duration::from_millis(500));
}
});
@ -108,7 +167,7 @@ fn main() {
}
}
event::Key::Char('t') => {
app.show_episodes = !app.show_episodes;
app.show_chart = !app.show_chart;
}
_ => {}
}
@ -118,12 +177,18 @@ fn main() {
if app.progress > 100 {
app.progress = 0;
}
app.data.insert(0, rand::random::<u8>() as u64);
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.selected += 1;
if app.selected >= app.items.len() {
app.selected = 0;
}
app.color_index += 1;
if app.color_index >= app.colors.len() {
app.color_index = 0;
}
}
}
}
@ -136,7 +201,7 @@ fn draw(terminal: &mut Terminal, app: &App) {
.direction(Direction::Vertical)
.alignment(Alignment::Left)
.chunks(&[Size::Fixed(7), Size::Min(5), Size::Fixed(3)])
.render(&terminal.area(), |chunks, tree| {
.render(&Terminal::size().unwrap(), |chunks, tree| {
tree.add(Block::default().borders(border::ALL).title("Graphs").render(&chunks[0]));
tree.add(Group::default()
.direction(Direction::Vertical)
@ -144,21 +209,21 @@ fn draw(terminal: &mut Terminal, app: &App) {
.margin(1)
.chunks(&[Size::Fixed(2), Size::Fixed(3)])
.render(&chunks[0], |chunks, tree| {
tree.add(Gauge::new()
tree.add(Gauge::default()
.block(*Block::default().title("Gauge:"))
.bg(Color::Yellow)
.percent(app.progress)
.render(&chunks[0]));
tree.add(Sparkline::new()
tree.add(Sparkline::default()
.block(*Block::default().title("Sparkline:"))
.fg(Color::Green)
.data(&app.data)
.render(&chunks[1]));
}));
let sizes = if app.show_episodes {
vec![Size::Min(20), Size::Max(40)]
let sizes = if app.show_chart {
vec![Size::Max(40), Size::Min(20)]
} else {
vec![Size::Min(20)]
vec![Size::Max(40)]
};
tree.add(Group::default()
.direction(Direction::Horizontal)
@ -166,7 +231,7 @@ fn draw(terminal: &mut Terminal, app: &App) {
.chunks(&sizes)
.render(&chunks[1], |chunks, tree| {
tree.add(List::default()
.block(*Block::default().borders(border::ALL).title("Podcasts"))
.block(*Block::default().borders(border::ALL).title("List"))
.items(&app.items)
.select(app.selected)
.formatter(|i, s| {
@ -178,14 +243,22 @@ fn draw(terminal: &mut Terminal, app: &App) {
(format!("{} {}", prefix, i), fg, Color::Black)
})
.render(&chunks[0]));
if app.show_episodes {
tree.add(Block::default()
.borders(border::ALL)
.title("Episodes")
if app.show_chart {
tree.add(Chart::default()
.block(*Block::default()
.borders(border::ALL)
.title("Chart"))
.fg(Color::Cyan)
.axis([0, 40])
.data(&app.data2)
.render(&chunks[1]));
}
}));
tree.add(Block::default().borders(border::ALL).title("Footer").render(&chunks[2]));
tree.add(Text::default()
.block(*Block::default().borders(border::ALL).title("Footer"))
.fg(app.colors[app.color_index])
.text("This is a footer")
.render(&chunks[2]));
});
terminal.render(ui);
}

@ -71,12 +71,11 @@ impl Buffer {
}
pub fn next_pos(&self, x: u16, y: u16) -> Option<(u16, u16)> {
let mut nx = x + 1;
let mut ny = y;
if nx >= self.area.width {
nx = 0;
ny = y + 1;
}
let (nx, ny) = if x + 1 > self.area.width {
(0, y + 1)
} else {
(x + 1, y)
};
if ny >= self.area.height {
None
} else {
@ -121,6 +120,13 @@ impl Buffer {
}
}
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);
f(&mut self.content[i]);
}
pub fn get(&self, x: u16, y: u16) -> &Cell {
let i = self.index_of(x, y);
&self.content[i]

@ -150,17 +150,17 @@ pub fn split(area: &Rect,
if let Some(last) = elements.last() {
constraints.push(match *dir {
Direction::Horizontal => {
last.x + last.width | EQ(WEAK) | (dest_area.x + dest_area.width) as f64
(last.x + last.width) | EQ(WEAK) | (dest_area.x + dest_area.width) as f64
}
Direction::Vertical => {
last.y + last.height | EQ(WEAK) | (dest_area.y + dest_area.height) as f64
(last.y + last.height) | EQ(WEAK) | (dest_area.y + dest_area.height) as f64
}
})
}
match *dir {
Direction::Horizontal => {
for pair in elements.windows(2) {
constraints.push(pair[0].x + pair[0].width | EQ(REQUIRED) | pair[1].x);
constraints.push((pair[0].x + pair[0].width) | EQ(REQUIRED) | pair[1].x);
}
for (i, size) in sizes.iter().enumerate() {
let cs = [elements[i].y | EQ(REQUIRED) | dest_area.y as f64,
@ -175,7 +175,7 @@ pub fn split(area: &Rect,
}
Direction::Vertical => {
for pair in elements.windows(2) {
constraints.push(pair[0].y + pair[0].height | EQ(REQUIRED) | pair[1].y);
constraints.push((pair[0].y + pair[0].height) | EQ(REQUIRED) | pair[1].y);
}
for (i, size) in sizes.iter().enumerate() {
let cs = [elements[i].x | EQ(REQUIRED) | dest_area.x as f64,
@ -345,7 +345,7 @@ impl Group {
&self.alignment,
self.margin,
&self.chunks);
let mut node = Node { children: Vec::new() };
let mut node = Node { children: Vec::with_capacity(chunks.len()) };
f(&chunks, &mut node);
Tree::Node(node)
}

@ -19,3 +19,5 @@ pub mod bar {
pub const ONE_QUATER: char = '▂';
pub const ONE_EIGHTH: char = '▁';
}
pub const DOT: char = '•';

@ -10,40 +10,36 @@ use widgets::WidgetType;
use layout::{Rect, Tree};
pub struct Terminal {
width: u16,
height: u16,
stdout: RawTerminal<io::Stdout>,
previous: HashMap<(WidgetType, Rect), u64>,
cache: HashMap<(WidgetType, Rect), u64>,
}
impl Terminal {
pub fn new() -> Result<Terminal, io::Error> {
let terminal = try!(termion::terminal_size());
let stdout = try!(io::stdout().into_raw_mode());
Ok(Terminal {
width: terminal.0,
height: terminal.1,
stdout: stdout,
previous: HashMap::new(),
cache: HashMap::new(),
})
}
pub fn area(&self) -> Rect {
Rect {
pub fn size() -> Result<Rect, io::Error> {
let terminal = try!(termion::terminal_size());
Ok(Rect {
x: 0,
y: 0,
width: self.width,
height: self.height,
}
width: terminal.0,
height: terminal.1,
})
}
pub fn render(&mut self, ui: Tree) {
debug!("Render Pass");
let mut buffers: Vec<Buffer> = Vec::new();
let mut previous: HashMap<(WidgetType, Rect), u64> = HashMap::new();
for node in ui.into_iter() {
let mut cache: HashMap<(WidgetType, Rect), u64> = HashMap::new();
for node in ui {
let area = *node.buffer.area();
match self.previous.remove(&(node.widget_type, area)) {
match self.cache.remove(&(node.widget_type, area)) {
Some(h) => {
if h == node.hash {
debug!("Skip {:?} at {:?}", node.widget_type, area);
@ -57,16 +53,16 @@ impl Terminal {
debug!("Render {:?} at {:?}", node.widget_type, area);
}
}
previous.insert((node.widget_type, area), node.hash);
cache.insert((node.widget_type, area), node.hash);
}
for (&(t, a), _h) in &self.previous {
for &(t, a) in self.cache.keys() {
buffers.insert(0, Buffer::empty(a));
debug!("Erased {:?} at {:?}", t, a);
}
for buf in buffers {
self.render_buffer(&buf);
}
self.previous = previous;
self.cache = cache;
}
pub fn render_buffer(&mut self, buffer: &Buffer) {

@ -43,6 +43,17 @@ impl<'a> Block<'a> {
self
}
pub fn border_fg(&mut self, color: Color) -> &mut Block<'a> {
self.border_fg = color;
self
}
pub fn border_bg(&mut self, color: Color) -> &mut Block<'a> {
self.border_bg = color;
self
}
pub fn borders(&mut self, flag: border::Flags) -> &mut Block<'a> {
self.borders = flag;
self
@ -119,11 +130,11 @@ impl<'a> Widget for Block<'a> {
if self.borders.contains(border::BOTTOM | border::RIGHT) {
buf.set_symbol(area.width - 1, area.height - 1, Line::BottomRight.get());
}
if let Some(ref title) = self.title {
if let Some(title) = self.title {
let (margin_x, string) = if self.borders.intersects(border::LEFT) {
(1, format!(" {} ", title))
} else {
(0, format!("{}", title))
(0, String::from(title))
};
buf.set_string(margin_x, 0, &string, self.title_fg, self.title_bg);
}

@ -0,0 +1,88 @@
use std::cmp::min;
use widgets::{Widget, WidgetType, Block};
use buffer::Buffer;
use layout::Rect;
use style::Color;
use symbols;
#[derive(Hash)]
pub struct Chart<'a> {
block: Option<Block<'a>>,
fg: Color,
bg: Color,
axis: [u64; 2],
data: &'a [u64],
}
impl<'a> Default for Chart<'a> {
fn default() -> Chart<'a> {
Chart {
block: None,
fg: Color::White,
bg: Color::Black,
axis: [0, 1],
data: &[],
}
}
}
impl<'a> Chart<'a> {
pub fn block(&'a mut self, block: Block<'a>) -> &mut Chart<'a> {
self.block = Some(block);
self
}
pub fn bg(&mut self, bg: Color) -> &mut Chart<'a> {
self.bg = bg;
self
}
pub fn fg(&mut self, fg: Color) -> &mut Chart<'a> {
self.fg = fg;
self
}
pub fn axis(&mut self, axis: [u64; 2]) -> &mut Chart<'a> {
debug_assert!(self.axis[0] <= self.axis[1]);
self.axis = axis;
self
}
pub fn data(&mut self, data: &'a [u64]) -> &mut Chart<'a> {
self.data = data;
self
}
}
impl<'a> Widget for Chart<'a> {
fn buffer(&self, area: &Rect) -> Buffer {
let (mut buf, chart_area) = match self.block {
Some(ref b) => (b.buffer(area), b.inner(*area)),
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| {
c.symbol = symbols::DOT;
c.fg = self.fg;
c.bg = self.bg;
})
}
}
buf
}
fn widget_type(&self) -> WidgetType {
WidgetType::Chart
}
}

@ -25,8 +25,8 @@ pub struct Gauge<'a> {
bg: Color,
}
impl<'a> Gauge<'a> {
pub fn new() -> Gauge<'a> {
impl<'a> Default for Gauge<'a> {
fn default() -> Gauge<'a> {
Gauge {
block: None,
percent: 0,
@ -34,7 +34,9 @@ impl<'a> Gauge<'a> {
fg: Color::Black,
}
}
}
impl<'a> Gauge<'a> {
pub fn block(&'a mut self, block: Block<'a>) -> &mut Gauge<'a> {
self.block = Some(block);
self

@ -1,5 +1,4 @@
use std::cmp::min;
use std::fmt::Display;
use std::hash::{Hash, Hasher};
use buffer::Buffer;
@ -62,7 +61,7 @@ impl<'a, T> List<'a, T>
}
impl<'a, T> Widget for List<'a, T>
where T: Display + Hash
where T: Hash
{
fn buffer(&self, area: &Rect) -> Buffer {
@ -81,8 +80,8 @@ impl<'a, T> Widget for List<'a, T>
};
for i in 0..bound {
let index = i + offset;
let ref item = self.items[index];
let ref formatter = self.formatter;
let item = &self.items[index];
let formatter = &self.formatter;
let (mut string, fg, bg) = formatter(item, self.selected == index);
string.truncate(list_area.width as usize);
buf.set_string(1, 1 + i as u16, &string, fg, bg);

@ -1,12 +1,16 @@
mod block;
mod text;
mod list;
mod gauge;
mod sparkline;
mod chart;
pub use self::block::Block;
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;
use std::hash::Hash;
@ -43,7 +47,7 @@ pub mod border {
}
impl Line {
fn get<'a>(&self) -> char {
fn get(&self) -> char {
match *self {
Line::TopRight => '┐',
Line::Vertical => '│',
@ -59,7 +63,7 @@ impl Line {
}
}
fn hline<'a>(x: u16, y: u16, len: u16, fg: Color, bg: Color) -> Buffer {
fn hline(x: u16, y: u16, len: u16, fg: Color, bg: Color) -> Buffer {
Buffer::filled(Rect {
x: x,
y: y,
@ -72,7 +76,7 @@ fn hline<'a>(x: u16, y: u16, len: u16, fg: Color, bg: Color) -> Buffer {
bg: bg,
})
}
fn vline<'a>(x: u16, y: u16, len: u16, fg: Color, bg: Color) -> Buffer {
fn vline(x: u16, y: u16, len: u16, fg: Color, bg: Color) -> Buffer {
Buffer::filled(Rect {
x: x,
y: y,
@ -89,9 +93,11 @@ fn vline<'a>(x: u16, y: u16, len: u16, fg: Color, bg: Color) -> Buffer {
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy)]
pub enum WidgetType {
Block,
Text,
List,
Gauge,
Sparkline,
Chart,
}
pub trait Widget: Hash {

@ -15,8 +15,8 @@ pub struct Sparkline<'a> {
max: Option<u64>,
}
impl<'a> Sparkline<'a> {
pub fn new() -> Sparkline<'a> {
impl<'a> Default for Sparkline<'a> {
fn default() -> Sparkline<'a> {
Sparkline {
block: None,
fg: Color::White,
@ -25,7 +25,9 @@ impl<'a> Sparkline<'a> {
max: None,
}
}
}
impl<'a> Sparkline<'a> {
pub fn block(&mut self, block: Block<'a>) -> &mut Sparkline<'a> {
self.block = Some(block);
self
@ -75,22 +77,22 @@ impl<'a> Widget for Sparkline<'a> {
.collect::<Vec<u64>>();
for j in (0..spark_area.height).rev() {
let mut line = String::with_capacity(max_index);
for i in 0..max_index {
line.push(match data[i] {
for d in data.iter_mut().take(max_index) {
line.push(match *d {
0 => ' ',
1 => bar::ONE_EIGHTH,
2 => bar::ONE_QUATER,
3 => bar::THREE_EIGHTHS,
4 => bar::HALF,
5 => bar::FIVE_EIGHTHS,
6 => bar::THREE_EIGHTHS,
7 => bar::THREE_QUATERS,
6 => bar::THREE_QUATERS,
7 => bar::SEVEN_EIGHTHS,
_ => bar::FULL,
});
if data[i] > 8 {
data[i] -= 8;
if *d > 8 {
*d -= 8;
} else {
data[i] = 0;
*d = 0;
}
}
buf.set_string(margin_x, margin_y + j, &line, self.fg, self.bg);

@ -0,0 +1,69 @@
use std::cmp::min;
use widgets::{Widget, WidgetType, Block};
use buffer::Buffer;
use layout::Rect;
use style::Color;
#[derive(Hash)]
pub struct Text<'a> {
block: Option<Block<'a>>,
fg: Color,
bg: Color,
text: &'a str,
}
impl<'a> Default for Text<'a> {
fn default() -> Text<'a> {
Text {
block: None,
fg: Color::White,
bg: Color::Black,
text: "",
}
}
}
impl<'a> Text<'a> {
pub fn block(&'a mut self, block: Block<'a>) -> &mut Text<'a> {
self.block = Some(block);
self
}
pub fn text(&mut self, text: &'a str) -> &mut Text<'a> {
self.text = text;
self
}
pub fn bg(&mut self, bg: Color) -> &mut Text<'a> {
self.bg = bg;
self
}
pub fn fg(&mut self, fg: Color) -> &mut Text<'a> {
self.fg = fg;
self
}
}
impl<'a> Widget for Text<'a> {
fn buffer(&self, area: &Rect) -> Buffer {
let (mut buf, text_area) = match self.block {
Some(b) => (b.buffer(area), b.inner(*area)),
None => (Buffer::empty(*area), *area),
};
let mut lines = self.text.lines().map(String::from).collect::<Vec<String>>();
let margin_x = text_area.x - area.x;
let margin_y = text_area.y - area.y;
let height = min(lines.len(), text_area.height as usize);
let width = text_area.width as usize;
for line in lines.iter_mut().take(height) {
line.truncate(width);
buf.set_string(margin_x, margin_y, line, self.fg, self.bg);
}
buf
}
fn widget_type(&self) -> WidgetType {
WidgetType::Text
}
}
Loading…
Cancel
Save