mamediff/
canvas.rs

1use std::{num::NonZeroUsize, ops::Range};
2
3use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
4
5use crate::terminal::TerminalSize;
6
7#[derive(Debug)]
8pub struct Canvas {
9    frame: Frame,
10    frame_row_offset: usize,
11    cursor: TokenPosition,
12    col_offset: usize,
13}
14
15impl Canvas {
16    pub fn new(frame_row_offset: usize, frame_size: TerminalSize) -> Self {
17        Self {
18            frame: Frame::new(frame_size),
19            frame_row_offset,
20            cursor: TokenPosition::ORIGIN,
21            col_offset: 0,
22        }
23    }
24
25    pub fn frame_row_range(&self) -> Range<usize> {
26        Range {
27            start: self.frame_row_offset,
28            end: self.frame_row_offset + self.frame.size.rows,
29        }
30    }
31
32    pub fn frame_size(&self) -> TerminalSize {
33        self.frame.size
34    }
35
36    pub fn is_frame_exceeded(&self) -> bool {
37        self.cursor.row >= self.frame_row_range().end
38    }
39
40    pub fn cursor(&self) -> TokenPosition {
41        self.cursor
42    }
43
44    pub fn set_cursor(&mut self, position: TokenPosition) {
45        self.cursor = position;
46    }
47
48    pub fn set_col_offset(&mut self, offset: usize) {
49        self.col_offset = offset;
50    }
51
52    pub fn draw(&mut self, token: Token) {
53        let cols = token.cols();
54        self.draw_at(self.cursor, token);
55        self.cursor.col += cols;
56    }
57
58    pub fn drawln(&mut self, token: Token) {
59        self.draw(token);
60        self.newline();
61    }
62
63    pub fn newline(&mut self) {
64        self.cursor.row += 1;
65        self.cursor.col = 0;
66    }
67
68    pub fn draw_at(&mut self, mut position: TokenPosition, token: Token) {
69        if !self.frame_row_range().contains(&position.row) {
70            return;
71        }
72
73        position.col += self.col_offset;
74
75        let i = position.row - self.frame_row_offset;
76        let line = &mut self.frame.lines[i];
77        line.draw_token(position.col, token);
78        line.split_off(self.frame.size.cols);
79    }
80
81    pub fn into_frame(self) -> Frame {
82        self.frame
83    }
84}
85
86#[derive(Debug, Clone)]
87pub struct Frame {
88    size: TerminalSize,
89    lines: Vec<FrameLine>,
90}
91
92impl Frame {
93    pub fn new(size: TerminalSize) -> Self {
94        Self {
95            size,
96            lines: vec![FrameLine::new(); size.rows],
97        }
98    }
99
100    pub fn dirty_lines<'a>(
101        &'a self,
102        prev: &'a Self,
103    ) -> impl 'a + Iterator<Item = (usize, &'a FrameLine)> {
104        self.lines
105            .iter()
106            .zip(prev.lines.iter())
107            .enumerate()
108            .filter_map(|(i, (l0, l1))| (l0 != l1).then_some((i, l0)))
109            .chain(self.lines.iter().enumerate().skip(prev.lines.len()))
110    }
111}
112
113#[derive(Debug, Default, Clone, PartialEq, Eq)]
114pub struct FrameLine {
115    tokens: Vec<Token>,
116}
117
118impl FrameLine {
119    pub fn new() -> Self {
120        Self::default()
121    }
122
123    pub fn tokens(&self) -> &[Token] {
124        &self.tokens
125    }
126
127    pub fn text(&self) -> String {
128        self.tokens.iter().map(|t| t.text.clone()).collect()
129    }
130
131    pub fn draw_token(&mut self, col: usize, token: Token) {
132        if let Some(n) = col.checked_sub(self.cols()).and_then(NonZeroUsize::new) {
133            let s: String = std::iter::repeat_n(' ', n.get()).collect();
134            self.tokens.push(Token::new(s));
135        }
136
137        let mut suffix = self.split_off(col);
138        let suffix = suffix.split_off(token.cols());
139        self.tokens.push(token);
140        self.tokens.extend(suffix.tokens);
141    }
142
143    fn split_off(&mut self, col: usize) -> Self {
144        let mut acc_cols = 0;
145        for i in 0..self.tokens.len() {
146            if acc_cols == col {
147                let suffix = self.tokens.split_off(i);
148                return Self { tokens: suffix };
149            }
150
151            let token_cols = self.tokens[i].cols();
152            acc_cols += token_cols;
153            if acc_cols == col {
154                continue;
155            } else if let Some(n) = acc_cols.checked_sub(col) {
156                let mut suffix = self.tokens.split_off(i);
157                let token_prefix_cols = token_cols - n;
158                let token_prefix = suffix[0].split_prefix_off(token_prefix_cols);
159                self.tokens.push(token_prefix);
160                return Self { tokens: suffix };
161            }
162        }
163
164        // `col` is out of range, so no splitting is needed.
165        Self::new()
166    }
167
168    pub fn cols(&self) -> usize {
169        self.tokens.iter().map(|t| t.cols()).sum()
170    }
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub enum TokenStyle {
175    Plain,
176    Bold,
177    Dim,
178    Underlined,
179}
180
181#[derive(Debug, Clone, PartialEq, Eq)]
182pub struct Token {
183    text: String,
184    style: TokenStyle,
185}
186
187impl Token {
188    pub fn new(text: impl Into<String>) -> Self {
189        Self::with_style(text, TokenStyle::Plain)
190    }
191
192    pub fn text(&self) -> &str {
193        &self.text
194    }
195
196    pub fn style(&self) -> TokenStyle {
197        self.style
198    }
199
200    pub fn with_style(text: impl Into<String>, style: TokenStyle) -> Self {
201        let mut text = text.into();
202        if text.chars().any(|c| c.is_control()) {
203            let mut escaped_text = String::new();
204            for c in text.chars() {
205                if c.is_control() {
206                    escaped_text.extend(c.escape_default());
207                } else {
208                    escaped_text.push(c);
209                }
210            }
211            text = escaped_text;
212        }
213        Self { text, style }
214    }
215
216    pub fn split_prefix_off(&mut self, col: usize) -> Self {
217        let mut acc_cols = 0;
218        for (i, c) in self.text.char_indices() {
219            if acc_cols == col {
220                let suffix = self.text.split_off(i);
221                return std::mem::replace(self, Self::with_style(suffix, self.style));
222            }
223
224            let next_acc_cols = acc_cols + c.width().expect("infallible");
225            if next_acc_cols > col {
226                // Not a char boundary.
227                let suffix = self.text.split_off(i + c.len_utf8());
228                let suffix = Self::with_style(suffix, self.style);
229                let _ = self.text.pop();
230                for _ in acc_cols..col {
231                    self.text.push('…');
232                }
233                return std::mem::replace(self, suffix);
234            }
235            acc_cols = next_acc_cols;
236        }
237
238        std::mem::replace(self, Self::with_style(String::new(), self.style))
239    }
240
241    pub fn cols(&self) -> usize {
242        self.text.width()
243    }
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
247pub struct TokenPosition {
248    pub row: usize,
249    pub col: usize,
250}
251
252impl TokenPosition {
253    pub const ORIGIN: Self = Self { row: 0, col: 0 };
254
255    pub fn row(row: usize) -> Self {
256        Self::row_col(row, 0)
257    }
258
259    pub fn row_col(row: usize, col: usize) -> Self {
260        Self { row, col }
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn canvas() -> orfail::Result<()> {
270        let size = TerminalSize { rows: 2, cols: 4 };
271
272        // No dirty lines.
273        let frame0 = Canvas::new(1, size).into_frame();
274        let frame1 = Canvas::new(1, size).into_frame();
275        assert_eq!(frame1.dirty_lines(&frame0).count(), 0);
276
277        // Draw lines.
278        let mut canvas = Canvas::new(1, size);
279        canvas.draw_at(TokenPosition::row(0), Token::new("out of range"));
280        canvas.draw_at(TokenPosition::row(1), Token::new("hello"));
281        canvas.draw_at(TokenPosition::row_col(2, 2), Token::new("world"));
282        canvas.draw_at(TokenPosition::row(3), Token::new("out of range"));
283
284        let frame2 = canvas.into_frame();
285        assert_eq!(frame2.dirty_lines(&frame1).count(), 2);
286        assert_eq!(
287            frame2
288                .dirty_lines(&frame1)
289                .map(|(_, l)| l.text())
290                .collect::<Vec<_>>(),
291            ["hell", "  wo"],
292        );
293
294        // Draw another lines.
295        let mut canvas = Canvas::new(1, size);
296        canvas.draw_at(TokenPosition::row(1), Token::new("hello"));
297
298        let frame3 = canvas.into_frame();
299        assert_eq!(frame3.dirty_lines(&frame2).count(), 1);
300        assert_eq!(
301            frame3
302                .dirty_lines(&frame2)
303                .map(|(_, l)| l.text())
304                .collect::<Vec<_>>(),
305            [""],
306        );
307
308        Ok(())
309    }
310
311    #[test]
312    fn frame_line() -> orfail::Result<()> {
313        let mut line = FrameLine::new();
314
315        line.draw_token(2, Token::new("foo"));
316        assert_eq!(line.text(), "  foo");
317
318        line.draw_token(4, Token::new("bar"));
319        assert_eq!(line.text(), "  fobar");
320
321        line.draw_token(7, Token::new("baz"));
322        assert_eq!(line.text(), "  fobarbaz");
323
324        line.draw_token(6, Token::new("qux"));
325        assert_eq!(line.text(), "  fobaquxz");
326
327        // Control chars are escaped.
328        line.draw_token(0, Token::new("0\n1"));
329        assert_eq!(line.text(), "0\\n1baquxz");
330
331        Ok(())
332    }
333}