Skip to main content

flux_tui/components/
textbox.rs

1use 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
16/// Single-line text input method
17/// Allows navigating through the string, removing (using Backspace) and adding characters at the '_' sign
18/// The cursor will hide the grapheme behind it
19///
20/// Design (including Border):
21/// ```text
22/// ┌─────┐
23/// │Tex_1│
24/// └─────┘
25/// ```
26pub struct Textbox {
27	text: String,
28	grapheme_count: usize,
29	cursor: usize,
30	offset: usize,
31	has_focus: bool,
32	// Actual current size of the component
33	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	/// `fat_cursor`: `true` will just highlight the current position, `false` will use _ instead
122	/// as cursor
123	pub fn set_cursor_type(&mut self, fat_cursor: bool) {
124		self.fat_cursor = fat_cursor;
125	}
126
127	/// Foreground color when the textbox has focus
128	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	/// Returns the maximum length of the inner text
141	pub fn get_max_length(&self) -> usize {
142		self.max_length
143	}
144
145	/// Sets the maximum length of the inner text (number of graphemes)
146	pub fn set_max_length(&mut self, length: usize) {
147		self.max_length = length;
148	}
149
150	/// Returns the inner text
151	pub fn get_text(&self) -> &str {
152		&self.text
153	}
154
155	/// Updates the inner text
156	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	/// Clears the inner text
168	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		// Do nothing if its a control character
206		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		// Prevent issues with grapheme clusters
236		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}