feat(terminal): Add after-draw() cursor control to Frame (#91) (#309)

pull/325/head
Alexander Batischev 4 years ago committed by GitHub
parent 2b48409cfd
commit 8c2ee0ed85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -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"

@ -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)

@ -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

@ -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())

@ -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)

@ -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') => {

@ -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) => {

@ -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);

@ -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)

@ -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(

@ -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())

@ -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.

@ -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()

@ -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)
{

@ -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)

@ -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)

@ -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)

@ -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 {

@ -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},
@ -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)
@ -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()
@ -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 {

@ -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")
@ -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)

@ -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>
@ -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>
@ -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()?;
@ -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 {
@ -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;

@ -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);

@ -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)

@ -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))
@ -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))
@ -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(

@ -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)

@ -15,7 +15,7 @@ fn widgets_list_should_highlight_the_selected_item() {
let mut state = ListState::default();
state.select(Some(1));
terminal
.draw(|mut f| {
.draw(|f| {
let size = f.size();
let items = vec![
Text::raw("Item 1"),
@ -71,7 +71,7 @@ fn widgets_list_should_truncate_items() {
state.select(case.selected);
let items = case.items.drain(..);
terminal
.draw(|mut f| {
.draw(|f| {
let list = List::new(items)
.block(Block::default().borders(Borders::RIGHT))
.highlight_symbol(">> ");

@ -18,7 +18,7 @@ fn widgets_paragraph_can_wrap_its_content() {
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
.draw(|f| {
let size = f.size();
let text = [Text::raw(SAMPLE_STRING)];
let paragraph = Paragraph::new(text.iter())
@ -85,7 +85,7 @@ fn widgets_paragraph_renders_double_width_graphemes() {
let s = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点では、";
terminal
.draw(|mut f| {
.draw(|f| {
let size = f.size();
let text = [Text::raw(s)];
let paragraph = Paragraph::new(text.iter())
@ -117,7 +117,7 @@ fn widgets_paragraph_renders_mixed_width_graphemes() {
let s = "aコンピュータ上で文字を扱う場合、";
terminal
.draw(|mut f| {
.draw(|f| {
let size = f.size();
let text = [Text::raw(s)];
let paragraph = Paragraph::new(text.iter())

@ -11,7 +11,7 @@ fn widgets_table_column_spacing_can_be_changed() {
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
.draw(|f| {
let size = f.size();
let table = Table::new(
["Head1", "Head2", "Head3"].iter(),
@ -112,7 +112,7 @@ fn widgets_table_columns_widths_can_use_fixed_length_constraints() {
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
.draw(|f| {
let size = f.size();
let table = Table::new(
["Head1", "Head2", "Head3"].iter(),
@ -203,7 +203,7 @@ fn widgets_table_columns_widths_can_use_percentage_constraints() {
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
.draw(|f| {
let size = f.size();
let table = Table::new(
["Head1", "Head2", "Head3"].iter(),
@ -312,7 +312,7 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
.draw(|f| {
let size = f.size();
let table = Table::new(
["Head1", "Head2", "Head3"].iter(),

@ -5,7 +5,7 @@ fn widgets_tabs_should_not_panic_on_narrow_areas() {
let backend = TestBackend::new(1, 1);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
.draw(|f| {
let tabs = Tabs::default().titles(&["Tab1", "Tab2"]);
f.render_widget(
tabs,
@ -27,7 +27,7 @@ fn widgets_tabs_should_truncate_the_last_item() {
let backend = TestBackend::new(10, 1);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|mut f| {
.draw(|f| {
let tabs = Tabs::default().titles(&["Tab1", "Tab2"]);
f.render_widget(
tabs,

Loading…
Cancel
Save