tui_additions/widgets/
textfield.rs

1use std::fmt::Display;
2
3use ratatui::{
4    style::{Color, Style},
5    text::{Line, Span},
6    widgets::{Paragraph, Widget},
7};
8use unicode_segmentation::UnicodeSegmentation;
9
10#[derive(Clone)]
11pub struct TextField {
12    pub content: String,
13    pub scroll: usize,
14    pub cursor: usize,
15    pub style: Style,
16    pub text_style: Style,
17    pub cursor_style: Style,
18    pub width: Option<u16>,
19}
20
21impl Widget for TextField {
22    fn render(self, area: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) {
23        if let Some(width) = self.width {
24            if width != area.width {
25                panic!("width mismatch");
26            }
27        } else {
28            panic!("unknown width");
29        }
30
31        let unicode = UnicodeSegmentation::graphemes(self.content.as_str(), true);
32
33        let cursor_at_end = self.cursor == unicode.clone().count();
34        let mut spans = vec![Span::styled(
35            unicode
36                .clone()
37                .skip(self.scroll)
38                .take(self.cursor - self.scroll)
39                .collect::<String>(),
40            self.text_style,
41        )];
42
43        if cursor_at_end {
44            spans.push(Span::styled(String::from(' '), self.cursor_style));
45        } else {
46            spans.push(Span::styled(
47                unicode
48                    .clone()
49                    .skip(self.cursor)
50                    .take(1)
51                    .collect::<String>(),
52                self.cursor_style,
53            ));
54            spans.push(Span::styled(
55                unicode.clone().skip(self.cursor + 1).collect::<String>(),
56                self.text_style,
57            ));
58        }
59
60        let paragraph = Paragraph::new(Line::from(spans)).style(self.style);
61        paragraph.render(area, buf);
62    }
63}
64
65impl Default for TextField {
66    fn default() -> Self {
67        Self {
68            content: String::default(),
69            scroll: 0,
70            cursor: 0,
71            style: Style::default(),
72            text_style: Style::default(),
73            cursor_style: Style::default().bg(Color::Gray),
74            width: None,
75        }
76    }
77}
78
79impl TextField {
80    pub fn insert(&mut self, index: usize, c: char) -> Result<(), TextFieldError> {
81        self.content = format!(
82            "{}{}{}",
83            UnicodeSegmentation::graphemes(self.content.as_str(), true)
84                .take(index)
85                .collect::<String>(),
86            c,
87            UnicodeSegmentation::graphemes(self.content.as_str(), true)
88                .skip(index)
89                .collect::<String>()
90        );
91        self.cursor += 1;
92        self.update()?;
93        Ok(())
94    }
95
96    pub fn remove(&mut self, index: usize) -> Result<(), TextFieldError> {
97        if self.cursor == 0 {
98            return Ok(());
99        }
100        let s = self.content.clone();
101        let mut s = UnicodeSegmentation::graphemes(s.as_str(), true).collect::<Vec<_>>();
102        s.remove(index - 1);
103        self.content = s.into_iter().collect();
104        self.cursor -= 1;
105        self.update()?;
106        Ok(())
107    }
108
109    pub fn push(&mut self, c: char) -> Result<(), TextFieldError> {
110        self.insert(self.cursor, c)
111    }
112
113    pub fn pop(&mut self) -> Result<(), TextFieldError> {
114        self.remove(self.cursor)
115    }
116
117    pub fn left(&mut self) -> Result<(), TextFieldError> {
118        if self.cursor == 0 {
119            return Ok(());
120        }
121
122        self.cursor -= 1;
123        self.update()
124    }
125
126    pub fn right(&mut self) -> Result<(), TextFieldError> {
127        if self.cursor == self.content.len() {
128            return Ok(());
129        }
130
131        self.cursor += 1;
132        self.update()
133    }
134
135    pub fn first(&mut self) -> Result<(), TextFieldError> {
136        self.cursor = 0;
137        self.update()
138    }
139
140    pub fn last(&mut self) -> Result<(), TextFieldError> {
141        self.cursor = self.content.len();
142        self.update()
143    }
144}
145
146impl TextField {
147    pub fn set_width(&mut self, width: u16) {
148        self.width = Some(width)
149    }
150
151    pub fn update(&mut self) -> Result<(), TextFieldError> {
152        let width = if let Some(width) = self.width {
153            width
154        } else {
155            return Err(TextFieldError::UnknownWidth);
156        };
157
158        if self.scroll > self.cursor {
159            self.scroll = self.cursor;
160        } else if self.scroll + width as usize - 1 < self.cursor {
161            self.scroll = self.cursor - width as usize + 1;
162        }
163
164        let len = UnicodeSegmentation::graphemes(self.content.as_str(), true).count();
165
166        if self.cursor > len {
167            self.cursor = len;
168        }
169
170        Ok(())
171    }
172}
173
174#[derive(Debug)]
175pub enum TextFieldError {
176    UnknownWidth,
177}
178
179impl Display for TextFieldError {
180    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181        f.write_fmt(format_args!("{:?}", self))
182    }
183}