use crate::input::KeybindAction;
use crate::text::{
TabPolicy, byte_index_for_char_index, cell_column_for_char_index, cell_width_char,
char_index_from_cell_column, clip_to_cells,
};
use crate::widgets::WidgetArea;
use crate::window::CursorSpec;
use crate::{Color, ColorPair, Event, InteractionCache, InteractionId, Result, Window};
#[derive(Debug, Clone)]
pub struct TextInputState {
text: String,
cursor: usize, selection_anchor: Option<usize>, view_col: u16, focused: bool,
pub last_x: u16,
pub last_y: u16,
pub last_w: u16,
}
impl Default for TextInputState {
fn default() -> Self {
Self::new()
}
}
impl TextInputState {
pub fn new() -> Self {
Self {
text: String::new(),
cursor: 0,
selection_anchor: None,
view_col: 0,
focused: false,
last_x: 0,
last_y: 0,
last_w: 0,
}
}
pub fn text(&self) -> &str {
&self.text
}
pub fn set_text(&mut self, text: impl Into<String>) {
self.text = text.into();
self.cursor = self.len_chars();
self.selection_anchor = None;
self.view_col = 0;
}
pub fn clear(&mut self) {
self.text.clear();
self.cursor = 0;
self.selection_anchor = None;
self.view_col = 0;
}
pub fn is_focused(&self) -> bool {
self.focused
}
pub fn set_focused(&mut self, focused: bool) {
self.focused = focused;
if !focused {
self.selection_anchor = None;
}
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn selection(&self) -> Option<(usize, usize)> {
let a = self.selection_anchor?;
if a == self.cursor {
return None;
}
Some((a.min(self.cursor), a.max(self.cursor)))
}
pub fn clear_selection(&mut self) {
self.selection_anchor = None;
}
pub fn select_all(&mut self) {
let len = self.len_chars();
self.selection_anchor = Some(0);
self.cursor = len;
}
pub fn has_selection(&self) -> bool {
self.selection().is_some()
}
pub fn delete_selection(&mut self) -> bool {
let Some((start, end)) = self.selection() else {
return false;
};
self.delete_range_chars(start, end);
self.cursor = start;
self.selection_anchor = None;
true
}
pub fn insert_char(&mut self, ch: char) {
if self.delete_selection() {
}
self.insert_str_at_cursor(&ch.to_string());
}
pub fn insert_str(&mut self, s: &str) {
if s.is_empty() {
return;
}
if self.delete_selection() {
}
self.insert_str_at_cursor(s);
}
pub fn backspace(&mut self) {
if self.delete_selection() {
return;
}
if self.cursor == 0 {
return;
}
let start = self.cursor.saturating_sub(1);
let end = self.cursor;
self.delete_range_chars(start, end);
self.cursor = start;
}
pub fn delete_forward(&mut self) {
if self.delete_selection() {
return;
}
let len = self.len_chars();
if self.cursor >= len {
return;
}
self.delete_range_chars(self.cursor, self.cursor + 1);
}
pub fn move_left(&mut self, selecting: bool) {
self.begin_or_clear_selection(selecting);
self.cursor = self.cursor.saturating_sub(1);
if !selecting {
self.selection_anchor = None;
}
}
pub fn move_right(&mut self, selecting: bool) {
self.begin_or_clear_selection(selecting);
let len = self.len_chars();
self.cursor = (self.cursor + 1).min(len);
if !selecting {
self.selection_anchor = None;
}
}
pub fn move_home(&mut self, selecting: bool) {
self.begin_or_clear_selection(selecting);
self.cursor = 0;
if !selecting {
self.selection_anchor = None;
}
}
pub fn move_end(&mut self, selecting: bool) {
self.begin_or_clear_selection(selecting);
self.cursor = self.len_chars();
if !selecting {
self.selection_anchor = None;
}
}
pub fn copy_selection(&self) -> Option<String> {
let (start, end) = self.selection()?;
Some(self.slice_chars(start, end))
}
pub fn cut_selection(&mut self) -> Option<String> {
let (start, end) = self.selection()?;
let cut = self.slice_chars(start, end);
self.delete_range_chars(start, end);
self.cursor = start;
self.selection_anchor = None;
Some(cut)
}
pub fn handle_event(&mut self, event: Event) -> bool {
if !self.focused {
return false;
}
if let Event::KeyWithModifiers(k) = &event {
if k.mods.shift {
match k.key {
crate::KeyKind::Left => {
self.move_left(true);
return true;
}
crate::KeyKind::Right => {
self.move_right(true);
return true;
}
_ => {}
}
}
}
let event = event.as_legacy_key_event().unwrap_or(event);
match event {
Event::Character(c) => {
if !c.is_control() {
self.insert_char(c);
}
true
}
Event::Paste(text) => {
self.insert_str(&text);
true
}
Event::Backspace => {
self.backspace();
true
}
Event::Delete => {
self.delete_forward();
true
}
Event::KeyLeft => {
self.move_left(false);
true
}
Event::KeyRight => {
self.move_right(false);
true
}
Event::KeyUp | Event::KeyDown => {
false
}
Event::Enter => {
false
}
Event::Escape => {
self.clear_selection();
true
}
Event::Keybind(action) => match action {
KeybindAction::SelectAll => {
self.select_all();
true
}
KeybindAction::Copy => {
self.copy_selection();
true
}
KeybindAction::Cut => {
self.cut_selection();
true
}
KeybindAction::Paste => {
true
}
_ => false,
},
_ => false,
}
}
pub fn click_set_cursor(&mut self, x: u16) {
let local_x = x.saturating_sub(self.last_x);
let idx = self.char_index_from_cell_column(local_x.saturating_add(self.view_col));
self.cursor = idx;
self.selection_anchor = None;
}
pub fn drag_select_to(&mut self, x: u16) {
let clamped_x = if self.last_w == 0 {
self.last_x
} else {
let min_x = self.last_x;
let max_x_inclusive = self.last_x.saturating_add(self.last_w.saturating_sub(1));
x.clamp(min_x, max_x_inclusive)
};
let local_x = clamped_x.saturating_sub(self.last_x);
let idx = self.char_index_from_cell_column(local_x.saturating_add(self.view_col));
if self.selection_anchor.is_none() {
self.selection_anchor = Some(self.cursor);
}
self.cursor = idx;
}
fn ensure_cursor_visible(&mut self, field_cells: u16) {
if field_cells == 0 {
self.view_col = 0;
return;
}
let caret_col = self.cell_column_for_char_index(self.cursor);
if caret_col < self.view_col {
self.view_col = caret_col;
return;
}
let viewport_end = self.view_col.saturating_add(field_cells.saturating_sub(1));
if caret_col > viewport_end {
self.view_col = caret_col.saturating_sub(field_cells.saturating_sub(1));
}
}
fn begin_or_clear_selection(&mut self, selecting: bool) {
if selecting {
if self.selection_anchor.is_none() {
self.selection_anchor = Some(self.cursor);
}
} else {
self.selection_anchor = None;
}
}
fn len_chars(&self) -> usize {
self.text.chars().count()
}
fn insert_str_at_cursor(&mut self, s: &str) {
let byte_idx = self.byte_index_for_char_index(self.cursor);
self.text.insert_str(byte_idx, s);
self.cursor += s.chars().count();
}
fn delete_range_chars(&mut self, start: usize, end: usize) {
if start >= end {
return;
}
let a = self.byte_index_for_char_index(start);
let b = self.byte_index_for_char_index(end);
self.text.replace_range(a..b, "");
}
fn slice_chars(&self, start: usize, end: usize) -> String {
if start >= end {
return String::new();
}
let a = self.byte_index_for_char_index(start);
let b = self.byte_index_for_char_index(end);
self.text[a..b].to_string()
}
fn byte_index_for_char_index(&self, char_idx: usize) -> usize {
byte_index_for_char_index(&self.text, char_idx)
}
fn cell_column_for_char_index(&self, char_idx: usize) -> u16 {
cell_column_for_char_index(&self.text, char_idx)
}
fn char_index_from_cell_column(&self, col: u16) -> usize {
char_index_from_cell_column(&self.text, col)
}
}
#[derive(Debug, Clone)]
pub struct TextInput {
x: u16,
y: u16,
width: u16,
placeholder: Option<String>,
show_border: bool,
text_color: ColorPair,
placeholder_color: ColorPair,
selection_color: ColorPair,
border_color: ColorPair,
cursor_color: Option<ColorPair>, }
impl Default for TextInput {
fn default() -> Self {
Self::new()
}
}
impl TextInput {
pub fn new() -> Self {
Self {
x: 0,
y: 0,
width: 0,
placeholder: None,
show_border: false,
text_color: ColorPair::new(Color::White, Color::Transparent),
placeholder_color: ColorPair::new(Color::DarkGray, Color::Transparent),
selection_color: ColorPair::new(Color::Black, Color::LightBlue),
border_color: ColorPair::new(Color::LightGray, Color::Transparent),
cursor_color: None,
}
}
pub fn with_position(mut self, x: u16, y: u16) -> Self {
self.x = x;
self.y = y;
self
}
pub fn with_width(mut self, width: u16) -> Self {
self.width = width;
self
}
pub fn with_placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = Some(placeholder.into());
self
}
pub fn with_border(mut self, border: bool) -> Self {
self.show_border = border;
self
}
pub fn with_text_color(mut self, colors: ColorPair) -> Self {
self.text_color = colors;
self
}
pub fn with_placeholder_color(mut self, colors: ColorPair) -> Self {
self.placeholder_color = colors;
self
}
pub fn with_selection_color(mut self, colors: ColorPair) -> Self {
self.selection_color = colors;
self
}
pub fn with_border_color(mut self, colors: ColorPair) -> Self {
self.border_color = colors;
self
}
pub fn with_cursor_cell_color(mut self, colors: ColorPair) -> Self {
self.cursor_color = Some(colors);
self
}
pub fn draw(&self, window: &mut dyn Window, state: &mut TextInputState) -> Result<()> {
state.last_x = self.x;
state.last_y = self.y;
state.last_w = self.width;
if self.width == 0 {
return Ok(());
}
let (content_x, content_w) = if self.show_border {
window.write_str_colored(self.y, self.x, "[", self.border_color)?;
window.write_str_colored(
self.y,
self.x + self.width.saturating_sub(1),
"]",
self.border_color,
)?;
(self.x.saturating_add(1), self.width.saturating_sub(2))
} else {
(self.x, self.width)
};
if content_w > 0 {
let spaces = " ".repeat(content_w as usize);
window.write_str(self.y, content_x, &spaces)?;
}
let has_text = !state.text.is_empty();
let display_owned: String = if has_text {
state.text.clone()
} else {
self.placeholder.clone().unwrap_or_default()
};
state.ensure_cursor_visible(content_w.saturating_sub(1));
let left_skip = state.view_col;
let visible = content_w;
let after_skip = if left_skip == 0 {
display_owned.clone()
} else {
let mut acc: u16 = 0;
let mut start_char = 0usize;
for (i, ch) in display_owned.chars().enumerate() {
let w = cell_width_char(ch);
if w == 0 {
continue;
}
if acc.saturating_add(w) > left_skip {
start_char = i;
break;
}
acc = acc.saturating_add(w);
start_char = i + 1;
}
display_owned.chars().skip(start_char).collect::<String>()
};
let clipped = clip_to_cells(&after_skip, visible, TabPolicy::SingleCell);
if has_text {
self.draw_with_selection(window, state, content_x, content_w)?;
} else {
window.write_str_colored(self.y, content_x, &clipped, self.placeholder_color)?;
}
if state.focused {
let caret_col = state.cell_column_for_char_index(state.cursor);
let caret_visible_col = caret_col.saturating_sub(state.view_col);
let caret_x =
content_x.saturating_add(caret_visible_col.min(content_w.saturating_sub(1)));
window.request_cursor(CursorSpec {
x: caret_x,
y: self.y,
visible: true,
});
if let Some(colors) = self.cursor_color {
let ch = self
.char_at_cell_column(&state.text, caret_col)
.unwrap_or(' ');
window.write_str_colored(self.y, caret_x, &ch.to_string(), colors)?;
}
}
Ok(())
}
pub fn draw_with_id(
&self,
window: &mut dyn Window,
state: &mut TextInputState,
ui: &mut InteractionCache,
id: InteractionId,
) -> Result<()> {
let height: u16 = 1;
let area = WidgetArea::new(self.x, self.y, self.width, height);
ui.register_focusable(id, area);
if state.is_focused() {
ui.register_draggable(id, area);
}
self.draw(window, state)
}
fn draw_with_selection(
&self,
window: &mut dyn Window,
state: &TextInputState,
content_x: u16,
content_w: u16,
) -> Result<()> {
if content_w == 0 {
return Ok(());
}
let visible_cells = content_w;
let view_start = state.view_col;
let view_end = state.view_col.saturating_add(visible_cells);
let selection = state.selection();
let mut col: u16 = 0;
let mut abs_col: u16 = 0;
for (i, ch) in state.text.chars().enumerate() {
let w = cell_width_char(ch);
if w == 0 {
continue;
}
let ch_start = abs_col;
let ch_end = abs_col.saturating_add(w);
if ch_end <= view_start {
abs_col = ch_end;
continue;
}
if ch_start >= view_end {
break;
}
let vis_x = ch_start.saturating_sub(view_start);
if vis_x >= visible_cells {
break;
}
let in_sel = selection.map(|(a, b)| i >= a && i < b).unwrap_or(false);
let colors = if in_sel {
self.selection_color
} else {
self.text_color
};
window.write_str_colored(state.last_y, content_x + vis_x, &ch.to_string(), colors)?;
abs_col = ch_end;
col = col.saturating_add(w);
}
Ok(())
}
fn char_at_cell_column(&self, s: &str, col: u16) -> Option<char> {
let mut acc: u16 = 0;
for ch in s.chars() {
let w = cell_width_char(ch);
if w == 0 {
continue;
}
let next = acc.saturating_add(w);
if col < next {
return Some(ch);
}
acc = next;
}
None
}
}