use std::borrow::Cow;
use ratatui::prelude::*;
use crate::prelude::*;
use super::*;
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct Textbox {
pub name: Cow<'static, str>,
pub hidden: bool,
value: String,
caret: usize,
}
impl Textbox {
pub fn set_value(&mut self, value: impl Into<String>) {
self.value = value.into();
self.caret = self.max_caret();
}
pub fn value(&self) -> &str {
&self.value
}
fn split_caret(&self) -> [&str; 3] {
let (a, b) = self.value.split_at(self.caret);
let (b, c) = b.chars()
.nth(0)
.map(|first| b.split_at(first.len_utf8()))
.unwrap_or(("", ""));
[a, b, c]
}
fn max_caret(&self) -> usize {
self.value.len()
}
fn step(&self, direction: Direction) -> usize {
let [pre, caret, _] = self.split_caret();
match direction {
Direction::Left => pre.chars()
.nth_back(0)
.map(|last| self.caret - last.len_utf8())
.unwrap_or(0),
Direction::Right => self.caret + caret.len(),
}
}
fn scan(&self, direction: Direction) -> usize {
let [pre, caret, post] = self.split_caret();
let (string, fallback) = match direction {
Direction::Left => (pre, 0),
Direction::Right => (post, self.max_caret()),
};
if self.hidden {
return fallback
}
fn iter(mut it: impl Iterator<Item = (usize, char)>, mut prev_ws: bool) -> Option<usize> {
it.find_map(|(index, curr)| {
let curr_ws = curr.is_whitespace();
let valid = !prev_ws && curr_ws;
prev_ws = curr_ws;
valid.then_some(index)
})
}
let chars = string.char_indices();
let index = match direction {
Direction::Left => iter(chars.rev(), true),
Direction::Right => iter(chars, caret
.chars()
.nth_back(0)
.map_or(false, char::is_whitespace)
)
.map(|index| index + self.caret + caret.len()),
};
index.unwrap_or(fallback)
}
}
impl Field for Textbox {
type Value = String;
type Builder = Builder<false>;
fn name(&self) -> &str {
&self.name
}
fn input(&mut self, key: KeyEvent) -> InputResult {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
let (new_caret, result) = match (key.code, ctrl) {
(KeyCode::Left, false) => (self.step(Direction::Left), InputResult::Consumed),
(KeyCode::Right, false) => (self.step(Direction::Right), InputResult::Consumed),
(KeyCode::Left, true) => (self.scan(Direction::Left), InputResult::Consumed),
(KeyCode::Right, true) => (self.scan(Direction::Right), InputResult::Consumed),
(KeyCode::Home, _) => (0, InputResult::Consumed),
(KeyCode::End, _) => (self.max_caret(), InputResult::Consumed),
(KeyCode::Backspace, false) if self.caret > 0 => {
let new = self.step(Direction::Left);
self.value.remove(new);
(new, InputResult::Updated)
}
(KeyCode::Delete, false) if self.caret < self.max_caret() => {
self.value.remove(self.caret);
(self.caret, InputResult::Updated)
}
(KeyCode::Backspace | KeyCode::Char('w'), true) if self.caret > 0 => {
let end = self.scan(Direction::Left);
self.value.drain(end..self.caret);
(end, InputResult::Updated)
}
(KeyCode::Delete | KeyCode::Char('d'), true) if self.caret < self.max_caret() => {
let end = self.scan(Direction::Right);
self.value.drain(self.caret..end);
(self.caret, InputResult::Updated)
}
(KeyCode::Char(c), false) => {
self.value.insert(self.caret, c);
(self.caret + c.len_utf8(), InputResult::Updated)
}
_ => (self.caret, InputResult::Ignored),
};
self.caret = new_caret;
result
}
fn format(&self, focused: bool) -> Text {
let visibility = match self.hidden {
true => |s: &str| s.chars()
.map(|_| '•')
.collect(),
false => ToOwned::to_owned,
};
match focused {
true => {
let [pre, caret, post] = self.split_caret().map(visibility);
let caret = match caret.is_empty() {
true => " ".to_owned(),
false => caret,
};
Line::from(vec![
Span::raw(pre),
Span::styled(caret, Style::new().reversed()),
Span::raw(post),
]).into()
}
false => {
visibility(&self.value).into()
}
}
}
fn value(&self) -> &String {
&self.value
}
fn into_value(self) -> String {
self.value
}
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct Builder<const NAME: bool>(Textbox);
impl Default for Builder<false> {
fn default() -> Self {
Self(Textbox {
name: Default::default(),
value: Default::default(),
hidden: false,
caret: 0,
})
}
}
impl<const NAME: bool> Builder<NAME> {
pub fn name(self, name: impl Into<Cow<'static, str>>) -> Builder<true> {
let name = name.into();
Builder(Textbox{ name, ..self.0 })
}
pub fn value(mut self, value: impl Into<String>) -> Self {
self.0.set_value(value);
self
}
pub fn hidden(self) -> Self {
Builder(Textbox{ hidden: true, ..self.0 })
}
}
impl Build for Builder<true> {
type Field = Textbox;
fn build(self) -> Textbox {
self.0
}
}
enum Direction {
Left,
Right,
}