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 pub fn bg(mut self, bg: Color) -> Self {
28 self.bg_color = bg;
29 self
30 }
31
32 pub fn fg(mut self, fg: Color) -> Self {
34 self.fg_color = fg;
35 self
36 }
37
38 pub fn hint_color(mut self, hint: Color) -> Self {
40 self.hint_color = hint;
41 self
42 }
43
44 pub fn cursor_color(mut self, cursor: Color) -> Self {
46 self.cursor_color = cursor;
47 self
48 }
49
50 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}