flux_tui/components/
textbox.rs1use super::*;
2use crate::canvas::Canvas;
3
4use std::num::NonZero;
5use std::rc::Rc;
6use std::time::{Duration, Instant};
7
8use crossterm::event::KeyCode;
9use unicode_segmentation::UnicodeSegmentation;
10use unicode_width::*;
11
12
13pub type TextChangedCallback = dyn Fn(&mut Textbox);
14pub type TextboxKeyHandler = fn(&mut Textbox, event: &mut WindowEvent);
15
16pub struct Textbox {
27 text: String,
28 grapheme_count: usize,
29 cursor: usize,
30 offset: usize,
31 has_focus: bool,
32 width: usize,
34 max_length: usize,
35 base: WidgetBase,
36 readonly: bool,
37
38 focus_color: Option<Color>,
39 fat_cursor: bool,
40
41 last_change: Instant,
42 delay: Duration,
43 callback: Rc<TextChangedCallback>,
44 key_handler: TextboxKeyHandler,
45}
46
47impl Default for Textbox {
48 fn default() -> Self {
49 let mut s = Self {
50 text: String::new(),
51 width: usize::MIN,
52 max_length: 128,
53 base: WidgetBase::default(),
54 has_focus: false,
55 cursor: usize::MIN,
56 offset: usize::MIN,
57 grapheme_count: usize::MIN,
58 readonly: false,
59
60 focus_color: None,
61 fat_cursor: true,
62
63 last_change: Instant::now(),
64 delay: Self::DELAY_DEFAULT,
65 callback: Rc::new(Self::default_callback),
66 key_handler: Self::default_key_handler,
67 };
68 s.base.constraints.y.min = NonZero::<TSize>::MIN;
69 s.base.constraints.y.max = Size::Fixed(NonZero::<TSize>::MIN);
70 s
71 }
72}
73
74impl Textbox {
75 pub const DELAY_DEFAULT: Duration = Duration::from_millis(250);
76 pub const CURSOR: Grapheme = Grapheme::new_unchecked("_", GlyphWidth::Half);
77 pub const CURSOR_WIDE: Grapheme = Grapheme::new_unchecked("_", GlyphWidth::Half);
78
79 pub fn default_callback(_: &mut Self) {}
80
81 pub fn default_key_handler(&mut self, event: &mut WindowEvent) {
82 if let Event::Key(k) = event.raw() {
83 match k.code {
84 KeyCode::Left => {
85 self.move_cursor_left();
86 event.handled = true;
87 }
88 KeyCode::Right => {
89 self.move_cursor_right();
90 event.handled = true;
91 }
92 KeyCode::Backspace => {
93 if !self.readonly {
94 self.remove_glyph_before_cursor();
95 }
96 event.handled = true;
97 }
98 KeyCode::Char(ch) => {
99 if !self.readonly {
100 self.insert_char_at_cursor(ch);
101 }
102 event.handled = true;
103 }
104 _ => {}
105 }
106 }
107 }
108
109 pub fn set_text_changed_callback<F: Fn(&mut Self) + 'static>(&mut self, callback: F) {
110 self.callback = Rc::new(callback);
111 }
112
113 pub fn set_callback_delay(&mut self, duration: Duration) {
114 self.delay = duration;
115 }
116
117 pub fn set_key_handler(&mut self, f: TextboxKeyHandler) {
118 self.key_handler = f;
119 }
120
121 pub fn set_cursor_type(&mut self, fat_cursor: bool) {
124 self.fat_cursor = fat_cursor;
125 }
126
127 pub fn set_focus_color(&mut self, focus_color: Option<Color>) {
129 self.focus_color = focus_color;
130 }
131
132 pub fn is_readonly(&self) -> bool {
133 self.readonly
134 }
135
136 pub fn set_readonly(&mut self, readonly: bool) {
137 self.readonly = readonly;
138 }
139
140 pub fn get_max_length(&self) -> usize {
142 self.max_length
143 }
144
145 pub fn set_max_length(&mut self, length: usize) {
147 self.max_length = length;
148 }
149
150 pub fn get_text(&self) -> &str {
152 &self.text
153 }
154
155 pub fn set_text(&mut self, text: &str) {
157 self.text.clear();
158 self.text.push_str(text);
159 self.grapheme_count = self.text.graphemes(true).count();
160
161 self.cursor = self.grapheme_count % self.width;
162 self.offset = self.grapheme_count - self.cursor;
163
164 self.text_changed();
165 }
166
167 pub fn clear(&mut self) {
169 self.text.clear();
170 self.grapheme_count = usize::MIN;
171 self.cursor = usize::MIN;
172 self.offset = usize::MIN;
173 self.text_changed();
174 }
175
176 pub fn move_cursor_left(&mut self) {
177 if self.actual_cursor_pos() > usize::MIN {
178 self.decrease_cursor();
179 }
180 }
181
182 pub fn move_cursor_right(&mut self) {
183 if self.actual_cursor_pos() < self.grapheme_count {
184 self.increase_cursor();
185 self.check_cursor();
186 }
187 }
188
189 pub fn remove_glyph_before_cursor(&mut self) {
190 let cursor = self.actual_cursor_pos();
191 if cursor > usize::MIN {
192 let mut iter = self.text.grapheme_indices(true).skip(cursor - 1);
193 if let Some((idx, str)) = iter.next() {
194 (0..str.chars().count()).for_each(|_| {
195 self.text.remove(idx);
196 });
197 self.decrease_cursor();
198 }
199 self.grapheme_count = self.text.graphemes(true).count();
200 }
201 }
202
203 pub fn insert_char_at_cursor(&mut self, ch: char) {
204 let cursor = self.actual_cursor_pos();
205 let ch_width = match ch.width() {
207 Some(w) => w,
208 None => return,
209 };
210
211 if self.grapheme_count < self.max_length {
212 if cursor == self.grapheme_count {
213 if self.get_current_visible_width() == self.width - ch_width {
214 self.cursor -= 1;
215 self.offset += 1;
216 }
217 self.cursor += 1;
218 }
219 else {
220 self.increase_cursor();
221 }
222
223 let mut iter = self.text.grapheme_indices(true).skip(cursor);
224 match iter.next().map(|(idx, _)| idx) {
225 Some(idx) => self.text.insert(idx, ch),
226 None => self.text.push(ch),
227 }
228 self.grapheme_count = self.text.graphemes(true).count();
229 self.check_cursor();
230 self.text_changed();
231 }
232 }
233
234 fn check_cursor(&mut self) {
235 let len = self.text.width();
237 if self.actual_cursor_pos() > len {
238 let diff = self.actual_cursor_pos() - len;
239 self.cursor -= diff;
240 }
241 }
242
243 fn actual_cursor_pos(&self) -> usize {
244 self.cursor + self.offset
245 }
246
247 fn get_current_visible_width(&self) -> usize {
248 let mut iter = self.text.grapheme_indices(true).skip(self.offset);
249 let start = iter.next().map(|x| x.0).unwrap_or(usize::MIN);
250 let mut iter = self
251 .text
252 .grapheme_indices(true)
253 .skip(self.offset + self.cursor + 1);
254 let end = iter.next().map(|x| x.0).unwrap_or(self.text.len());
255 self.text[start..end].width()
256 }
257
258 fn is_cursor_at_max_width(&self) -> bool {
259 self.get_current_visible_width() == self.width
260 }
261
262 fn increase_cursor(&mut self) {
263 if self.is_cursor_at_max_width() {
264 self.offset += 1;
265 }
266 else {
267 self.cursor += 1;
268 }
269 }
270
271 fn decrease_cursor(&mut self) {
272 if self.offset > usize::MIN && self.cursor == usize::MIN {
273 self.offset -= 1;
274 }
275 else {
276 self.cursor -= 1;
277 }
278 }
279
280 fn text_changed(&mut self) {
281 if self.last_change.elapsed() > self.delay {
282 let cb = self.callback.clone();
283 cb(self);
284 }
285
286 self.last_change = Instant::now();
287 }
288}
289
290impl Window for Textbox {
291 fn render(&self, canvas: &mut Canvas) {
292 let mut row = canvas.get_row_variable_width(TSize::MIN).unwrap();
293 let mut graphemes = self.text.graphemes(true).skip(self.offset);
294 let mut counter = usize::MIN;
295 while row.cursor() < row.width() {
296 let gr = graphemes.next();
297 let str;
298 let mut style = Style::None;
299 let mut fg = None;
300
301 if !self.base.enabled {
302 fg = self.base.colors.disabled;
303 }
304 else if self.has_focus {
305 fg = self.focus_color;
306 }
307
308 fg = fg.or(self.base.colors.base_fg);
309
310 if self.has_focus && counter == self.cursor {
311 if self.fat_cursor {
312 str = gr
313 .map(|v| Grapheme::from(v).unwrap())
314 .unwrap_or(Grapheme::PLACEHOLDER);
315 style = Style::Reverse | Style::ResetAfter;
316 }
317 else {
318 let width = gr.map(|x| x.width()).unwrap_or(usize::MIN);
319 str = match width {
320 2 => Self::CURSOR_WIDE,
321 _ => Self::CURSOR,
322 };
323 }
324 }
325 else {
326 str = gr
327 .map(|v| Grapheme::from(v).unwrap())
328 .unwrap_or(Grapheme::PLACEHOLDER);
329 }
330 row.add_grapheme(str, fg, self.base.colors.base_bg, style)
331 .ok();
332 counter += 1;
333 }
334 }
335
336 fn handle_event(&mut self, event: &mut WindowEvent) {
337 match event.raw() {
338 Event::Resize(w, _) => self.width = *w as usize,
339 Event::FocusGained => self.has_focus = true,
340 Event::FocusLost => self.has_focus = false,
341 _ => (self.key_handler)(self, event),
342 }
343 }
344
345 fn is_enabled(&self) -> bool {
346 self.base.enabled
347 }
348}
349
350impl WindowLayout for Textbox {
351 fn desired_size(&self, available_size: TPoint) -> TPoint {
352 self.base.desired_size(available_size)
353 }
354
355 fn alignment(&self) -> (HorizontalAlignment, VerticalAlignment) {
356 self.base.alignment
357 }
358
359 fn margin(&self) -> Thickness {
360 self.base.margin
361 }
362
363 fn border(&self) -> BorderStyle {
364 self.base.border
365 }
366
367 fn is_visible(&self) -> bool {
368 self.base.visibility
369 }
370}
371
372impl HasWindowUID for Textbox {
373 fn uid(&self) -> WindowUID {
374 self.base.uid
375 }
376}
377
378impl Widget for Textbox {
379 fn set_alignment(&mut self, horizontal: HorizontalAlignment, vertical: VerticalAlignment) {
380 self.base.alignment = (horizontal, vertical);
381 self.provoke_changed_property(WindowProperty::Alignment);
382 }
383
384 fn set_visibility(&mut self, visibility: bool) {
385 self.base.visibility = visibility;
386 self.provoke_changed_property(WindowProperty::IsVisible);
387 }
388
389 fn set_width(&mut self, width: Size) {
390 self.base.size.x = width;
391 self.provoke_changed_property(WindowProperty::Size);
392 }
393
394 fn set_height(&mut self, _: Size) {}
395
396 fn set_margin(&mut self, margin: Thickness) {
397 self.base.margin = margin;
398 self.provoke_changed_property(WindowProperty::Margin);
399 }
400
401 fn set_border(&mut self, border: BorderStyle) {
402 self.base.border = border;
403 self.provoke_changed_property(WindowProperty::Border);
404 }
405
406 fn set_enabled_state(&mut self, is_enabled: bool) {
407 self.base.enabled = is_enabled;
408 }
409
410 fn set_width_constraint(&mut self, width: SizeConstraint) {
411 self.base.constraints.x = width;
412 self.provoke_changed_property(WindowProperty::Size);
413 }
414
415 fn set_height_constraint(&mut self, _: SizeConstraint) {}
416}
417
418impl WidgetColors for Textbox {
419 fn set_disabled_color(&mut self, color: Option<Color>) {
420 self.base.colors.disabled = color;
421 }
422
423 fn set_base_fg_color(&mut self, color: Option<Color>) {
424 self.base.colors.base_fg = color;
425 }
426
427 fn set_base_bg_color(&mut self, color: Option<Color>) {
428 self.base.colors.base_bg = color;
429 }
430}