use ratatui::{
layout::Rect,
style::{Color, Style},
text::{Line, Span},
widgets::{Block, BorderType, Borders, Paragraph, Wrap},
Frame,
};
use unicode_width::UnicodeWidthStr;
use tmai_core::state::AppState;
pub struct InputWidget;
impl InputWidget {
pub fn render(frame: &mut Frame, area: Rect, state: &AppState) {
let buffer = state.get_input();
let cursor_pos = state.get_cursor_position();
let is_focused = state.is_input_mode();
let target_name = state.selected_target().unwrap_or("None");
let title = format!(" Input -> {} ", target_name);
let border_color = if is_focused {
Color::Green
} else {
Color::DarkGray
};
let block = Block::default()
.title(title)
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(border_color));
let lines: Vec<Line> = Self::build_lines_with_cursor(buffer, cursor_pos, is_focused);
let paragraph = Paragraph::new(lines)
.block(block)
.wrap(Wrap { trim: false });
frame.render_widget(paragraph, area);
if is_focused {
Self::set_cursor_position(frame, area, buffer, cursor_pos);
}
}
fn build_lines_with_cursor(
buffer: &str,
cursor_pos: usize,
is_focused: bool,
) -> Vec<Line<'static>> {
let cursor_style = Style::default().fg(Color::Black).bg(Color::Green);
let text_style = Style::default().fg(Color::White);
let hint_style = Style::default().fg(Color::DarkGray);
if buffer.is_empty() {
if is_focused {
return vec![Line::from(vec![
Span::styled("\u{2588}", cursor_style), Span::styled(" (Enter: send, Esc: cancel)", hint_style),
])];
} else {
return vec![Line::from(vec![Span::styled(
"Press 'i' to input, 1-9 for selection",
hint_style,
)])];
}
}
let before_cursor = &buffer[..cursor_pos];
let after_cursor = &buffer[cursor_pos..];
let cursor_char = after_cursor.chars().next();
let after_cursor_rest = if let Some(c) = cursor_char {
&after_cursor[c.len_utf8()..]
} else {
""
};
let mut lines = Vec::new();
let before_lines: Vec<&str> = before_cursor.split('\n').collect();
let after_lines: Vec<&str> = after_cursor_rest.split('\n').collect();
for line_text in &before_lines[..before_lines.len().saturating_sub(1)] {
lines.push(Line::from(vec![Span::styled(
line_text.to_string(),
text_style,
)]));
}
let cursor_line_before = before_lines.last().unwrap_or(&"");
let cursor_line_after_first = after_lines.first().unwrap_or(&"");
if is_focused {
let cursor_display = if let Some(c) = cursor_char {
if c == '\n' {
"\u{2588}".to_string()
} else {
c.to_string()
}
} else {
"\u{2588}".to_string()
};
lines.push(Line::from(vec![
Span::styled(cursor_line_before.to_string(), text_style),
Span::styled(cursor_display, cursor_style),
Span::styled(cursor_line_after_first.to_string(), text_style),
]));
} else {
lines.push(Line::from(vec![
Span::styled(cursor_line_before.to_string(), text_style),
Span::styled(
format!(
"{}{}",
cursor_char.map(|c| c.to_string()).unwrap_or_default(),
cursor_line_after_first
),
text_style,
),
]));
}
for line_text in &after_lines[1..] {
lines.push(Line::from(vec![Span::styled(
line_text.to_string(),
text_style,
)]));
}
lines
}
fn set_cursor_position(frame: &mut Frame, area: Rect, buffer: &str, cursor_pos: usize) {
let before_cursor = &buffer[..cursor_pos];
let lines: Vec<&str> = before_cursor.split('\n').collect();
let line_count = lines.len();
let last_line = lines.last().unwrap_or(&"");
let column_width = last_line.width() as u16;
let cursor_y = area.y + 1 + (line_count.saturating_sub(1)) as u16;
let cursor_x = area.x + 1 + column_width;
let cursor_x = cursor_x.min(area.x + area.width.saturating_sub(2));
let cursor_y = cursor_y.min(area.y + area.height.saturating_sub(2));
frame.set_cursor_position((cursor_x, cursor_y));
}
pub fn calculate_height(buffer: &str, max_height: u16) -> u16 {
let line_count = buffer.split('\n').count() as u16;
(line_count + 2).max(3).min(max_height)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_height() {
assert_eq!(InputWidget::calculate_height("", 10), 3);
assert_eq!(InputWidget::calculate_height("hello", 10), 3);
assert_eq!(InputWidget::calculate_height("a\nb\nc", 10), 5);
assert_eq!(InputWidget::calculate_height("a\nb\nc\nd\ne\nf", 5), 5);
}
}