matchmaker/ui/
input.rs

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