stynx_code_tui/widgets/
input_box.rs1use ratatui::{
2 buffer::Buffer,
3 layout::Rect,
4 style::{Modifier, Style},
5 text::{Line, Span},
6 widgets::{Block, BorderType, Borders, Paragraph, Widget},
7};
8
9use crate::state::{InputMode, InputState};
10use crate::theme;
11
12pub struct InputBox<'a> {
13 pub state: &'a InputState,
14 pub focused: bool,
15}
16
17impl<'a> InputBox<'a> {
18 pub fn new(state: &'a InputState, focused: bool) -> Self {
19 Self { state, focused }
20 }
21}
22
23impl<'a> Widget for InputBox<'a> {
24 fn render(self, area: Rect, buf: &mut Buffer) {
25 let (mode_label, mode_color) = match self.state.mode {
26 InputMode::Insert => (" › INSERT ", theme::FOAM()),
27 InputMode::Normal => (" ‹ NORMAL ", theme::GOLD()),
28 };
29
30 let border_color = if self.focused { theme::IRIS() } else { theme::OVERLAY() };
31
32 let hint = Span::styled(
33 " ↵ send esc · ",
34 Style::default().fg(theme::SUBTLE()).add_modifier(Modifier::DIM),
35 );
36
37 let block = Block::default()
38 .borders(Borders::ALL)
39 .border_type(BorderType::Rounded)
40 .border_style(Style::default().fg(border_color))
41 .title(Span::styled(
42 mode_label,
43 Style::default().fg(mode_color).add_modifier(Modifier::BOLD),
44 ))
45 .title_bottom(hint);
46
47 let inner = block.inner(area);
48
49 let buffer_lines: Vec<&str> = if self.state.buffer.is_empty() {
50 Vec::new()
51 } else {
52 self.state.buffer.split('\n').collect()
53 };
54
55 let lines: Vec<Line<'static>> = if buffer_lines.is_empty() {
56 let hint = if self.state.suggestion.is_empty() {
57 " Type a message…".to_string()
58 } else {
59 format!(" {}", self.state.suggestion)
60 };
61 vec![Line::from(Span::styled(
62 hint,
63 Style::default().fg(theme::MUTED()).add_modifier(Modifier::ITALIC),
64 ))]
65 } else {
66 buffer_lines
67 .iter()
68 .enumerate()
69 .map(|(i, l)| {
70 if i == buffer_lines.len() - 1 && !self.state.suggestion.is_empty() {
71 Line::from(vec![
72 Span::styled(format!(" {l}"), Style::default().fg(theme::TEXT())),
73 Span::styled(
74 self.state.suggestion.clone(),
75 Style::default()
76 .fg(theme::MUTED())
77 .add_modifier(Modifier::DIM),
78 ),
79 ])
80 } else {
81 Line::from(Span::styled(format!(" {l}"), Style::default().fg(theme::TEXT())))
82 }
83 })
84 .collect()
85 };
86
87 Paragraph::new(lines)
88 .block(block)
89 .style(Style::default().bg(theme::SURFACE()))
90 .render(area, buf);
91
92 if self.focused && inner.width > 0 && inner.height > 0 {
93 let (line, col) = self.state.cursor_line_col();
94 let cx = inner.x.saturating_add(1).saturating_add(u16::try_from(col).unwrap_or(u16::MAX));
95 let cy = inner.y.saturating_add(u16::try_from(line).unwrap_or(u16::MAX));
96 let right = inner.x.saturating_add(inner.width);
97 let bottom = inner.y.saturating_add(inner.height);
98 if cx < right && cy < bottom {
99 buf[(cx, cy)].set_style(Style::default().bg(theme::HL_HIGH()).fg(theme::TEXT()));
100 }
101 }
102 }
103}