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, 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}