Skip to main content

stynx_code_tui/widgets/
input_box.rs

1use ratatui::{
2    buffer::Buffer,
3    layout::Rect,
4    style::{Modifier, Style},
5    text::{Line, Span},
6    widgets::{Block, 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::PINE()),
27            InputMode::Normal => (" NORMAL ", theme::GOLD()),
28        };
29
30        let border_color = if self.focused { theme::PINE() } else { theme::HL_MED() };
31
32        let block = Block::default()
33            .borders(Borders::ALL)
34            .border_style(Style::default().fg(border_color))
35            .title(Span::styled(
36                mode_label,
37                Style::default().fg(mode_color).add_modifier(Modifier::BOLD),
38            ));
39
40        let inner = block.inner(area);
41
42        let buffer_lines: Vec<&str> = if self.state.buffer.is_empty() {
43            Vec::new()
44        } else {
45            self.state.buffer.split('\n').collect()
46        };
47
48        let lines: Vec<Line<'static>> = if buffer_lines.is_empty() {
49            let hint = if self.state.suggestion.is_empty() {
50                "Type a message…".to_string()
51            } else {
52                self.state.suggestion.clone()
53            };
54            vec![Line::from(Span::styled(
55                hint,
56                Style::default().fg(theme::MUTED()),
57            ))]
58        } else {
59            buffer_lines
60                .iter()
61                .enumerate()
62                .map(|(i, l)| {
63                    if i == buffer_lines.len() - 1 && !self.state.suggestion.is_empty() {
64                        Line::from(vec![
65                            Span::styled(l.to_string(), Style::default().fg(theme::TEXT())),
66                            Span::styled(
67                                self.state.suggestion.clone(),
68                                Style::default()
69                                    .fg(theme::MUTED())
70                                    .add_modifier(Modifier::DIM),
71                            ),
72                        ])
73                    } else {
74                        Line::from(Span::styled(l.to_string(), Style::default().fg(theme::TEXT())))
75                    }
76                })
77                .collect()
78        };
79
80        Paragraph::new(lines)
81            .block(block)
82            .style(Style::default().bg(theme::SURFACE()))
83            .render(area, buf);
84
85        if self.focused && inner.width > 0 && inner.height > 0 {
86            let (line, col) = self.state.cursor_line_col();
87            let cx = inner.x.saturating_add(u16::try_from(col).unwrap_or(u16::MAX));
88            let cy = inner.y.saturating_add(u16::try_from(line).unwrap_or(u16::MAX));
89            let right = inner.x.saturating_add(inner.width);
90            let bottom = inner.y.saturating_add(inner.height);
91            if cx < right && cy < bottom {
92                buf[(cx, cy)].set_style(Style::default().bg(theme::HL_HIGH()).fg(theme::TEXT()));
93            }
94        }
95    }
96}