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 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 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 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 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 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 line.draw_token(0, Token::new("0\n1"));
329 assert_eq!(line.text(), "0\\n1baquxz");
330
331 Ok(())
332 }
333}