diff --git a/CHANGELOG.md b/CHANGELOG.md index dad6381..46cfb8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.toml b/Cargo.toml index 514b958..474ca19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Florian Dehau "] 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" diff --git a/examples/barchart.rs b/examples/barchart.rs index 829e5d7..7063410 100644 --- a/examples/barchart.rs +++ b/examples/barchart.rs @@ -70,7 +70,7 @@ fn main() -> Result<(), Box> { let mut app = App::new(); loop { - terminal.draw(|mut f| { + terminal.draw(|f| { let chunks = Layout::default() .direction(Direction::Vertical) .margin(2) diff --git a/examples/block.rs b/examples/block.rs index 4c9bed0..d828bae 100644 --- a/examples/block.rs +++ b/examples/block.rs @@ -25,7 +25,7 @@ fn main() -> Result<(), Box> { 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 diff --git a/examples/canvas.rs b/examples/canvas.rs index a8d3f03..eb6a80e 100644 --- a/examples/canvas.rs +++ b/examples/canvas.rs @@ -92,7 +92,7 @@ fn main() -> Result<(), Box> { 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()) diff --git a/examples/chart.rs b/examples/chart.rs index b899b33..66a3e7a 100644 --- a/examples/chart.rs +++ b/examples/chart.rs @@ -79,7 +79,7 @@ fn main() -> Result<(), Box> { let mut app = App::new(); loop { - terminal.draw(|mut f| { + terminal.draw(|f| { let size = f.size(); let chunks = Layout::default() .direction(Direction::Vertical) diff --git a/examples/crossterm_demo.rs b/examples/crossterm_demo.rs index b344f2f..4d8c474 100644 --- a/examples/crossterm_demo.rs +++ b/examples/crossterm_demo.rs @@ -73,7 +73,7 @@ fn main() -> Result<(), Box> { 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') => { diff --git a/examples/curses_demo.rs b/examples/curses_demo.rs index 3dd4a8c..b8c3a6a 100644 --- a/examples/curses_demo.rs +++ b/examples/curses_demo.rs @@ -40,7 +40,7 @@ fn main() -> Result<(), Box> { 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) => { diff --git a/examples/custom_widget.rs b/examples/custom_widget.rs index b9dd7e3..d01ef3b 100644 --- a/examples/custom_widget.rs +++ b/examples/custom_widget.rs @@ -42,7 +42,7 @@ fn main() -> Result<(), Box> { 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); diff --git a/examples/gauge.rs b/examples/gauge.rs index 5d41036..becf5c1 100644 --- a/examples/gauge.rs +++ b/examples/gauge.rs @@ -63,7 +63,7 @@ fn main() -> Result<(), Box> { let mut app = App::new(); loop { - terminal.draw(|mut f| { + terminal.draw(|f| { let chunks = Layout::default() .direction(Direction::Vertical) .margin(2) diff --git a/examples/layout.rs b/examples/layout.rs index 16b11b6..9dece7a 100644 --- a/examples/layout.rs +++ b/examples/layout.rs @@ -23,7 +23,7 @@ fn main() -> Result<(), Box> { let events = Events::new(); loop { - terminal.draw(|mut f| { + terminal.draw(|f| { let chunks = Layout::default() .direction(Direction::Vertical) .constraints( diff --git a/examples/list.rs b/examples/list.rs index 7397f9f..b1b5153 100644 --- a/examples/list.rs +++ b/examples/list.rs @@ -88,7 +88,7 @@ fn main() -> Result<(), Box> { 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()) diff --git a/examples/paragraph.rs b/examples/paragraph.rs index 89fb21a..788f5fb 100644 --- a/examples/paragraph.rs +++ b/examples/paragraph.rs @@ -25,7 +25,7 @@ fn main() -> Result<(), Box> { let mut scroll: u16 = 0; loop { - terminal.draw(|mut f| { + terminal.draw(|f| { let size = f.size(); // Words made "loooong" to demonstrate line breaking. diff --git a/examples/popup.rs b/examples/popup.rs index 55504a8..ea7714c 100644 --- a/examples/popup.rs +++ b/examples/popup.rs @@ -54,7 +54,7 @@ fn main() -> Result<(), Box> { let events = Events::new(); loop { - terminal.draw(|mut f| { + terminal.draw(|f| { let size = f.size(); let chunks = Layout::default() diff --git a/examples/rustbox_demo.rs b/examples/rustbox_demo.rs index d65d5b0..a00dc62 100644 --- a/examples/rustbox_demo.rs +++ b/examples/rustbox_demo.rs @@ -34,7 +34,7 @@ fn main() -> Result<(), Box> { 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) { diff --git a/examples/sparkline.rs b/examples/sparkline.rs index 1b6a056..d574a05 100644 --- a/examples/sparkline.rs +++ b/examples/sparkline.rs @@ -65,7 +65,7 @@ fn main() -> Result<(), Box> { let mut app = App::new(); loop { - terminal.draw(|mut f| { + terminal.draw(|f| { let chunks = Layout::default() .direction(Direction::Vertical) .margin(2) diff --git a/examples/table.rs b/examples/table.rs index 6990407..a41ab62 100644 --- a/examples/table.rs +++ b/examples/table.rs @@ -88,7 +88,7 @@ fn main() -> Result<(), Box> { // Input loop { - terminal.draw(|mut f| { + terminal.draw(|f| { let rects = Layout::default() .constraints([Constraint::Percentage(100)].as_ref()) .margin(5) diff --git a/examples/tabs.rs b/examples/tabs.rs index 9bf97bf..70a9a46 100644 --- a/examples/tabs.rs +++ b/examples/tabs.rs @@ -37,7 +37,7 @@ fn main() -> Result<(), Box> { // Main loop loop { - terminal.draw(|mut f| { + terminal.draw(|f| { let size = f.size(); let chunks = Layout::default() .direction(Direction::Vertical) diff --git a/examples/termion_demo.rs b/examples/termion_demo.rs index ed6130c..7560441 100644 --- a/examples/termion_demo.rs +++ b/examples/termion_demo.rs @@ -39,7 +39,7 @@ fn main() -> Result<(), Box> { 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 { diff --git a/examples/user_input.rs b/examples/user_input.rs index fb42bcf..b170c1c 100644 --- a/examples/user_input.rs +++ b/examples/user_input.rs @@ -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> { 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> { .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> { 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 { diff --git a/src/lib.rs b/src/lib.rs index 8139ed1..b45fa9d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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) diff --git a/src/terminal.rs b/src/terminal.rs index abef8cd..54877ee 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -29,6 +29,12 @@ where B: Backend, { terminal: &'a mut Terminal, + + /// 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 Drop for Terminal @@ -115,7 +131,7 @@ impl Terminal 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> { 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 { - 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(&mut self, f: F) -> io::Result<()> where - F: FnOnce(Frame), + F: FnOnce(&mut Frame), { // 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; diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 55b1a3b..b8c1c97 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -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); diff --git a/tests/widgets_block.rs b/tests/widgets_block.rs index a19c6d3..3adbd3b 100644 --- a/tests/widgets_block.rs +++ b/tests/widgets_block.rs @@ -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) diff --git a/tests/widgets_chart.rs b/tests/widgets_chart.rs index c851359..bd23db9 100644 --- a/tests/widgets_chart.rs +++ b/tests/widgets_chart.rs @@ -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( diff --git a/tests/widgets_gauge.rs b/tests/widgets_gauge.rs index 681e4db..8acc62c 100644 --- a/tests/widgets_gauge.rs +++ b/tests/widgets_gauge.rs @@ -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) diff --git a/tests/widgets_list.rs b/tests/widgets_list.rs index 3a2479b..d04cf77 100644 --- a/tests/widgets_list.rs +++ b/tests/widgets_list.rs @@ -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(">> "); diff --git a/tests/widgets_paragraph.rs b/tests/widgets_paragraph.rs index d3cd548..180f94b 100644 --- a/tests/widgets_paragraph.rs +++ b/tests/widgets_paragraph.rs @@ -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()) diff --git a/tests/widgets_table.rs b/tests/widgets_table.rs index c606a54..a2c800f 100644 --- a/tests/widgets_table.rs +++ b/tests/widgets_table.rs @@ -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(), diff --git a/tests/widgets_tabs.rs b/tests/widgets_tabs.rs index 58e40bd..67e435d 100644 --- a/tests/widgets_tabs.rs +++ b/tests/widgets_tabs.rs @@ -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,