Skip to content

Commit

Permalink
feat(terminal): Add after-draw() cursor control to Frame (fdehau#91) (f…
Browse files Browse the repository at this point in the history
  • Loading branch information
Minoru authored and 24seconds committed Nov 30, 2020
1 parent 36f37b6 commit 0599b8e
Show file tree
Hide file tree
Showing 30 changed files with 102 additions and 58 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@

## To be released

### Breaking Changes

* `Terminal::draw()` now requires a closure that takes `&mut Frame`.

### Features

* Add feature-gated (`serde`) serialization of `Style`.
* Add `Frame::set_cursor()` to dictate where the cursor should be placed after
the call to `Terminial::draw()`. Calling this method would also make the
cursor visible after the call to `draw()`. See example usage in
*examples/user_input.rs*.

## v0.9.5 - 2020-05-21

Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ authors = ["Florian Dehau <work@fdehau.com>"]
description = """
A library to build rich terminal user interfaces or dashboards
"""
documentation = "https://docs.rs/tui/0.9.5/tui/"
documentation = "https://docs.rs/tui/0.10.0/tui/"
keywords = ["tui", "terminal", "dashboard"]
repository = "https://github.com/fdehau/tui-rs"
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion examples/barchart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut app = App::new();

loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
Expand Down
2 changes: 1 addition & 1 deletion examples/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let events = Events::new();

loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
// Wrapping block for a group
// Just draw the block and the group on the same area and build the group
// with at least a margin of 1
Expand Down
2 changes: 1 addition & 1 deletion examples/canvas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut app = App::new();

loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
Expand Down
2 changes: 1 addition & 1 deletion examples/chart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut app = App::new();

loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
let size = f.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
Expand Down
2 changes: 1 addition & 1 deletion examples/crossterm_demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ fn main() -> Result<(), Box<dyn Error>> {
terminal.clear()?;

loop {
terminal.draw(|mut f| ui::draw(&mut f, &mut app))?;
terminal.draw(|f| ui::draw(f, &mut app))?;
match rx.recv()? {
Event::Input(event) => match event.code {
KeyCode::Char('q') => {
Expand Down
2 changes: 1 addition & 1 deletion examples/curses_demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut last_tick = Instant::now();
let tick_rate = Duration::from_millis(cli.tick_rate);
loop {
terminal.draw(|mut f| ui::draw(&mut f, &mut app))?;
terminal.draw(|f| ui::draw(f, &mut app))?;
if let Some(input) = terminal.backend_mut().get_curses_mut().get_input() {
match input {
easycurses::Input::Character(c) => {
Expand Down
2 changes: 1 addition & 1 deletion examples/custom_widget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let events = Events::new();

loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
let size = f.size();
let label = Label::default().text("Test");
f.render_widget(label, size);
Expand Down
2 changes: 1 addition & 1 deletion examples/gauge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut app = App::new();

loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
Expand Down
2 changes: 1 addition & 1 deletion examples/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let events = Events::new();

loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
Expand Down
2 changes: 1 addition & 1 deletion examples/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut app = App::new();

loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
Expand Down
2 changes: 1 addition & 1 deletion examples/paragraph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ fn main() -> Result<(), Box<dyn Error>> {

let mut scroll: u16 = 0;
loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
let size = f.size();

// Words made "loooong" to demonstrate line breaking.
Expand Down
2 changes: 1 addition & 1 deletion examples/popup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let events = Events::new();

loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
let size = f.size();

let chunks = Layout::default()
Expand Down
2 changes: 1 addition & 1 deletion examples/rustbox_demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut last_tick = Instant::now();
let tick_rate = Duration::from_millis(cli.tick_rate);
loop {
terminal.draw(|mut f| ui::draw(&mut f, &mut app))?;
terminal.draw(|f| ui::draw(f, &mut app))?;
if let Ok(rustbox::Event::KeyEvent(key)) =
terminal.backend().rustbox().peek_event(tick_rate, false)
{
Expand Down
2 changes: 1 addition & 1 deletion examples/sparkline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut app = App::new();

loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
Expand Down
2 changes: 1 addition & 1 deletion examples/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ fn main() -> Result<(), Box<dyn Error>> {

// Input
loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
let rects = Layout::default()
.constraints([Constraint::Percentage(100)].as_ref())
.margin(5)
Expand Down
2 changes: 1 addition & 1 deletion examples/tabs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ fn main() -> Result<(), Box<dyn Error>> {

// Main loop
loop {
terminal.draw(|mut f| {
terminal.draw(|f| {
let size = f.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
Expand Down
2 changes: 1 addition & 1 deletion examples/termion_demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ fn main() -> Result<(), Box<dyn Error>> {

let mut app = App::new("Termion demo", cli.enhanced_graphics);
loop {
terminal.draw(|mut f| ui::draw(&mut f, &mut app))?;
terminal.draw(|f| ui::draw(f, &mut app))?;

match events.next()? {
Event::Input(key) => match key {
Expand Down
36 changes: 19 additions & 17 deletions examples/user_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,8 @@
mod util;

use crate::util::event::{Event, Events};
use std::{
error::Error,
io::{self, Write},
};
use termion::{
cursor::Goto, event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen,
};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
layout::{Constraint, Direction, Layout},
Expand Down Expand Up @@ -71,7 +66,7 @@ fn main() -> Result<(), Box<dyn Error>> {

loop {
// Draw UI
terminal.draw(|mut f| {
terminal.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
Expand All @@ -98,6 +93,22 @@ fn main() -> Result<(), Box<dyn Error>> {
.style(Style::default().fg(Color::Yellow))
.block(Block::default().borders(Borders::ALL).title("Input"));
f.render_widget(input, chunks[1]);
match app.input_mode {
InputMode::Normal =>
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
{}

InputMode::Editing => {
// Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering
f.set_cursor(
// Put cursor past the end of the input text
chunks[1].x + app.input.width() as u16 + 1,
// Move one line down, from the border to the input line
chunks[1].y + 1,
)
}
}

let messages = app
.messages
.iter()
Expand All @@ -108,15 +119,6 @@ fn main() -> Result<(), Box<dyn Error>> {
f.render_widget(messages, chunks[2]);
})?;

// Put the cursor back inside the input box
write!(
terminal.backend_mut(),
"{}",
Goto(4 + app.input.width() as u16, 5)
)?;
// stdout is buffered, flush it to see the effect immediately when hitting backspace
io::stdout().flush().ok();

// Handle input
if let Event::Input(input) = events.next()? {
match app.input_mode {
Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
//! let stdout = io::stdout().into_raw_mode()?;
//! let backend = TermionBackend::new(stdout);
//! let mut terminal = Terminal::new(backend)?;
//! terminal.draw(|mut f| {
//! terminal.draw(|f| {
//! let size = f.size();
//! let block = Block::default()
//! .title("Block")
Expand Down Expand Up @@ -116,7 +116,7 @@
//! let stdout = io::stdout().into_raw_mode()?;
//! let backend = TermionBackend::new(stdout);
//! let mut terminal = Terminal::new(backend)?;
//! terminal.draw(|mut f| {
//! terminal.draw(|f| {
//! let chunks = Layout::default()
//! .direction(Direction::Vertical)
//! .margin(1)
Expand Down
40 changes: 36 additions & 4 deletions src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ where
B: Backend,
{
terminal: &'a mut Terminal<B>,

/// Where should the cursor be after drawing this frame?
///
/// If `None`, the cursor is hidden and its position is controlled by the backend. If `Some((x,
/// y))`, the cursor is shown and placed at `(x, y)` after the call to `Terminal::draw()`.
cursor_position: Option<(u16, u16)>,
}

impl<'a, B> Frame<'a, B>
Expand Down Expand Up @@ -95,6 +101,16 @@ where
{
widget.render(area, self.terminal.current_buffer_mut(), state);
}

/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
/// coordinates. If this method is not called, the cursor will be hidden.
///
/// Note that this will interfere with calls to `Terminal::hide_cursor()`,
/// `Terminal::show_cursor()`, and `Terminal::set_cursor()`. Pick one of the APIs and stick
/// with it.
pub fn set_cursor(&mut self, x: u16, y: u16) {
self.cursor_position = Some((x, y));
}
}

impl<B> Drop for Terminal<B>
Expand All @@ -115,7 +131,7 @@ impl<B> Terminal<B>
where
B: Backend,
{
/// Wrapper around Termion initialization. Each buffer is initialized with a blank string and
/// Wrapper around Terminal initialization. Each buffer is initialized with a blank string and
/// default colors for the foreground and the background
pub fn new(backend: B) -> io::Result<Terminal<B>> {
let size = backend.size()?;
Expand All @@ -130,7 +146,10 @@ where

/// Get a Frame object which provides a consistent view into the terminal state for rendering.
pub fn get_frame(&mut self) -> Frame<B> {
Frame { terminal: self }
Frame {
terminal: self,
cursor_position: None,
}
}

pub fn current_buffer_mut(&mut self) -> &mut Buffer {
Expand Down Expand Up @@ -178,17 +197,30 @@ where
/// and prepares for the next draw call.
pub fn draw<F>(&mut self, f: F) -> io::Result<()>
where
F: FnOnce(Frame<B>),
F: FnOnce(&mut Frame<B>),
{
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
// and the terminal (if growing), which may OOB.
self.autoresize()?;

f(self.get_frame());
let mut frame = self.get_frame();
f(&mut frame);
// We can't change the cursor position right away because we have to flush the frame to
// stdout first. But we also can't keep the frame around, since it holds a &mut to
// Terminal. Thus, we're taking the important data out of the Frame and dropping it.
let cursor_position = frame.cursor_position;

// Draw to stdout
self.flush()?;

match cursor_position {
None => self.hide_cursor()?,
Some((x, y)) => {
self.show_cursor()?;
self.set_cursor(x, y)?;
}
}

// Swap buffers
self.buffers[1 - self.current].reset();
self.current = 1 - self.current;
Expand Down
2 changes: 1 addition & 1 deletion src/widgets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ pub trait Widget {
/// ]);
///
/// loop {
/// terminal.draw(|mut f| {
/// terminal.draw(|f| {
/// // The items managed by the application are transformed to something
/// // that is understood by tui.
/// let items = events.items.iter().map(Text::raw);
Expand Down
2 changes: 1 addition & 1 deletion tests/widgets_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ fn widgets_block_renders() {
let backend = TestBackend::new(10, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
.draw(|f| {
let block = Block::default()
.title("Title")
.borders(Borders::ALL)
Expand Down
6 changes: 3 additions & 3 deletions tests/widgets_chart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ fn widgets_chart_can_have_axis_with_zero_length_bounds() {
let mut terminal = Terminal::new(backend).unwrap();

terminal
.draw(|mut f| {
.draw(|f| {
let datasets = [Dataset::default()
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Magenta))
Expand Down Expand Up @@ -42,7 +42,7 @@ fn widgets_chart_handles_overflows() {
let mut terminal = Terminal::new(backend).unwrap();

terminal
.draw(|mut f| {
.draw(|f| {
let datasets = [Dataset::default()
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Magenta))
Expand Down Expand Up @@ -79,7 +79,7 @@ fn widgets_chart_can_have_empty_datasets() {
let mut terminal = Terminal::new(backend).unwrap();

terminal
.draw(|mut f| {
.draw(|f| {
let datasets = [Dataset::default().data(&[]).graph_type(Line)];
let chart = Chart::default()
.block(
Expand Down
2 changes: 1 addition & 1 deletion tests/widgets_gauge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ fn widgets_gauge_renders() {
let backend = TestBackend::new(40, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
Expand Down
Loading

0 comments on commit 0599b8e

Please sign in to comment.