Skip to content

Commit

Permalink
feat(examples): user_input example cursor movement (ratatui#302)
Browse files Browse the repository at this point in the history
The user_input example now responds to left/right and allows the
character at the cursor position to be deleted / inserted.

Co-authored-by: Leon Sautour <[email protected]>
  • Loading branch information
2 people authored and a-kenji committed Jul 16, 2023
1 parent af07eb6 commit b62d12c
Showing 1 changed file with 77 additions and 13 deletions.
90 changes: 77 additions & 13 deletions examples/user_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@ use std::{error::Error, io};
/// started.
///
/// This is a very simple example:
/// * A input box always focused. Every character you type is registered
/// here
/// * Pressing Backspace erases a character
/// * An input box always focused. Every character you type is registered
/// here.
/// * An entered character is inserted at the cursor position.
/// * Pressing Backspace erases the left character before the cursor position
/// * Pressing Enter pushes the current input in the history of previous
/// messages
/// messages.
/// **Note: ** as this is a relatively simple example unicode characters are unsupported and
/// their use will result in undefined behaviour.
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::prelude::*;
use unicode_width::UnicodeWidthStr;

enum InputMode {
Normal,
Expand All @@ -28,6 +30,8 @@ enum InputMode {
struct App {
/// Current value of the input box
input: String,
/// Position of cursor in the editor area.
cursor_position: usize,
/// Current input mode
input_mode: InputMode,
/// History of recorded messages
Expand All @@ -40,10 +44,65 @@ impl Default for App {
input: String::new(),
input_mode: InputMode::Normal,
messages: Vec::new(),
cursor_position: 0,
}
}
}

impl App {
fn move_cursor_left(&mut self) {
let cursor_moved_left = self.cursor_position.saturating_sub(1);
self.cursor_position = self.clamp_cursor(cursor_moved_left);
}

fn move_cursor_right(&mut self) {
let cursor_moved_right = self.cursor_position.saturating_add(1);
self.cursor_position = self.clamp_cursor(cursor_moved_right);
}

fn enter_char(&mut self, new_char: char) {
self.input.insert(self.cursor_position, new_char);

self.move_cursor_right();
}

fn delete_char(&mut self) {
let is_not_cursor_leftmost = self.cursor_position != 0;
if is_not_cursor_leftmost {
// Method "remove" is not used on the saved text for deleting the selected char.
// Reason: Using remove on String works on bytes instead of the chars.
// Using remove would require special care because of char boundaries.

let current_index = self.cursor_position;
let from_left_to_current_index = current_index - 1;

// Getting all characters before the selected character.
let before_char_to_delete = self.input.chars().take(from_left_to_current_index);
// Getting all characters after selected character.
let after_char_to_delete = self.input.chars().skip(current_index);

// Put all characters together except the selected one.
// By leaving the selected one out, it is forgotten and therefore deleted.
self.input = before_char_to_delete.chain(after_char_to_delete).collect();
self.move_cursor_left();
}
}

fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
new_cursor_pos.clamp(0, self.input.len())
}

fn reset_cursor(&mut self) {
self.cursor_position = 0;
}

fn submit_message(&mut self) {
self.messages.push(self.input.clone());
self.input.clear();
self.reset_cursor();
}
}

fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
Expand Down Expand Up @@ -88,14 +147,18 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
_ => {}
},
InputMode::Editing if key.kind == KeyEventKind::Press => match key.code {
KeyCode::Enter => {
app.messages.push(app.input.drain(..).collect());
}
KeyCode::Char(c) => {
app.input.push(c);
KeyCode::Enter => app.submit_message(),
KeyCode::Char(to_insert) => {
app.enter_char(to_insert);
}
KeyCode::Backspace => {
app.input.pop();
app.delete_char();
}
KeyCode::Left => {
app.move_cursor_left();
}
KeyCode::Right => {
app.move_cursor_right();
}
KeyCode::Esc => {
app.input_mode = InputMode::Normal;
Expand Down Expand Up @@ -165,8 +228,9 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
// Make the cursor visible and ask ratatui 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,
// Draw the cursor at the current position in the input field.
// This position is can be controlled via the left and right arrow key
chunks[1].x + app.cursor_position as u16 + 1,
// Move one line down, from the border to the input line
chunks[1].y + 1,
)
Expand Down

0 comments on commit b62d12c

Please sign in to comment.