mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-24 21:53:21 +00:00
First commit
This commit is contained in:
commit
459201bc65
11 changed files with 843 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
target
|
||||
Cargo.lock
|
9
Cargo.toml
Normal file
9
Cargo.toml
Normal file
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "tui"
|
||||
version = "0.1.0"
|
||||
authors = ["Florian Dehau <florian.dehau@telecomnancy.net>"]
|
||||
|
||||
[dependencies]
|
||||
termion = "1.1.1"
|
||||
bitflags = "0.7"
|
||||
cassowary = "0.2.0"
|
6
Makefile
Normal file
6
Makefile
Normal file
|
@ -0,0 +1,6 @@
|
|||
build:
|
||||
cargo build
|
||||
test:
|
||||
cargo test
|
||||
watch:
|
||||
watchman-make -p 'src/**/*.rs' -t build -p 'test/**/*.rs' -t test
|
94
examples/prototype.rs
Normal file
94
examples/prototype.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
extern crate tui;
|
||||
extern crate termion;
|
||||
|
||||
use std::thread;
|
||||
use std::sync::mpsc;
|
||||
use std::io::{Write, stdin};
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::Terminal;
|
||||
use tui::widgets::{Widget, Block, Border};
|
||||
use tui::layout::{Group, Direction, Alignment, Size};
|
||||
|
||||
struct App {
|
||||
name: String,
|
||||
fetching: bool,
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Quit,
|
||||
Redraw,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
let mut app = App {
|
||||
name: String::from("Test app"),
|
||||
fetching: false,
|
||||
};
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
thread::spawn(move || {
|
||||
let tx = tx.clone();
|
||||
let stdin = stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
match evt {
|
||||
event::Key::Char('q') => {
|
||||
tx.send(Event::Quit).unwrap();
|
||||
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 => {
|
||||
break;
|
||||
}
|
||||
Event::Redraw => {}
|
||||
}
|
||||
}
|
||||
terminal.show_cursor();
|
||||
}
|
||||
|
||||
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)
|
||||
.title("Header")
|
||||
.render(&chunks[0]),
|
||||
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()
|
||||
.borders(Border::ALL)
|
||||
.title("Episodes")
|
||||
.render(&chunks[1])]
|
||||
}),
|
||||
Block::default().borders(Border::ALL).title("Footer").render(&chunks[2])]
|
||||
});
|
||||
terminal.render(&ui);
|
||||
}
|
150
src/buffer.rs
Normal file
150
src/buffer.rs
Normal file
|
@ -0,0 +1,150 @@
|
|||
use layout::Rect;
|
||||
use style::Color;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Cell {
|
||||
pub symbol: char,
|
||||
pub fg: Color,
|
||||
pub bg: Color,
|
||||
}
|
||||
|
||||
impl Default for Cell {
|
||||
fn default() -> Cell {
|
||||
Cell {
|
||||
symbol: ' ',
|
||||
fg: Color::White,
|
||||
bg: Color::Black,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Buffer {
|
||||
area: Rect,
|
||||
content: Vec<Cell>,
|
||||
}
|
||||
|
||||
impl Default for Buffer {
|
||||
fn default() -> Buffer {
|
||||
Buffer {
|
||||
area: Default::default(),
|
||||
content: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
pub fn empty(area: Rect) -> Buffer {
|
||||
let cell: Cell = Default::default();
|
||||
Buffer::filled(area, cell)
|
||||
}
|
||||
|
||||
pub fn filled(area: Rect, cell: Cell) -> Buffer {
|
||||
let size = area.area() as usize;
|
||||
let mut content = Vec::with_capacity(size);
|
||||
for _ in 0..size {
|
||||
content.push(cell.clone());
|
||||
}
|
||||
Buffer {
|
||||
area: area,
|
||||
content: content,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn content(&self) -> &[Cell] {
|
||||
&self.content
|
||||
}
|
||||
|
||||
pub fn area(&self) -> &Rect {
|
||||
&self.area
|
||||
}
|
||||
|
||||
pub fn index_of(&self, x: u16, y: u16) -> usize {
|
||||
let index = (y * self.area.width + x) as usize;
|
||||
debug_assert!(index < self.content.len());
|
||||
index
|
||||
}
|
||||
|
||||
pub fn pos_of(&self, i: usize) -> (u16, u16) {
|
||||
debug_assert!(self.area.width != 0);
|
||||
(i as u16 % self.area.width, i as u16 / self.area.width)
|
||||
}
|
||||
|
||||
pub fn next_pos(&self, x: u16, y: u16) -> Option<(u16, u16)> {
|
||||
let mut nx = x + 1;
|
||||
let mut ny = y;
|
||||
if nx >= self.area.width {
|
||||
nx = 0;
|
||||
ny = y + 1;
|
||||
}
|
||||
if ny >= self.area.height {
|
||||
None
|
||||
} else {
|
||||
Some((nx, ny))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set(&mut self, x: u16, y: u16, cell: Cell) {
|
||||
let i = self.index_of(x, y);
|
||||
self.content[i] = cell;
|
||||
}
|
||||
|
||||
pub fn set_symbol(&mut self, x: u16, y: u16, symbol: char) {
|
||||
let i = self.index_of(x, y);
|
||||
self.content[i].symbol = symbol;
|
||||
}
|
||||
|
||||
pub fn set_string(&mut self, x: u16, y: u16, string: &str) {
|
||||
let mut cursor = (x, y);
|
||||
for c in string.chars() {
|
||||
let index = self.index_of(cursor.0, cursor.1);
|
||||
self.content[index].symbol = c;
|
||||
match self.next_pos(cursor.0, cursor.1) {
|
||||
Some(c) => {
|
||||
cursor = c;
|
||||
}
|
||||
None => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, x: u16, y: u16) -> &Cell {
|
||||
let i = self.index_of(x, y);
|
||||
&self.content[i]
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, other: &Buffer) {
|
||||
let area = self.area.union(&other.area);
|
||||
let cell: Cell = Default::default();
|
||||
self.content.resize(area.area() as usize, cell.clone());
|
||||
|
||||
// Move original content to the appropriate space
|
||||
let offset_x = self.area.x - area.x;
|
||||
let offset_y = self.area.y - area.y;
|
||||
let size = self.area.area() as usize;
|
||||
for i in (0..size).rev() {
|
||||
let (x, y) = self.pos_of(i);
|
||||
// New index in content
|
||||
let k = ((y + offset_y) * area.width + (x + offset_x)) as usize;
|
||||
self.content[k] = self.content[i].clone();
|
||||
if i != k {
|
||||
self.content[i] = cell.clone();
|
||||
}
|
||||
}
|
||||
|
||||
// Push content of the other buffer into this one (may erase previous
|
||||
// data)
|
||||
let offset_x = other.area.x - area.x;
|
||||
let offset_y = other.area.y - area.y;
|
||||
let size = other.area.area() as usize;
|
||||
for i in 0..size {
|
||||
let (x, y) = other.pos_of(i);
|
||||
// New index in content
|
||||
let k = ((y + offset_y) * area.width + (x + offset_x)) as usize;
|
||||
self.content[k] = other.content[i].clone();
|
||||
}
|
||||
self.area = area;
|
||||
}
|
||||
}
|
263
src/layout.rs
Normal file
263
src/layout.rs
Normal file
|
@ -0,0 +1,263 @@
|
|||
use std::cmp::{min, max};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use cassowary::{Solver, Variable, Constraint};
|
||||
use cassowary::WeightedRelation::*;
|
||||
use cassowary::strength::{WEAK, MEDIUM, STRONG, REQUIRED};
|
||||
|
||||
use buffer::Buffer;
|
||||
|
||||
pub enum Alignment {
|
||||
Top,
|
||||
Left,
|
||||
Center,
|
||||
Bottom,
|
||||
Right,
|
||||
}
|
||||
|
||||
pub enum Direction {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Rect {
|
||||
pub x: u16,
|
||||
pub y: u16,
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
impl Default for Rect {
|
||||
fn default() -> Rect {
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn area(&self) -> u16 {
|
||||
self.width * self.height
|
||||
}
|
||||
|
||||
pub fn inner(&self, spacing: u16) -> Rect {
|
||||
if self.width - spacing < 0 || self.height - spacing < 0 {
|
||||
Rect::default()
|
||||
} else {
|
||||
Rect {
|
||||
x: self.x + spacing,
|
||||
y: self.y + spacing,
|
||||
width: self.width - 2 * spacing,
|
||||
height: self.height - 2 * spacing,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn union(&self, other: &Rect) -> Rect {
|
||||
let x1 = min(self.x, other.x);
|
||||
let y1 = min(self.y, other.y);
|
||||
let x2 = max(self.x + self.width, other.x + other.width);
|
||||
let y2 = max(self.y + self.height, other.y + other.height);
|
||||
Rect {
|
||||
x: x1,
|
||||
y: y1,
|
||||
width: x2 - x1,
|
||||
height: y2 - y1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intersection(&self, other: &Rect) -> Rect {
|
||||
let x1 = max(self.x, other.x);
|
||||
let y1 = max(self.y, other.y);
|
||||
let x2 = min(self.x + self.width, other.x + other.width);
|
||||
let y2 = min(self.y + self.height, other.y + other.height);
|
||||
Rect {
|
||||
x: x1,
|
||||
y: y1,
|
||||
width: x2 - x1,
|
||||
height: y2 - y1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intersects(&self, other: &Rect) -> bool {
|
||||
self.x < other.x + other.width && self.x + self.width > other.x &&
|
||||
self.y < other.y + other.height && self.y + self.height > other.y
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Size {
|
||||
Fixed(f64),
|
||||
Percent(f64),
|
||||
}
|
||||
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// extern crate tui;
|
||||
/// 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)]);
|
||||
/// }
|
||||
///
|
||||
/// ```
|
||||
pub fn split(area: &Rect, dir: &Direction, align: &Alignment, 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>>();
|
||||
for (i, e) in elements.iter().enumerate() {
|
||||
vars.insert(e.x, (i, 0));
|
||||
vars.insert(e.y, (i, 1));
|
||||
vars.insert(e.width, (i, 2));
|
||||
vars.insert(e.height, (i, 3));
|
||||
}
|
||||
let mut constraints: Vec<Constraint> = Vec::new();
|
||||
if let Some(size) = sizes.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,
|
||||
})
|
||||
}
|
||||
if let Some(size) = sizes.last() {
|
||||
let last = elements.last().unwrap();
|
||||
constraints.push(match *dir {
|
||||
Direction::Horizontal => {
|
||||
last.x + last.width | EQ(REQUIRED) | (area.x + area.width) as f64
|
||||
}
|
||||
Direction::Vertical => {
|
||||
last.y + last.height | EQ(REQUIRED) | (area.y + area.height) as f64
|
||||
}
|
||||
})
|
||||
}
|
||||
match *dir {
|
||||
Direction::Horizontal => {
|
||||
for pair in elements.windows(2) {
|
||||
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,
|
||||
match *size {
|
||||
Size::Fixed(f) => elements[i].width | EQ(REQUIRED) | f,
|
||||
Size::Percent(p) => {
|
||||
elements[i].width | EQ(WEAK) | area.width as f64 * p / 100.0
|
||||
}
|
||||
}];
|
||||
constraints.extend_from_slice(&cs);
|
||||
}
|
||||
}
|
||||
Direction::Vertical => {
|
||||
for pair in elements.windows(2) {
|
||||
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,
|
||||
match *size {
|
||||
Size::Fixed(f) => elements[i].height | EQ(REQUIRED) | f,
|
||||
Size::Percent(p) => {
|
||||
elements[i].height | EQ(WEAK) | area.height as f64 * p / 100.0
|
||||
}
|
||||
}];
|
||||
constraints.extend_from_slice(&cs);
|
||||
}
|
||||
}
|
||||
}
|
||||
solver.add_constraints(&constraints).unwrap();
|
||||
for &(var, value) in solver.fetch_changes() {
|
||||
let (index, attr) = vars[&var];
|
||||
match attr {
|
||||
0 => {
|
||||
results[index].x = value as u16;
|
||||
}
|
||||
1 => {
|
||||
results[index].y = value as u16;
|
||||
}
|
||||
2 => {
|
||||
results[index].width = value as u16;
|
||||
}
|
||||
3 => {
|
||||
results[index].height = value as u16;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
struct Element {
|
||||
x: Variable,
|
||||
y: Variable,
|
||||
width: Variable,
|
||||
height: Variable,
|
||||
}
|
||||
|
||||
impl Element {
|
||||
fn new() -> Element {
|
||||
Element {
|
||||
x: Variable::new(),
|
||||
y: Variable::new(),
|
||||
width: Variable::new(),
|
||||
height: Variable::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Group {
|
||||
direction: Direction,
|
||||
alignment: Alignment,
|
||||
chunks: Vec<Size>,
|
||||
}
|
||||
|
||||
impl Default for Group {
|
||||
fn default() -> Group {
|
||||
Group {
|
||||
direction: Direction::Horizontal,
|
||||
alignment: Alignment::Left,
|
||||
chunks: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Group {
|
||||
pub fn direction(&mut self, direction: Direction) -> &mut Group {
|
||||
self.direction = direction;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn alignment(&mut self, alignment: Alignment) -> &mut Group {
|
||||
self.alignment = alignment;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn chunks(&mut self, chunks: &[Size]) -> &mut Group {
|
||||
self.chunks = Vec::from(chunks);
|
||||
self
|
||||
}
|
||||
pub fn render<F>(&self, area: &Rect, f: F) -> Buffer
|
||||
where F: Fn(&[Rect]) -> Vec<Buffer>
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
18
src/lib.rs
Normal file
18
src/lib.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
extern crate termion;
|
||||
#[macro_use]
|
||||
extern crate bitflags;
|
||||
extern crate cassowary;
|
||||
|
||||
mod buffer;
|
||||
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() {}
|
||||
}
|
61
src/style.rs
Normal file
61
src/style.rs
Normal file
|
@ -0,0 +1,61 @@
|
|||
use termion;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Color {
|
||||
Black,
|
||||
Red,
|
||||
Green,
|
||||
Yellow,
|
||||
Magenta,
|
||||
Cyan,
|
||||
Gray,
|
||||
DarkGray,
|
||||
LightRed,
|
||||
LightGreen,
|
||||
LightYellow,
|
||||
LightMagenta,
|
||||
LightCyan,
|
||||
White,
|
||||
Rgb(u8, u8, u8),
|
||||
}
|
||||
|
||||
impl Color {
|
||||
pub fn fg(&self) -> String {
|
||||
match *self {
|
||||
Color::Black => format!("{}", termion::color::Fg(termion::color::Black)),
|
||||
Color::Red => format!("{}", termion::color::Fg(termion::color::Red)),
|
||||
Color::Green => format!("{}", termion::color::Fg(termion::color::Green)),
|
||||
Color::Yellow => format!("{}", termion::color::Fg(termion::color::Yellow)),
|
||||
Color::Magenta => format!("{}", termion::color::Fg(termion::color::Magenta)),
|
||||
Color::Cyan => format!("{}", termion::color::Fg(termion::color::Cyan)),
|
||||
Color::Gray => format!("{}", termion::color::Fg(termion::color::Rgb(146, 131, 116))),
|
||||
Color::DarkGray => format!("{}", termion::color::Fg(termion::color::Rgb(80, 73, 69))),
|
||||
Color::LightRed => format!("{}", termion::color::Fg(termion::color::LightRed)),
|
||||
Color::LightGreen => format!("{}", termion::color::Fg(termion::color::LightGreen)),
|
||||
Color::LightYellow => format!("{}", termion::color::Fg(termion::color::LightYellow)),
|
||||
Color::LightMagenta => format!("{}", termion::color::Fg(termion::color::LightMagenta)),
|
||||
Color::LightCyan => format!("{}", termion::color::Fg(termion::color::LightCyan)),
|
||||
Color::White => format!("{}", termion::color::Fg(termion::color::White)),
|
||||
Color::Rgb(r, g, b) => format!("{}", termion::color::Fg(termion::color::Rgb(r, g, b))),
|
||||
}
|
||||
}
|
||||
pub fn bg(&self) -> String {
|
||||
match *self {
|
||||
Color::Black => format!("{}", termion::color::Bg(termion::color::Black)),
|
||||
Color::Red => format!("{}", termion::color::Bg(termion::color::Red)),
|
||||
Color::Green => format!("{}", termion::color::Bg(termion::color::Green)),
|
||||
Color::Yellow => format!("{}", termion::color::Bg(termion::color::Yellow)),
|
||||
Color::Magenta => format!("{}", termion::color::Bg(termion::color::Magenta)),
|
||||
Color::Cyan => format!("{}", termion::color::Bg(termion::color::Cyan)),
|
||||
Color::Gray => format!("{}", termion::color::Bg(termion::color::Rgb(146, 131, 116))),
|
||||
Color::DarkGray => format!("{}", termion::color::Bg(termion::color::Rgb(80, 73, 69))),
|
||||
Color::LightRed => format!("{}", termion::color::Bg(termion::color::LightRed)),
|
||||
Color::LightGreen => format!("{}", termion::color::Bg(termion::color::LightGreen)),
|
||||
Color::LightYellow => format!("{}", termion::color::Bg(termion::color::LightYellow)),
|
||||
Color::LightMagenta => format!("{}", termion::color::Bg(termion::color::LightMagenta)),
|
||||
Color::LightCyan => format!("{}", termion::color::Bg(termion::color::LightCyan)),
|
||||
Color::White => format!("{}", termion::color::Bg(termion::color::White)),
|
||||
Color::Rgb(r, g, b) => format!("{}", termion::color::Bg(termion::color::Rgb(r, g, b))),
|
||||
}
|
||||
}
|
||||
}
|
64
src/terminal.rs
Normal file
64
src/terminal.rs
Normal file
|
@ -0,0 +1,64 @@
|
|||
use std::iter;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
|
||||
use termion;
|
||||
use termion::raw::{IntoRawMode, RawTerminal};
|
||||
|
||||
use buffer::Buffer;
|
||||
use layout::Rect;
|
||||
|
||||
pub struct Terminal {
|
||||
stdout: RawTerminal<io::Stdout>,
|
||||
width: u16,
|
||||
height: u16,
|
||||
}
|
||||
|
||||
impl Terminal {
|
||||
pub fn new() -> Result<Terminal, io::Error> {
|
||||
let terminal = try!(termion::terminal_size());
|
||||
let stdout = try!(io::stdout().into_raw_mode());
|
||||
Ok(Terminal {
|
||||
stdout: stdout,
|
||||
width: terminal.0,
|
||||
height: terminal.1,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn area(&self) -> Rect {
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&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);
|
||||
write!(self.stdout,
|
||||
"{}{}{}{}",
|
||||
termion::cursor::Goto(x + 1, y + 1),
|
||||
cell.fg.fg(),
|
||||
cell.bg.bg(),
|
||||
cell.symbol)
|
||||
.unwrap();
|
||||
}
|
||||
self.stdout.flush().unwrap();
|
||||
}
|
||||
pub fn clear(&mut self) {
|
||||
write!(self.stdout, "{}", termion::clear::All).unwrap();
|
||||
self.stdout.flush().unwrap();
|
||||
}
|
||||
pub fn hide_cursor(&mut self) {
|
||||
write!(self.stdout, "{}", termion::cursor::Hide).unwrap();
|
||||
self.stdout.flush().unwrap();
|
||||
}
|
||||
|
||||
pub fn show_cursor(&mut self) {
|
||||
write!(self.stdout, "{}", termion::cursor::Show).unwrap();
|
||||
self.stdout.flush().unwrap();
|
||||
}
|
||||
}
|
94
src/widgets/block.rs
Normal file
94
src/widgets/block.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
|
||||
use buffer::Buffer;
|
||||
use layout::Rect;
|
||||
use style::Color;
|
||||
use widgets::{Widget, Border, Line, vline, hline};
|
||||
|
||||
pub struct Block<'a> {
|
||||
title: Option<&'a str>,
|
||||
borders: Border::Flags,
|
||||
border_fg: Color,
|
||||
border_bg: Color,
|
||||
}
|
||||
|
||||
impl<'a> Default for Block<'a> {
|
||||
fn default() -> Block<'a> {
|
||||
Block {
|
||||
title: None,
|
||||
borders: Border::NONE,
|
||||
border_fg: Color::White,
|
||||
border_bg: Color::Black,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Block<'a> {
|
||||
pub fn title(&mut self, title: &'a str) -> &mut Block<'a> {
|
||||
self.title = Some(title);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn borders(&mut self, flag: Border::Flags) -> &mut Block<'a> {
|
||||
self.borders = flag;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Block<'a> {
|
||||
fn render(&self, area: &Rect) -> Buffer {
|
||||
|
||||
let mut buf = Buffer::empty(*area);
|
||||
|
||||
if area.area() == 0 {
|
||||
return buf;
|
||||
}
|
||||
|
||||
if self.borders == Border::NONE {
|
||||
return buf;
|
||||
}
|
||||
|
||||
// Sides
|
||||
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) {
|
||||
let line = hline(area.x, area.y, area.width, self.border_fg, self.border_bg);
|
||||
buf.merge(&line);
|
||||
}
|
||||
if self.borders.intersects(Border::RIGHT) {
|
||||
let line = vline(area.x + area.width - 1,
|
||||
area.y,
|
||||
area.height,
|
||||
self.border_fg,
|
||||
self.border_bg);
|
||||
buf.merge(&line);
|
||||
}
|
||||
if self.borders.intersects(Border::BOTTOM) {
|
||||
let line = hline(area.x,
|
||||
area.y + area.height - 1,
|
||||
area.width,
|
||||
self.border_fg,
|
||||
self.border_bg);
|
||||
buf.merge(&line);
|
||||
}
|
||||
|
||||
// Corners
|
||||
if self.borders.contains(Border::LEFT | Border::TOP) {
|
||||
buf.set_symbol(0, 0, Line::TopLeft.get());
|
||||
}
|
||||
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) {
|
||||
buf.set_symbol(0, area.height - 1, Line::BottomLeft.get());
|
||||
}
|
||||
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 {
|
||||
buf.set_string(1, 0, &format!(" {} ", title));
|
||||
}
|
||||
buf
|
||||
}
|
||||
}
|
82
src/widgets/mod.rs
Normal file
82
src/widgets/mod.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
mod block;
|
||||
|
||||
pub use self::block::Block;
|
||||
|
||||
use buffer::{Buffer, Cell};
|
||||
use layout::Rect;
|
||||
use style::Color;
|
||||
|
||||
enum Line {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
TopLeft,
|
||||
TopRight,
|
||||
BottomLeft,
|
||||
BottomRight,
|
||||
VerticalLeft,
|
||||
VerticalRight,
|
||||
HorizontalDown,
|
||||
HorizontalUp,
|
||||
}
|
||||
|
||||
pub mod Border {
|
||||
bitflags! {
|
||||
pub flags Flags: u32 {
|
||||
const NONE = 0b00000001,
|
||||
const TOP = 0b00000010,
|
||||
const RIGHT = 0b00000100,
|
||||
const BOTTOM = 0b0001000,
|
||||
const LEFT = 0b00010000,
|
||||
const ALL = TOP.bits | RIGHT.bits | BOTTOM.bits | LEFT.bits,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Line {
|
||||
fn get<'a>(&self) -> char {
|
||||
match *self {
|
||||
Line::TopRight => '┐',
|
||||
Line::Vertical => '│',
|
||||
Line::Horizontal => '─',
|
||||
Line::TopLeft => '┌',
|
||||
Line::BottomRight => '┘',
|
||||
Line::BottomLeft => '└',
|
||||
Line::VerticalLeft => '┤',
|
||||
Line::VerticalRight => '├',
|
||||
Line::HorizontalDown => '┬',
|
||||
Line::HorizontalUp => '┴',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn hline<'a>(x: u16, y: u16, len: u16, fg: Color, bg: Color) -> Buffer {
|
||||
Buffer::filled(Rect {
|
||||
x: x,
|
||||
y: y,
|
||||
width: len,
|
||||
height: 1,
|
||||
},
|
||||
Cell {
|
||||
symbol: Line::Horizontal.get(),
|
||||
fg: fg,
|
||||
bg: bg,
|
||||
})
|
||||
}
|
||||
fn vline<'a>(x: u16, y: u16, len: u16, fg: Color, bg: Color) -> Buffer {
|
||||
Buffer::filled(Rect {
|
||||
x: x,
|
||||
y: y,
|
||||
width: 1,
|
||||
height: len,
|
||||
},
|
||||
Cell {
|
||||
symbol: Line::Vertical.get(),
|
||||
fg: fg,
|
||||
bg: bg,
|
||||
})
|
||||
}
|
||||
|
||||
pub trait Widget {
|
||||
fn render(&self, area: &Rect) -> Buffer;
|
||||
}
|
Loading…
Reference in a new issue