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 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 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 line.draw_token(0, Token::new("0\n1"));
245 assert_eq!(line.text(), "0\\n1baquxz");
246
247 Ok(())
248 }
249}