Skip to content

Commit 09c3fe9

Browse files
Add cursor to TextInputComponent for better commit message support (#117)
see #46
1 parent 0a541a3 commit 09c3fe9

File tree

1 file changed

+106
-14
lines changed

1 file changed

+106
-14
lines changed

src/components/textinput.rs

Lines changed: 106 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@ use crate::{
88
};
99
use anyhow::Result;
1010
use crossterm::event::{Event, KeyCode, KeyModifiers};
11-
use std::borrow::Cow;
1211
use strings::commands;
1312
use tui::{
1413
backend::Backend,
1514
layout::Rect,
16-
style::Style,
15+
style::{Modifier, Style},
1716
widgets::{Clear, Text},
1817
Frame,
1918
};
@@ -25,6 +24,7 @@ pub struct TextInputComponent {
2524
msg: String,
2625
visible: bool,
2726
theme: Theme,
27+
cursor_position: usize,
2828
}
2929

3030
impl TextInputComponent {
@@ -40,19 +40,56 @@ impl TextInputComponent {
4040
theme: *theme,
4141
title: title.to_string(),
4242
default_msg: default_msg.to_string(),
43+
cursor_position: 0,
4344
}
4445
}
4546

46-
///
47+
/// Clear the `msg`.
4748
pub fn clear(&mut self) {
4849
self.msg.clear();
4950
}
5051

51-
///
52+
/// Get the `msg`.
5253
pub const fn get_text(&self) -> &String {
5354
&self.msg
5455
}
5556

57+
/// Move the cursor right one char.
58+
fn incr_cursor(&mut self) {
59+
if let Some(pos) = self.next_char_position() {
60+
self.cursor_position = pos;
61+
}
62+
}
63+
64+
/// Move the cursor left one char.
65+
fn decr_cursor(&mut self) {
66+
let mut new_pos: usize = 0;
67+
for (bytes, _) in self.msg.char_indices() {
68+
if bytes >= self.cursor_position {
69+
break;
70+
}
71+
new_pos = bytes;
72+
}
73+
self.cursor_position = new_pos;
74+
}
75+
76+
/// Get the position of the next char, or, if the cursor points
77+
/// to the last char, the `msg.len()`.
78+
/// Returns None when the cursor is already at `msg.len()`.
79+
fn next_char_position(&self) -> Option<usize> {
80+
let mut char_indices =
81+
self.msg[self.cursor_position..].char_indices();
82+
if char_indices.next().is_some() {
83+
if let Some((bytes, _)) = char_indices.next() {
84+
Some(self.cursor_position + bytes)
85+
} else {
86+
Some(self.msg.len())
87+
}
88+
} else {
89+
None
90+
}
91+
}
92+
5693
///
5794
pub fn set_text(&mut self, msg: String) {
5895
self.msg = msg;
@@ -71,16 +108,43 @@ impl DrawableComponent for TextInputComponent {
71108
_rect: Rect,
72109
) -> Result<()> {
73110
if self.visible {
74-
let txt = if self.msg.is_empty() {
75-
[Text::Styled(
76-
Cow::from(self.default_msg.as_str()),
111+
let mut txt: Vec<tui::widgets::Text> = Vec::new();
112+
if self.msg.is_empty() {
113+
txt.push(Text::styled(
114+
self.default_msg.as_str(),
77115
self.theme.text(false, false),
78-
)]
116+
));
79117
} else {
80-
[Text::Styled(
81-
Cow::from(self.msg.clone()),
82-
Style::default(),
83-
)]
118+
// the portion of the text before the cursor is added
119+
// if the cursor is not at the first character
120+
if self.cursor_position > 0 {
121+
txt.push(Text::styled(
122+
&self.msg[..self.cursor_position],
123+
Style::default(),
124+
));
125+
}
126+
127+
txt.push(Text::styled(
128+
if let Some(pos) = self.next_char_position() {
129+
&self.msg[self.cursor_position..pos]
130+
} else {
131+
// if the cursor is at the end of the msg
132+
// a whitespace is used to underline
133+
" "
134+
},
135+
Style::default().modifier(Modifier::UNDERLINED),
136+
));
137+
138+
// the final portion of the text is added if there is
139+
// still remaining characters
140+
if let Some(pos) = self.next_char_position() {
141+
if pos < self.msg.len() {
142+
txt.push(Text::styled(
143+
&self.msg[pos..],
144+
Style::default(),
145+
));
146+
}
147+
}
84148
};
85149

86150
let area = ui::centered_rect(60, 20, f.size());
@@ -128,11 +192,39 @@ impl Component for TextInputComponent {
128192
return Ok(true);
129193
}
130194
KeyCode::Char(c) if !is_ctrl => {
131-
self.msg.push(c);
195+
self.msg.insert(self.cursor_position, c);
196+
self.incr_cursor();
197+
return Ok(true);
198+
}
199+
KeyCode::Delete => {
200+
if self.cursor_position < self.msg.len() {
201+
self.msg.remove(self.cursor_position);
202+
}
132203
return Ok(true);
133204
}
134205
KeyCode::Backspace => {
135-
self.msg.pop();
206+
if self.cursor_position > 0 {
207+
self.decr_cursor();
208+
if self.cursor_position < self.msg.len() {
209+
}
210+
self.msg.remove(self.cursor_position);
211+
}
212+
return Ok(true);
213+
}
214+
KeyCode::Left => {
215+
self.decr_cursor();
216+
return Ok(true);
217+
}
218+
KeyCode::Right => {
219+
self.incr_cursor();
220+
return Ok(true);
221+
}
222+
KeyCode::Home => {
223+
self.cursor_position = 0;
224+
return Ok(true);
225+
}
226+
KeyCode::End => {
227+
self.cursor_position = self.msg.len();
136228
return Ok(true);
137229
}
138230
_ => (),

0 commit comments

Comments
 (0)