use ratatui_core::layout::Rect;
use ratatui_core::style::Style;
use unicode_width::UnicodeWidthChar;
use crate::event::{Event, KeyCode};
use crate::geometry::Size;
use crate::surface::Surface;
use crate::view::{RenderCtx, View};
#[derive(Clone, Debug)]
pub struct TextInputState {
lines: Vec<String>,
row: usize,
col: usize,
mode: TextInputMode,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TextInputMode {
#[default]
SubmitOnEnter,
SubmitOnShiftEnter,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextInputEvent {
Changed,
Submit,
}
impl Default for TextInputState {
fn default() -> Self {
Self::new()
}
}
impl TextInputState {
pub fn new() -> Self {
Self {
lines: vec![String::new()],
row: 0,
col: 0,
mode: TextInputMode::default(),
}
}
pub fn set_mode(&mut self, mode: TextInputMode) {
self.mode = mode;
}
pub fn mode(&self) -> TextInputMode {
self.mode
}
pub fn handle_enter(&mut self, shift: bool) -> TextInputEvent {
let submit = match self.mode {
TextInputMode::SubmitOnEnter => !shift,
TextInputMode::SubmitOnShiftEnter => shift,
};
if submit {
TextInputEvent::Submit
} else {
self.newline();
TextInputEvent::Changed
}
}
pub fn from_text(text: &str) -> Self {
let mut s = Self::new();
s.set_text(text);
s
}
pub fn text(&self) -> String {
self.lines.join("\n")
}
pub fn is_empty(&self) -> bool {
self.lines.len() == 1 && self.lines[0].is_empty()
}
pub fn line_count(&self) -> usize {
self.lines.len()
}
pub fn cursor(&self) -> (usize, usize) {
(self.row, self.col)
}
pub fn set_text(&mut self, text: &str) {
self.lines = text.split('\n').map(str::to_string).collect();
if self.lines.is_empty() {
self.lines.push(String::new());
}
self.row = self.lines.len() - 1;
self.col = self.lines[self.row].chars().count();
}
pub fn clear(&mut self) {
let mode = self.mode;
*self = Self::new();
self.mode = mode;
}
pub fn set_cursor(&mut self, row: usize, col: usize) {
self.row = row.min(self.lines.len().saturating_sub(1));
self.col = col.min(self.lines[self.row].chars().count());
}
fn row_chars(&self, row: usize) -> Vec<char> {
self.lines[row].chars().collect()
}
fn set_row(&mut self, row: usize, chars: Vec<char>) {
self.lines[row] = chars.into_iter().collect();
}
pub fn insert_char(&mut self, ch: char) {
if ch == '\n' {
self.newline();
return;
}
let mut chars = self.row_chars(self.row);
let at = self.col.min(chars.len());
chars.insert(at, ch);
self.set_row(self.row, chars);
self.col = at + 1;
}
pub fn insert_str(&mut self, s: &str) {
for ch in s.chars() {
self.insert_char(ch);
}
}
pub fn newline(&mut self) {
let chars = self.row_chars(self.row);
let at = self.col.min(chars.len());
let tail: String = chars[at..].iter().collect();
let head: String = chars[..at].iter().collect();
self.lines[self.row] = head;
self.lines.insert(self.row + 1, tail);
self.row += 1;
self.col = 0;
}
pub fn backspace(&mut self) {
if self.col > 0 {
let mut chars = self.row_chars(self.row);
chars.remove(self.col - 1);
self.col -= 1;
self.set_row(self.row, chars);
} else if self.row > 0 {
let cur = self.lines.remove(self.row);
self.row -= 1;
self.col = self.lines[self.row].chars().count();
self.lines[self.row].push_str(&cur);
}
}
pub fn delete(&mut self) {
let mut chars = self.row_chars(self.row);
if self.col < chars.len() {
chars.remove(self.col);
self.set_row(self.row, chars);
} else if self.row + 1 < self.lines.len() {
let next = self.lines.remove(self.row + 1);
self.lines[self.row].push_str(&next);
}
}
fn clamp_col(&mut self) {
self.col = self.col.min(self.lines[self.row].chars().count());
}
pub fn move_left(&mut self) {
if self.col > 0 {
self.col -= 1;
} else if self.row > 0 {
self.row -= 1;
self.col = self.lines[self.row].chars().count();
}
}
pub fn move_right(&mut self) {
let len = self.lines[self.row].chars().count();
if self.col < len {
self.col += 1;
} else if self.row + 1 < self.lines.len() {
self.row += 1;
self.col = 0;
}
}
pub fn move_up(&mut self) {
if self.row > 0 {
self.row -= 1;
self.clamp_col();
} else {
self.col = 0;
}
}
pub fn move_down(&mut self) {
if self.row + 1 < self.lines.len() {
self.row += 1;
self.clamp_col();
} else {
self.col = self.lines[self.row].chars().count();
}
}
pub fn move_home(&mut self) {
self.col = 0;
}
pub fn move_end(&mut self) {
self.col = self.lines[self.row].chars().count();
}
fn prev_word_col(&self) -> usize {
let chars = self.row_chars(self.row);
let mut i = self.col.min(chars.len());
while i > 0 && chars[i - 1].is_whitespace() {
i -= 1;
}
while i > 0 && !chars[i - 1].is_whitespace() {
i -= 1;
}
i
}
fn next_word_col(&self) -> usize {
let chars = self.row_chars(self.row);
let len = chars.len();
let mut i = self.col.min(len);
while i < len && chars[i].is_whitespace() {
i += 1;
}
while i < len && !chars[i].is_whitespace() {
i += 1;
}
i
}
pub fn move_word_left(&mut self) {
if self.col == 0 {
self.move_left();
return;
}
self.col = self.prev_word_col();
}
pub fn move_word_right(&mut self) {
if self.col >= self.lines[self.row].chars().count() {
self.move_right();
return;
}
self.col = self.next_word_col();
}
pub fn delete_word_left(&mut self) {
if self.col == 0 {
self.backspace();
return;
}
let start = self.prev_word_col();
let mut chars = self.row_chars(self.row);
chars.drain(start..self.col);
self.col = start;
self.set_row(self.row, chars);
}
pub fn delete_word_right(&mut self) {
let len = self.lines[self.row].chars().count();
if self.col >= len {
self.delete();
return;
}
let end = self.next_word_col();
let mut chars = self.row_chars(self.row);
chars.drain(self.col..end);
self.set_row(self.row, chars);
}
pub fn kill_to_line_end(&mut self) {
let mut chars = self.row_chars(self.row);
if self.col < chars.len() {
chars.truncate(self.col);
self.set_row(self.row, chars);
} else {
self.delete();
}
}
pub fn kill_to_line_start(&mut self) {
let chars = self.row_chars(self.row);
let tail: Vec<char> = chars[self.col.min(chars.len())..].to_vec();
self.set_row(self.row, tail);
self.col = 0;
}
pub fn handle(&mut self, event: &Event) -> Option<TextInputEvent> {
match event {
Event::Key(k) if k.ctrl && !k.alt => match k.code {
KeyCode::Char('a') => {
self.move_home();
Some(TextInputEvent::Changed)
}
KeyCode::Char('e') => {
self.move_end();
Some(TextInputEvent::Changed)
}
KeyCode::Char('f') => {
self.move_right();
Some(TextInputEvent::Changed)
}
KeyCode::Char('b') => {
self.move_left();
Some(TextInputEvent::Changed)
}
KeyCode::Char('p') => {
self.move_up();
Some(TextInputEvent::Changed)
}
KeyCode::Char('n') => {
self.move_down();
Some(TextInputEvent::Changed)
}
KeyCode::Char('h') => {
self.backspace();
Some(TextInputEvent::Changed)
}
KeyCode::Char('d') => {
self.delete();
Some(TextInputEvent::Changed)
}
KeyCode::Char('j') => {
self.newline();
Some(TextInputEvent::Changed)
}
KeyCode::Char('k') => {
self.kill_to_line_end();
Some(TextInputEvent::Changed)
}
KeyCode::Char('u') => {
self.kill_to_line_start();
Some(TextInputEvent::Changed)
}
KeyCode::Char('w') => {
self.delete_word_left();
Some(TextInputEvent::Changed)
}
_ => None,
},
Event::Key(k) if k.alt && !k.ctrl => match k.code {
KeyCode::Char('f') => {
self.move_word_right();
Some(TextInputEvent::Changed)
}
KeyCode::Char('b') => {
self.move_word_left();
Some(TextInputEvent::Changed)
}
KeyCode::Char('d') => {
self.delete_word_right();
Some(TextInputEvent::Changed)
}
KeyCode::Backspace => {
self.delete_word_left();
Some(TextInputEvent::Changed)
}
_ => None,
},
Event::Key(k) if !k.ctrl && !k.alt => match k.code {
KeyCode::Char(c) => {
self.insert_char(c);
Some(TextInputEvent::Changed)
}
KeyCode::Enter => Some(self.handle_enter(k.shift)),
KeyCode::Backspace => {
self.backspace();
Some(TextInputEvent::Changed)
}
KeyCode::Delete => {
self.delete();
Some(TextInputEvent::Changed)
}
KeyCode::Left => {
self.move_left();
Some(TextInputEvent::Changed)
}
KeyCode::Right => {
self.move_right();
Some(TextInputEvent::Changed)
}
KeyCode::Up => {
self.move_up();
Some(TextInputEvent::Changed)
}
KeyCode::Down => {
self.move_down();
Some(TextInputEvent::Changed)
}
KeyCode::Home => {
self.move_home();
Some(TextInputEvent::Changed)
}
KeyCode::End => {
self.move_end();
Some(TextInputEvent::Changed)
}
_ => None,
},
Event::Paste(text) => {
self.insert_str(text);
Some(TextInputEvent::Changed)
}
_ => None,
}
}
pub fn visual_height(&self, width: u16) -> u16 {
wrap_visual_rows(&self.lines, width).len().max(1) as u16
}
fn visual_cursor(&self, width: u16) -> (u16, u16) {
visual_cursor_at(&self.lines, self.row, self.col, width)
}
pub fn scroll_offset(&self, width: u16, height: u16) -> u16 {
self.visual_cursor(width)
.0
.saturating_sub(height.saturating_sub(1))
}
pub fn cursor_screen(&self, area: Rect) -> (u16, u16) {
let (vrow, vcol) = self.visual_cursor(area.width);
let offset = vrow.saturating_sub(area.height.saturating_sub(1));
let x = area
.x
.saturating_add(vcol.min(area.width.saturating_sub(1)));
let y = area
.y
.saturating_add((vrow - offset).min(area.height.saturating_sub(1)));
(x, y)
}
}
struct VisualRow {
logical: usize,
start: usize,
chars: Vec<char>,
}
fn visual_cursor_at(lines: &[String], row: usize, col: usize, width: u16) -> (u16, u16) {
let rows = wrap_visual_rows(lines, width);
let mut last_on_line: Option<(usize, usize)> = None; for (vi, vr) in rows.iter().enumerate() {
if vr.logical > row {
break;
}
if vr.logical != row {
continue;
}
let end = vr.start + vr.chars.len();
last_on_line = Some((vi, vr.start));
if col >= vr.start && col < end {
return (vi as u16, (col - vr.start) as u16);
}
}
if let Some((vi, start)) = last_on_line {
return (vi as u16, col.saturating_sub(start) as u16);
}
(rows.len().saturating_sub(1) as u16, 0)
}
fn wrap_visual_rows(lines: &[String], width: u16) -> Vec<VisualRow> {
let width = width.max(1) as usize;
let mut rows = Vec::new();
for (r, line) in lines.iter().enumerate() {
let chars: Vec<char> = line.chars().collect();
if chars.is_empty() {
rows.push(VisualRow {
logical: r,
start: 0,
chars: Vec::new(),
});
continue;
}
let mut start = 0;
let mut last_filled = false;
while start < chars.len() {
let remaining = chars.len() - start;
let end = if remaining <= width {
chars.len()
} else {
let hard = start + width;
let brk = (start + 1..hard).rev().find(|&i| chars[i] == ' ');
brk.map(|i| i + 1).unwrap_or(hard)
};
last_filled = end - start == width;
rows.push(VisualRow {
logical: r,
start,
chars: chars[start..end].to_vec(),
});
start = end;
}
if last_filled {
rows.push(VisualRow {
logical: r,
start: chars.len(),
chars: Vec::new(),
});
}
}
rows
}
pub struct TextInput {
lines: Vec<String>,
cursor: (usize, usize),
style: Style,
}
impl TextInput {
pub fn new(state: &TextInputState) -> Self {
Self {
lines: state.lines.clone(),
cursor: (state.row, state.col),
style: Style::default(),
}
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
fn scroll_offset(&self, width: u16, height: u16) -> u16 {
visual_cursor_at(&self.lines, self.cursor.0, self.cursor.1, width)
.0
.saturating_sub(height.saturating_sub(1))
}
}
impl View for TextInput {
fn measure(&self, available: Size) -> Size {
let height = wrap_visual_rows(&self.lines, available.width).len().max(1) as u16;
Size::new(available.width, height)
}
fn render(&self, area: Rect, surface: &mut Surface, _ctx: &RenderCtx) {
if area.width == 0 || area.height == 0 {
return;
}
let offset = self.scroll_offset(area.width, area.height) as usize;
for (i, vr) in wrap_visual_rows(&self.lines, area.width)
.into_iter()
.enumerate()
.skip(offset)
{
let y = area.y.saturating_add((i - offset) as u16);
if y >= area.bottom() {
break;
}
let mut x = area.x;
for ch in vr.chars {
let w = UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
if w == 0 || x >= area.right() {
continue;
}
surface.set(x, y, ch, self.style);
x = x.saturating_add(w);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::{Event, Key, KeyCode};
use crate::test_support::{render_el, render_view_rows};
use crate::view::element;
use ratatui_core::layout::Rect;
fn press(state: &mut TextInputState, code: KeyCode) -> bool {
matches!(
state.handle(&Event::Key(Key::new(code))),
Some(TextInputEvent::Changed)
)
}
fn press_shift(state: &mut TextInputState, code: KeyCode) -> bool {
matches!(
state.handle(&Event::Key(Key {
code,
ctrl: false,
alt: false,
shift: true,
})),
Some(TextInputEvent::Changed)
)
}
fn press_ctrl(state: &mut TextInputState, code: KeyCode) -> bool {
matches!(
state.handle(&Event::Key(Key {
code,
ctrl: true,
alt: false,
shift: false,
})),
Some(TextInputEvent::Changed)
)
}
fn press_alt(state: &mut TextInputState, code: KeyCode) -> bool {
matches!(
state.handle(&Event::Key(Key {
code,
ctrl: false,
alt: true,
shift: false,
})),
Some(TextInputEvent::Changed)
)
}
fn type_str(state: &mut TextInputState, s: &str) {
for ch in s.chars() {
assert!(press(state, KeyCode::Char(ch)));
}
}
#[test]
fn text_input_starts_empty() {
let state = TextInputState::new();
assert!(state.is_empty());
assert_eq!(state.text(), "");
assert_eq!(state.cursor(), (0, 0));
assert_eq!(state.line_count(), 1);
}
#[test]
fn text_input_types_and_edits() {
let mut state = TextInputState::new();
type_str(&mut state, "helo");
assert_eq!(state.text(), "helo");
assert_eq!(state.cursor(), (0, 4));
press(&mut state, KeyCode::Left);
press(&mut state, KeyCode::Left);
assert_eq!(state.cursor(), (0, 2));
assert!(press(&mut state, KeyCode::Char('l')));
assert_eq!(state.text(), "hello");
assert_eq!(state.cursor(), (0, 3));
}
#[test]
fn text_input_backspace_and_delete() {
let mut state = TextInputState::from_text("abc");
assert_eq!(state.cursor(), (0, 3));
press(&mut state, KeyCode::Backspace);
assert_eq!(state.text(), "ab");
press(&mut state, KeyCode::Home);
press(&mut state, KeyCode::Delete);
assert_eq!(state.text(), "b");
assert_eq!(state.cursor(), (0, 0));
}
#[test]
fn text_input_newline_splits_and_backspace_joins() {
let mut state = TextInputState::from_text("abcd");
press(&mut state, KeyCode::Home);
press(&mut state, KeyCode::Right);
press(&mut state, KeyCode::Right);
assert_eq!(state.cursor(), (0, 2));
assert!(press_shift(&mut state, KeyCode::Enter));
assert_eq!(state.text(), "ab\ncd");
assert_eq!(state.line_count(), 2);
assert_eq!(state.cursor(), (1, 0));
press(&mut state, KeyCode::Backspace);
assert_eq!(state.text(), "abcd");
assert_eq!(state.cursor(), (0, 2));
assert_eq!(state.line_count(), 1);
}
#[test]
fn text_input_vertical_movement_clamps_column() {
let mut state = TextInputState::from_text("longline\nhi");
press(&mut state, KeyCode::Up);
assert_eq!(state.cursor(), (0, 2));
press(&mut state, KeyCode::End);
assert_eq!(state.cursor(), (0, 8));
press(&mut state, KeyCode::Down);
assert_eq!(state.cursor(), (1, 2));
}
#[test]
fn text_input_paste_inserts_multiline() {
let mut state = TextInputState::new();
assert_eq!(
state.handle(&Event::Paste("one\ntwo".to_string())),
Some(TextInputEvent::Changed)
);
assert_eq!(state.text(), "one\ntwo");
assert_eq!(state.line_count(), 2);
assert_eq!(state.cursor(), (1, 3));
}
#[test]
fn text_input_unbound_ctrl_keys_ignored() {
let mut state = TextInputState::from_text("x");
assert!(!press_ctrl(&mut state, KeyCode::Char('z')));
assert_eq!(state.text(), "x");
}
#[test]
fn text_input_emacs_cursor_bindings() {
let mut state = TextInputState::from_text("hello");
assert!(press_ctrl(&mut state, KeyCode::Char('a')));
assert_eq!(state.cursor(), (0, 0));
assert!(press_ctrl(&mut state, KeyCode::Char('f')));
assert_eq!(state.cursor(), (0, 1));
assert!(press_ctrl(&mut state, KeyCode::Char('e')));
assert_eq!(state.cursor(), (0, 5));
assert!(press_ctrl(&mut state, KeyCode::Char('b')));
assert_eq!(state.cursor(), (0, 4));
state = TextInputState::from_text("ab\ncd");
press(&mut state, KeyCode::Home);
assert!(press_ctrl(&mut state, KeyCode::Char('p')));
assert_eq!(state.cursor(), (0, 0));
assert!(press_ctrl(&mut state, KeyCode::Char('n')));
assert_eq!(state.cursor(), (1, 0));
}
#[test]
fn text_input_emacs_delete_bindings() {
let mut state = TextInputState::from_text("abc");
assert!(press_ctrl(&mut state, KeyCode::Char('h')));
assert_eq!(state.text(), "ab");
press(&mut state, KeyCode::Home);
assert!(press_ctrl(&mut state, KeyCode::Char('d')));
assert_eq!(state.text(), "b");
}
#[test]
fn text_input_kill_to_line_end_and_start() {
let mut state = TextInputState::from_text("hello world");
press(&mut state, KeyCode::Home);
press(&mut state, KeyCode::Right);
press(&mut state, KeyCode::Right);
press(&mut state, KeyCode::Right);
press(&mut state, KeyCode::Right);
press(&mut state, KeyCode::Right); assert!(press_ctrl(&mut state, KeyCode::Char('k')));
assert_eq!(state.text(), "hello");
assert_eq!(state.cursor(), (0, 5));
let mut state = TextInputState::from_text("ab\ncd");
press(&mut state, KeyCode::Home);
press(&mut state, KeyCode::Up);
press(&mut state, KeyCode::End);
assert!(press_ctrl(&mut state, KeyCode::Char('k')));
assert_eq!(state.text(), "abcd");
let mut state = TextInputState::from_text("hello world");
assert!(press_ctrl(&mut state, KeyCode::Char('u')));
assert_eq!(state.text(), "");
assert_eq!(state.cursor(), (0, 0));
}
#[test]
fn text_input_word_move_and_delete() {
let mut state = TextInputState::from_text("foo bar baz");
assert!(press_alt(&mut state, KeyCode::Char('b')));
assert_eq!(state.cursor(), (0, 8)); assert!(press_alt(&mut state, KeyCode::Char('b')));
assert_eq!(state.cursor(), (0, 4));
assert!(press_ctrl(&mut state, KeyCode::Char('w')));
assert_eq!(state.text(), "bar baz");
assert_eq!(state.cursor(), (0, 0));
assert!(press_alt(&mut state, KeyCode::Char('f')));
assert_eq!(state.cursor(), (0, 3)); assert!(press_alt(&mut state, KeyCode::Char('d')));
assert_eq!(state.text(), "bar");
let mut state = TextInputState::from_text("alpha beta");
assert!(press_alt(&mut state, KeyCode::Backspace));
assert_eq!(state.text(), "alpha ");
}
#[test]
fn text_input_scrolls_to_cursor_when_taller_than_area() {
let mut state = TextInputState::new();
for i in 0..10 {
type_str(&mut state, &format!("line{i}"));
if i < 9 {
state.newline();
}
}
let out = render_view_rows(&TextInput::new(&state), 10, 3);
assert_eq!(out, vec!["line7", "line8", "line9"]);
let (_, y) = state.cursor_screen(Rect::new(0, 0, 10, 3));
assert_eq!(y, 2);
for _ in 0..9 {
state.move_up();
}
let out = render_view_rows(&TextInput::new(&state), 10, 3);
assert_eq!(out, vec!["line0", "line1", "line2"]);
assert_eq!(state.cursor_screen(Rect::new(0, 0, 10, 3)).1, 0);
}
#[test]
fn text_input_renders_wrapped_rows() {
let mut state = TextInputState::new();
type_str(&mut state, "abcdef");
assert_eq!(state.visual_height(4), 2);
let out = render_view_rows(&TextInput::new(&state), 4, 2);
assert_eq!(out[0], "abcd");
assert_eq!(out[1], "ef");
}
#[test]
fn text_input_word_wraps_at_spaces() {
let mut state = TextInputState::new();
type_str(&mut state, "hello world foo");
assert_eq!(state.visual_height(8), 3);
let out = render_view_rows(&TextInput::new(&state), 8, 3);
assert_eq!(out[0], "hello");
assert_eq!(out[1], "world");
assert_eq!(out[2], "foo");
}
#[test]
fn text_input_hard_breaks_overlong_word() {
let mut state = TextInputState::new();
type_str(&mut state, "abcdefghij");
assert_eq!(state.visual_height(4), 3);
let out = render_view_rows(&TextInput::new(&state), 4, 3);
assert_eq!(out[0], "abcd");
assert_eq!(out[1], "efgh");
assert_eq!(out[2], "ij");
}
#[test]
fn text_input_cursor_tracks_word_wrap() {
let mut state = TextInputState::from_text("hello world");
let area = Rect::new(0, 0, 8, 3);
assert_eq!(state.cursor_screen(area), (5, 1));
state.move_home();
for _ in 0..6 {
state.move_right();
}
assert_eq!(state.cursor_screen(area), (0, 1));
}
#[test]
fn text_input_cursor_screen_follows_wrap() {
let mut state = TextInputState::new();
type_str(&mut state, "abcd");
assert_eq!(state.visual_height(4), 2);
let area = Rect::new(2, 1, 4, 3);
assert_eq!(state.cursor_screen(area), (2, 2));
press(&mut state, KeyCode::Home);
assert_eq!(state.cursor_screen(area), (2, 1));
}
#[test]
fn text_input_set_and_clear() {
let mut state = TextInputState::new();
state.set_text("hello\nworld");
assert_eq!(state.cursor(), (1, 5));
assert_eq!(state.line_count(), 2);
state.clear();
assert!(state.is_empty());
assert_eq!(state.cursor(), (0, 0));
}
#[test]
fn text_input_set_cursor_clamps() {
let mut state = TextInputState::from_text("hi\nthere");
state.set_cursor(0, 1);
assert_eq!(state.cursor(), (0, 1));
state.set_cursor(9, 9);
assert_eq!(state.cursor(), (1, 5));
}
#[test]
fn text_input_composes_into_view_tree() {
let mut state = TextInputState::new();
type_str(&mut state, "hi");
let tree = element(TextInput::new(&state));
let out = render_el(&tree, 4, 1);
assert_eq!(out[0], "hi");
}
#[test]
fn submit_on_enter_mode_uses_shift_enter_for_newline() {
let mut state = TextInputState::new();
assert_eq!(state.mode(), TextInputMode::SubmitOnEnter);
assert_eq!(state.handle_enter(false), TextInputEvent::Submit);
assert_eq!(state.handle_enter(true), TextInputEvent::Changed);
assert_eq!(state.text(), "\n");
}
#[test]
fn submit_on_shift_enter_mode_reverses_enter_chords() {
let mut state = TextInputState::new();
state.set_mode(TextInputMode::SubmitOnShiftEnter);
assert_eq!(state.handle_enter(false), TextInputEvent::Changed);
assert_eq!(state.handle_enter(true), TextInputEvent::Submit);
assert_eq!(state.text(), "\n");
}
#[test]
fn handle_honors_submit_on_enter_mode_for_shift_enter_newline() {
let mut state = TextInputState::new();
assert_eq!(state.mode(), TextInputMode::SubmitOnEnter);
type_str(&mut state, "one");
let shift_enter = Event::Key(Key {
code: KeyCode::Enter,
ctrl: false,
alt: false,
shift: true,
});
assert_eq!(
state.handle(&shift_enter),
Some(TextInputEvent::Changed),
"Shift+Enter must insert a newline under SubmitOnEnter"
);
assert_eq!(state.text(), "one\n");
type_str(&mut state, "two");
assert_eq!(
state.handle(&Event::Key(Key::new(KeyCode::Enter))),
Some(TextInputEvent::Submit),
"plain Enter must submit under SubmitOnEnter, not insert another newline"
);
assert_eq!(
state.text(),
"one\ntwo",
"submit must leave the draft text intact"
);
}
#[test]
fn handle_honors_submit_on_shift_enter_mode() {
let mut state = TextInputState::new();
state.set_mode(TextInputMode::SubmitOnShiftEnter);
type_str(&mut state, "one");
assert_eq!(
state.handle(&Event::Key(Key::new(KeyCode::Enter))),
Some(TextInputEvent::Changed)
);
assert_eq!(state.text(), "one\n");
type_str(&mut state, "two");
let shift_enter = Event::Key(Key {
code: KeyCode::Enter,
ctrl: false,
alt: false,
shift: true,
});
assert_eq!(state.handle(&shift_enter), Some(TextInputEvent::Submit));
assert_eq!(state.text(), "one\ntwo");
}
#[test]
fn clear_preserves_enter_mode() {
let mut state = TextInputState::new();
state.set_mode(TextInputMode::SubmitOnShiftEnter);
type_str(&mut state, "draft");
state.clear();
assert!(state.is_empty());
assert_eq!(state.mode(), TextInputMode::SubmitOnShiftEnter);
}
#[test]
fn handle_ctrl_j_inserts_newline() {
let mut state = TextInputState::new();
type_str(&mut state, "ab");
assert!(press_ctrl(&mut state, KeyCode::Char('j')));
assert_eq!(state.text(), "ab\n");
assert_eq!(state.cursor(), (1, 0));
}
}