Add list widget and improve rendering

pull/3/head
Florian Dehau 8 years ago
parent 459201bc65
commit 13f6a5a98b

1
.gitignore vendored

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

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

@ -1,4 +1,7 @@
extern crate tui; extern crate tui;
#[macro_use]
extern crate log;
extern crate log4rs;
extern crate termion; extern crate termion;
use std::thread; use std::thread;
@ -8,25 +11,51 @@ use std::io::{Write, stdin};
use termion::event; use termion::event;
use termion::input::TermRead; 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::Terminal;
use tui::widgets::{Widget, Block, Border}; use tui::widgets::{Widget, Block, List, Border};
use tui::layout::{Group, Direction, Alignment, Size}; use tui::layout::{Group, Direction, Alignment, Size};
struct App { struct App {
name: String, name: String,
fetching: bool, fetching: bool,
items: Vec<String>,
selected: usize,
} }
enum Event { enum Event {
Quit, Input(event::Key),
Redraw,
} }
fn main() { 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 { let mut app = App {
name: String::from("Test app"), name: String::from("Test app"),
fetching: false, fetching: false,
items: ["1", "2", "3"].into_iter().map(|e| String::from(*e)).collect(),
selected: 0,
}; };
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
@ -35,29 +64,39 @@ fn main() {
let stdin = stdin(); let stdin = stdin();
for c in stdin.keys() { for c in stdin.keys() {
let evt = c.unwrap(); let evt = c.unwrap();
match evt { tx.send(Event::Input(evt)).unwrap();
event::Key::Char('q') => { if evt == event::Key::Char('q') {
tx.send(Event::Quit).unwrap(); break;
break;
}
event::Key::Char('r') => {
tx.send(Event::Redraw).unwrap();
}
_ => {}
} }
} }
}); });
let mut terminal = Terminal::new().unwrap(); let mut terminal = Terminal::new().unwrap();
terminal.clear(); terminal.clear();
terminal.hide_cursor(); terminal.hide_cursor();
loop { loop {
draw(&mut terminal, &app); draw(&mut terminal, &app);
let evt = rx.recv().unwrap(); let evt = rx.recv().unwrap();
match evt { match evt {
Event::Quit => { Event::Input(input) => {
break; 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(); terminal.show_cursor();
@ -68,27 +107,34 @@ fn draw(terminal: &mut Terminal, app: &App) {
let ui = Group::default() let ui = Group::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.alignment(Alignment::Left) .alignment(Alignment::Left)
.chunks(&[Size::Fixed(3.0), Size::Percent(100.0), Size::Fixed(3.0)]) .chunks(&[Size::Fixed(3), Size::Percent(100), Size::Fixed(3)])
.render(&terminal.area(), |chunks| { .render(&terminal.area(), |chunks, tree| {
vec![Block::default() tree.add(Block::default()
.borders(Border::TOP | Border::BOTTOM) .borders(Border::ALL)
.title("Header") .title("Header")
.render(&chunks[0]), .render(&chunks[0]));
Group::default() tree.add(Group::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.alignment(Alignment::Left) .alignment(Alignment::Left)
.chunks(&[Size::Percent(50.0), Size::Percent(50.0)]) .chunks(&[Size::Percent(50), Size::Percent(50)])
.render(&chunks[1], |chunks| { .render(&chunks[1], |chunks, tree| {
vec![Block::default() tree.add(List::default()
.borders(Border::ALL) .block(|b| {
.title("Podcasts") b.borders(Border::ALL).title("Podcasts");
.render(&chunks[0]), })
Block::default() .items(&app.items)
.borders(Border::ALL) .select(app.selected)
.title("Episodes") .formatter(|i, s| {
.render(&chunks[1])] let prefix = if s { ">" } else { "*" };
}), format!("{} {}", prefix, i)
Block::default().borders(Border::ALL).title("Footer").render(&chunks[2])] })
.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);
} }

@ -5,8 +5,11 @@ use cassowary::{Solver, Variable, Constraint};
use cassowary::WeightedRelation::*; use cassowary::WeightedRelation::*;
use cassowary::strength::{WEAK, MEDIUM, STRONG, REQUIRED}; use cassowary::strength::{WEAK, MEDIUM, STRONG, REQUIRED};
use util::hash;
use buffer::Buffer; use buffer::Buffer;
use widgets::WidgetType;
#[derive(Hash)]
pub enum Alignment { pub enum Alignment {
Top, Top,
Left, Left,
@ -15,12 +18,13 @@ pub enum Alignment {
Right, Right,
} }
#[derive(Hash)]
pub enum Direction { pub enum Direction {
Horizontal, Horizontal,
Vertical, Vertical,
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rect { pub struct Rect {
pub x: u16, pub x: u16,
pub y: u16, pub y: u16,
@ -98,10 +102,10 @@ impl Rect {
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Hash)]
pub enum Size { pub enum Size {
Fixed(f64), Fixed(u16),
Percent(f64), Percent(u16),
} }
/// # Examples /// # 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, let cs = [elements[i].y | EQ(REQUIRED) | area.y as f64,
elements[i].height | EQ(REQUIRED) | area.height as f64, elements[i].height | EQ(REQUIRED) | area.height as f64,
match *size { 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) => { 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); 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, let cs = [elements[i].x | EQ(REQUIRED) | area.x as f64,
elements[i].width | EQ(REQUIRED) | area.width as f64, elements[i].width | EQ(REQUIRED) | area.width as f64,
match *size { 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) => { 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); 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 { pub struct Group {
direction: Direction, direction: Direction,
alignment: Alignment, alignment: Alignment,
@ -249,15 +314,12 @@ impl Group {
self.chunks = Vec::from(chunks); self.chunks = Vec::from(chunks);
self self
} }
pub fn render<F>(&self, area: &Rect, f: F) -> Buffer pub fn render<F>(&self, area: &Rect, f: F) -> Tree
where F: Fn(&[Rect]) -> Vec<Buffer> where F: Fn(&[Rect], &mut Node)
{ {
let chunks = split(area, &self.direction, &self.alignment, &self.chunks); let chunks = split(area, &self.direction, &self.alignment, &self.chunks);
let results = f(&chunks); let mut node = Node { children: Vec::new() };
let mut result = results[0].clone(); let results = f(&chunks, &mut node);
for r in results.iter().skip(1) { Tree::Node(node)
result.merge(&r);
}
result
} }
} }

@ -1,9 +1,12 @@
extern crate termion; extern crate termion;
#[macro_use] #[macro_use]
extern crate bitflags; extern crate bitflags;
#[macro_use]
extern crate log;
extern crate cassowary; extern crate cassowary;
mod buffer; mod buffer;
mod util;
pub mod terminal; pub mod terminal;
pub mod widgets; pub mod widgets;
pub mod style; pub mod style;

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

@ -1,17 +1,20 @@
use std::iter; use std::iter;
use std::io; use std::io;
use std::io::Write; use std::io::Write;
use std::collections::HashMap;
use termion; use termion;
use termion::raw::{IntoRawMode, RawTerminal}; use termion::raw::{IntoRawMode, RawTerminal};
use buffer::Buffer; use buffer::Buffer;
use layout::Rect; use widgets::WidgetType;
use layout::{Rect, Tree, Node, Leaf};
pub struct Terminal { pub struct Terminal {
stdout: RawTerminal<io::Stdout>,
width: u16, width: u16,
height: u16, height: u16,
stdout: RawTerminal<io::Stdout>,
previous: HashMap<(WidgetType, u64), Rect>,
} }
impl Terminal { impl Terminal {
@ -19,9 +22,10 @@ impl Terminal {
let terminal = try!(termion::terminal_size()); let terminal = try!(termion::terminal_size());
let stdout = try!(io::stdout().into_raw_mode()); let stdout = try!(io::stdout().into_raw_mode());
Ok(Terminal { Ok(Terminal {
stdout: stdout,
width: terminal.0, width: terminal.0,
height: terminal.1, 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() { for (i, cell) in buffer.content().iter().enumerate() {
let (lx, ly) = buffer.pos_of(i); let (lx, ly) = buffer.pos_of(i);
let (x, y) = (lx + buffer.area().x, ly + buffer.area().y); let (x, y) = (lx + buffer.area().x, ly + buffer.area().y);
@ -50,6 +78,7 @@ impl Terminal {
} }
pub fn clear(&mut self) { pub fn clear(&mut self) {
write!(self.stdout, "{}", termion::clear::All).unwrap(); write!(self.stdout, "{}", termion::clear::All).unwrap();
write!(self.stdout, "{}", termion::cursor::Goto(1, 1)).unwrap();
self.stdout.flush().unwrap(); self.stdout.flush().unwrap();
} }
pub fn hide_cursor(&mut self) { pub fn hide_cursor(&mut self) {

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

@ -2,8 +2,9 @@
use buffer::Buffer; use buffer::Buffer;
use layout::Rect; use layout::Rect;
use style::Color; use style::Color;
use widgets::{Widget, Border, Line, vline, hline}; use widgets::{Widget, WidgetType, Border, Line, vline, hline};
#[derive(Hash)]
pub struct Block<'a> { pub struct Block<'a> {
title: Option<&'a str>, title: Option<&'a str>,
borders: Border::Flags, borders: Border::Flags,
@ -35,7 +36,7 @@ impl<'a> Block<'a> {
} }
impl<'a> Widget for 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); let mut buf = Buffer::empty(*area);
@ -91,4 +92,8 @@ impl<'a> Widget for Block<'a> {
} }
buf buf
} }
fn widget_type(&self) -> WidgetType {
WidgetType::Block
}
} }

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

@ -1,9 +1,13 @@
mod block; mod block;
mod list;
pub use self::block::Block; pub use self::block::Block;
pub use self::list::List;
use std::hash::{Hash, SipHasher, Hasher};
use util::hash;
use buffer::{Buffer, Cell}; use buffer::{Buffer, Cell};
use layout::Rect; use layout::{Rect, Tree, Leaf};
use style::Color; use style::Color;
enum Line { enum Line {
@ -77,6 +81,23 @@ fn vline<'a>(x: u16, y: u16, len: u16, fg: Color, bg: Color) -> Buffer {
}) })
} }
pub trait Widget { #[derive(Debug, Hash, Eq, PartialEq, Clone, Copy)]
fn render(&self, area: &Rect) -> Buffer; 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,
})
}
} }

Loading…
Cancel
Save