From 13f6a5a98b24407cceba4151b1506dcfb8ec7893 Mon Sep 17 00:00:00 2001 From: Florian Dehau Date: Tue, 11 Oct 2016 19:54:35 +0200 Subject: [PATCH] Add list widget and improve rendering --- .gitignore | 1 + Cargo.toml | 2 + examples/prototype.rs | 120 +++++++++++++++++++++++++++++------------- src/layout.rs | 94 +++++++++++++++++++++++++++------ src/lib.rs | 3 ++ src/style.rs | 2 +- src/terminal.rs | 37 +++++++++++-- src/util.rs | 7 +++ src/widgets/block.rs | 9 +++- src/widgets/list.rs | 98 ++++++++++++++++++++++++++++++++++ src/widgets/mod.rs | 27 ++++++++-- 11 files changed, 337 insertions(+), 63 deletions(-) create mode 100644 src/util.rs create mode 100644 src/widgets/list.rs diff --git a/.gitignore b/.gitignore index a9d37c5..f7c8d39 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target Cargo.lock +*.log diff --git a/Cargo.toml b/Cargo.toml index 132d49c..da15ec4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,5 @@ authors = ["Florian Dehau "] termion = "1.1.1" bitflags = "0.7" cassowary = "0.2.0" +log = "0.3" +log4rs = "*" diff --git a/examples/prototype.rs b/examples/prototype.rs index 924c6d3..959b9fc 100644 --- a/examples/prototype.rs +++ b/examples/prototype.rs @@ -1,4 +1,7 @@ extern crate tui; +#[macro_use] +extern crate log; +extern crate log4rs; extern crate termion; use std::thread; @@ -8,25 +11,51 @@ use std::io::{Write, stdin}; use termion::event; use termion::input::TermRead; +use log::LogLevelFilter; +use log4rs::append::console::ConsoleAppender; +use log4rs::append::file::FileAppender; +use log4rs::encode::pattern::PatternEncoder; +use log4rs::config::{Appender, Config, Logger, Root}; + use tui::Terminal; -use tui::widgets::{Widget, Block, Border}; +use tui::widgets::{Widget, Block, List, Border}; use tui::layout::{Group, Direction, Alignment, Size}; struct App { name: String, fetching: bool, + items: Vec, + selected: usize, } enum Event { - Quit, - Redraw, + Input(event::Key), } fn main() { + let log = FileAppender::builder() + .encoder(Box::new(PatternEncoder::new("{d} - {m}{n}"))) + .build("prototype.log") + .unwrap(); + + let config = Config::builder() + .appender(Appender::builder().build("log", Box::new(log))) + .logger(Logger::builder() + .appender("log") + .additive(false) + .build("log", LogLevelFilter::Info)) + .build(Root::builder().appender("log").build(LogLevelFilter::Info)) + .unwrap(); + + let handle = log4rs::init_config(config).unwrap(); + info!("Start"); + 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, }; let (tx, rx) = mpsc::channel(); @@ -35,29 +64,39 @@ fn main() { let stdin = stdin(); for c in stdin.keys() { let evt = c.unwrap(); - match evt { - event::Key::Char('q') => { - tx.send(Event::Quit).unwrap(); - break; - } - event::Key::Char('r') => { - tx.send(Event::Redraw).unwrap(); - } - _ => {} + tx.send(Event::Input(evt)).unwrap(); + if evt == event::Key::Char('q') { + break; } } }); + let mut terminal = Terminal::new().unwrap(); terminal.clear(); terminal.hide_cursor(); + loop { draw(&mut terminal, &app); let evt = rx.recv().unwrap(); match evt { - Event::Quit => { - break; + Event::Input(input) => { + match input { + event::Key::Char('q') => { + break; + } + event::Key::Up => { + if app.selected > 0 { + app.selected -= 1 + }; + } + event::Key::Down => { + if app.selected < app.items.len() - 1 { + app.selected += 1; + } + } + _ => {} + } } - Event::Redraw => {} } } terminal.show_cursor(); @@ -68,27 +107,34 @@ fn draw(terminal: &mut Terminal, app: &App) { let ui = Group::default() .direction(Direction::Vertical) .alignment(Alignment::Left) - .chunks(&[Size::Fixed(3.0), Size::Percent(100.0), Size::Fixed(3.0)]) - .render(&terminal.area(), |chunks| { - vec![Block::default() - .borders(Border::TOP | Border::BOTTOM) - .title("Header") - .render(&chunks[0]), - Group::default() - .direction(Direction::Horizontal) - .alignment(Alignment::Left) - .chunks(&[Size::Percent(50.0), Size::Percent(50.0)]) - .render(&chunks[1], |chunks| { - vec![Block::default() - .borders(Border::ALL) - .title("Podcasts") - .render(&chunks[0]), - Block::default() - .borders(Border::ALL) - .title("Episodes") - .render(&chunks[1])] - }), - Block::default().borders(Border::ALL).title("Footer").render(&chunks[2])] + .chunks(&[Size::Fixed(3), Size::Percent(100), Size::Fixed(3)]) + .render(&terminal.area(), |chunks, tree| { + tree.add(Block::default() + .borders(Border::ALL) + .title("Header") + .render(&chunks[0])); + tree.add(Group::default() + .direction(Direction::Horizontal) + .alignment(Alignment::Left) + .chunks(&[Size::Percent(50), Size::Percent(50)]) + .render(&chunks[1], |chunks, tree| { + tree.add(List::default() + .block(|b| { + b.borders(Border::ALL).title("Podcasts"); + }) + .items(&app.items) + .select(app.selected) + .formatter(|i, s| { + let prefix = if s { ">" } else { "*" }; + format!("{} {}", prefix, i) + }) + .render(&chunks[0])); + tree.add(Block::default() + .borders(Border::ALL) + .title("Episodes") + .render(&chunks[1])); + })); + tree.add(Block::default().borders(Border::ALL).title("Footer").render(&chunks[2])); }); - terminal.render(&ui); + terminal.render(ui); } diff --git a/src/layout.rs b/src/layout.rs index c2b8ba3..a7bbb42 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -5,8 +5,11 @@ use cassowary::{Solver, Variable, Constraint}; use cassowary::WeightedRelation::*; use cassowary::strength::{WEAK, MEDIUM, STRONG, REQUIRED}; +use util::hash; use buffer::Buffer; +use widgets::WidgetType; +#[derive(Hash)] pub enum Alignment { Top, Left, @@ -15,12 +18,13 @@ pub enum Alignment { Right, } +#[derive(Hash)] pub enum Direction { Horizontal, Vertical, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Rect { pub x: u16, pub y: u16, @@ -98,10 +102,10 @@ impl Rect { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Hash)] pub enum Size { - Fixed(f64), - Percent(f64), + Fixed(u16), + Percent(u16), } /// # Examples @@ -153,9 +157,9 @@ pub fn split(area: &Rect, dir: &Direction, align: &Alignment, sizes: &[Size]) -> let cs = [elements[i].y | EQ(REQUIRED) | area.y as f64, elements[i].height | EQ(REQUIRED) | area.height as f64, match *size { - Size::Fixed(f) => elements[i].width | EQ(REQUIRED) | f, + Size::Fixed(f) => elements[i].width | EQ(REQUIRED) | f as f64, Size::Percent(p) => { - elements[i].width | EQ(WEAK) | area.width as f64 * p / 100.0 + elements[i].width | EQ(WEAK) | (area.width * p) as f64 / 100.0 } }]; constraints.extend_from_slice(&cs); @@ -169,9 +173,9 @@ pub fn split(area: &Rect, dir: &Direction, align: &Alignment, sizes: &[Size]) -> let cs = [elements[i].x | EQ(REQUIRED) | area.x as f64, elements[i].width | EQ(REQUIRED) | area.width as f64, match *size { - Size::Fixed(f) => elements[i].height | EQ(REQUIRED) | f, + Size::Fixed(f) => elements[i].height | EQ(REQUIRED) | f as f64, Size::Percent(p) => { - elements[i].height | EQ(WEAK) | area.height as f64 * p / 100.0 + elements[i].height | EQ(WEAK) | (area.height * p) as f64 / 100.0 } }]; constraints.extend_from_slice(&cs); @@ -218,6 +222,67 @@ impl Element { } } +pub enum Tree { + Node(Node), + Leaf(Leaf), +} + +impl IntoIterator for Tree { + type Item = Leaf; + type IntoIter = WidgetIterator; + + fn into_iter(self) -> WidgetIterator { + WidgetIterator::new(self) + } +} + +pub struct WidgetIterator { + stack: Vec, +} + +impl WidgetIterator { + fn new(tree: Tree) -> WidgetIterator { + WidgetIterator { stack: vec![tree] } + } +} + +impl Iterator for WidgetIterator { + type Item = Leaf; + fn next(&mut self) -> Option { + match self.stack.pop() { + Some(t) => { + match t { + Tree::Node(n) => { + let index = self.stack.len(); + for c in n.children { + self.stack.insert(index, c); + } + self.next() + } + Tree::Leaf(l) => Some(l), + } + } + None => None, + } + } +} + +pub struct Node { + pub children: Vec, +} + +impl Node { + pub fn add(&mut self, node: Tree) { + self.children.push(node); + } +} + +pub struct Leaf { + pub widget_type: WidgetType, + pub hash: u64, + pub buffer: Buffer, +} + pub struct Group { direction: Direction, alignment: Alignment, @@ -249,15 +314,12 @@ impl Group { self.chunks = Vec::from(chunks); self } - pub fn render(&self, area: &Rect, f: F) -> Buffer - where F: Fn(&[Rect]) -> Vec + pub fn render(&self, area: &Rect, f: F) -> Tree + where F: Fn(&[Rect], &mut Node) { let chunks = split(area, &self.direction, &self.alignment, &self.chunks); - let results = f(&chunks); - let mut result = results[0].clone(); - for r in results.iter().skip(1) { - result.merge(&r); - } - result + let mut node = Node { children: Vec::new() }; + let results = f(&chunks, &mut node); + Tree::Node(node) } } diff --git a/src/lib.rs b/src/lib.rs index f236f18..b5d897e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,12 @@ extern crate termion; #[macro_use] extern crate bitflags; +#[macro_use] +extern crate log; extern crate cassowary; mod buffer; +mod util; pub mod terminal; pub mod widgets; pub mod style; diff --git a/src/style.rs b/src/style.rs index 680bd92..7b0cade 100644 --- a/src/style.rs +++ b/src/style.rs @@ -1,6 +1,6 @@ use termion; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Hash)] pub enum Color { Black, Red, diff --git a/src/terminal.rs b/src/terminal.rs index 8719ca2..35d94ac 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -1,17 +1,20 @@ use std::iter; use std::io; use std::io::Write; +use std::collections::HashMap; use termion; use termion::raw::{IntoRawMode, RawTerminal}; use buffer::Buffer; -use layout::Rect; +use widgets::WidgetType; +use layout::{Rect, Tree, Node, Leaf}; pub struct Terminal { - stdout: RawTerminal, width: u16, height: u16, + stdout: RawTerminal, + previous: HashMap<(WidgetType, u64), Rect>, } impl Terminal { @@ -19,9 +22,10 @@ impl Terminal { let terminal = try!(termion::terminal_size()); let stdout = try!(io::stdout().into_raw_mode()); Ok(Terminal { - stdout: stdout, width: terminal.0, height: terminal.1, + stdout: stdout, + previous: HashMap::new(), }) } @@ -34,7 +38,31 @@ impl Terminal { } } - pub fn render(&mut self, buffer: &Buffer) { + pub fn render(&mut self, ui: Tree) { + let mut buffers: Vec = Vec::new(); + let mut previous: HashMap<(WidgetType, u64), Rect> = HashMap::new(); + for node in ui.into_iter() { + let area = *node.buffer.area(); + match self.previous.remove(&(node.widget_type, node.hash)) { + Some(r) => { + if r != area { + buffers.push(node.buffer); + } + } + None => { + buffers.push(node.buffer); + } + } + previous.insert((node.widget_type, node.hash), area); + } + for buf in buffers { + self.render_buffer(&buf); + info!("{:?}", buf.area()); + } + self.previous = previous; + } + + pub fn render_buffer(&mut self, buffer: &Buffer) { for (i, cell) in buffer.content().iter().enumerate() { let (lx, ly) = buffer.pos_of(i); let (x, y) = (lx + buffer.area().x, ly + buffer.area().y); @@ -50,6 +78,7 @@ impl Terminal { } pub fn clear(&mut self) { write!(self.stdout, "{}", termion::clear::All).unwrap(); + write!(self.stdout, "{}", termion::cursor::Goto(1, 1)).unwrap(); self.stdout.flush().unwrap(); } pub fn hide_cursor(&mut self) { diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..69d215e --- /dev/null +++ b/src/util.rs @@ -0,0 +1,7 @@ +use std::hash::{Hash, SipHasher, Hasher}; + +pub fn hash(t: &T) -> u64 { + let mut s = SipHasher::new(); + t.hash(&mut s); + s.finish() +} diff --git a/src/widgets/block.rs b/src/widgets/block.rs index 1cfa197..2086731 100644 --- a/src/widgets/block.rs +++ b/src/widgets/block.rs @@ -2,8 +2,9 @@ use buffer::Buffer; use layout::Rect; use style::Color; -use widgets::{Widget, Border, Line, vline, hline}; +use widgets::{Widget, WidgetType, Border, Line, vline, hline}; +#[derive(Hash)] pub struct Block<'a> { title: Option<&'a str>, borders: Border::Flags, @@ -35,7 +36,7 @@ impl<'a> Block<'a> { } impl<'a> Widget for Block<'a> { - fn render(&self, area: &Rect) -> Buffer { + fn buffer(&self, area: &Rect) -> Buffer { let mut buf = Buffer::empty(*area); @@ -91,4 +92,8 @@ impl<'a> Widget for Block<'a> { } buf } + + fn widget_type(&self) -> WidgetType { + WidgetType::Block + } } diff --git a/src/widgets/list.rs b/src/widgets/list.rs new file mode 100644 index 0000000..2dc8a89 --- /dev/null +++ b/src/widgets/list.rs @@ -0,0 +1,98 @@ +use std::cmp::{min, max}; +use std::fmt::Display; +use std::hash::{Hash, Hasher}; + +use buffer::Buffer; +use widgets::{Widget, WidgetType, Block}; +use style::Color; +use layout::Rect; + +pub struct List<'a, T> { + block: Block<'a>, + selected: usize, + formatter: Box String>, + items: Vec, +} + +impl<'a, T> Hash for List<'a, T> + where T: Hash +{ + fn hash(&self, state: &mut H) { + self.block.hash(state); + self.selected.hash(state); + self.items.hash(state); + } +} + +impl<'a, T> Default for List<'a, T> { + fn default() -> List<'a, T> { + List { + block: Block::default(), + selected: 0, + formatter: Box::new(|e: &T, selected: bool| String::from("")), + items: Vec::new(), + } + } +} + +impl<'a, T> List<'a, T> + where T: Clone +{ + pub fn block(&'a mut self, f: F) -> &mut List<'a, T> + where F: Fn(&mut Block) + { + f(&mut self.block); + self + } + + pub fn formatter(&'a mut self, f: F) -> &mut List<'a, T> + where F: 'static + Fn(&T, bool) -> String + { + self.formatter = Box::new(f); + self + } + + pub fn items(&'a mut self, items: &'a [T]) -> &mut List<'a, T> { + self.items = items.to_vec(); + self + } + + pub fn select(&'a mut self, index: usize) -> &mut List<'a, T> { + self.selected = index; + self + } +} + +impl<'a, T> Widget for List<'a, T> + where T: Display + Hash +{ + fn buffer(&self, area: &Rect) -> Buffer { + let mut buf = self.block.buffer(area); + if area.area() == 0 { + return buf; + } + + let list_length = self.items.len(); + let list_area = area.inner(1); + let list_height = list_area.height as usize; + let bound = min(list_height, list_length); + let offset = if self.selected > list_height { + min(self.selected - list_height, list_length - list_height) + } else { + 0 + }; + for i in 0..bound { + let index = i + offset; + let ref item = self.items[index]; + let ref formatter = self.formatter; + let mut string = formatter(item, self.selected == index); + string.truncate(list_area.width as usize); + buf.set_string(1, 1 + i as u16, &string); + } + buf + } + + fn widget_type(&self) -> WidgetType { + WidgetType::List + } +} diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index cd59285..6dff1d4 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -1,9 +1,13 @@ mod block; +mod list; pub use self::block::Block; +pub use self::list::List; +use std::hash::{Hash, SipHasher, Hasher}; +use util::hash; use buffer::{Buffer, Cell}; -use layout::Rect; +use layout::{Rect, Tree, Leaf}; use style::Color; enum Line { @@ -77,6 +81,23 @@ fn vline<'a>(x: u16, y: u16, len: u16, fg: Color, bg: Color) -> Buffer { }) } -pub trait Widget { - fn render(&self, area: &Rect) -> Buffer; +#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy)] +pub enum WidgetType { + Block, + List, +} + +pub trait Widget: Hash { + fn buffer(&self, area: &Rect) -> Buffer; + fn widget_type(&self) -> WidgetType; + fn render(&self, area: &Rect) -> Tree { + let widget_type = self.widget_type(); + let hash = hash(&self); + let buffer = self.buffer(area); + Tree::Leaf(Leaf { + widget_type: widget_type, + hash: hash, + buffer: buffer, + }) + } }