rust_kanban/ui/text_box/
helper_enums.rs

1use crate::ui::text_box::{
2    helper_structs::{CursorPos, TextBoxViewport},
3    utils::{find_word_start_backward, find_word_start_forward},
4};
5use ratatui::style::Style;
6use std::{
7    cmp::{self, Ordering},
8    fmt,
9};
10
11#[derive(Clone, Debug)]
12pub enum TextBoxEditKind {
13    InsertChar(char),
14    DeleteChar(char),
15    InsertNewline,
16    DeleteNewline,
17    InsertStr(String),
18    DeleteStr(String),
19    InsertChunk(Vec<String>),
20    DeleteChunk(Vec<String>),
21}
22
23impl TextBoxEditKind {
24    pub fn apply(&self, lines: &mut Vec<String>, before: &CursorPos, after: &CursorPos) {
25        match self {
26            TextBoxEditKind::InsertChar(c) => {
27                lines[before.row].insert(before.offset, *c);
28            }
29            TextBoxEditKind::DeleteChar(_) => {
30                lines[before.row].remove(after.offset);
31            }
32            TextBoxEditKind::InsertNewline => {
33                let line = &mut lines[before.row];
34                let next_line = line[before.offset..].to_string();
35                line.truncate(before.offset);
36                lines.insert(before.row + 1, next_line);
37            }
38            TextBoxEditKind::DeleteNewline => {
39                debug_assert!(before.row > 0, "invalid pos: {:?}", before);
40                let line = lines.remove(before.row);
41                lines[before.row - 1].push_str(&line);
42            }
43            TextBoxEditKind::InsertStr(s) => {
44                lines[before.row].insert_str(before.offset, s.as_str());
45            }
46            TextBoxEditKind::DeleteStr(s) => {
47                lines[after.row].drain(after.offset..after.offset + s.len());
48            }
49            TextBoxEditKind::InsertChunk(c) => {
50                debug_assert!(c.len() > 1, "Chunk size must be > 1: {:?}", c);
51
52                // Handle first line of chunk
53                let first_line = &mut lines[before.row];
54                let mut last_line = first_line.drain(before.offset..).as_str().to_string();
55                first_line.push_str(&c[0]);
56
57                // Handle last line of chunk
58                let next_row = before.row + 1;
59                last_line.insert_str(0, c.last().unwrap());
60                lines.insert(next_row, last_line);
61
62                // Handle middle lines of chunk
63                lines.splice(next_row..next_row, c[1..c.len() - 1].iter().cloned());
64            }
65            TextBoxEditKind::DeleteChunk(c) => {
66                debug_assert!(c.len() > 1, "Chunk size must be > 1: {:?}", c);
67
68                // Remove middle lines of chunk
69                let mut last_line = lines
70                    .drain(after.row + 1..after.row + c.len())
71                    .last()
72                    .unwrap();
73                // Remove last line of chunk
74                last_line.drain(..c[c.len() - 1].len());
75
76                // Remove first line of chunk and concat remaining
77                let first_line = &mut lines[after.row];
78                first_line.truncate(after.offset);
79                first_line.push_str(&last_line);
80            }
81        }
82    }
83
84    pub fn invert(&self) -> Self {
85        use TextBoxEditKind::*;
86        match self.clone() {
87            InsertChar(c) => DeleteChar(c),
88            DeleteChar(c) => InsertChar(c),
89            InsertNewline => DeleteNewline,
90            DeleteNewline => InsertNewline,
91            InsertStr(s) => DeleteStr(s),
92            DeleteStr(s) => InsertStr(s),
93            InsertChunk(c) => DeleteChunk(c),
94            DeleteChunk(c) => InsertChunk(c),
95        }
96    }
97}
98
99#[derive(PartialEq, Eq, Clone, Copy)]
100pub enum CharKind {
101    Space,
102    Punctuation,
103    Other,
104}
105
106impl CharKind {
107    pub fn new(c: char) -> Self {
108        if c.is_whitespace() {
109            Self::Space
110        } else if c.is_ascii_punctuation() {
111            Self::Punctuation
112        } else {
113            Self::Other
114        }
115    }
116}
117
118pub enum TextBoxScroll {
119    Delta { rows: i16, cols: i16 },
120    PageDown,
121    PageUp,
122}
123
124impl TextBoxScroll {
125    pub(crate) fn scroll(self, viewport: &mut TextBoxViewport) {
126        let (rows, cols) = match self {
127            Self::Delta { rows, cols } => (rows, cols),
128            Self::PageDown => {
129                let (_, _, _, height) = viewport.rect();
130                (height as i16, 0)
131            }
132            Self::PageUp => {
133                let (_, _, _, height) = viewport.rect();
134                (-(height as i16), 0)
135            }
136        };
137        viewport.scroll(rows, cols);
138    }
139}
140
141impl From<(i16, i16)> for TextBoxScroll {
142    fn from((rows, cols): (i16, i16)) -> Self {
143        Self::Delta { rows, cols }
144    }
145}
146
147#[derive(Debug, Clone)]
148pub enum YankText {
149    Piece(String),
150    Chunk(Vec<String>),
151}
152
153impl Default for YankText {
154    fn default() -> Self {
155        Self::Piece(String::new())
156    }
157}
158
159impl From<String> for YankText {
160    fn from(s: String) -> Self {
161        Self::Piece(s)
162    }
163}
164impl From<Vec<String>> for YankText {
165    fn from(mut c: Vec<String>) -> Self {
166        match c.len() {
167            0 => Self::default(),
168            1 => Self::Piece(c.remove(0)),
169            _ => Self::Chunk(c),
170        }
171    }
172}
173
174impl fmt::Display for YankText {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        match self {
177            Self::Piece(s) => write!(f, "{}", s),
178            Self::Chunk(ss) => write!(f, "{}", ss.join("\n")),
179        }
180    }
181}
182
183#[derive(Eq, PartialEq)]
184pub enum Boundary {
185    Cursor(Style),
186    Select(Style),
187    End,
188}
189
190impl PartialOrd for Boundary {
191    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
192        Some(self.cmp(other))
193    }
194}
195
196impl Ord for Boundary {
197    fn cmp(&self, other: &Self) -> Ordering {
198        fn rank(b: &Boundary) -> u8 {
199            match b {
200                Boundary::Cursor(_) => 2,
201                Boundary::Select(_) => 1,
202                Boundary::End => 0,
203            }
204        }
205        rank(self).cmp(&rank(other))
206    }
207}
208
209impl Boundary {
210    pub fn style(&self) -> Option<Style> {
211        match self {
212            Boundary::Cursor(s) => Some(*s),
213            Boundary::Select(s) => Some(*s),
214            Boundary::End => None,
215        }
216    }
217}
218
219#[derive(Clone, Copy, Debug)]
220pub enum CursorMove {
221    Forward,
222    Back,
223    Up,
224    Down,
225    Head,
226    End,
227    Top,
228    Bottom,
229    WordForward,
230    WordBack,
231    ParagraphForward,
232    ParagraphBack,
233    Jump(u16, u16),
234    InViewport,
235}
236
237impl CursorMove {
238    pub(crate) fn next_cursor(
239        &self,
240        (row, col): (usize, usize),
241        lines: &[String],
242        viewport: &TextBoxViewport,
243    ) -> Option<(usize, usize)> {
244        use CursorMove::*;
245
246        fn fit_col(col: usize, line: &str) -> usize {
247            cmp::min(col, line.chars().count())
248        }
249
250        match self {
251            Forward if col >= lines[row].chars().count() => {
252                (row + 1 < lines.len()).then_some((row + 1, 0))
253            }
254            Forward => Some((row, col + 1)),
255            Back if col == 0 => {
256                let row = row.checked_sub(1)?;
257                Some((row, lines[row].chars().count()))
258            }
259            Back => Some((row, col - 1)),
260            Up => {
261                let row = row.checked_sub(1)?;
262                Some((row, fit_col(col, &lines[row])))
263            }
264            Down => Some((row + 1, fit_col(col, lines.get(row + 1)?))),
265            Head => Some((row, 0)),
266            End => Some((row, lines[row].chars().count())),
267            Top => Some((0, fit_col(col, &lines[0]))),
268            Bottom => {
269                let row = lines.len() - 1;
270                Some((row, fit_col(col, &lines[row])))
271            }
272            WordForward => {
273                if let Some(col) = find_word_start_forward(&lines[row], col) {
274                    Some((row, col))
275                } else if row + 1 < lines.len() {
276                    Some((row + 1, 0))
277                } else {
278                    Some((row, lines[row].chars().count()))
279                }
280            }
281            WordBack => {
282                if let Some(col) = find_word_start_backward(&lines[row], col) {
283                    Some((row, col))
284                } else if row > 0 {
285                    Some((row - 1, lines[row - 1].chars().count()))
286                } else {
287                    Some((row, 0))
288                }
289            }
290            ParagraphForward => {
291                let mut prev_is_empty = lines[row].is_empty();
292                for (row, line) in lines.iter().enumerate().skip(row + 1) {
293                    let is_empty = line.is_empty();
294                    if !is_empty && prev_is_empty {
295                        return Some((row, fit_col(col, line)));
296                    }
297                    prev_is_empty = is_empty;
298                }
299                let row = lines.len() - 1;
300                Some((row, fit_col(col, &lines[row])))
301            }
302            ParagraphBack => {
303                let row = row.checked_sub(1)?;
304                let mut prev_is_empty = lines[row].is_empty();
305                for row in (0..row).rev() {
306                    let is_empty = lines[row].is_empty();
307                    if is_empty && !prev_is_empty {
308                        return Some((row + 1, fit_col(col, &lines[row + 1])));
309                    }
310                    prev_is_empty = is_empty;
311                }
312                Some((0, fit_col(col, &lines[0])))
313            }
314            Jump(row, col) => {
315                let row = cmp::min(*row as usize, lines.len() - 1);
316                let col = fit_col(*col as usize, &lines[row]);
317                Some((row, col))
318            }
319            InViewport => {
320                let (row_top, col_top, row_bottom, col_bottom) = viewport.position();
321
322                let row = row.clamp(row_top as usize, row_bottom as usize);
323                let row = cmp::min(row, lines.len() - 1);
324                let col = col.clamp(col_top as usize, col_bottom as usize);
325                let col = fit_col(col, &lines[row]);
326
327                Some((row, col))
328            }
329        }
330    }
331}