summaryrefslogtreecommitdiffstats
path: root/tui-react
diff options
context:
space:
mode:
authorSebastian Thiel <sthiel@thoughtworks.com>2019-06-05 17:46:28 +0530
committerSebastian Thiel <sthiel@thoughtworks.com>2019-06-05 17:46:28 +0530
commit3aa9b0168425706b6bdfa4eb2b9335da24bc15fd (patch)
treef370b0e32ef7f852f087320e3cd76b5d8b78a6fd /tui-react
parent80ae2ac79c1525886c613452c835099eeae97c4d (diff)
Add tui-react as library - it's proven (enough)...
...to be working and worth a slot on crates.io :D
Diffstat (limited to 'tui-react')
-rw-r--r--tui-react/Cargo.toml12
-rw-r--r--tui-react/src/block.rs168
-rw-r--r--tui-react/src/lib.rs7
-rw-r--r--tui-react/src/list.rs89
-rw-r--r--tui-react/src/terminal.rs176
5 files changed, 452 insertions, 0 deletions
diff --git a/tui-react/Cargo.toml b/tui-react/Cargo.toml
new file mode 100644
index 0000000..11bfb4e
--- /dev/null
+++ b/tui-react/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "tui-react"
+version = "0.1.0"
+authors = ["Sebastian Thiel <sthiel@thoughtworks.com>"]
+edition = "2018"
+repository = "https://github.com/Byron/dua-cli"
+description = "TUI widgets using a react-like paradigm, allowing mutable component state and render properties."
+license = "MIT"
+
+[dependencies]
+tui = "0.6.0"
+log = "0.4.6"
diff --git a/tui-react/src/block.rs b/tui-react/src/block.rs
new file mode 100644
index 0000000..ee6fece
--- /dev/null
+++ b/tui-react/src/block.rs
@@ -0,0 +1,168 @@
+//! Derived from TUI-rs, license: MIT, Copyright (c) 2016 Florian Dehau
+use super::ToplevelComponent;
+use std::borrow::Borrow;
+use std::marker::PhantomData;
+use tui::{
+ buffer::Buffer, layout::Rect, style::Color, style::Style, symbols::line, widgets::Borders,
+};
+
+pub fn fill_background(area: Rect, buf: &mut Buffer, color: Color) {
+ for y in area.top()..area.bottom() {
+ for x in area.left()..area.right() {
+ buf.get_mut(x, y).set_bg(color);
+ }
+ }
+}
+
+#[derive(Clone, Copy, Default)]
+pub struct Block<'a, T>(PhantomData<&'a T>);
+
+pub struct BlockProps<'a> {
+ /// Optional title place on the upper left of the block
+ pub title: Option<&'a str>,
+ /// Title style
+ pub title_style: Style,
+ /// Visible borders
+ pub borders: Borders,
+ /// Border style
+ pub border_style: Style,
+ /// Widget style
+ pub style: Style,
+}
+
+impl<'a> Default for BlockProps<'a> {
+ fn default() -> BlockProps<'a> {
+ BlockProps {
+ title: None,
+ title_style: Default::default(),
+ borders: Borders::NONE,
+ border_style: Default::default(),
+ style: Default::default(),
+ }
+ }
+}
+
+impl<'a> BlockProps<'a> {
+ /// Compute the inner area of a block based on its border visibility rules.
+ pub fn inner(&self, area: Rect) -> Rect {
+ if area.width < 2 || area.height < 2 {
+ return Rect::default();
+ }
+ let mut inner = area;
+ if self.borders.intersects(Borders::LEFT) {
+ inner.x += 1;
+ inner.width -= 1;
+ }
+ if self.borders.intersects(Borders::TOP) || self.title.is_some() {
+ inner.y += 1;
+ inner.height -= 1;
+ }
+ if self.borders.intersects(Borders::RIGHT) {
+ inner.width -= 1;
+ }
+ if self.borders.intersects(Borders::BOTTOM) {
+ inner.height -= 1;
+ }
+ inner
+ }
+
+ pub fn render(&self, area: Rect, buf: &mut Buffer) {
+ Block::<()>::default().render(self, area, buf);
+ }
+}
+
+impl<'a, T> ToplevelComponent for Block<'a, T> {
+ type Props = BlockProps<'a>;
+
+ fn render(&mut self, props: impl Borrow<Self::Props>, area: Rect, buf: &mut Buffer) {
+ if area.width < 2 || area.height < 2 {
+ return;
+ }
+ let BlockProps {
+ title,
+ title_style,
+ borders,
+ border_style,
+ style,
+ } = props.borrow();
+
+ fill_background(area, buf, style.bg);
+
+ // Sides
+ if borders.intersects(Borders::LEFT) {
+ for y in area.top()..area.bottom() {
+ buf.get_mut(area.left(), y)
+ .set_symbol(line::VERTICAL)
+ .set_style(*border_style);
+ }
+ }
+ if borders.intersects(Borders::TOP) {
+ for x in area.left()..area.right() {
+ buf.get_mut(x, area.top())
+ .set_symbol(line::HORIZONTAL)
+ .set_style(*border_style);
+ }
+ }
+ if borders.intersects(Borders::RIGHT) {
+ let x = area.right() - 1;
+ for y in area.top()..area.bottom() {
+ buf.get_mut(x, y)
+ .set_symbol(line::VERTICAL)
+ .set_style(*border_style);
+ }
+ }
+ if borders.intersects(Borders::BOTTOM) {
+ let y = area.bottom() - 1;
+ for x in area.left()..area.right() {
+ buf.get_mut(x, y)
+ .set_symbol(line::HORIZONTAL)
+ .set_style(*border_style);
+ }
+ }
+
+ // Corners
+ if borders.contains(Borders::LEFT | Borders::TOP) {
+ buf.get_mut(area.left(), area.top())
+ .set_symbol(line::TOP_LEFT)
+ .set_style(*border_style);
+ }
+ if borders.contains(Borders::RIGHT | Borders::TOP) {
+ buf.get_mut(area.right() - 1, area.top())
+ .set_symbol(line::TOP_RIGHT)
+ .set_style(*border_style);
+ }
+ if borders.contains(Borders::LEFT | Borders::BOTTOM) {
+ buf.get_mut(area.left(), area.bottom() - 1)
+ .set_symbol(line::BOTTOM_LEFT)
+ .set_style(*border_style);
+ }
+ if borders.contains(Borders::RIGHT | Borders::BOTTOM) {
+ buf.get_mut(area.right() - 1, area.bottom() - 1)
+ .set_symbol(line::BOTTOM_RIGHT)
+ .set_style(*border_style);
+ }
+
+ if area.width > 2 {
+ if let Some(title) = title {
+ let lx = if borders.intersects(Borders::LEFT) {
+ 1
+ } else {
+ 0
+ };
+ let rx = if borders.intersects(Borders::RIGHT) {
+ 1
+ } else {
+ 0
+ };
+ let width = area.width - lx - rx;
+ buf.set_stringn(
+ area.left() + lx,
+ area.top(),
+ title,
+ width as usize,
+ *title_style,
+ );
+ }
+ }
+ }
+}
diff --git a/tui-react/src/lib.rs b/tui-react/src/lib.rs
new file mode 100644
index 0000000..e8af82b
--- /dev/null
+++ b/tui-react/src/lib.rs
@@ -0,0 +1,7 @@
+mod block;
+mod list;
+mod terminal;
+
+pub use block::*;
+pub use list::*;
+pub use terminal::*;
diff --git a/tui-react/src/list.rs b/tui-react/src/list.rs
new file mode 100644
index 0000000..5975a85
--- /dev/null
+++ b/tui-react/src/list.rs
@@ -0,0 +1,89 @@
+use super::BlockProps;
+use std::borrow::Borrow;
+use std::iter::repeat;
+use tui::{
+ buffer::Buffer,
+ layout::Rect,
+ widgets::{Paragraph, Text, Widget},
+};
+
+pub fn fill_background_to_right(mut s: String, entire_width: u16) -> String {
+ match (s.len(), entire_width as usize) {
+ (x, y) if x >= y => s,
+ (x, y) => {
+ s.extend(repeat(' ').take(y - x));
+ s
+ }
+ }
+}
+
+#[derive(Default)] // TODO: remove Clone derive
+pub struct ReactList {
+ /// The index at which the list last started. Used for scrolling
+ offset: usize,
+}
+
+impl ReactList {
+ fn list_offset_for(&self, entry_in_view: Option<usize>, height: usize) -> usize {
+ match entry_in_view {
+ Some(pos) => match height as usize {
+ h if self.offset + h - 1 < pos => pos - h + 1,
+ _ if self.offset > pos => pos,
+ _ => self.offset,
+ },
+ None => 0,
+ }
+ }
+}
+
+#[derive(Default)]
+pub struct ReactListProps<'b> {
+ pub block: Option<BlockProps<'b>>,
+ pub entry_in_view: Option<usize>,
+}
+
+impl ReactList {
+ pub fn render<'a, 't>(
+ &mut self,
+ props: impl Borrow<ReactListProps<'a>>,
+ items: impl IntoIterator<Item = Vec<Text<'t>>>,
+ area: Rect,
+ buf: &mut Buffer,
+ ) {
+ let ReactListProps {
+ block,
+ entry_in_view,
+ } = props.borrow();
+
+ let list_area = match block {
+ Some(b) => {
+ b.render(area, buf);
+ b.inner(area)
+ }
+ None => area,
+ };
+ self.offset = self.list_offset_for(*entry_in_view, list_area.height as usize);
+
+ if list_area.width < 1 || list_area.height < 1 {
+ return;
+ }
+
+ for (i, text_iterator) in items
+ .into_iter()
+ .skip(self.offset)
+ .enumerate()
+ .take(list_area.height as usize)
+ {
+ let (x, y) = (list_area.left(), list_area.top() + i as u16);
+ Paragraph::new(text_iterator.iter()).draw(
+ Rect {
+ x,
+ y,
+ width: list_area.width,
+ height: 1,
+ },
+ buf,
+ );
+ }
+ }
+}
diff --git a/tui-react/src/terminal.rs b/tui-react/src/terminal.rs
new file mode 100644
index 0000000..bc7c37b
--- /dev/null
+++ b/tui-react/src/terminal.rs
@@ -0,0 +1,176 @@
+//! Derived from TUI-rs, license: MIT, Copyright (c) 2016 Florian Dehau
+use log::error;
+use std::{borrow::Borrow, io};
+
+use tui::{backend::Backend, buffer::Buffer, layout::Rect};
+
+/// A component meant to be rendered by `Terminal::render(...)`.
+/// All other components don't have to implement this trait, and instead
+/// provide a render method by convention, tuned towards their needs using whichever
+/// generic types or lifetimes they need.
+pub trait ToplevelComponent {
+ type Props;
+
+ fn render(&mut self, props: impl Borrow<Self::Props>, area: Rect, buf: &mut Buffer);
+}
+
+#[derive(Debug)]
+pub struct Terminal<B>
+where
+ B: Backend,
+{
+ pub backend: B,
+ buffers: [Buffer; 2],
+ current: usize,
+ hidden_cursor: bool,
+ known_size: Rect,
+}
+
+impl<B> Drop for Terminal<B>
+where
+ B: Backend,
+{
+ fn drop(&mut self) {
+ // Attempt to restore the cursor state
+ if self.hidden_cursor {
+ if let Err(err) = self.show_cursor() {
+ error!("Failed to show the cursor: {}", err);
+ }
+ }
+ }
+}
+
+impl<B> Terminal<B>
+where
+ B: Backend,
+{
+ pub fn new(backend: B) -> io::Result<Terminal<B>> {
+ let size = backend.size()?;
+ Ok(Terminal {
+ backend,
+ buffers: [Buffer::empty(size), Buffer::empty(size)],
+ current: 0,
+ hidden_cursor: false,
+ known_size: size,
+ })
+ }
+
+ pub fn current_buffer_mut(&mut self) -> &mut Buffer {
+ &mut self.buffers[self.current]
+ }
+
+ pub fn reconcile_and_flush(&mut self) -> io::Result<()> {
+ let previous_buffer = &self.buffers[1 - self.current];
+ let current_buffer = &self.buffers[self.current];
+ let updates = previous_buffer.diff(current_buffer);
+ self.backend.draw(updates.into_iter())
+ }
+
+ pub fn resize(&mut self, area: Rect) -> io::Result<()> {
+ self.buffers[self.current].resize(area);
+ self.buffers[1 - self.current].reset();
+ self.buffers[1 - self.current].resize(area);
+ self.known_size = area;
+ self.backend.clear()
+ }
+
+ pub fn autoresize(&mut self) -> io::Result<()> {
+ let size = self.size()?;
+ if self.known_size != size {
+ self.resize(size)?;
+ }
+ Ok(())
+ }
+
+ pub fn render<C>(&mut self, component: &mut C, props: impl Borrow<C::Props>) -> io::Result<()>
+ where
+ C: ToplevelComponent,
+ {
+ // Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
+ // and the terminal (if growing), which may OOB.
+ self.autoresize()?;
+
+ component.render(props, self.known_size, self.current_buffer_mut());
+
+ self.reconcile_and_flush()?;
+
+ self.buffers[1 - self.current].reset();
+ self.current = 1 - self.current;
+
+ self.backend.flush()?;
+ Ok(())
+ }
+
+ pub fn hide_cursor(&mut self) -> io::Result<()> {
+ self.backend.hide_cursor()?;
+ self.hidden_cursor = true;
+ Ok(())
+ }
+ pub fn show_cursor(&mut self) -> io::Result<()> {
+ self.backend.show_cursor()?;
+ self.hidden_cursor = false;
+ Ok(())
+ }
+ #[allow(unused)]
+ pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
+ self.backend.get_cursor()
+ }
+ #[allow(unused)]
+ pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
+ self.backend.set_cursor(x, y)
+ }
+ #[allow(unused)]
+ pub fn clear(&mut self) -> io::Result<()> {
+ self.backend.clear()
+ }
+ pub fn size(&self) -> io::Result<Rect> {
+ self.backend.size()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use tui::backend::TestBackend;
+
+ #[derive(Default, Clone)]
+ struct ComplexProps {
+ x: usize,
+ y: String,
+ }
+
+ #[derive(Default)]
+ struct StatefulComponent {
+ x: usize,
+ }
+
+ #[derive(Default)]
+ struct StatelessComponent;
+
+ impl ToplevelComponent for StatefulComponent {
+ type Props = usize;
+
+ fn render(&mut self, props: impl Borrow<Self::Props>, _area: Rect, _buf: &mut Buffer) {
+ self.x += *props.borrow();
+ }
+ }
+
+ impl ToplevelComponent for StatelessComponent {
+ type Props = ComplexProps;
+ fn render(&mut self, _props: impl Borrow<Self::Props>, _area: Rect, _buf: &mut Buffer) {
+ // does not matter - we want to see it compiles essentially
+ }
+ }
+
+ #[test]
+ fn it_does_render_with_simple_and_complex_props() {
+ let mut term = Terminal::new(TestBackend::new(20, 20)).unwrap();
+ let mut c = StatefulComponent::default();
+
+ term.render(&mut c, 3usize).ok();
+ assert_eq!(c.x, 3);
+
+ let mut c = StatelessComponent::default();
+ term.render(&mut c, ComplexProps::default()).ok();
+ }
+}