Add list widget and improve rendering

This commit is contained in:
Florian Dehau 2016-10-11 19:54:35 +02:00
parent 459201bc65
commit 13f6a5a98b
11 changed files with 337 additions and 63 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
target
Cargo.lock
*.log

View file

@ -7,3 +7,5 @@ authors = ["Florian Dehau <florian.dehau@telecomnancy.net>"]
termion = "1.1.1"
bitflags = "0.7"
cassowary = "0.2.0"
log = "0.3"
log4rs = "*"

View file

@ -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<String>,
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();
tx.send(Event::Input(evt)).unwrap();
if evt == event::Key::Char('q') {
break;
}
event::Key::Char('r') => {
tx.send(Event::Redraw).unwrap();
}
_ => {}
}
}
});
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 => {
Event::Input(input) => {
match input {
event::Key::Char('q') => {
break;
}
Event::Redraw => {}
event::Key::Up => {
if app.selected > 0 {
app.selected -= 1
};
}
event::Key::Down => {
if app.selected < app.items.len() - 1 {
app.selected += 1;
}
}
_ => {}
}
}
}
}
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)
.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]),
Group::default()
.render(&chunks[0]));
tree.add(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()
.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])]
}),
Block::default().borders(Border::ALL).title("Footer").render(&chunks[2])]
.render(&chunks[1]));
}));
tree.add(Block::default().borders(Border::ALL).title("Footer").render(&chunks[2]));
});
terminal.render(&ui);
terminal.render(ui);
}

View file

@ -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<Tree>,
}
impl WidgetIterator {
fn new(tree: Tree) -> WidgetIterator {
WidgetIterator { stack: vec![tree] }
}
}
impl Iterator for WidgetIterator {
type Item = Leaf;
fn next(&mut self) -> Option<Leaf> {
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<Tree>,
}
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<F>(&self, area: &Rect, f: F) -> Buffer
where F: Fn(&[Rect]) -> Vec<Buffer>
pub fn render<F>(&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)
}
}

View file

@ -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;

View file

@ -1,6 +1,6 @@
use termion;
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, Hash)]
pub enum Color {
Black,
Red,

View file

@ -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<io::Stdout>,
width: u16,
height: u16,
stdout: RawTerminal<io::Stdout>,
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<Buffer> = 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) {

7
src/util.rs Normal file
View file

@ -0,0 +1,7 @@
use std::hash::{Hash, SipHasher, Hasher};
pub fn hash<T: Hash>(t: &T) -> u64 {
let mut s = SipHasher::new();
t.hash(&mut s);
s.finish()
}

View file

@ -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
}
}

98
src/widgets/list.rs Normal file
View file

@ -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<Fn(&T, bool) -> String>,
items: Vec<T>,
}
impl<'a, T> Hash for List<'a, T>
where T: Hash
{
fn hash<H: Hasher>(&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<F>(&'a mut self, f: F) -> &mut List<'a, T>
where F: Fn(&mut Block)
{
f(&mut self.block);
self
}
pub fn formatter<F>(&'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
}
}

View file

@ -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,
})
}
}