matchmaker/ui/
input.rs

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