mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-10 07:04:17 +00:00
Add list widget and improve rendering
This commit is contained in:
parent
459201bc65
commit
13f6a5a98b
11 changed files with 337 additions and 63 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
|||
target
|
||||
Cargo.lock
|
||||
*.log
|
||||
|
|
|
@ -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 = "*"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use termion;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, Hash)]
|
||||
pub enum Color {
|
||||
Black,
|
||||
Red,
|
||||
|
|
|
@ -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
7
src/util.rs
Normal 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()
|
||||
}
|
|
@ -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
98
src/widgets/list.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue