Add gauge, fix rendering and cleanup code

pull/3/head
Florian Dehau 8 years ago
parent 93f3263e2b
commit 5b5d37ee69

@ -5,6 +5,7 @@ extern crate log4rs;
extern crate termion;
use std::thread;
use std::time;
use std::sync::mpsc;
use std::io::{Write, stdin};
@ -18,7 +19,7 @@ use log4rs::encode::pattern::PatternEncoder;
use log4rs::config::{Appender, Config, Logger, Root};
use tui::Terminal;
use tui::widgets::{Widget, Block, List, Border};
use tui::widgets::{Widget, Block, List, Gauge, border};
use tui::layout::{Group, Direction, Alignment, Size};
struct App {
@ -27,26 +28,24 @@ struct App {
items: Vec<String>,
selected: usize,
show_episodes: bool,
progress: u16,
}
enum Event {
Input(event::Key),
Tick,
}
fn main() {
let log = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new("{d} - {m}{n}")))
.encoder(Box::new(PatternEncoder::new("{l} / {d(%H:%M:%S)} / {M}:{L}{n}{m}{n}{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))
.build(Root::builder().appender("log").build(LogLevelFilter::Debug))
.unwrap();
let handle = log4rs::init_config(config).unwrap();
@ -58,21 +57,30 @@ fn main() {
items: ["1", "2", "3"].into_iter().map(|e| String::from(*e)).collect(),
selected: 0,
show_episodes: false,
progress: 0,
};
let (tx, rx) = mpsc::channel();
let input_tx = tx.clone();
thread::spawn(move || {
let tx = tx.clone();
let stdin = stdin();
for c in stdin.keys() {
let evt = c.unwrap();
tx.send(Event::Input(evt)).unwrap();
input_tx.send(Event::Input(evt)).unwrap();
if evt == event::Key::Char('q') {
break;
}
}
});
thread::spawn(move || {
let tx = tx.clone();
loop {
tx.send(Event::Tick).unwrap();
thread::sleep(time::Duration::from_millis(1000));
}
});
let mut terminal = Terminal::new().unwrap();
terminal.clear();
terminal.hide_cursor();
@ -102,6 +110,12 @@ fn main() {
_ => {}
}
}
Event::Tick => {
app.progress += 5;
if app.progress > 100 {
app.progress = 0;
}
}
}
}
terminal.show_cursor();
@ -112,12 +126,22 @@ fn draw(terminal: &mut Terminal, app: &App) {
let ui = Group::default()
.direction(Direction::Vertical)
.alignment(Alignment::Left)
.chunks(&[Size::Fixed(3), Size::Percent(100), Size::Fixed(3)])
.chunks(&[Size::Fixed(5), Size::Percent(80), Size::Fixed(10)])
.render(&terminal.area(), |chunks, tree| {
tree.add(Block::default()
.borders(Border::ALL)
.title("Header")
.render(&chunks[0]));
tree.add(Block::default().borders(border::ALL).title("Gauges").render(&chunks[0]));
tree.add(Group::default()
.direction(Direction::Vertical)
.alignment(Alignment::Left)
.margin(1)
.chunks(&[Size::Fixed(1), Size::Fixed(1), Size::Fixed(1)])
.render(&chunks[0], |chunks, tree| {
tree.add(Gauge::new()
.percent(app.progress)
.render(&chunks[0]));
tree.add(Gauge::new()
.percent(app.progress)
.render(&chunks[2]));
}));
let sizes = if app.show_episodes {
vec![Size::Percent(50), Size::Percent(50)]
} else {
@ -130,7 +154,7 @@ fn draw(terminal: &mut Terminal, app: &App) {
.render(&chunks[1], |chunks, tree| {
tree.add(List::default()
.block(|b| {
b.borders(Border::ALL).title("Podcasts");
b.borders(border::ALL).title("Podcasts");
})
.items(&app.items)
.select(app.selected)
@ -141,12 +165,12 @@ fn draw(terminal: &mut Terminal, app: &App) {
.render(&chunks[0]));
if app.show_episodes {
tree.add(Block::default()
.borders(Border::ALL)
.borders(border::ALL)
.title("Episodes")
.render(&chunks[1]));
}
}));
tree.add(Block::default().borders(Border::ALL).title("Footer").render(&chunks[2]));
tree.add(Block::default().borders(border::ALL).title("Footer").render(&chunks[2]));
});
terminal.render(ui);
}

@ -0,0 +1,22 @@
extern crate termion;
#[macro_use]
extern crate bitflags;
#[macro_use]
extern crate log;
extern crate cassowary;
mod buffer;
mod util;
pub mod symbols;
pub mod terminal;
pub mod widgets;
pub mod style;
pub mod layout;
pub use self::terminal::Terminal;
#[cfg(test)]
mod tests {
#[test]
fn it_works() {}
}

@ -94,6 +94,15 @@ impl Buffer {
self.content[i].symbol = symbol;
}
pub fn set_fg(&mut self, x: u16, y: u16, color: Color) {
let i = self.index_of(x, y);
self.content[i].fg = color;
}
pub fn set_bg(&mut self, x: u16, y: u16, color: Color) {
let i = self.index_of(x, y);
self.content[i].bg = color;
}
pub fn set_string(&mut self, x: u16, y: u16, string: &str) {
let mut cursor = (x, y);
for c in string.chars() {

@ -3,9 +3,8 @@ use std::collections::HashMap;
use cassowary::{Solver, Variable, Constraint};
use cassowary::WeightedRelation::*;
use cassowary::strength::{WEAK, MEDIUM, STRONG, REQUIRED};
use cassowary::strength::{WEAK, MEDIUM, REQUIRED};
use util::hash;
use buffer::Buffer;
use widgets::WidgetType;
@ -24,7 +23,7 @@ pub enum Direction {
Vertical,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Hash, Debug, Clone, Copy, Eq, PartialEq)]
pub struct Rect {
pub x: u16,
pub y: u16,
@ -46,10 +45,10 @@ impl Default for Rect {
impl Rect {
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
Rect {
x: 0,
y: 0,
width: 0,
height: 0,
x: x,
y: y,
width: width,
height: height,
}
}
@ -57,15 +56,15 @@ impl Rect {
self.width * self.height
}
pub fn inner(&self, spacing: u16) -> Rect {
if self.width - spacing < 0 || self.height - spacing < 0 {
pub fn inner(&self, margin: u16) -> Rect {
if self.width < 2 * margin || self.height < 2 * margin {
Rect::default()
} else {
Rect {
x: self.x + spacing,
y: self.y + spacing,
width: self.width - 2 * spacing,
height: self.height - 2 * spacing,
x: self.x + margin,
y: self.y + margin,
width: self.width - 2 * margin,
height: self.height - 2 * margin,
}
}
}
@ -114,16 +113,25 @@ pub enum Size {
/// use tui::layout::{Rect, Size, Alignment, Direction, split};
///
/// fn main() {
/// let chunks = split(&Rect{x: 2, y: 2, width: 10, height: 10}, Direction::Vertical,
/// Alignment::Left, &[Size::Fixed(5.0), Size::Percent(80.0)]);
/// let chunks = split(&Rect{x: 2, y: 2, width: 10, height: 10},
/// &Direction::Vertical,
/// &Alignment::Left,
/// 0,
/// &[Size::Fixed(5), Size::Percent(80)]);
/// }
///
/// ```
pub fn split(area: &Rect, dir: &Direction, align: &Alignment, sizes: &[Size]) -> Vec<Rect> {
pub fn split(area: &Rect,
dir: &Direction,
align: &Alignment,
margin: u16,
sizes: &[Size])
-> Vec<Rect> {
let mut solver = Solver::new();
let mut vars: HashMap<Variable, (usize, usize)> = HashMap::new();
let elements = sizes.iter().map(|e| Element::new()).collect::<Vec<Element>>();
let mut results = sizes.iter().map(|e| Rect::default()).collect::<Vec<Rect>>();
let elements = sizes.iter().map(|_| Element::new()).collect::<Vec<Element>>();
let mut results = sizes.iter().map(|_| Rect::default()).collect::<Vec<Rect>>();
let dest_area = area.inner(margin);
for (i, e) in elements.iter().enumerate() {
vars.insert(e.x, (i, 0));
vars.insert(e.y, (i, 1));
@ -131,20 +139,19 @@ pub fn split(area: &Rect, dir: &Direction, align: &Alignment, sizes: &[Size]) ->
vars.insert(e.height, (i, 3));
}
let mut constraints: Vec<Constraint> = Vec::new();
if let Some(size) = sizes.first() {
if let Some(first) = elements.first() {
constraints.push(match *dir {
Direction::Horizontal => elements[0].x | EQ(REQUIRED) | area.x as f64,
Direction::Vertical => elements[0].y | EQ(REQUIRED) | area.y as f64,
Direction::Horizontal => first.x | EQ(REQUIRED) | dest_area.x as f64,
Direction::Vertical => first.y | EQ(REQUIRED) | dest_area.y as f64,
})
}
if let Some(size) = sizes.last() {
let last = elements.last().unwrap();
if let Some(last) = elements.last() {
constraints.push(match *dir {
Direction::Horizontal => {
last.x + last.width | EQ(REQUIRED) | (area.x + area.width) as f64
last.x + last.width | EQ(REQUIRED) | (dest_area.x + dest_area.width) as f64
}
Direction::Vertical => {
last.y + last.height | EQ(REQUIRED) | (area.y + area.height) as f64
last.y + last.height | EQ(REQUIRED) | (dest_area.y + dest_area.height) as f64
}
})
}
@ -154,12 +161,13 @@ pub fn split(area: &Rect, dir: &Direction, align: &Alignment, sizes: &[Size]) ->
constraints.push(pair[0].x + pair[0].width | LE(REQUIRED) | pair[1].x);
}
for (i, size) in sizes.iter().enumerate() {
let cs = [elements[i].y | EQ(REQUIRED) | area.y as f64,
elements[i].height | EQ(REQUIRED) | area.height as f64,
let cs = [elements[i].y | EQ(REQUIRED) | dest_area.y as f64,
elements[i].height | EQ(REQUIRED) | dest_area.height as f64,
match *size {
Size::Fixed(f) => elements[i].width | EQ(REQUIRED) | f as f64,
Size::Fixed(f) => elements[i].width | EQ(MEDIUM) | f as f64,
Size::Percent(p) => {
elements[i].width | EQ(WEAK) | (area.width * p) as f64 / 100.0
elements[i].width | EQ(WEAK) |
(dest_area.width * p) as f64 / 100.0
}
}];
constraints.extend_from_slice(&cs);
@ -170,12 +178,13 @@ pub fn split(area: &Rect, dir: &Direction, align: &Alignment, sizes: &[Size]) ->
constraints.push(pair[0].y + pair[0].height | LE(REQUIRED) | pair[1].y);
}
for (i, size) in sizes.iter().enumerate() {
let cs = [elements[i].x | EQ(REQUIRED) | area.x as f64,
elements[i].width | EQ(REQUIRED) | area.width as f64,
let cs = [elements[i].x | EQ(REQUIRED) | dest_area.x as f64,
elements[i].width | EQ(REQUIRED) | dest_area.width as f64,
match *size {
Size::Fixed(f) => elements[i].height | EQ(REQUIRED) | f as f64,
Size::Percent(p) => {
elements[i].height | EQ(WEAK) | (area.height * p) as f64 / 100.0
elements[i].height | EQ(WEAK) |
(dest_area.height * p) as f64 / 100.0
}
}];
constraints.extend_from_slice(&cs);
@ -286,6 +295,7 @@ pub struct Leaf {
pub struct Group {
direction: Direction,
alignment: Alignment,
margin: u16,
chunks: Vec<Size>,
}
@ -294,6 +304,7 @@ impl Default for Group {
Group {
direction: Direction::Horizontal,
alignment: Alignment::Left,
margin: 0,
chunks: Vec::new(),
}
}
@ -310,6 +321,11 @@ impl Group {
self
}
pub fn margin(&mut self, margin: u16) -> &mut Group {
self.margin = margin;
self
}
pub fn chunks(&mut self, chunks: &[Size]) -> &mut Group {
self.chunks = Vec::from(chunks);
self
@ -317,9 +333,13 @@ impl Group {
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 chunks = split(area,
&self.direction,
&self.alignment,
self.margin,
&self.chunks);
let mut node = Node { children: Vec::new() };
let results = f(&chunks, &mut node);
f(&chunks, &mut node);
Tree::Node(node)
}
}

@ -7,6 +7,7 @@ extern crate cassowary;
mod buffer;
mod util;
pub mod symbols;
pub mod terminal;
pub mod widgets;
pub mod style;

@ -0,0 +1,10 @@
pub mod block {
pub const FULL: char = '█';
pub const SEVEN_EIGHTHS: char = '▉';
pub const THREE_QUATERS: char = '▊';
pub const FIVE_EIGHTHS: char = '▋';
pub const HALF: char = '▌';
pub const THREE_EIGHTHS: char = '▍';
pub const ONE_QUATER: char = '▎';
pub const ONE_EIGHTH: char = '▏';
}

@ -1,4 +1,3 @@
use std::iter;
use std::io;
use std::io::Write;
use std::collections::HashMap;
@ -8,13 +7,13 @@ use termion::raw::{IntoRawMode, RawTerminal};
use buffer::Buffer;
use widgets::WidgetType;
use layout::{Rect, Tree, Node, Leaf};
use layout::{Rect, Tree};
pub struct Terminal {
width: u16,
height: u16,
stdout: RawTerminal<io::Stdout>,
previous: HashMap<(WidgetType, u64), Rect>,
previous: HashMap<(WidgetType, Rect), u64>,
}
impl Terminal {
@ -39,29 +38,33 @@ impl Terminal {
}
pub fn render(&mut self, ui: Tree) {
info!("Render");
debug!("Render Pass");
let mut buffers: Vec<Buffer> = Vec::new();
let mut previous: HashMap<(WidgetType, u64), Rect> = HashMap::new();
let mut previous: HashMap<(WidgetType, Rect), u64> = 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 {
match self.previous.remove(&(node.widget_type, area)) {
Some(h) => {
if h == node.hash {
debug!("Skip {:?} at {:?}", node.widget_type, area);
} else {
debug!("Update {:?} at {:?}", node.widget_type, area);
buffers.push(node.buffer);
}
}
None => {
buffers.push(node.buffer);
debug!("Render {:?} at {:?}", node.widget_type, area);
}
}
previous.insert((node.widget_type, node.hash), area);
previous.insert((node.widget_type, area), node.hash);
}
for (_, area) in &self.previous {
buffers.insert(0, Buffer::empty(*area));
for (&(t, a), h) in &self.previous {
buffers.insert(0, Buffer::empty(a));
debug!("Erased {:?} at {:?}", t, a);
}
for buf in buffers {
self.render_buffer(&buf);
info!("{:?}", buf.area());
}
self.previous = previous;
}

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

@ -2,12 +2,12 @@
use buffer::Buffer;
use layout::Rect;
use style::Color;
use widgets::{Widget, WidgetType, Border, Line, vline, hline};
use widgets::{Widget, WidgetType, border, Line, vline, hline};
#[derive(Hash)]
#[derive(Hash, Clone, Copy)]
pub struct Block<'a> {
title: Option<&'a str>,
borders: Border::Flags,
borders: border::Flags,
border_fg: Color,
border_bg: Color,
}
@ -16,7 +16,7 @@ impl<'a> Default for Block<'a> {
fn default() -> Block<'a> {
Block {
title: None,
borders: Border::NONE,
borders: border::NONE,
border_fg: Color::White,
border_bg: Color::Black,
}
@ -29,35 +29,31 @@ impl<'a> Block<'a> {
self
}
pub fn borders(&mut self, flag: Border::Flags) -> &mut Block<'a> {
pub fn borders(&mut self, flag: border::Flags) -> &mut Block<'a> {
self.borders = flag;
self
}
}
impl<'a> Widget for Block<'a> {
fn buffer(&self, area: &Rect) -> Buffer {
fn _buffer(&self, area: &Rect) -> Buffer {
let mut buf = Buffer::empty(*area);
if area.area() == 0 {
return buf;
}
if self.borders == Border::NONE {
if self.borders == border::NONE {
return buf;
}
// Sides
if self.borders.intersects(Border::LEFT) {
if self.borders.intersects(border::LEFT) {
let line = vline(area.x, area.y, area.height, self.border_fg, self.border_bg);
buf.merge(&line);
}
if self.borders.intersects(Border::TOP) {
if self.borders.intersects(border::TOP) {
let line = hline(area.x, area.y, area.width, self.border_fg, self.border_bg);
buf.merge(&line);
}
if self.borders.intersects(Border::RIGHT) {
if self.borders.intersects(border::RIGHT) {
let line = vline(area.x + area.width - 1,
area.y,
area.height,
@ -65,7 +61,7 @@ impl<'a> Widget for Block<'a> {
self.border_bg);
buf.merge(&line);
}
if self.borders.intersects(Border::BOTTOM) {
if self.borders.intersects(border::BOTTOM) {
let line = hline(area.x,
area.y + area.height - 1,
area.width,
@ -75,16 +71,16 @@ impl<'a> Widget for Block<'a> {
}
// Corners
if self.borders.contains(Border::LEFT | Border::TOP) {
if self.borders.contains(border::LEFT | border::TOP) {
buf.set_symbol(0, 0, Line::TopLeft.get());
}
if self.borders.contains(Border::RIGHT | Border::TOP) {
if self.borders.contains(border::RIGHT | border::TOP) {
buf.set_symbol(area.width - 1, 0, Line::TopRight.get());
}
if self.borders.contains(Border::BOTTOM | Border::LEFT) {
if self.borders.contains(border::BOTTOM | border::LEFT) {
buf.set_symbol(0, area.height - 1, Line::BottomLeft.get());
}
if self.borders.contains(Border::BOTTOM | Border::RIGHT) {
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 {

@ -0,0 +1,75 @@
use widgets::{Widget, WidgetType, Block};
use buffer::Buffer;
use style::Color;
use layout::Rect;
/// Progress bar widget
///
/// # Examples:
///
/// ```
/// extern crate tui;
/// use tui::widgets::{Widget, Gauge, Block, border};
///
/// fn main() {
/// Gauge::new()
/// .block(*Block::default().borders(border::ALL).title("Progress"))
/// .percent(20);
/// }
/// ```
#[derive(Hash)]
pub struct Gauge<'a> {
block: Option<Block<'a>>,
percent: u16,
fg: Color,
bg: Color,
}
impl<'a> Gauge<'a> {
pub fn new() -> Gauge<'a> {
Gauge {
block: None,
percent: 0,
bg: Color::White,
fg: Color::Black,
}
}
pub fn block(&'a mut self, block: Block<'a>) -> &mut Gauge<'a> {
self.block = Some(block);
self
}
pub fn percent(&mut self, percent: u16) -> &mut Gauge<'a> {
self.percent = percent;
self
}
}
impl<'a> Widget for Gauge<'a> {
fn _buffer(&self, area: &Rect) -> Buffer {
let (mut buf, gauge_area) = match self.block {
Some(ref b) => (b._buffer(area), area.inner(1)),
None => (Buffer::empty(*area), *area),
};
if gauge_area.height < 1 {
return buf;
} else {
let margin = gauge_area.x - area.x;
let width = (gauge_area.width * self.percent) / 100;
for i in 0..width {
buf.set_bg(margin + i, margin, self.bg);
buf.set_fg(margin + i, margin, self.fg);
}
let percent_string = format!("{}%", self.percent);
let len = percent_string.len() as u16;
let middle = gauge_area.width / 2 - len / 2;
buf.set_string(middle, margin, &percent_string);
}
buf
}
fn widget_type(&self) -> WidgetType {
WidgetType::Gauge
}
}

@ -1,10 +1,9 @@
use std::cmp::{min, max};
use std::cmp::min;
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> {
@ -29,7 +28,7 @@ impl<'a, T> Default for List<'a, T> {
List {
block: Block::default(),
selected: 0,
formatter: Box::new(|e: &T, selected: bool| String::from("")),
formatter: Box::new(|_, _| String::from("")),
items: Vec::new(),
}
}
@ -66,7 +65,7 @@ impl<'a, T> List<'a, T>
impl<'a, T> Widget for List<'a, T>
where T: Display + Hash
{
fn buffer(&self, area: &Rect) -> Buffer {
fn _buffer(&self, area: &Rect) -> Buffer {
let mut buf = self.block.buffer(area);
if area.area() == 0 {
return buf;

@ -1,9 +1,12 @@
mod block;
mod list;
mod gauge;
pub use self::block::Block;
pub use self::list::List;
use std::hash::{Hash, SipHasher, Hasher};
pub use self::gauge::Gauge;
use std::hash::Hash;
use util::hash;
use buffer::{Buffer, Cell};
@ -23,7 +26,7 @@ enum Line {
HorizontalUp,
}
pub mod Border {
pub mod border {
bitflags! {
pub flags Flags: u32 {
const NONE = 0b00000001,
@ -53,7 +56,6 @@ impl Line {
}
}
fn hline<'a>(x: u16, y: u16, len: u16, fg: Color, bg: Color) -> Buffer {
Buffer::filled(Rect {
x: x,
@ -85,14 +87,21 @@ fn vline<'a>(x: u16, y: u16, len: u16, fg: Color, bg: Color) -> Buffer {
pub enum WidgetType {
Block,
List,
Gauge,
}
pub trait Widget: Hash {
fn buffer(&self, area: &Rect) -> Buffer;
fn _buffer(&self, area: &Rect) -> Buffer;
fn buffer(&self, area: &Rect) -> Buffer {
match area.area() {
0 => Buffer::empty(*area),
_ => self._buffer(area),
}
}
fn widget_type(&self) -> WidgetType;
fn render(&self, area: &Rect) -> Tree {
let widget_type = self.widget_type();
let hash = hash(&self);
let hash = hash(&self, area);
let buffer = self.buffer(area);
Tree::Leaf(Leaf {
widget_type: widget_type,

Loading…
Cancel
Save