diff --git a/examples/prototype.rs b/examples/prototype.rs index 760a721..811cffb 100644 --- a/examples/prototype.rs +++ b/examples/prototype.rs @@ -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, + rng: rand::ThreadRng, +} + +impl RandomSignal { + fn new(r: Range) -> RandomSignal { + RandomSignal { + range: r, + rng: rand::thread_rng(), + } + } +} + +impl Iterator for RandomSignal { + type Item = u64; + fn next(&mut self) -> Option { + 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 { + self.x += 1.0; + Some(((self.x * 1.0 / self.period).sin() + 1.0) * self.scale) + } +} + struct App { name: String, fetching: bool, items: Vec, selected: usize, - show_episodes: bool, + show_chart: bool, progress: u16, data: Vec, + data2: Vec, + 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::() 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::() 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); } diff --git a/src/buffer.rs b/src/buffer.rs index 7db145b..06dc5b8 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -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(&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] diff --git a/src/layout.rs b/src/layout.rs index 907b20e..d6cf172 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -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) } diff --git a/src/symbols.rs b/src/symbols.rs index aea230c..fba8b28 100644 --- a/src/symbols.rs +++ b/src/symbols.rs @@ -19,3 +19,5 @@ pub mod bar { pub const ONE_QUATER: char = '▂'; pub const ONE_EIGHTH: char = '▁'; } + +pub const DOT: char = '•'; diff --git a/src/terminal.rs b/src/terminal.rs index e2c0b1e..54f8db9 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -10,40 +10,36 @@ use widgets::WidgetType; use layout::{Rect, Tree}; pub struct Terminal { - width: u16, - height: u16, stdout: RawTerminal, - previous: HashMap<(WidgetType, Rect), u64>, + cache: HashMap<(WidgetType, Rect), u64>, } impl Terminal { pub fn new() -> Result { - 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 { + 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 = 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) { diff --git a/src/widgets/block.rs b/src/widgets/block.rs index ba205e3..25c88ad 100644 --- a/src/widgets/block.rs +++ b/src/widgets/block.rs @@ -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); } diff --git a/src/widgets/chart.rs b/src/widgets/chart.rs new file mode 100644 index 0000000..8eb67b5 --- /dev/null +++ b/src/widgets/chart.rs @@ -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>, + 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 + } +} diff --git a/src/widgets/gauge.rs b/src/widgets/gauge.rs index 61cb9f7..57c3418 100644 --- a/src/widgets/gauge.rs +++ b/src/widgets/gauge.rs @@ -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 diff --git a/src/widgets/list.rs b/src/widgets/list.rs index 8c26a5c..9ba576d 100644 --- a/src/widgets/list.rs +++ b/src/widgets/list.rs @@ -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); diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 2cb2831..8c6ad95 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -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 { diff --git a/src/widgets/sparkline.rs b/src/widgets/sparkline.rs index f3bc2f7..24ce2d6 100644 --- a/src/widgets/sparkline.rs +++ b/src/widgets/sparkline.rs @@ -15,8 +15,8 @@ pub struct Sparkline<'a> { max: Option, } -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::>(); 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); diff --git a/src/widgets/text.rs b/src/widgets/text.rs new file mode 100644 index 0000000..1639c42 --- /dev/null +++ b/src/widgets/text.rs @@ -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>, + 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::>(); + 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 + } +}