diff --git a/examples/prototype.rs b/examples/prototype.rs index 297013c..2243060 100644 --- a/examples/prototype.rs +++ b/examples/prototype.rs @@ -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, 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); } diff --git a/src/.lib.rs.rustfmt b/src/.lib.rs.rustfmt new file mode 100644 index 0000000..d7e4735 --- /dev/null +++ b/src/.lib.rs.rustfmt @@ -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() {} +} diff --git a/src/buffer.rs b/src/buffer.rs index 39ca611..d437d6b 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -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() { diff --git a/src/layout.rs b/src/layout.rs index a7bbb42..5cb48bd 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -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 { +pub fn split(area: &Rect, + dir: &Direction, + align: &Alignment, + margin: u16, + sizes: &[Size]) + -> Vec { let mut solver = Solver::new(); let mut vars: HashMap = HashMap::new(); - let elements = sizes.iter().map(|e| Element::new()).collect::>(); - let mut results = sizes.iter().map(|e| Rect::default()).collect::>(); + let elements = sizes.iter().map(|_| Element::new()).collect::>(); + let mut results = sizes.iter().map(|_| Rect::default()).collect::>(); + 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 = 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, } @@ -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(&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) } } diff --git a/src/lib.rs b/src/lib.rs index b5d897e..d7e4735 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ extern crate cassowary; mod buffer; mod util; +pub mod symbols; pub mod terminal; pub mod widgets; pub mod style; diff --git a/src/symbols.rs b/src/symbols.rs new file mode 100644 index 0000000..1dda230 --- /dev/null +++ b/src/symbols.rs @@ -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 = '▏'; +} diff --git a/src/terminal.rs b/src/terminal.rs index 8ff1124..80e9b8b 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -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, - 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 = 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; } diff --git a/src/util.rs b/src/util.rs index 69d215e..7b2c686 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,7 +1,10 @@ use std::hash::{Hash, SipHasher, Hasher}; -pub fn hash(t: &T) -> u64 { +use layout::Rect; + +pub fn hash(t: &T, area: &Rect) -> u64 { let mut s = SipHasher::new(); t.hash(&mut s); + area.hash(&mut s); s.finish() } diff --git a/src/widgets/block.rs b/src/widgets/block.rs index 2086731..ffc2f81 100644 --- a/src/widgets/block.rs +++ b/src/widgets/block.rs @@ -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 { diff --git a/src/widgets/gauge.rs b/src/widgets/gauge.rs new file mode 100644 index 0000000..512d401 --- /dev/null +++ b/src/widgets/gauge.rs @@ -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>, + 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 + } +} diff --git a/src/widgets/list.rs b/src/widgets/list.rs index 2dc8a89..391db7a 100644 --- a/src/widgets/list.rs +++ b/src/widgets/list.rs @@ -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; diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 6dff1d4..29fc616 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -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,