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