Skip to main content

hac_client/pages/
input.rs

1use ratatui::buffer::Buffer;
2use ratatui::layout::Rect;
3use ratatui::style::{Style, Styled};
4use ratatui::widgets::{Block, Borders, Paragraph, StatefulWidget, Widget};
5
6/// input component used in forms and everywhere else that the user can
7/// input text to a single, named field
8pub struct Input<'a> {
9    colors: &'a hac_colors::Colors,
10    focused: bool,
11    name: String,
12    placeholder: Option<String>,
13}
14
15impl<'a> Input<'a> {
16    pub fn new(colors: &'a hac_colors::Colors, name: String) -> Self {
17        Input {
18            colors,
19            focused: false,
20            name,
21            placeholder: None,
22        }
23    }
24
25    pub fn placeholder(self, placeholder: String) -> Self {
26        Input {
27            colors: self.colors,
28            focused: self.focused,
29            name: self.name,
30            placeholder: Some(placeholder),
31        }
32    }
33
34    pub fn focus(&mut self) {
35        self.focused = true;
36    }
37
38    fn build_input(&self, value: String, size: Rect) -> Paragraph<'_> {
39        let border_color = if self.focused {
40            Style::default().fg(self.colors.normal.red)
41        } else {
42            Style::default().fg(self.colors.primary.hover)
43        };
44
45        let (value, style) = if value.is_empty() {
46            let color = Style::default().fg(self.colors.normal.magenta);
47            (self.placeholder.clone().unwrap_or_default(), color)
48        } else {
49            let color = Style::default().fg(self.colors.normal.white);
50            (value, color)
51        };
52
53        let without_space = format!(
54            "{value}{}",
55            " ".repeat(size.width.saturating_sub(value.len() as u16) as usize)
56        );
57        Paragraph::new(without_space)
58            .block(
59                Block::default()
60                    .title(self.name.clone())
61                    .title_style(Style::default().fg(self.colors.normal.white))
62                    .borders(Borders::ALL)
63                    .border_style(border_color),
64            )
65            .set_style(style)
66    }
67}
68
69impl StatefulWidget for Input<'_> {
70    type State = String;
71
72    fn render(self, size: Rect, buf: &mut Buffer, state: &mut Self::State) {
73        let input = self.build_input(state.to_string(), size);
74        input.render(size, buf);
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn test_build_input_with_placeholder_unfocused() {
84        let colors = hac_colors::Colors::default();
85        let input = Input::new(&colors, "my input".into()).placeholder("my placeholder".into());
86        let expected = Paragraph::new(vec!["my placeholder".into()])
87            .block(
88                Block::default()
89                    .title("my input".to_string())
90                    .title_style(Style::default().fg(colors.normal.white))
91                    .borders(Borders::ALL)
92                    .border_style(Style::default().fg(colors.primary.hover)),
93            )
94            .style(Style::default().fg(colors.normal.magenta));
95
96        let size = Rect::new(0, 0, 10, 10);
97        let result = input.build_input("".into(), size);
98
99        assert_eq!(expected, result);
100    }
101
102    #[test]
103    fn test_build_input_with_placeholder_focused() {
104        let colors = hac_colors::Colors::default();
105        let mut input = Input::new(&colors, "my input".into()).placeholder("my placeholder".into());
106        let expected = Paragraph::new(vec!["my placeholder".into()])
107            .block(
108                Block::default()
109                    .title("my input".to_string())
110                    .title_style(Style::default().fg(colors.normal.white))
111                    .borders(Borders::ALL)
112                    .border_style(Style::default().fg(colors.normal.red)),
113            )
114            .style(Style::default().fg(colors.normal.magenta));
115
116        input.focus();
117        let size = Rect::new(0, 0, 10, 10);
118        let result = input.build_input("".into(), size);
119
120        assert_eq!(expected, result);
121    }
122
123    #[test]
124    fn test_build_input_with_value_unfocused() {
125        let colors = hac_colors::Colors::default();
126        let input = Input::new(&colors, "my input".into()).placeholder("my placeholder".into());
127        let expected = Paragraph::new(vec!["my value".into()])
128            .block(
129                Block::default()
130                    .title("my input".to_string())
131                    .title_style(Style::default().fg(colors.normal.white))
132                    .borders(Borders::ALL)
133                    .border_style(Style::default().fg(colors.primary.hover)),
134            )
135            .style(Style::default().fg(colors.normal.white));
136
137        let size = Rect::new(0, 0, 8, 3);
138        let result = input.build_input("my value".into(), size);
139
140        assert_eq!(expected, result);
141    }
142
143    #[test]
144    fn test_build_input_with_value_focused() {
145        let colors = hac_colors::Colors::default();
146        let mut input = Input::new(&colors, "my input".into()).placeholder("my placeholder".into());
147        let expected = Paragraph::new(vec!["my value".into()])
148            .block(
149                Block::default()
150                    .title("my input".to_string())
151                    .title_style(Style::default().fg(colors.normal.white))
152                    .borders(Borders::ALL)
153                    .border_style(Style::default().fg(colors.normal.red)),
154            )
155            .style(Style::default().fg(colors.normal.white));
156
157        input.focus();
158        let size = Rect::new(0, 0, 8, 3);
159        let result = input.build_input("my value".into(), size);
160
161        assert_eq!(expected, result);
162    }
163}