mamediff/
canvas.rs

1use std::{fmt::Write, num::NonZeroUsize, ops::Range};
2
3use tuinix::{TerminalPosition, TerminalSize, TerminalStyle};
4
5#[derive(Debug)]
6pub struct Canvas {
7    frame: Frame,
8    frame_row_offset: usize,
9    cursor: TerminalPosition,
10}
11
12impl Canvas {
13    pub fn new(frame_row_offset: usize, frame_size: TerminalSize) -> Self {
14        Self {
15            frame: Frame::new(frame_size),
16            frame_row_offset,
17            cursor: TerminalPosition::ZERO,
18        }
19    }
20
21    pub fn frame_row_range(&self) -> Range<usize> {
22        Range {
23            start: self.frame_row_offset,
24            end: self.frame_row_offset + self.frame.size.rows,
25        }
26    }
27
28    pub fn frame_size(&self) -> TerminalSize {
29        self.frame.size
30    }
31
32    pub fn is_frame_exceeded(&self) -> bool {
33        self.cursor.row >= self.frame_row_range().end
34    }
35
36    pub fn cursor(&self) -> TerminalPosition {
37        self.cursor
38    }
39
40    pub fn set_cursor(&mut self, position: TerminalPosition) {
41        self.cursor = position;
42    }
43
44    pub fn draw(&mut self, token: Token) {
45        let cols = token.cols();
46        self.draw_at(self.cursor, token);
47        self.cursor.col += cols;
48    }
49
50    pub fn drawln(&mut self, token: Token) {
51        self.draw(token);
52        self.newline();
53    }
54
55    pub fn newline(&mut self) {
56        self.cursor.row += 1;
57        self.cursor.col = 0;
58    }
59
60    pub fn draw_at(&mut self, position: TerminalPosition, token: Token) {
61        if !self.frame_row_range().contains(&position.row) {
62            return;
63        }
64
65        let i = position.row - self.frame_row_offset;
66        let line = &mut self.frame.lines[i];
67        line.draw_token(position.col, token);
68        line.split_off(self.frame.size.cols);
69    }
70
71    pub fn into_frame(self) -> mame::terminal::UnicodeTerminalFrame {
72        let mut frame = mame::terminal::UnicodeTerminalFrame::new(self.frame_size());
73        for line in self.frame.lines {
74            for token in line.tokens {
75                let _ = write!(frame, "{}{}", token.style, token.text);
76            }
77            let _ = writeln!(frame, "{}", TerminalStyle::RESET);
78        }
79        frame
80    }
81}
82
83#[derive(Debug, Clone)]
84pub struct Frame {
85    size: TerminalSize,
86    lines: Vec<FrameLine>,
87}
88
89impl Frame {
90    pub fn new(size: TerminalSize) -> Self {
91        Self {
92            size,
93            lines: vec![FrameLine::new(); size.rows],
94        }
95    }
96}
97
98#[derive(Debug, Default, Clone, PartialEq, Eq)]
99pub struct FrameLine {
100    tokens: Vec<Token>,
101}
102
103impl FrameLine {
104    pub fn new() -> Self {
105        Self::default()
106    }
107
108    pub fn tokens(&self) -> &[Token] {
109        &self.tokens
110    }
111
112    pub fn text(&self) -> String {
113        self.tokens.iter().map(|t| t.text.clone()).collect()
114    }
115
116    pub fn draw_token(&mut self, col: usize, token: Token) {
117        if let Some(n) = col.checked_sub(self.cols()).and_then(NonZeroUsize::new) {
118            let s: String = std::iter::repeat_n(' ', n.get()).collect();
119            self.tokens.push(Token::new(s));
120        }
121
122        let mut suffix = self.split_off(col);
123        let suffix = suffix.split_off(token.cols());
124        self.tokens.push(token);
125        self.tokens.extend(suffix.tokens);
126    }
127
128    fn split_off(&mut self, col: usize) -> Self {
129        let mut acc_cols = 0;
130        for i in 0..self.tokens.len() {
131            if acc_cols == col {
132                let suffix = self.tokens.split_off(i);
133                return Self { tokens: suffix };
134            }
135
136            let token_cols = self.tokens[i].cols();
137            acc_cols += token_cols;
138            if acc_cols == col {
139                continue;
140            } else if let Some(n) = acc_cols.checked_sub(col) {
141                let mut suffix = self.tokens.split_off(i);
142                let token_prefix_cols = token_cols - n;
143                let token_prefix = suffix[0].split_prefix_off(token_prefix_cols);
144                self.tokens.push(token_prefix);
145                return Self { tokens: suffix };
146            }
147        }
148
149        // `col` is out of range, so no splitting is needed.
150        Self::new()
151    }
152
153    pub fn cols(&self) -> usize {
154        self.tokens.iter().map(|t| t.cols()).sum()
155    }
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct Token {
160    text: String,
161    style: TerminalStyle,
162}
163
164impl Token {
165    pub fn new(text: impl Into<String>) -> Self {
166        Self::with_style(text, TerminalStyle::new())
167    }
168
169    pub fn text(&self) -> &str {
170        &self.text
171    }
172
173    pub fn style(&self) -> TerminalStyle {
174        self.style
175    }
176
177    pub fn with_style(text: impl Into<String>, style: TerminalStyle) -> Self {
178        let mut text = text.into();
179        if text.chars().any(|c| c.is_control()) {
180            let mut escaped_text = String::new();
181            for c in text.chars() {
182                if c.is_control() {
183                    escaped_text.extend(c.escape_default());
184                } else {
185                    escaped_text.push(c);
186                }
187            }
188            text = escaped_text;
189        }
190        Self { text, style }
191    }
192
193    pub fn split_prefix_off(&mut self, col: usize) -> Self {
194        let mut acc_cols = 0;
195        for (i, c) in self.text.char_indices() {
196            if acc_cols == col {
197                let suffix = self.text.split_off(i);
198                return std::mem::replace(self, Self::with_style(suffix, self.style));
199            }
200
201            let next_acc_cols = acc_cols + mame::terminal::char_cols(c);
202            if next_acc_cols > col {
203                // Not a char boundary.
204                let suffix = self.text.split_off(i + c.len_utf8());
205                let suffix = Self::with_style(suffix, self.style);
206                let _ = self.text.pop();
207                for _ in acc_cols..col {
208                    self.text.push('…');
209                }
210                return std::mem::replace(self, suffix);
211            }
212            acc_cols = next_acc_cols;
213        }
214
215        std::mem::replace(self, Self::with_style(String::new(), self.style))
216    }
217
218    pub fn cols(&self) -> usize {
219        mame::terminal::str_cols(&self.text)
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn frame_line() -> orfail::Result<()> {
229        let mut line = FrameLine::new();
230
231        line.draw_token(2, Token::new("foo"));
232        assert_eq!(line.text(), "  foo");
233
234        line.draw_token(4, Token::new("bar"));
235        assert_eq!(line.text(), "  fobar");
236
237        line.draw_token(7, Token::new("baz"));
238        assert_eq!(line.text(), "  fobarbaz");
239
240        line.draw_token(6, Token::new("qux"));
241        assert_eq!(line.text(), "  fobaquxz");
242
243        // Control chars are escaped.
244        line.draw_token(0, Token::new("0\n1"));
245        assert_eq!(line.text(), "0\\n1baquxz");
246
247        Ok(())
248    }
249}