matchmaker/ui/
input.rs

1use ratatui::{
2    layout::{Position, Rect},
3    widgets::Paragraph,
4};
5use unicode_segmentation::UnicodeSegmentation;
6use unicode_width::UnicodeWidthStr;
7
8use crate::{config::InputConfig, utils::text::grapheme_index_to_byte_index};
9
10#[derive(Debug, Clone)]
11pub struct InputUI {
12    pub cursor: u16, // grapheme index
13    pub input: String,
14    pub config: InputConfig,
15}
16
17impl InputUI {
18    pub fn new(config: InputConfig) -> Self {
19        Self {
20            cursor: 0,
21            input: "".into(),
22            config,
23        }
24    }
25
26    pub fn make_input(&self) -> Paragraph<'_> {
27        let mut input = Paragraph::new(format!("{}{}", &self.config.prompt, self.input.as_str()))
28            .style(self.config.input_fg);
29
30        input = input.block(self.config.border.as_block());
31
32        input
33    }
34
35    pub fn cursor_offset(&self, rect: &Rect) -> Position {
36        let border = self.config.border.sides;
37        Position::new(
38            rect.x + self.cursor + self.config.prompt.width() as u16 + !border.is_empty() as u16,
39            rect.y + !border.is_empty() as u16,
40        )
41    }
42
43    pub fn height(&self) -> u16 {
44        let mut height = 1;
45        height += 2 * !self.config.border.sides.is_empty() as u16;
46
47        height
48    }
49    pub fn forward_char(&mut self) {
50        // Check against the total number of graphemes
51        if self.cursor < self.input.graphemes(true).count() as u16 {
52            self.cursor += 1;
53        }
54    }
55
56    pub fn backward_char(&mut self) {
57        if self.cursor > 0 {
58            self.cursor -= 1;
59        }
60    }
61
62    // todo: lowpri: maintain a grapheme buffer to optimize
63
64    pub fn insert_char(&mut self, c: char) {
65        let old_grapheme_count = self.input.graphemes(true).count() as u16;
66        let byte_index = grapheme_index_to_byte_index(&self.input, self.cursor);
67        self.input.insert(byte_index, c);
68        let new_grapheme_count = self.input.graphemes(true).count() as u16;
69        if new_grapheme_count > old_grapheme_count {
70            self.cursor += 1;
71        }
72    }
73
74    pub fn set_input(&mut self, new_input: String, new_cursor: u16) {
75        let grapheme_count = new_input.graphemes(true).count() as u16;
76        self.input = new_input;
77        self.cursor = new_cursor.min(grapheme_count);
78    }
79
80    pub fn forward_word(&mut self) {
81        let post = self.input.graphemes(true).skip(self.cursor as usize);
82
83        let mut in_word = false;
84
85        for g in post {
86            self.cursor += 1;
87            if g.chars().all(|c| c.is_whitespace()) {
88                if in_word {
89                    return;
90                }
91            } else {
92                in_word = true;
93            }
94        }
95    }
96
97    pub fn backward_word(&mut self) {
98        let mut in_word = false;
99
100        let pre: Vec<&str> = self
101            .input
102            .graphemes(true)
103            .take(self.cursor as usize)
104            .collect();
105
106        for g in pre.iter().rev() {
107            self.cursor -= 1;
108
109            if g.chars().all(|c| c.is_whitespace()) {
110                if in_word {
111                    return;
112                }
113            } else {
114                in_word = true;
115            }
116        }
117
118        self.cursor = 0;
119    }
120
121    pub fn delete(&mut self) {
122        if self.cursor > 0 {
123            let byte_start = grapheme_index_to_byte_index(&self.input, self.cursor - 1);
124            let byte_end = grapheme_index_to_byte_index(&self.input, self.cursor);
125
126            self.input.replace_range(byte_start..byte_end, "");
127            self.cursor -= 1;
128        }
129    }
130
131    pub fn delete_word(&mut self) {
132        let old_cursor_grapheme = self.cursor;
133        self.backward_word();
134        let new_cursor_grapheme = self.cursor;
135
136        let byte_start = grapheme_index_to_byte_index(&self.input, new_cursor_grapheme);
137        let byte_end = grapheme_index_to_byte_index(&self.input, old_cursor_grapheme);
138
139        self.input.replace_range(byte_start..byte_end, "");
140    }
141
142    pub fn delete_line_start(&mut self) {
143        let byte_end = grapheme_index_to_byte_index(&self.input, self.cursor);
144
145        self.input.replace_range(0..byte_end, "");
146        self.cursor = 0;
147    }
148
149    pub fn delete_line_end(&mut self) {
150        let byte_index = grapheme_index_to_byte_index(&self.input, self.cursor);
151
152        // Truncate operates on the byte index
153        self.input.truncate(byte_index);
154    }
155
156    pub fn cancel(&mut self) {
157        self.input.clear();
158        self.cursor = 0;
159    }
160}