tui_textbox/
lib.rs

1use crossterm::event::{KeyCode, KeyModifiers};
2use ratatui::{style::{Color, Style}, widgets::StatefulWidget};
3
4pub struct Textbox {
5    bg_color: Color,
6    fg_color: Color,
7    hint_color: Color,
8    cursor_color: Color,
9    render_cursor: bool
10}
11
12impl Default for Textbox {
13    fn default() -> Self {
14        Self {
15            bg_color: Color::LightBlue,
16            fg_color: Color::White,
17            hint_color: Color::Gray,
18            cursor_color: Color::LightRed,
19            render_cursor: true
20        }
21    }
22}
23
24impl Textbox {
25
26    /// Defines background color of textbox
27    pub fn bg(mut self, bg: Color) -> Self {
28        self.bg_color = bg;
29        self
30    }
31
32    /// Defines foreground color (text color) of textbox
33    pub fn fg(mut self, fg: Color) -> Self {
34        self.fg_color = fg;
35        self
36    }
37
38    /// Defines the color of hint_text
39    pub fn hint_color(mut self, hint: Color) -> Self {
40        self.hint_color = hint;
41        self
42    }
43
44    /// Defines the color of cursor
45    pub fn cursor_color(mut self, cursor: Color) -> Self {
46        self.cursor_color = cursor;
47        self
48    }
49
50    /// Defines the visibility of cursor
51    pub fn render_cursor(mut self, render: bool) -> Self {
52        self.render_cursor = render;
53        self
54    }
55
56}
57
58pub struct TextboxState {
59    pub cursor_pos: usize,
60    pub text: String,
61    pub hint_text: Option<String>,
62    start: usize
63}
64
65impl Default for TextboxState {
66    fn default() -> Self {
67        Self {
68            cursor_pos: Default::default(),
69            text: Default::default(),
70            hint_text: Some("<hint text>".to_string()),
71            start: 0
72        }
73    }
74}
75
76impl TextboxState {
77
78    pub fn handle_events(&mut self, key_code: KeyCode, key_modifiers: KeyModifiers) {
79        match (key_code, key_modifiers) {
80            (KeyCode::Left, _) => {
81                self.cursor_pos = if self.cursor_pos > 0 { self.cursor_pos - 1 } else { self.cursor_pos };
82            },
83            (KeyCode::Right, _) => {
84                self.cursor_pos = if self.cursor_pos < self.text.len() { self.cursor_pos + 1 } else { self.text.len() };
85            },
86            (KeyCode::Backspace, _) => {
87                if self.cursor_pos > 0 {
88                    self.cursor_pos = std::cmp::max(self.cursor_pos - 1, 0);
89                    self.text.remove(self.cursor_pos);
90                }
91            },
92            (KeyCode::Delete, _) => {
93                if self.cursor_pos < self.text.len() {
94                    self.text.remove(self.cursor_pos);
95
96                    if self.cursor_pos == self.text.len() && self.text.len() > 0 {
97                        self.cursor_pos = self.cursor_pos - 1;
98                    }
99                }
100            },
101            (KeyCode::Char(x), _) => {
102                self.text.insert(self.cursor_pos, x);
103                self.cursor_pos = self.cursor_pos + 1;
104            },
105            (_, _) => {}
106        }
107    }
108}
109
110impl StatefulWidget for Textbox {
111    type State = TextboxState;
112
113    fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer, state: &mut Self::State) {
114        buf.set_style(area, Style::default().bg(self.bg_color));
115        if state.text.len() > 0 {
116            let w = usize::from(area.width) - 1;
117            if state.cursor_pos > state.start + w  {
118               state.start = state.cursor_pos - w;
119            };
120
121            if state.cursor_pos < state.start {
122                state.start = state.cursor_pos;
123            }
124
125            let end = std::cmp::min(state.start + w + 1, state.text.len());
126
127            let visible_text = &state.text[state.start..end];
128            buf.set_string(area.x, area.y, visible_text, Style::default().bg(self.bg_color).fg(self.fg_color));
129        } else {
130            if let Some(hint) = state.hint_text.as_ref() {
131                buf.set_string(area.x, area.y, hint.clone(), Style::default().bg(self.bg_color).fg(self.hint_color));
132            }
133
134        }
135
136        if self.render_cursor {
137            let pos_char = state.text.chars().nth(state.cursor_pos).unwrap_or(' ');
138            let cur_pos = u16::try_from(state.cursor_pos.checked_sub(state.start).unwrap_or(0)).unwrap_or(0);
139
140            buf.set_string(area.x + cur_pos, area.y, format!("{}", &pos_char), Style::default().bg(self.cursor_color).fg(self.fg_color));
141        }
142    }
143}