1use std::{
2 io::{self, Write},
3 ops,
4};
5
6use super::{Backend, ClearType, DisplayBackend, MoveDirection, Size};
7use crate::{
8 layout::Layout,
9 style::{Attributes, Color},
10};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13struct Cell {
14 value: Option<char>,
15 fg: Color,
16 bg: Color,
17 attributes: Attributes,
18}
19
20impl Default for Cell {
21 fn default() -> Self {
22 Self {
23 value: None,
24 fg: Color::Reset,
25 bg: Color::Reset,
26 attributes: Attributes::empty(),
27 }
28 }
29}
30
31#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
32struct Cursor {
33 x: u16,
34 y: u16,
35}
36
37impl Cursor {
38 fn to_linear(self, width: u16) -> usize {
39 (self.x + self.y * width) as usize
40 }
41}
42
43impl From<Cursor> for (u16, u16) {
44 fn from(c: Cursor) -> Self {
45 (c.x, c.y)
46 }
47}
48
49#[derive(Debug, Clone)]
56pub struct TestBackend {
57 cells: Vec<Cell>,
58 cursor: Cursor,
59 size: Size,
60 raw: bool,
61 hidden_cursor: bool,
62 current_fg: Color,
63 current_bg: Color,
64 current_attributes: Attributes,
65 viewport_start: usize,
66}
67
68impl PartialEq for TestBackend {
69 fn eq(&self, other: &Self) -> bool {
74 self.viewport() == other.viewport()
75 && self.size == other.size
76 && self.hidden_cursor == other.hidden_cursor
77 && (self.hidden_cursor || self.cursor == other.cursor)
78 }
79}
80
81impl Eq for TestBackend {}
82
83impl TestBackend {
84 pub fn new(size: Size) -> Self {
86 Self::new_with_layout(size, Layout::new(0, size))
87 }
88
89 pub fn new_with_layout(size: Size, layout: Layout) -> Self {
91 let mut this = Self {
92 cells: [Cell::default()].repeat(size.area() as usize),
93 cursor: Cursor::default(),
94 size,
95 raw: false,
96 hidden_cursor: false,
97 current_fg: Color::Reset,
98 current_bg: Color::Reset,
99 current_attributes: Attributes::empty(),
100 viewport_start: 0,
101 };
102
103 this.move_x(layout.line_offset + layout.offset_x);
104 this.move_y(layout.offset_y);
105
106 this
107 }
108
109 pub fn from_lines(lines: &[&str], size: Size) -> Self {
120 let mut backend = Self::new(size);
121
122 assert!(lines.len() <= size.height as usize);
123 let last_i = lines.len() - 1;
124
125 for (i, line) in lines.iter().enumerate() {
126 for c in line.chars() {
127 assert!(backend.cursor.x + 1 < backend.size.width);
128 backend.put_char(c);
129 }
130 if i < last_i {
131 backend.move_x(0);
132 backend.add_y(1);
133 }
134 }
135
136 backend
137 }
138
139 pub fn reset_with_layout(&mut self, layout: Layout) {
141 self.clear_range(..);
142 self.move_x(layout.offset_x + layout.line_offset);
143 self.move_y(layout.offset_y);
144 }
145
146 fn viewport(&self) -> &[Cell] {
147 &self.cells[self.viewport_start..(self.viewport_start + self.size.area() as usize)]
148 }
149
150 fn move_x(&mut self, x: u16) {
151 self.cursor.x = x.min(self.size.width.wrapping_sub(1));
153 }
154
155 fn move_y(&mut self, y: u16) {
156 self.cursor.y = y.min(self.size.height.wrapping_sub(1));
158 }
159
160 fn add_x(&mut self, x: u16) {
161 let x = self.cursor.x + x;
162 let dy = x / self.size.width;
163 self.cursor.x = x % self.size.width;
164 self.move_y(self.cursor.y + dy);
165 }
166
167 fn sub_x(&mut self, x: u16) {
168 self.cursor.x = self.cursor.x.saturating_sub(x);
169 }
170
171 fn add_y(&mut self, y: u16) {
172 self.move_y(self.cursor.y + y)
173 }
174
175 fn sub_y(&mut self, y: u16) {
176 self.cursor.y = self.cursor.y.saturating_sub(y);
177 }
178
179 fn cell_i(&self) -> usize {
180 self.viewport_start + self.cursor.to_linear(self.size.width)
181 }
182
183 fn cell(&mut self) -> &mut Cell {
184 let i = self.cell_i();
185 &mut self.cells[i]
186 }
187
188 fn clear_range<R: ops::RangeBounds<usize>>(&mut self, range: R) {
189 let start = match range.start_bound() {
190 ops::Bound::Included(&start) => start,
191 ops::Bound::Excluded(start) => start.checked_add(1).unwrap(),
192 ops::Bound::Unbounded => 0,
193 };
194
195 let end = match range.end_bound() {
196 ops::Bound::Included(end) => end.checked_add(1).unwrap(),
197 ops::Bound::Excluded(&end) => end,
198 ops::Bound::Unbounded => self.cells.len(),
199 };
200
201 self.cells[start..end]
202 .iter_mut()
203 .for_each(|c| *c = Cell::default());
204 }
205
206 fn put_char(&mut self, c: char) {
207 match c {
208 '\n' => {
209 self.add_y(1);
210 if !self.raw {
211 self.cursor.x = 0;
212 }
213 }
214 '\r' => self.cursor.x = 0,
215 '\t' => {
216 let x = 8 + self.cursor.x - (self.cursor.x % 8);
217 if x >= self.size.width && self.cursor.y < self.size.width - 1 {
218 self.cursor.x = 0;
219 self.cursor.y += 1;
220 } else {
221 self.move_x(x);
222 }
223 }
224 c => {
225 self.cell().value = Some(c);
226 self.cell().attributes = self.current_attributes;
227 self.cell().fg = self.current_fg;
228 self.cell().bg = self.current_bg;
229 self.add_x(1);
230 }
231 }
232 }
233
234 #[cfg(any(feature = "crossterm", feature = "termion"))]
235 fn assertion_failed(&self, other: &Self) {
236 panic!(
237 r#"assertion failed: `(left == right)`
238 left:
239{}
240right:
241{}
242"#,
243 self, other
244 );
245 }
246
247 #[cfg(not(any(feature = "crossterm", feature = "termion")))]
248 fn assertion_failed(&self, other: &Self) {
249 panic!(
250 r#"assertion failed: `(left == right)`
251 left:
252`TestBackend` {:p}
253right:
254`TestBackend` {:p}
255
256Enable any of the default backends to view what the `TestBackend`s looked like
257"#,
258 self, other
259 );
260 }
261
262 pub fn assert_eq(&self, other: &Self) {
265 if *self != *other {
266 self.assertion_failed(other);
267 }
268 }
269}
270
271impl Write for TestBackend {
272 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
273 std::str::from_utf8(buf)
274 .map_err(|_| io::ErrorKind::InvalidInput)?
275 .chars()
276 .for_each(|c| self.put_char(c));
277
278 Ok(buf.len())
279 }
280
281 fn flush(&mut self) -> io::Result<()> {
282 Ok(())
283 }
284}
285
286impl DisplayBackend for TestBackend {
287 fn set_attributes(&mut self, attributes: Attributes) -> io::Result<()> {
288 self.current_attributes = attributes;
289 Ok(())
290 }
291
292 fn set_fg(&mut self, color: Color) -> io::Result<()> {
293 self.current_fg = color;
294 Ok(())
295 }
296
297 fn set_bg(&mut self, color: Color) -> io::Result<()> {
298 self.current_bg = color;
299 Ok(())
300 }
301}
302
303impl Backend for TestBackend {
304 fn enable_raw_mode(&mut self) -> io::Result<()> {
305 self.raw = true;
306 Ok(())
307 }
308
309 fn disable_raw_mode(&mut self) -> io::Result<()> {
310 self.raw = false;
311 Ok(())
312 }
313
314 fn hide_cursor(&mut self) -> io::Result<()> {
315 self.hidden_cursor = true;
316 Ok(())
317 }
318
319 fn show_cursor(&mut self) -> io::Result<()> {
320 self.hidden_cursor = false;
321 Ok(())
322 }
323
324 fn get_cursor_pos(&mut self) -> io::Result<(u16, u16)> {
325 Ok(self.cursor.into())
326 }
327
328 fn move_cursor_to(&mut self, x: u16, y: u16) -> io::Result<()> {
329 self.move_x(x);
330 self.move_y(y);
331 Ok(())
332 }
333
334 fn move_cursor(&mut self, direction: MoveDirection) -> io::Result<()> {
335 match direction {
336 MoveDirection::Up(n) => self.sub_y(n),
337 MoveDirection::Down(n) => self.add_y(n),
338 MoveDirection::Left(n) => self.sub_x(n),
339 MoveDirection::Right(n) => self.add_y(n),
340 MoveDirection::NextLine(n) => {
341 self.cursor.x = 0;
342 self.add_y(n);
343 }
344 MoveDirection::Column(n) => self.move_x(n),
345 MoveDirection::PrevLine(n) => {
346 self.cursor.x = 0;
347 self.sub_y(n);
348 }
349 }
350 Ok(())
351 }
352
353 fn scroll(&mut self, dist: i16) -> io::Result<()> {
354 if dist.is_positive() {
355 self.viewport_start = self
356 .viewport_start
357 .saturating_sub(dist as usize * self.size.width as usize);
358 } else {
359 self.viewport_start += (-dist as usize) * self.size.width as usize;
360 let new_len = self.viewport_start + self.size.area() as usize;
361
362 if new_len > self.cells.len() {
363 self.cells.resize_with(new_len, Cell::default)
364 };
365 }
366 Ok(())
367 }
368
369 fn clear(&mut self, clear_type: ClearType) -> io::Result<()> {
370 match clear_type {
371 ClearType::All => self.clear_range(..),
372 ClearType::FromCursorDown => self.clear_range(self.cell_i()..),
373 ClearType::FromCursorUp => self.clear_range(..=self.cell_i()),
374 ClearType::CurrentLine => {
375 let s = (self.cursor.y * self.size.width) as usize;
376 let e = ((self.cursor.y + 1) * self.size.width) as usize;
377 self.clear_range(s..e)
378 }
379 ClearType::UntilNewLine => {
380 let e = ((self.cursor.y + 1) * self.size.width) as usize;
381 self.clear_range(self.cell_i()..e)
382 }
383 }
384 Ok(())
385 }
386
387 fn size(&self) -> io::Result<Size> {
388 Ok(self.size)
389 }
390}
391
392#[cfg(any(feature = "crossterm", feature = "termion"))]
393#[cfg_attr(docsrs, doc(cfg(any(feature = "crossterm", feature = "termion"))))]
394impl std::fmt::Display for TestBackend {
395 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
403 let mut buf = Vec::with_capacity(self.size.area() as usize);
404
405 if let Err(e) = self.write_to_buf(&mut buf) {
406 return write!(f, "<could not render TestBackend: {}>", e);
407 }
408
409 match std::str::from_utf8(&buf) {
410 Ok(s) => write!(f, "{}", s),
411 Err(e) => write!(f, "<could not render TestBackend: {}>", e),
412 }
413 }
414}
415
416fn map_reset(c: Color, to: Color) -> Color {
417 match c {
418 Color::Reset => to,
419 c => c,
420 }
421}
422
423impl TestBackend {
424 pub fn write_to_backend<B: DisplayBackend>(&self, mut backend: B) -> io::Result<()> {
430 let mut fg = Color::Reset;
431 let mut bg = Color::Reset;
432 let mut attributes = Attributes::empty();
433
434 let cursor = if self.hidden_cursor {
435 usize::MAX
436 } else {
437 self.cursor.to_linear(self.size.width)
438 };
439
440 let width = self.size.width as usize;
441
442 let symbol_set = crate::symbols::current();
443
444 write!(backend, "{}", symbol_set.box_top_left)?;
445 for _ in 0..self.size.width {
446 write!(backend, "{}", symbol_set.box_horizontal)?;
447 }
448 writeln!(backend, "{}", symbol_set.box_top_right)?;
449
450 for (i, cell) in self.viewport().iter().enumerate() {
451 if i % width == 0 {
452 write!(backend, "{}", symbol_set.box_vertical)?;
453 }
454
455 if cell.attributes != attributes {
456 backend.set_attributes(cell.attributes)?;
457 attributes = cell.attributes;
458 }
459
460 let (cell_fg, cell_bg) = if i == cursor {
461 (
462 map_reset(cell.bg, Color::Black),
463 map_reset(cell.fg, Color::Grey),
464 )
465 } else {
466 (cell.fg, cell.bg)
467 };
468
469 if cell_fg != fg {
470 backend.set_fg(cell_fg)?;
471 fg = cell_fg;
472 }
473 if cell_bg != bg {
474 backend.set_bg(cell_bg)?;
475 bg = cell_bg;
476 }
477
478 write!(backend, "{}", cell.value.unwrap_or(' '))?;
479
480 if (i + 1) % width == 0 {
481 if !attributes.is_empty() {
482 backend.set_attributes(Attributes::empty())?;
483 attributes = Attributes::empty();
484 }
485 if fg != Color::Reset {
486 fg = Color::Reset;
487 backend.set_fg(fg)?;
488 }
489 if bg != Color::Reset {
490 bg = Color::Reset;
491 backend.set_bg(bg)?;
492 }
493 writeln!(backend, "{}", symbol_set.box_vertical)?;
494 }
495 }
496
497 write!(backend, "{}", symbol_set.box_bottom_left)?;
498 for _ in 0..self.size.width {
499 write!(backend, "{}", symbol_set.box_horizontal)?;
500 }
501 write!(backend, "{}", symbol_set.box_bottom_right)?;
502
503 backend.flush()
504 }
505
506 #[cfg(any(feature = "crossterm", feature = "termion"))]
514 #[cfg_attr(docsrs, doc(cfg(any(feature = "crossterm", feature = "termion"))))]
515 pub fn write_to_buf<W: Write>(&self, buf: W) -> io::Result<()> {
516 #[cfg(feature = "crossterm")]
517 return self.write_to_backend(super::CrosstermBackend::new(buf));
518 #[cfg(all(not(feature = "crossterm"), feature = "termion"))]
519 return self.write_to_backend(super::TermionDisplayBackend::new(buf));
520 }
521}