rust_kanban/ui/text_box/
helper_structs.rs

1use crate::{
2    ui::text_box::{
3        helper_enums::{Boundary, TextBoxEditKind},
4        TextBox,
5    },
6    util::{num_digits, spaces},
7};
8use portable_atomic::AtomicU64;
9use ratatui::{
10    buffer::Buffer,
11    layout::Rect,
12    style::Style,
13    text::{Line, Span, Text},
14    widgets::{Paragraph, Widget},
15};
16use std::{
17    borrow::Cow,
18    cmp::{self, Ordering},
19    collections::VecDeque,
20    iter,
21};
22use unicode_width::UnicodeWidthChar;
23
24#[derive(Debug, Clone)]
25pub struct CursorPos {
26    pub row: usize,
27    pub col: usize,
28    pub offset: usize,
29}
30
31impl CursorPos {
32    pub fn new(row: usize, col: usize, offset: usize) -> Self {
33        Self { row, col, offset }
34    }
35}
36
37#[derive(Clone, Debug)]
38pub struct TextBoxEdit {
39    kind: TextBoxEditKind,
40    before: CursorPos,
41    after: CursorPos,
42}
43
44impl TextBoxEdit {
45    pub fn new(kind: TextBoxEditKind, before: CursorPos, after: CursorPos) -> Self {
46        Self {
47            kind,
48            before,
49            after,
50        }
51    }
52
53    pub fn redo(&self, lines: &mut Vec<String>) {
54        self.kind.apply(lines, &self.before, &self.after);
55    }
56
57    pub fn undo(&self, lines: &mut Vec<String>) {
58        self.kind.invert().apply(lines, &self.after, &self.before); // Undo is redo of inverted edit
59    }
60
61    pub fn cursor_before(&self) -> (usize, usize) {
62        (self.before.row, self.before.col)
63    }
64
65    pub fn cursor_after(&self) -> (usize, usize) {
66        (self.after.row, self.after.col)
67    }
68}
69
70#[derive(Clone, Debug)]
71pub struct TextBoxHistory {
72    index: usize,
73    max_items: usize,
74    edits: VecDeque<TextBoxEdit>,
75}
76
77impl TextBoxHistory {
78    pub fn new(max_items: usize) -> Self {
79        Self {
80            index: 0,
81            max_items,
82            edits: VecDeque::new(),
83        }
84    }
85
86    pub fn push(&mut self, edit: TextBoxEdit) {
87        if self.max_items == 0 {
88            return;
89        }
90
91        if self.edits.len() == self.max_items {
92            self.edits.pop_front();
93            self.index = self.index.saturating_sub(1);
94        }
95
96        if self.index < self.edits.len() {
97            self.edits.truncate(self.index);
98        }
99
100        self.index += 1;
101        self.edits.push_back(edit);
102    }
103
104    pub fn redo(&mut self, lines: &mut Vec<String>) -> Option<(usize, usize)> {
105        if self.index == self.edits.len() {
106            return None;
107        }
108        let edit = &self.edits[self.index];
109        edit.redo(lines);
110        self.index += 1;
111        Some(edit.cursor_after())
112    }
113
114    pub fn undo(&mut self, lines: &mut Vec<String>) -> Option<(usize, usize)> {
115        self.index = self.index.checked_sub(1)?;
116        let edit = &self.edits[self.index];
117        edit.undo(lines);
118        Some(edit.cursor_before())
119    }
120
121    pub fn max_items(&self) -> usize {
122        self.max_items
123    }
124}
125
126#[derive(Default, Debug)]
127pub struct TextBoxViewport(AtomicU64);
128
129impl Clone for TextBoxViewport {
130    fn clone(&self) -> Self {
131        let u = self.0.load(std::sync::atomic::Ordering::Relaxed);
132        TextBoxViewport(AtomicU64::new(u))
133    }
134}
135
136impl TextBoxViewport {
137    pub fn scroll_top(&self) -> (u16, u16) {
138        let u = self.0.load(std::sync::atomic::Ordering::Relaxed);
139        ((u >> 16) as u16, u as u16)
140    }
141
142    pub fn rect(&self) -> (u16, u16, u16, u16) {
143        let u = self.0.load(std::sync::atomic::Ordering::Relaxed);
144        let width = (u >> 48) as u16;
145        let height = (u >> 32) as u16;
146        let row = (u >> 16) as u16;
147        let col = u as u16;
148        (row, col, width, height)
149    }
150
151    pub fn position(&self) -> (u16, u16, u16, u16) {
152        let (row_top, col_top, width, height) = self.rect();
153        let row_bottom = row_top.saturating_add(height).saturating_sub(1);
154        let col_bottom = col_top.saturating_add(width).saturating_sub(1);
155
156        (
157            row_top,
158            col_top,
159            cmp::max(row_top, row_bottom),
160            cmp::max(col_top, col_bottom),
161        )
162    }
163
164    fn store(&self, row: u16, col: u16, width: u16, height: u16) {
165        let u =
166            ((width as u64) << 48) | ((height as u64) << 32) | ((row as u64) << 16) | col as u64;
167        self.0.store(u, std::sync::atomic::Ordering::Relaxed);
168    }
169
170    pub fn scroll(&mut self, rows: i16, cols: i16) {
171        fn apply_scroll(pos: u16, delta: i16) -> u16 {
172            if delta >= 0 {
173                pos.saturating_add(delta as u16)
174            } else {
175                pos.saturating_sub(-delta as u16)
176            }
177        }
178
179        let u = self.0.get_mut();
180        let row = apply_scroll((*u >> 16) as u16, rows);
181        let col = apply_scroll(*u as u16, cols);
182        *u = (*u & 0xffff_ffff_0000_0000) | ((row as u64) << 16) | (col as u64);
183    }
184}
185
186pub struct TextBoxRenderer<'a>(&'a TextBox<'a>);
187
188impl<'a> TextBoxRenderer<'a> {
189    pub fn new(textarea: &'a TextBox<'a>) -> Self {
190        Self(textarea)
191    }
192
193    #[inline]
194    fn text(&self, top_row: usize, height: usize) -> Text<'a> {
195        let lines_len = self.0.lines().len();
196        let line_num_len = num_digits(lines_len);
197        let bottom_row = cmp::min(top_row + height, lines_len);
198        let mut lines = Vec::with_capacity(bottom_row - top_row);
199        for (i, line) in self.0.lines()[top_row..bottom_row].iter().enumerate() {
200            lines.push(
201                self.0
202                    .get_formatted_line(line.as_str(), top_row + i, line_num_len),
203            );
204        }
205        Text::from(lines)
206    }
207}
208
209impl<'a> Widget for TextBoxRenderer<'a> {
210    fn render(self, area: Rect, buf: &mut Buffer) {
211        let Rect { width, height, .. } = if let Some(b) = self.0.block() {
212            b.inner(area)
213        } else {
214            area
215        };
216
217        fn next_scroll_top(prev_top: u16, cursor: u16, length: u16) -> u16 {
218            if cursor < prev_top {
219                cursor
220            } else if prev_top + length <= cursor {
221                cursor + 1 - length
222            } else {
223                prev_top
224            }
225        }
226
227        let cursor = self.0.cursor();
228        let (top_row, top_col) = self.0.viewport.scroll_top();
229        let top_row = next_scroll_top(top_row, cursor.0 as u16, height);
230        let top_col = next_scroll_top(top_col, cursor.1 as u16, width);
231
232        let (text, style) = if !self.0.placeholder.is_empty() && self.0.is_empty() {
233            let text = Text::from(self.0.placeholder.as_str());
234            (text, self.0.placeholder_style)
235        } else {
236            (self.text(top_row as usize, height as usize), self.0.style())
237        };
238
239        let mut text_area = area;
240        let mut inner = Paragraph::new(text)
241            .style(style)
242            .alignment(self.0.alignment());
243        if let Some(b) = self.0.block() {
244            text_area = b.inner(area);
245            b.clone().render(area, buf)
246        }
247        if top_col != 0 {
248            inner = inner.scroll((0, top_col));
249        }
250
251        self.0.viewport.store(top_row, top_col, width, height);
252
253        inner.render(text_area, buf);
254    }
255}
256
257pub struct TextLineFormatter<'a> {
258    line: &'a str,
259    spans: Vec<Span<'a>>,
260    boundaries: Vec<(Boundary, usize)>,
261    style_begin: Style,
262    cursor_at_end: bool,
263    cursor_style: Style,
264    tab_len: u8,
265    mask: Option<char>,
266    select_at_end: bool,
267    select_style: Style,
268}
269
270impl<'a> TextLineFormatter<'a> {
271    pub fn new(
272        line: &'a str,
273        cursor_style: Style,
274        tab_len: u8,
275        mask: Option<char>,
276        select_style: Style,
277    ) -> Self {
278        Self {
279            line,
280            spans: vec![],
281            boundaries: vec![],
282            style_begin: Style::default(),
283            cursor_at_end: false,
284            cursor_style,
285            tab_len,
286            mask,
287            select_at_end: false,
288            select_style,
289        }
290    }
291
292    pub fn line_number(&mut self, row: usize, line_num_len: u8, style: Style) {
293        let pad = spaces(line_num_len - num_digits(row + 1) + 1);
294        self.spans
295            .push(Span::styled(format!("{}{}) ", pad, row + 1), style));
296    }
297
298    pub fn cursor_line(&mut self, cursor_col: usize, style: Style) {
299        if let Some((start, c)) = self.line.char_indices().nth(cursor_col) {
300            self.boundaries
301                .push((Boundary::Cursor(self.cursor_style), start));
302            self.boundaries.push((Boundary::End, start + c.len_utf8()));
303        } else {
304            self.cursor_at_end = true;
305        }
306        self.style_begin = style;
307    }
308
309    pub fn selection(
310        &mut self,
311        current_row: usize,
312        start_row: usize,
313        start_off: usize,
314        end_row: usize,
315        end_off: usize,
316    ) {
317        let (start, end) = if current_row == start_row {
318            if start_row == end_row {
319                (start_off, end_off)
320            } else {
321                self.select_at_end = true;
322                (start_off, self.line.len())
323            }
324        } else if current_row == end_row {
325            (0, end_off)
326        } else if start_row < current_row && current_row < end_row {
327            self.select_at_end = true;
328            (0, self.line.len())
329        } else {
330            return;
331        };
332        if start != end {
333            self.boundaries
334                .push((Boundary::Select(self.select_style), start));
335            self.boundaries.push((Boundary::End, end));
336        }
337    }
338
339    pub fn into_line(self) -> Line<'a> {
340        let Self {
341            line,
342            mut spans,
343            mut boundaries,
344            tab_len,
345            style_begin,
346            cursor_style,
347            cursor_at_end,
348            mask,
349            select_at_end,
350            select_style,
351        } = self;
352        let mut builder = DisplayTextBuilder::new(tab_len, mask);
353
354        if boundaries.is_empty() {
355            let built = builder.build(line);
356            if !built.is_empty() {
357                spans.push(Span::styled(built, style_begin));
358            }
359            if cursor_at_end {
360                spans.push(Span::styled(" ", cursor_style));
361            } else if select_at_end {
362                spans.push(Span::styled(" ", select_style));
363            }
364            return Line::from(spans);
365        }
366
367        boundaries.sort_unstable_by(|(l, i), (r, j)| match i.cmp(j) {
368            Ordering::Equal => l.cmp(r),
369            o => o,
370        });
371
372        let mut style = style_begin;
373        let mut start = 0;
374        let mut stack = vec![];
375
376        for (next_boundary, end) in boundaries {
377            if start < end {
378                spans.push(Span::styled(builder.build(&line[start..end]), style));
379            }
380
381            style = if let Some(s) = next_boundary.style() {
382                stack.push(style);
383                s
384            } else {
385                stack.pop().unwrap_or(style_begin)
386            };
387            start = end;
388        }
389
390        if start != line.len() {
391            spans.push(Span::styled(builder.build(&line[start..]), style));
392        }
393
394        if cursor_at_end {
395            spans.push(Span::styled(" ", cursor_style));
396        } else if select_at_end {
397            spans.push(Span::styled(" ", select_style));
398        }
399
400        Line::from(spans)
401    }
402}
403
404struct DisplayTextBuilder {
405    tab_len: u8,
406    width: usize,
407    mask: Option<char>,
408}
409
410impl DisplayTextBuilder {
411    fn new(tab_len: u8, mask: Option<char>) -> Self {
412        Self {
413            tab_len,
414            width: 0,
415            mask,
416        }
417    }
418
419    fn build<'s>(&mut self, s: &'s str) -> Cow<'s, str> {
420        if let Some(ch) = self.mask {
421            // Note: We don't need to track width on masking text since width of tab character is fixed
422            let masked = iter::repeat(ch).take(s.chars().count()).collect();
423            return Cow::Owned(masked);
424        }
425
426        let tab = spaces(self.tab_len);
427        let mut buf = String::new();
428        for (i, c) in s.char_indices() {
429            if c == '\t' {
430                if buf.is_empty() {
431                    buf.reserve(s.len());
432                    buf.push_str(&s[..i]);
433                }
434                if self.tab_len > 0 {
435                    let len = self.tab_len as usize - (self.width % self.tab_len as usize);
436                    buf.push_str(&tab[..len]);
437                    self.width += len;
438                }
439            } else {
440                if !buf.is_empty() {
441                    buf.push(c);
442                }
443                self.width += c.width().unwrap_or(0);
444            }
445        }
446
447        if !buf.is_empty() {
448            Cow::Owned(buf)
449        } else {
450            Cow::Borrowed(s)
451        }
452    }
453}