use std::fmt;
use ratatui::{
Frame,
layout::Rect,
style::Style,
widgets::{Block, Borders},
};
use tui_textarea::{CursorMove, CursorRenderMode, TextArea, WrapMode};
use super::theme::MissionControlTheme;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SingleLineDisplay {
Plain,
Redacted,
}
#[derive(Clone)]
pub(crate) struct SingleLineField {
textarea: TextArea<'static>,
display: SingleLineDisplay,
}
impl SingleLineField {
pub(crate) fn plain() -> Self {
Self::new(SingleLineDisplay::Plain)
}
pub(crate) fn redacted() -> Self {
Self::new(SingleLineDisplay::Redacted)
}
fn new(display: SingleLineDisplay) -> Self {
Self {
textarea: configured_textarea(String::new()),
display,
}
}
pub(crate) fn value(&self) -> &str {
self.textarea.lines().first().map_or("", String::as_str)
}
pub(crate) fn is_empty(&self) -> bool {
self.value().is_empty()
}
pub(crate) fn set_value(&mut self, value: impl AsRef<str>) {
let normalized = normalize_single_line(value.as_ref());
self.textarea = configured_textarea(normalized);
self.textarea.move_cursor(CursorMove::End);
}
pub(crate) fn insert_char(&mut self, ch: char) -> bool {
let mut encoded = [0; 4];
self.insert_str(ch.encode_utf8(&mut encoded))
}
pub(crate) fn insert_str(&mut self, value: &str) -> bool {
let normalized = normalize_single_line(value);
!normalized.is_empty() && self.textarea.insert_str(normalized)
}
pub(crate) fn backspace(&mut self) -> bool {
self.textarea.delete_char()
}
pub(crate) fn delete_forward(&mut self) -> bool {
self.textarea.delete_next_char()
}
pub(crate) fn move_left(&mut self) {
self.textarea.move_cursor(CursorMove::Back);
}
pub(crate) fn move_right(&mut self) {
self.textarea.move_cursor(CursorMove::Forward);
}
pub(crate) fn move_home(&mut self) {
self.textarea.move_cursor(CursorMove::Head);
}
pub(crate) fn move_end(&mut self) {
self.textarea.move_cursor(CursorMove::End);
}
pub(crate) fn clear(&mut self) {
self.textarea = configured_textarea(String::new());
}
pub(crate) fn take(&mut self) -> Option<String> {
if self.value().trim().is_empty() {
return None;
}
let value = self.value().to_string();
self.clear();
Some(value)
}
pub(crate) fn render(
&self,
frame: &mut Frame<'_>,
area: Rect,
label: &str,
placeholder: &str,
focused: bool,
theme: MissionControlTheme,
) {
if area.is_empty() {
return;
}
let title = if focused {
format!(" {label} • editing ")
} else {
format!(" {label} ")
};
let block = Block::default()
.borders(Borders::ALL)
.title(title)
.border_style(theme.border(focused))
.style(theme.panel_alt());
let inner = block.inner(area);
frame.render_widget(block, area);
if inner.is_empty() {
return;
}
let mut projection = self.render_projection();
projection.set_style(theme.panel_alt());
projection.set_cursor_line_style(Style::default());
projection.set_cursor_render_mode(CursorRenderMode::Hidden);
projection.set_placeholder_text(placeholder);
projection.set_placeholder_style(theme.muted(false));
frame.render_widget(&projection, inner);
if focused && let Some(position) = projection.rendered_cursor_position() {
frame.set_cursor_position(position);
}
}
fn render_projection(&self) -> TextArea<'static> {
let cursor_col = self.textarea.cursor().1;
let display = match self.display {
SingleLineDisplay::Plain => self.value().to_string(),
SingleLineDisplay::Redacted => "•".repeat(self.value().chars().count()),
};
let mut projection = configured_textarea(display);
for _ in 0..cursor_col {
projection.move_cursor(CursorMove::Forward);
}
projection
}
}
impl Default for SingleLineField {
fn default() -> Self {
Self::plain()
}
}
impl fmt::Debug for SingleLineField {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let value = match self.display {
SingleLineDisplay::Plain => self.value(),
SingleLineDisplay::Redacted if self.is_empty() => "<empty>",
SingleLineDisplay::Redacted => "<redacted>",
};
formatter
.debug_struct("SingleLineField")
.field("value", &value)
.field("cursor", &self.textarea.cursor().1)
.field("display", &self.display)
.finish()
}
}
impl PartialEq for SingleLineField {
fn eq(&self, other: &Self) -> bool {
self.value() == other.value()
&& self.textarea.cursor() == other.textarea.cursor()
&& self.display == other.display
}
}
impl PartialEq<&str> for SingleLineField {
fn eq(&self, other: &&str) -> bool {
self.value() == *other
}
}
impl PartialEq<String> for SingleLineField {
fn eq(&self, other: &String) -> bool {
self.value() == other
}
}
impl Eq for SingleLineField {}
fn configured_textarea(value: String) -> TextArea<'static> {
let mut textarea = TextArea::from([value]);
textarea.set_wrap_mode(WrapMode::None);
textarea.set_cursor_render_mode(CursorRenderMode::Hidden);
textarea
}
fn normalize_single_line(value: &str) -> String {
let mut normalized = String::with_capacity(value.len());
let mut previous_was_cr = false;
for ch in value.chars() {
match ch {
'\r' => {
normalized.push(' ');
previous_was_cr = true;
}
'\n' if previous_was_cr => {
previous_was_cr = false;
}
'\n' | '\t' => {
normalized.push(' ');
previous_was_cr = false;
}
ch if ch.is_control() => {
previous_was_cr = false;
}
ch => {
normalized.push(ch);
previous_was_cr = false;
}
}
}
normalized
}