Input fields and KeyCodes

This commit is contained in:
Gijs Burghoorn 2021-12-29 15:38:47 +01:00
parent 9f58364bb2
commit 3ba23fff7f
3 changed files with 233 additions and 48 deletions

2
Cargo.lock generated
View file

@ -129,7 +129,7 @@ dependencies = [
]
[[package]]
name = "lemors"
name = "lemurs"
version = "0.1.0"
dependencies = [
"argh",

196
src/input_field.rs Normal file
View file

@ -0,0 +1,196 @@
use crossterm::event::KeyCode;
use tui::{
layout::{Alignment, Rect},
style::{Color, Style},
terminal::Frame,
text::{Span, Spans, Text},
widgets::{Block, Borders, Paragraph},
};
use unicode_width::UnicodeWidthStr;
pub enum InputFieldDisplayType {
Echo,
Replace(char),
}
pub struct InputFieldWidget {
title: String,
content: String,
cursor: u16,
display_type: InputFieldDisplayType,
}
impl InputFieldWidget {
pub fn new(title: impl ToString, display_type: InputFieldDisplayType) -> Self {
let title = title.to_string();
Self {
title,
content: String::new(),
cursor: 0,
display_type,
}
}
#[inline]
fn len(&self) -> usize {
self.content.len()
}
fn show_string(&self) -> String {
use InputFieldDisplayType::{Echo, Replace};
match self.display_type {
Echo => self.content.clone(),
Replace(character) => character.to_string().repeat(self.len()),
}
}
fn backspace(&mut self) {
if self.cursor == 0 {
return;
}
debug_assert!(usize::from(self.cursor) <= self.len());
self.cursor -= 1;
self.content.remove(self.cursor.into());
}
fn delete(&mut self) {
if usize::from(self.cursor) == self.len() {
return;
}
debug_assert!(usize::from(self.cursor) <= self.len());
self.content.remove(self.cursor.into());
}
fn insert(&mut self, character: char) {
// Make sure the cursor doesn't overflow
if self.len() == usize::from(u16::MAX) {
return;
}
debug_assert!(usize::from(self.cursor) <= self.len());
self.content.insert(self.cursor.into(), character);
self.cursor += 1;
}
fn right(&mut self) {
if usize::from(self.cursor) == self.len() {
return;
}
self.cursor += 1;
}
fn left(&mut self) {
if self.cursor == 0 {
return;
}
self.cursor -= 1;
}
pub fn render(
&self,
frame: &mut Frame<impl tui::backend::Backend>,
area: Rect,
is_focused: bool,
) {
let show_string = self.show_string();
let widget = Paragraph::new(show_string.as_ref())
.style(if is_focused {
Style::default().fg(Color::Yellow)
} else {
Style::default()
})
.block(
Block::default()
.borders(Borders::ALL)
.title(self.title.clone()),
);
frame.render_widget(widget, area);
if is_focused {
frame.set_cursor(area.x + self.content[..usize::from(self.cursor)].width() as u16 + 1, area.y + 1);
}
}
pub fn key_press(&mut self, key_code: KeyCode) {
match key_code {
KeyCode::Backspace => self.backspace(),
KeyCode::Delete => self.delete(),
KeyCode::Left => self.left(),
KeyCode::Right => self.right(),
KeyCode::Char(c) => self.insert(c),
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use InputFieldDisplayType::*;
#[test]
fn cursor_movement() {
// TODO: Verify Unicode behaviour
let mut input_field = InputFieldWidget::new("", Echo);
assert_eq!(input_field.cursor, 0);
input_field.insert('x');
assert_eq!(input_field.cursor, 1);
input_field.insert('x');
assert_eq!(input_field.cursor, 2);
input_field.insert('x');
assert_eq!(input_field.cursor, 3);
input_field.insert('x');
assert_eq!(input_field.cursor, 4);
input_field.right();
assert_eq!(input_field.cursor, 4);
input_field.left();
assert_eq!(input_field.cursor, 3);
input_field.left();
assert_eq!(input_field.cursor, 2);
input_field.left();
assert_eq!(input_field.cursor, 1);
input_field.left();
assert_eq!(input_field.cursor, 0);
input_field.left();
assert_eq!(input_field.cursor, 0);
input_field.right();
assert_eq!(input_field.cursor, 1);
input_field.backspace();
assert_eq!(input_field.cursor, 0);
}
#[test]
fn integration() {
let mut input_field = InputFieldWidget::new("", Echo);
assert_eq!(&input_field.show_string(), "");
input_field.backspace();
assert_eq!(&input_field.show_string(), "");
input_field.insert('x');
assert_eq!(&input_field.show_string(), "x");
input_field.insert('y');
assert_eq!(&input_field.show_string(), "xy");
input_field.backspace();
assert_eq!(&input_field.show_string(), "x");
input_field.insert('y');
assert_eq!(&input_field.show_string(), "xy");
input_field.left();
assert_eq!(&input_field.show_string(), "xy");
input_field.backspace();
assert_eq!(&input_field.show_string(), "y");
input_field.backspace();
assert_eq!(&input_field.show_string(), "y");
input_field.right();
assert_eq!(&input_field.show_string(), "y");
input_field.backspace();
assert_eq!(&input_field.show_string(), "");
}
}

View file

@ -1,5 +1,5 @@
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
@ -13,7 +13,9 @@ use tui::{
};
use unicode_width::UnicodeWidthStr;
mod input_field;
mod window_manager_selector;
use input_field::{InputFieldDisplayType, InputFieldWidget};
use window_manager_selector::{WindowManager, WindowManagerSelectorWidget};
enum InputMode {
@ -29,10 +31,10 @@ struct App {
window_manager_widget: WindowManagerSelectorWidget,
/// Current value of the Username
username: String,
username_widget: InputFieldWidget,
/// Current value of the Password
password: String,
password_widget: InputFieldWidget,
/// Current input mode
input_mode: InputMode,
@ -46,8 +48,8 @@ impl Default for App {
WindowManager::new("i3", "/usr/bin/i3"),
WindowManager::new("awesome", "/usr/bin/awesome"),
]),
username: String::new(),
password: String::new(),
username_widget: InputFieldWidget::new("Username", InputFieldDisplayType::Echo),
password_widget: InputFieldWidget::new("Password", InputFieldDisplayType::Replace('*')),
input_mode: InputMode::Normal,
}
}
@ -97,42 +99,55 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
_ => {}
},
InputMode::WindowManager => match key.code {
KeyCode::Enter | KeyCode::Tab => {
KeyCode::Enter | KeyCode::Tab | KeyCode::Down => {
app.input_mode = InputMode::Username;
}
KeyCode::Esc => {
app.input_mode = InputMode::Normal;
}
KeyCode::Left => {
KeyCode::Left | KeyCode::Char('h') => {
app.window_manager_widget.left();
}
KeyCode::Right => {
KeyCode::Right | KeyCode::Char('l') => {
app.window_manager_widget.right();
}
_ => {}
},
InputMode::Username => match key.code {
KeyCode::Enter | KeyCode::Tab => {
KeyCode::Enter | KeyCode::Down => {
app.input_mode = InputMode::Password;
}
KeyCode::Tab => {
if key.modifiers == KeyModifiers::SHIFT {
app.input_mode = InputMode::WindowManager;
} else {
app.input_mode = InputMode::Password;
}
}
KeyCode::Up => {
app.input_mode = InputMode::WindowManager;
}
KeyCode::Esc => {
app.input_mode = InputMode::Normal;
}
KeyCode::Char(c) => {
app.username.push(c);
}
_ => {}
key_code => app.username_widget.key_press(key_code),
},
InputMode::Password => match key.code {
KeyCode::Enter | KeyCode::Tab => {
KeyCode::Enter => {
todo!()
}
KeyCode::Tab => {
if key.modifiers == KeyModifiers::SHIFT {
app.input_mode = InputMode::Username;
}
}
KeyCode::Up => {
app.input_mode = InputMode::Username;
}
KeyCode::Esc => {
app.input_mode = InputMode::Normal;
}
KeyCode::Char(c) => {
app.password.push(c);
}
key_code => app.password_widget.key_press(key_code),
_ => {}
},
}
@ -166,35 +181,9 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
matches!(app.input_mode, InputMode::WindowManager),
);
let username = Paragraph::new(app.username.as_ref())
.style(match app.input_mode {
InputMode::Normal => Style::default(),
InputMode::WindowManager => Style::default(),
InputMode::Username => Style::default().fg(Color::Yellow),
InputMode::Password => Style::default(),
})
.block(Block::default().borders(Borders::ALL).title("Input"));
f.render_widget(username, chunks[4]);
app.username_widget
.render(f, chunks[4], matches!(app.input_mode, InputMode::Username));
let password = Paragraph::new(app.password.as_ref())
.style(match app.input_mode {
InputMode::Normal => Style::default(),
InputMode::WindowManager => Style::default(),
InputMode::Username => Style::default(),
InputMode::Password => Style::default().fg(Color::Yellow),
})
.block(Block::default().borders(Borders::ALL).title("Password"));
f.render_widget(password, chunks[6]);
match app.input_mode {
InputMode::Normal | InputMode::WindowManager => {}
InputMode::Username => f.set_cursor(
chunks[4].x + app.username.width() as u16 + 1,
chunks[4].y + 1,
),
InputMode::Password => f.set_cursor(
chunks[6].x + app.password.width() as u16 + 1,
chunks[6].y + 1,
),
}
app.password_widget
.render(f, chunks[6], matches!(app.input_mode, InputMode::Password));
}