use crate::_private::NonExhaustive;
use crate::event::{ReadOnly, TextOutcome};
use crate::util;
#[allow(unused_imports)]
use log::debug;
use rat_event::util::MouseFlags;
use rat_event::{ct_event, FocusKeys, HandleEvent, MouseOnly};
use rat_focus::{FocusFlag, HasFocusFlag};
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::prelude::BlockExt;
use ratatui::style::{Style, Stylize};
use ratatui::widgets::{Block, StatefulWidget, StatefulWidgetRef, Widget};
use std::cmp::{max, min};
use std::ops::Range;
use unicode_segmentation::UnicodeSegmentation;
#[derive(Debug, Default, Clone)]
pub struct TextInput<'a> {
block: Option<Block<'a>>,
style: Style,
focus_style: Option<Style>,
select_style: Option<Style>,
invalid_style: Option<Style>,
}
#[derive(Debug, Clone)]
pub struct TextInputStyle {
pub style: Style,
pub focus: Option<Style>,
pub select: Option<Style>,
pub invalid: Option<Style>,
pub non_exhaustive: NonExhaustive,
}
#[derive(Debug, Clone)]
pub struct TextInputState {
pub focus: FocusFlag,
pub invalid: bool,
pub area: Rect,
pub inner: Rect,
pub mouse: MouseFlags,
pub value: core::InputCore,
pub non_exhaustive: NonExhaustive,
}
impl Default for TextInputStyle {
fn default() -> Self {
Self {
style: Default::default(),
focus: Default::default(),
select: Default::default(),
invalid: Default::default(),
non_exhaustive: NonExhaustive,
}
}
}
impl<'a> TextInput<'a> {
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn styles(mut self, style: TextInputStyle) -> Self {
self.style = style.style;
self.focus_style = style.focus;
self.select_style = style.select;
self.invalid_style = style.invalid;
self
}
#[inline]
pub fn style(mut self, style: impl Into<Style>) -> Self {
self.style = style.into();
self
}
#[inline]
pub fn focus_style(mut self, style: impl Into<Style>) -> Self {
self.focus_style = Some(style.into());
self
}
#[inline]
pub fn select_style(mut self, style: impl Into<Style>) -> Self {
self.select_style = Some(style.into());
self
}
#[inline]
pub fn invalid_style(mut self, style: impl Into<Style>) -> Self {
self.invalid_style = Some(style.into());
self
}
#[inline]
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
}
impl<'a> StatefulWidgetRef for TextInput<'a> {
type State = TextInputState;
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
render_ref(self, area, buf, state);
}
}
impl<'a> StatefulWidget for TextInput<'a> {
type State = TextInputState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
render_ref(&self, area, buf, state);
}
}
fn render_ref(widget: &TextInput<'_>, area: Rect, buf: &mut Buffer, state: &mut TextInputState) {
state.area = area;
state.inner = widget.block.inner_if_some(area);
state.value.set_width(state.inner.width as usize);
widget.block.render(area, buf);
let area = state.inner.intersection(buf.area);
let focus_style = if let Some(focus_style) = widget.focus_style {
focus_style
} else {
widget.style
};
let select_style = if let Some(select_style) = widget.select_style {
select_style
} else {
Style::default().on_yellow()
};
let invalid_style = if let Some(invalid_style) = widget.invalid_style {
invalid_style
} else {
Style::default().red()
};
let (style, select_style) = if state.focus.get() {
if state.invalid {
(
focus_style.patch(invalid_style),
select_style.patch(invalid_style),
)
} else {
(focus_style, select_style)
}
} else {
if state.invalid {
(
widget.style.patch(invalid_style),
widget.style.patch(invalid_style),
)
} else {
(widget.style, widget.style)
}
};
buf.set_style(area, style);
let selection = state.value.selection();
let ox = state.offset();
let mut cit = state.value.value().graphemes(true).skip(state.offset());
let mut col = 0;
let mut cx = 0;
loop {
if col >= area.width {
break;
}
let ch = if let Some(c) = cit.next() { c } else { " " };
let tx = cx + ox;
let style = if selection.contains(&tx) {
select_style
} else {
style
};
let cell = buf.get_mut(area.x + col, area.y);
cell.set_symbol(ch);
cell.set_style(style);
let ww = unicode_display_width::width(ch) as u16;
for x in 1..ww {
let cell = buf.get_mut(area.x + col + x, area.y);
cell.set_symbol("");
cell.set_style(style);
}
col += max(ww, 1);
cx += 1;
}
}
impl Default for TextInputState {
fn default() -> Self {
Self {
focus: Default::default(),
invalid: false,
area: Default::default(),
inner: Default::default(),
mouse: Default::default(),
value: Default::default(),
non_exhaustive: NonExhaustive,
}
}
}
impl TextInputState {
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn set_invalid(&mut self, invalid: bool) {
self.invalid = invalid;
}
#[inline]
pub fn get_invalid(&self) -> bool {
self.invalid
}
#[inline]
pub fn clear(&mut self) -> bool {
if self.is_empty() {
false
} else {
self.value.clear();
true
}
}
#[inline]
pub fn offset(&self) -> usize {
self.value.offset()
}
#[inline]
pub fn set_offset(&mut self, offset: usize) {
self.value.set_offset(offset);
}
#[inline]
pub fn cursor(&self) -> usize {
self.value.cursor()
}
#[inline]
pub fn set_cursor(&mut self, cursor: usize, extend_selection: bool) -> bool {
self.value.set_cursor(cursor, extend_selection)
}
#[inline]
pub fn anchor(&self) -> usize {
self.value.anchor()
}
#[inline]
pub fn value(&self) -> &str {
self.value.value()
}
#[inline]
pub fn set_value<S: Into<String>>(&mut self, s: S) {
self.value.set_value(s);
}
#[inline]
pub fn is_empty(&self) -> bool {
self.value.is_empty()
}
#[inline]
pub fn len(&self) -> usize {
self.value.len()
}
#[inline]
pub fn has_selection(&self) -> bool {
self.value.has_selection()
}
#[inline]
pub fn selection(&self) -> Range<usize> {
self.value.selection()
}
#[inline]
pub fn set_selection(&mut self, anchor: usize, cursor: usize) -> bool {
let old_selection = self.value.selection();
self.value.set_cursor(anchor, false);
self.value.set_cursor(cursor, true);
old_selection != self.value.selection()
}
#[inline]
pub fn select_all(&mut self) -> bool {
let old_selection = self.value.selection();
self.value.set_cursor(0, false);
self.value.set_cursor(self.value.len(), true);
old_selection != self.value.selection()
}
#[inline]
pub fn selected_value(&self) -> &str {
util::split3(self.value.value(), self.value.selection()).1
}
#[inline]
pub fn insert_char(&mut self, c: char) -> bool {
self.value.insert_char(c)
}
#[inline]
pub fn delete_range(&mut self, range: Range<usize>) -> bool {
if range.is_empty() {
false
} else {
self.value.remove(range);
true
}
}
#[inline]
pub fn delete_next_word(&mut self) -> bool {
if self.value.has_selection() {
self.delete_range(self.value.selection())
} else {
let cp = self.value.cursor();
if let Some(ep) = self.value.next_word_boundary(cp) {
self.delete_range(cp..ep)
} else {
false
}
}
}
#[inline]
pub fn delete_prev_word(&mut self) -> bool {
if self.value.has_selection() {
self.delete_range(self.value.selection())
} else {
let cp = self.value.cursor();
if let Some(sp) = self.value.prev_word_boundary(cp) {
self.delete_range(sp..cp)
} else {
false
}
}
}
#[inline]
pub fn delete_prev_char(&mut self) -> bool {
if self.value.has_selection() {
self.value.remove(self.value.selection())
} else if self.value.cursor() == 0 {
false
} else {
self.value
.remove(self.value.cursor() - 1..self.value.cursor());
true
}
}
#[inline]
pub fn delete_next_char(&mut self) -> bool {
if self.value.has_selection() {
self.value.remove(self.value.selection())
} else if self.value.cursor() == self.value.len() {
false
} else {
self.value
.remove(self.value.cursor()..self.value.cursor() + 1);
true
}
}
#[inline]
pub fn move_to_next_word(&mut self, extend_selection: bool) -> bool {
let cp = self.value.cursor();
if let Some(cp) = self.value.next_word_boundary(cp) {
self.value.set_cursor(cp, extend_selection)
} else {
false
}
}
#[inline]
pub fn move_to_prev_word(&mut self, extend_selection: bool) -> bool {
let cp = self.value.cursor();
if let Some(cp) = self.value.prev_word_boundary(cp) {
self.value.set_cursor(cp, extend_selection)
} else {
false
}
}
#[inline]
pub fn move_to_next(&mut self, extend_selection: bool) -> bool {
let c = min(self.value.cursor() + 1, self.value.len());
self.value.set_cursor(c, extend_selection)
}
#[inline]
pub fn move_to_prev(&mut self, extend_selection: bool) -> bool {
let c = self.value.cursor().saturating_sub(1);
self.value.set_cursor(c, extend_selection)
}
#[inline]
pub fn move_to_line_start(&mut self, extend_selection: bool) -> bool {
let c = 0;
self.value.set_cursor(c, extend_selection)
}
#[inline]
pub fn move_to_line_end(&mut self, extend_selection: bool) -> bool {
let c = self.value.len();
self.value.set_cursor(c, extend_selection)
}
pub fn to_screen_col(&self, pos: usize) -> Option<u16> {
let px = pos;
let ox = self.value.offset();
let mut sx = 0;
let line = self.value.value_graphemes();
for c in line.skip(ox).take(px - ox) {
sx += unicode_display_width::width(c) as usize;
}
Some(sx as u16)
}
pub fn from_screen_col(&self, x: usize) -> Option<usize> {
let mut cx = 0;
let ox = self.value.offset();
let line = self.value.value_graphemes();
let mut test = 0;
for c in line.skip(ox) {
if test >= x {
break;
}
test += unicode_display_width::width(c) as usize;
cx += 1;
}
Some(cx + ox)
}
#[inline]
pub fn set_screen_cursor(&mut self, cursor: isize, extend_selection: bool) -> bool {
let sc = cursor;
let c = if sc < 0 {
self.value.offset().saturating_sub(-sc as usize)
} else {
if let Some(c) = self.from_screen_col(sc as usize) {
c
} else {
self.value.len()
}
};
let old_cursor = self.value.cursor();
let old_anchor = self.value.anchor();
self.value.set_cursor(c, extend_selection);
old_cursor != self.value.cursor() || old_anchor != self.value.anchor()
}
#[inline]
pub fn screen_cursor(&self) -> Option<(u16, u16)> {
if self.is_focused() {
let cx = self.value.cursor();
let ox = self.value.offset();
if cx < ox {
None
} else if cx > ox + self.inner.width as usize {
None
} else {
let sc = self.to_screen_col(cx).expect("valid_cursor");
Some((self.inner.x + sc, self.inner.y))
}
} else {
None
}
}
}
impl HasFocusFlag for TextInputState {
#[inline]
fn focus(&self) -> &FocusFlag {
&self.focus
}
#[inline]
fn area(&self) -> Rect {
self.area
}
}
impl HandleEvent<crossterm::event::Event, FocusKeys, TextOutcome> for TextInputState {
fn handle(&mut self, event: &crossterm::event::Event, _keymap: FocusKeys) -> TextOutcome {
let mut r = if self.is_focused() {
match event {
ct_event!(key press c)
| ct_event!(key press SHIFT-c)
| ct_event!(key press CONTROL_ALT-c) => self.insert_char(*c).into(),
ct_event!(keycode press Backspace) => self.delete_prev_char().into(),
ct_event!(keycode press Delete) => self.delete_next_char().into(),
ct_event!(keycode press CONTROL-Backspace) => self.delete_prev_word().into(),
ct_event!(keycode press CONTROL-Delete) => self.delete_next_word().into(),
ct_event!(key press CONTROL-'d') => self.clear().into(),
ct_event!(key release _)
| ct_event!(key release SHIFT-_)
| ct_event!(key release CONTROL_ALT-_)
| ct_event!(keycode release Backspace)
| ct_event!(keycode release Delete)
| ct_event!(keycode release CONTROL-Backspace)
| ct_event!(keycode release CONTROL-Delete)
| ct_event!(key release CONTROL-'d') => TextOutcome::Unchanged,
_ => TextOutcome::NotUsed,
}
} else {
TextOutcome::NotUsed
};
if r == TextOutcome::Changed {
r = TextOutcome::TextChanged;
}
if r == TextOutcome::NotUsed {
r = self.handle(event, ReadOnly);
}
r
}
}
impl HandleEvent<crossterm::event::Event, ReadOnly, TextOutcome> for TextInputState {
fn handle(&mut self, event: &crossterm::event::Event, _keymap: ReadOnly) -> TextOutcome {
let mut r = if self.is_focused() {
match event {
ct_event!(keycode press Left) => self.move_to_prev(false).into(),
ct_event!(keycode press Right) => self.move_to_next(false).into(),
ct_event!(keycode press CONTROL-Left) => self.move_to_prev_word(false).into(),
ct_event!(keycode press CONTROL-Right) => self.move_to_next_word(false).into(),
ct_event!(keycode press Home) => self.move_to_line_start(false).into(),
ct_event!(keycode press End) => self.move_to_line_end(false).into(),
ct_event!(keycode press SHIFT-Left) => self.move_to_prev(true).into(),
ct_event!(keycode press SHIFT-Right) => self.move_to_next(true).into(),
ct_event!(keycode press CONTROL_SHIFT-Left) => self.move_to_prev_word(true).into(),
ct_event!(keycode press CONTROL_SHIFT-Right) => self.move_to_next_word(true).into(),
ct_event!(keycode press SHIFT-Home) => self.move_to_line_start(true).into(),
ct_event!(keycode press SHIFT-End) => self.move_to_line_end(true).into(),
ct_event!(key press CONTROL-'a') => self.select_all().into(),
ct_event!(keycode release Left)
| ct_event!(keycode release Right)
| ct_event!(keycode release CONTROL-Left)
| ct_event!(keycode release CONTROL-Right)
| ct_event!(keycode release Home)
| ct_event!(keycode release End)
| ct_event!(keycode release SHIFT-Left)
| ct_event!(keycode release SHIFT-Right)
| ct_event!(keycode release CONTROL_SHIFT-Left)
| ct_event!(keycode release CONTROL_SHIFT-Right)
| ct_event!(keycode release SHIFT-Home)
| ct_event!(keycode release SHIFT-End)
| ct_event!(key release CONTROL-'a') => TextOutcome::Unchanged,
_ => TextOutcome::NotUsed,
}
} else {
TextOutcome::NotUsed
};
if r == TextOutcome::NotUsed {
r = self.handle(event, MouseOnly);
}
r
}
}
impl HandleEvent<crossterm::event::Event, MouseOnly, TextOutcome> for TextInputState {
fn handle(&mut self, event: &crossterm::event::Event, _keymap: MouseOnly) -> TextOutcome {
match event {
ct_event!(mouse any for m) if self.mouse.drag(self.area, m) => {
let c = (m.column as isize) - (self.inner.x as isize);
self.set_screen_cursor(c, true).into()
}
ct_event!(mouse down Left for column,row) => {
if self.gained_focus() {
TextOutcome::Unchanged
} else if self.inner.contains((*column, *row).into()) {
let c = column - self.inner.x;
self.set_screen_cursor(c as isize, false).into()
} else {
TextOutcome::NotUsed
}
}
_ => TextOutcome::NotUsed,
}
}
}
pub fn handle_events(
state: &mut TextInputState,
focus: bool,
event: &crossterm::event::Event,
) -> TextOutcome {
state.focus.set(focus);
state.handle(event, FocusKeys)
}
pub fn handle_readonly_events(
state: &mut TextInputState,
focus: bool,
event: &crossterm::event::Event,
) -> TextOutcome {
state.focus.set(focus);
state.handle(event, ReadOnly)
}
pub fn handle_mouse_events(
state: &mut TextInputState,
event: &crossterm::event::Event,
) -> TextOutcome {
state.handle(event, MouseOnly)
}
pub mod core {
use crate::util;
use crate::util::gr_len;
#[allow(unused_imports)]
use log::debug;
use std::cmp::min;
use std::iter::once;
use std::mem;
use std::ops::Range;
use unicode_segmentation::{Graphemes, UnicodeSegmentation};
#[derive(Debug, Default, Clone)]
pub struct InputCore {
value: String,
len: usize,
offset: usize,
width: usize,
cursor: usize,
anchor: usize,
char_buf: String,
buf: String,
}
impl InputCore {
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn offset(&self) -> usize {
self.offset
}
pub fn set_offset(&mut self, offset: usize) {
if offset > self.len {
self.offset = self.len;
} else if offset > self.cursor {
self.offset = self.cursor;
} else if offset + self.width < self.cursor {
self.offset = self.cursor - self.width;
} else {
self.offset = offset;
}
}
#[inline]
pub fn width(&self) -> usize {
self.width
}
#[inline]
pub fn set_width(&mut self, width: usize) {
self.width = width;
if self.offset + width < self.cursor {
self.offset = self.cursor - self.width;
}
}
pub fn set_cursor(&mut self, cursor: usize, extend_selection: bool) -> bool {
let old_selection = (self.cursor, self.anchor);
let c = min(self.len, cursor);
self.cursor = c;
if !extend_selection {
self.anchor = c;
}
if self.offset > c {
self.offset = c;
} else if self.offset + self.width < c {
self.offset = c - self.width;
}
(self.cursor, self.anchor) != old_selection
}
#[inline]
pub fn cursor(&self) -> usize {
self.cursor
}
#[inline]
pub fn anchor(&self) -> usize {
self.anchor
}
#[inline]
pub fn set_value<S: Into<String>>(&mut self, s: S) {
self.value = s.into();
self.len = self.value.graphemes(true).count();
self.cursor = 0;
self.offset = 0;
self.anchor = 0;
}
#[inline]
pub fn value(&self) -> &str {
self.value.as_str()
}
#[inline]
pub fn value_graphemes(&self) -> Graphemes<'_> {
self.value.graphemes(true)
}
#[inline]
pub fn clear(&mut self) {
self.set_value("");
}
#[inline]
pub fn is_empty(&self) -> bool {
self.value.is_empty()
}
#[inline]
pub fn len(&self) -> usize {
self.len
}
#[inline]
pub fn has_selection(&self) -> bool {
self.anchor != self.cursor
}
#[inline]
pub fn selection(&self) -> Range<usize> {
if self.cursor < self.anchor {
self.cursor..self.anchor
} else {
self.anchor..self.cursor
}
}
pub fn byte_pos(&self, byte_pos: usize) -> Option<usize> {
let mut pos = None;
for (gp, (bp, _cc)) in self
.value
.grapheme_indices(true)
.chain(once((self.len(), "")))
.enumerate()
{
if bp >= byte_pos {
pos = Some(gp);
break;
}
}
pos
}
pub fn byte_at(&self, pos: usize) -> Option<(usize, usize)> {
let mut byte_pos = None;
for (gp, (bp, cc)) in self
.value
.grapheme_indices(true)
.chain(once((self.value.len(), "")))
.enumerate()
{
if gp == pos {
byte_pos = Some((bp, bp + cc.len()));
break;
}
}
byte_pos
}
pub fn char_at(&self, pos: usize) -> Option<usize> {
let mut char_pos = 0;
for (gp, (_bp, cc)) in self
.value
.grapheme_indices(true)
.chain(once((self.len(), "")))
.enumerate()
{
if gp == pos {
return Some(char_pos);
}
char_pos += cc.chars().count();
}
None
}
pub fn char_pos(&self, char_pos: usize) -> Option<usize> {
let mut cp = 0;
for (gp, (_bp, cc)) in self
.value
.grapheme_indices(true)
.chain(once((self.len(), "")))
.enumerate()
{
if cp >= char_pos {
return Some(gp);
}
cp += cc.chars().count();
}
None
}
pub fn next_word_boundary(&self, pos: usize) -> Option<usize> {
let byte_pos = self.byte_at(pos)?;
let (_, str_after) = self.value.split_at(byte_pos.0);
let mut it = str_after.graphemes(true);
let mut init = true;
let mut gp = 0;
loop {
let Some(c) = it.next() else {
break;
};
if init {
if let Some(c) = c.chars().next() {
if !c.is_whitespace() {
init = false;
}
}
} else {
if let Some(c) = c.chars().next() {
if c.is_whitespace() {
break;
}
}
}
gp += 1;
}
Some(pos + gp)
}
pub fn prev_word_boundary(&self, pos: usize) -> Option<usize> {
let byte_pos = self.byte_at(pos)?;
let (str_before, _) = self.value.split_at(byte_pos.0);
let mut it = str_before.graphemes(true).rev();
let mut init = true;
let mut gp = gr_len(str_before);
loop {
let Some(c) = it.next() else {
break;
};
if init {
if let Some(c) = c.chars().next() {
if !c.is_whitespace() {
init = false;
}
}
} else {
if let Some(c) = c.chars().next() {
if c.is_whitespace() {
break;
}
}
}
gp -= 1;
}
Some(gp)
}
pub fn insert_char(&mut self, new: char) -> bool {
let selection = self.selection();
let mut char_buf = mem::take(&mut self.char_buf);
char_buf.clear();
char_buf.push(new);
let r = self.replace(selection, char_buf.as_str());
self.char_buf = char_buf;
r
}
#[inline]
pub fn remove(&mut self, range: Range<usize>) -> bool {
self.replace(range, "")
}
pub fn replace(&mut self, range: Range<usize>, new: &str) -> bool {
let (before_str, remove_str, after_str) =
util::split3(self.value.as_str(), range.clone());
self.buf.clear();
self.buf.push_str(before_str);
self.buf.push_str(remove_str);
let old_end = self.buf.graphemes(true).count();
self.buf.clear();
self.buf.push_str(before_str);
self.buf.push_str(new);
let new_end = self.buf.graphemes(true).count();
self.buf.push_str(after_str);
mem::swap(&mut self.value, &mut self.buf);
let change_len = new_end as isize - old_end as isize;
self.len = (self.len as isize + change_len) as usize;
if self.cursor < range.start {
} else if self.cursor <= range.end {
self.cursor = new_end;
} else {
self.cursor = (self.cursor as isize + change_len) as usize;
}
if self.anchor < range.start {
} else if self.anchor <= range.end {
self.anchor = new_end;
} else {
self.anchor = (self.anchor as isize + change_len) as usize;
}
if self.offset > self.cursor {
self.offset = self.cursor;
} else if self.offset + self.width < self.cursor {
self.offset = self.cursor - self.width;
}
true
}
}
}