matchmaker/ui/
input.rs

1use ratatui::{
2    layout::{Position, Rect},
3    style::Stylize,
4    text::{Line, Span},
5    widgets::Paragraph,
6};
7use unicode_segmentation::UnicodeSegmentation;
8// use unicode_width::UnicodeWidthStr;
9
10use crate::{config::InputConfig, utils::text::grapheme_index_to_byte_index};
11
12#[derive(Debug, Clone)]
13pub struct InputUI {
14    pub cursor: u16, // grapheme index
15    pub input: String,
16    pub config: InputConfig,
17    pub prompt: Span<'static>,
18}
19
20impl InputUI {
21    pub fn new(config: InputConfig) -> Self {
22        Self {
23            cursor: 0,
24            input: "".into(),
25            prompt: Span::from(config.prompt.clone()),
26            config,
27        }
28    }
29    // ---------- GETTERS ---------
30
31    pub fn len(&self) -> usize {
32        self.input.len()
33    }
34    pub fn is_empty(&self) -> bool {
35        self.input.is_empty()
36    }
37
38    pub fn cursor_offset(&self, rect: &Rect) -> Position {
39        let left = self.config.border.left();
40        let top = self.config.border.top();
41        Position::new(
42            rect.x + self.cursor + self.prompt.width() as u16 + left,
43            rect.y + top,
44        )
45    }
46
47    // ------------ SETTERS ---------------
48    pub fn set(&mut self, input: String, cursor: u16) {
49        let grapheme_count = input.graphemes(true).count() as u16;
50        self.input = input;
51        self.cursor = cursor.min(grapheme_count);
52    }
53    pub fn cancel(&mut self) {
54        self.input.clear();
55        self.cursor = 0;
56    }
57
58    // ---------- EDITING -------------
59    pub fn forward_char(&mut self) {
60        // Check against the total number of graphemes
61        if self.cursor < self.input.graphemes(true).count() as u16 {
62            self.cursor += 1;
63        }
64    }
65    pub fn backward_char(&mut self) {
66        if self.cursor > 0 {
67            self.cursor -= 1;
68        }
69    }
70    pub fn insert_char(&mut self, c: char) {
71        let old_grapheme_count = self.input.graphemes(true).count() as u16;
72        let byte_index = grapheme_index_to_byte_index(&self.input, self.cursor);
73        self.input.insert(byte_index, c);
74        let new_grapheme_count = self.input.graphemes(true).count() as u16;
75        if new_grapheme_count > old_grapheme_count {
76            self.cursor += 1;
77        }
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    // ---------------------------------------
157    pub fn make_input(&self) -> Paragraph<'_> {
158        let line = Line::from(vec![
159            self.prompt.clone(),
160            Span::raw(self.input.as_str())
161                .style(self.config.fg)
162                .add_modifier(self.config.modifier),
163        ]);
164
165        let mut input = Paragraph::new(line);
166
167        input = input.block(self.config.border.as_block());
168
169        input
170    }
171}