1use crate::backend::Backend;
4use crate::color::Color;
5use crate::event::Event;
6use crate::grid::{Grid, Pos, Rect, Size};
7use crate::style::Style;
8use crate::text::Line;
9use crate::tile::Tile;
10use core::time::Duration;
11#[cfg(not(feature = "egc"))]
12use unicode_width::UnicodeWidthChar;
13
14pub struct Terminal<B: Backend> {
21 current: Grid,
22 previous: Grid,
23 flattened_current: Grid,
28 flattened_previous: Grid,
29 backend: B,
30 drawing_style: Style,
31 queued_event: Option<Event>,
32 active_layer: u8,
34 flattened_stale: bool,
39}
40
41impl<B: Backend> Terminal<B> {
42 #[must_use]
45 pub fn new(backend: B) -> Self {
46 let size = backend.size();
47 let current = Grid::new(size.width, size.height);
48 let previous = Grid::new(size.width, size.height);
49 let flattened_current = Grid::new(size.width, size.height);
50 let flattened_previous = Grid::new(size.width, size.height);
51 Self {
52 current,
53 previous,
54 flattened_current,
55 flattened_previous,
56 backend,
57 drawing_style: Style::default(),
58 queued_event: None,
59 active_layer: 0,
60 flattened_stale: false,
61 }
62 }
63
64 pub const fn layer(&mut self, layer: u8) -> &mut Self {
69 self.active_layer = layer;
70 self
71 }
72
73 pub const fn fg(&mut self, color: Color) -> &mut Self {
75 self.drawing_style.fg = color;
76 self
77 }
78
79 pub const fn bg(&mut self, color: Color) -> &mut Self {
81 self.drawing_style.bg = color;
82 self
83 }
84
85 pub fn reset_style(&mut self) -> &mut Self {
87 self.drawing_style = Style::default();
88 self
89 }
90
91 #[must_use]
93 pub const fn style(&self) -> Style {
94 self.drawing_style
95 }
96
97 #[must_use]
99 pub const fn size(&self) -> Size {
100 Size {
101 width: self.current.width(),
102 height: self.current.height(),
103 }
104 }
105
106 #[must_use]
111 pub const fn area(&self) -> Rect {
112 Rect::new(0, 0, self.current.width(), self.current.height())
113 }
114
115 pub fn resize(&mut self, width: u16, height: u16) {
121 self.current.resize(width, height);
122 self.previous.resize(width, height);
123 self.flattened_current.resize(width, height);
124 self.flattened_previous.resize(width, height);
125 self.previous.clear_all();
128 self.flattened_previous.clear_all();
129 self.backend.resize(Size { width, height });
130 }
131
132 pub fn put(&mut self, x: u16, y: u16, ch: char) {
141 let style = self.drawing_style;
142 #[cfg(feature = "egc")]
143 {
144 let mut buf = [0u8; 4];
145 let s = ch.encode_utf8(&mut buf);
146 self.current
147 .write_grapheme(self.active_layer, x, y, s, style);
148 }
149 #[cfg(not(feature = "egc"))]
150 {
151 let tile = Tile::new(ch, style);
152 self.current.put_tile(self.active_layer, x, y, tile);
153 }
154 }
155
156 pub fn put_at(&mut self, pos: Pos, ch: char) {
161 self.put(pos.x, pos.y, ch);
162 }
163
164 #[must_use]
166 pub const fn grid(&self) -> &Grid {
167 &self.current
168 }
169
170 pub const fn grid_mut(&mut self) -> &mut Grid {
172 &mut self.current
173 }
174
175 #[must_use]
177 pub const fn backend(&self) -> &B {
178 &self.backend
179 }
180
181 pub const fn backend_mut(&mut self) -> &mut B {
183 &mut self.backend
184 }
185
186 pub fn clear(&mut self) {
188 self.current.clear(self.active_layer);
189 }
190
191 pub fn clear_all(&mut self) {
193 self.current.clear_all();
194 }
195
196 pub fn clear_region(&mut self, rect: Rect) {
198 for y in rect.top()..rect.bottom() {
199 for x in rect.left()..rect.right() {
200 if let Some(cell) = self.current.checked_get_mut(x, y) {
201 *cell = Tile::default();
202 }
203 }
204 }
205 }
206
207 pub fn put_styled(&mut self, x: u16, y: u16, ch: char, style: Style) {
209 #[cfg(feature = "egc")]
210 {
211 let mut buf = [0u8; 4];
212 let s = ch.encode_utf8(&mut buf);
213 self.current
214 .write_grapheme(self.active_layer, x, y, s, style);
215 }
216 #[cfg(not(feature = "egc"))]
217 {
218 let tile = Tile::new(ch, style);
219 self.current.put_tile(self.active_layer, x, y, tile);
220 }
221 }
222
223 pub fn put_offset(&mut self, x: u16, y: u16, dx: i16, dy: i16, ch: char) {
229 let tile = Tile::new(ch, self.drawing_style).with_offset(dx, dy);
230 self.current.put_tile(self.active_layer, x, y, tile);
231 }
232
233 pub fn print(&mut self, x: u16, y: u16, text: &str) {
239 let style = self.drawing_style;
240 #[cfg(feature = "egc")]
241 self.print_str_egc(x, y, text, style);
242 #[cfg(not(feature = "egc"))]
243 self.print_str_chars(x, y, text, style);
244 }
245
246 pub fn print_styled(&mut self, x: u16, y: u16, line: &Line) {
252 #[cfg(feature = "egc")]
253 {
254 use unicode_segmentation::UnicodeSegmentation;
255 use unicode_width::UnicodeWidthStr;
256 let mut cur_x = x;
257 for span in &line.spans {
258 for grapheme in span.content.graphemes(true) {
259 if grapheme == "\n" {
260 break;
261 }
262 #[allow(clippy::cast_possible_truncation)]
263 let w = grapheme.width() as u16;
264 if w == 0 {
265 continue;
266 }
267 if cur_x >= self.current.width() {
268 break;
269 }
270 self.current
271 .write_grapheme(self.active_layer, cur_x, y, grapheme, span.style);
272 cur_x += w;
273 }
274 }
275 }
276 #[cfg(not(feature = "egc"))]
277 {
278 use unicode_width::UnicodeWidthChar;
279 let mut cur_x = x;
280 for span in &line.spans {
281 for ch in span.content.chars() {
282 if ch == '\n' {
283 break;
284 }
285 #[allow(clippy::cast_possible_truncation)]
286 let w = UnicodeWidthChar::width(ch).unwrap_or(1) as u16;
287 if usize::from(cur_x) >= usize::from(self.current.width()) {
288 break;
289 }
290 let tile = Tile::new(ch, span.style);
291 self.current.put_tile(self.active_layer, cur_x, y, tile);
292 cur_x += w;
293 }
294 }
295 }
296 }
297
298 #[cfg(feature = "egc")]
308 pub fn print_box(
309 &mut self,
310 rect: Rect,
311 line: &Line,
312 h_align: crate::layout::HAlign,
313 v_align: crate::layout::VAlign,
314 ) {
315 crate::layout::TextLayout::new(line)
316 .rect(rect)
317 .h_align(h_align)
318 .v_align(v_align)
319 .render(self);
320 }
321
322 pub fn present(&mut self) -> Result<(), <B as Backend>::Error> {
354 if self.backend.composites_layers() {
355 if self.backend.needs_full_frame() {
357 let all = self.current.layers();
358 self.backend.draw_layers(all)?;
359 } else {
360 let diff = self.current.diff(&self.previous);
361 self.backend.draw_layers(diff)?;
362 }
363 } else if self.current.max_layer() == 0 && self.previous.max_layer() == 0 {
364 let diff = self.current.diff(&self.previous);
368 self.backend.draw_layers(diff)?;
369 self.flattened_stale = true;
370 } else {
371 if self.flattened_stale {
374 self.flattened_previous.clear_all();
377 self.flattened_stale = false;
378 }
379 self.current.flatten_into(&mut self.flattened_current);
380 let diff = self.flattened_current.diff(&self.flattened_previous);
381 self.backend.draw_layers(diff)?;
382 core::mem::swap(&mut self.flattened_current, &mut self.flattened_previous);
383 }
384 self.backend.flush()?;
385 core::mem::swap(&mut self.current, &mut self.previous);
386 self.current.clear_all();
387 Ok(())
388 }
389
390 pub fn poll(&mut self, timeout: Duration) -> Option<Event> {
399 let event = self
400 .queued_event
401 .take()
402 .or_else(|| self.backend.poll_event(timeout))?;
403 if let Event::Resize(w, h) = event {
404 self.resize(w, h);
405 }
406 Some(event)
407 }
408
409 pub fn read_blocking(&mut self) -> Event {
422 self.poll(Duration::MAX)
423 .expect("read_blocking() called but no events available")
424 }
425
426 pub fn drain_events(&mut self) -> impl Iterator<Item = Event> + use<'_, B> {
440 struct DrainEvents<'a, B: Backend> {
441 terminal: &'a mut Terminal<B>,
442 }
443
444 impl<B: Backend> Iterator for DrainEvents<'_, B> {
445 type Item = Event;
446
447 fn next(&mut self) -> Option<Event> {
448 self.terminal.poll(Duration::ZERO)
449 }
450 }
451
452 impl<B: Backend> core::iter::FusedIterator for DrainEvents<'_, B> {}
453
454 DrainEvents { terminal: self }
455 }
456
457 pub fn has_input(&mut self) -> bool {
463 if self.queued_event.is_some() {
464 true
465 } else if let Some(event) = self.backend.poll_event(Duration::ZERO) {
466 self.queued_event = Some(event);
467 true
468 } else {
469 false
470 }
471 }
472
473 #[cfg(feature = "egc")]
475 fn print_str_egc(&mut self, x: u16, y: u16, text: &str, style: Style) {
476 use unicode_segmentation::UnicodeSegmentation;
477 use unicode_width::UnicodeWidthStr;
478 let layer = self.active_layer;
479 let mut cur_x = x;
480 let mut cur_y = y;
481 for grapheme in text.graphemes(true) {
482 if grapheme == "\n" {
483 cur_x = x;
484 cur_y += 1;
485 continue;
486 }
487 #[allow(clippy::cast_possible_truncation)]
488 let w = grapheme.width() as u16;
489 if w == 0 {
490 continue;
491 }
492 self.current
493 .write_grapheme(layer, cur_x, cur_y, grapheme, style);
494 cur_x += w;
495 if cur_x >= self.current.width() {
496 cur_x = x;
497 cur_y += 1;
498 }
499 }
500 }
501
502 #[cfg(not(feature = "egc"))]
504 fn print_str_chars(&mut self, x: u16, y: u16, text: &str, style: Style) {
505 let mut cur_x = x;
506 let mut cur_y = y;
507 for c in text.chars() {
508 if c == '\n' {
509 cur_x = x;
510 cur_y += 1;
511 } else {
512 #[allow(clippy::cast_possible_truncation)]
513 let w = UnicodeWidthChar::width(c).unwrap_or(1) as u16;
514 let tile = Tile::new(c, style);
515 self.current.put_tile(self.active_layer, cur_x, cur_y, tile);
516 cur_x += w;
517 if usize::from(cur_x) >= usize::from(self.current.width()) {
518 cur_x = x;
519 cur_y += 1;
520 }
521 }
522 }
523 }
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529 use crate::backend::Headless;
530 use crate::tile::Tile;
531
532 #[test]
533 fn test_terminal_grid_mut() {
534 let backend = Headless::new(10, 10);
535 let mut terminal = Terminal::new(backend);
536
537 assert_eq!(terminal.grid().get(0, 0).glyph(), ' ');
538
539 terminal
540 .grid_mut()
541 .put(0, 0, Tile::new('X', Style::default()));
542
543 assert_eq!(terminal.grid().get(0, 0).glyph(), 'X');
544 }
545
546 #[test]
547 fn test_terminal_poll_and_read() {
548 let backend = Headless::new(10, 10);
549 let mut terminal = Terminal::new(backend);
550
551 assert_eq!(terminal.poll(Duration::ZERO), None);
552
553 terminal.backend_mut().push_event(Event::Close);
554 assert_eq!(terminal.poll(Duration::ZERO), Some(Event::Close));
555
556 terminal.backend_mut().push_event(Event::Resize(80, 25));
557 assert_eq!(terminal.read_blocking(), Event::Resize(80, 25));
558 }
559
560 #[test]
561 fn test_terminal_has_input() {
562 let backend = Headless::new(10, 10);
563 let mut terminal = Terminal::new(backend);
564
565 assert!(!terminal.has_input());
566
567 terminal.backend_mut().push_event(Event::Close);
568 assert!(terminal.has_input());
569 assert!(terminal.has_input()); assert_eq!(terminal.poll(Duration::ZERO), Some(Event::Close));
573
574 assert!(!terminal.has_input());
576 }
577
578 #[test]
579 #[should_panic(expected = "read_blocking() called but no events available")]
580 fn test_terminal_read_panic() {
581 let backend = Headless::new(10, 10);
582 let mut terminal = Terminal::new(backend);
583 let _ = terminal.read_blocking();
584 }
585
586 #[test]
589 fn test_present_composites_layers_for_cell_backend() {
590 let mut term = Terminal::new(Headless::new(3, 1));
593 term.layer(0).put(0, 0, '.');
594 term.layer(0).put(1, 0, '.');
595 term.layer(1).put(1, 0, '@');
596 term.present().expect("present failed");
597 assert_eq!(term.backend().grid().get(0, 0).glyph(), '.');
598 assert_eq!(term.backend().grid().get(1, 0).glyph(), '@');
600 }
601
602 #[test]
603 fn test_present_explicit_space_on_higher_layer_erases_and_sets_bg() {
604 let mut term = Terminal::new(Headless::new(2, 1));
608 term.layer(0).put(0, 0, 'x');
609 term.layer(1)
610 .put_styled(0, 0, ' ', Style::new().bg(Color::RED));
611 term.present().expect("present failed");
612 let cell = term.backend().grid().get(0, 0);
613 assert_eq!(cell.glyph(), ' ');
614 assert_eq!(cell.style().background(), Color::RED);
615 }
616
617 #[test]
618 fn test_present_single_layer_fast_path_matches_backend() {
619 let mut term = Terminal::new(Headless::new(3, 1));
622 term.put(0, 0, 'a');
623 term.present().expect("present failed");
624 assert_eq!(term.backend().grid().get(0, 0).glyph(), 'a');
625
626 term.put(0, 0, 'a');
629 term.put(2, 0, 'c');
630 term.present().expect("present failed");
631 assert_eq!(term.backend().grid().get(0, 0).glyph(), 'a');
632 assert_eq!(term.backend().grid().get(2, 0).glyph(), 'c');
633
634 term.put(0, 0, 'a');
636 term.present().expect("present failed");
637 assert_eq!(term.backend().grid().get(0, 0).glyph(), 'a');
638 assert_eq!(term.backend().grid().get(2, 0).glyph(), ' ');
639 }
640
641 #[test]
642 fn test_present_transition_single_to_multi_layer() {
643 let mut term = Terminal::new(Headless::new(2, 1));
647 term.layer(0).put(0, 0, '.');
648 term.layer(0).put(1, 0, '.');
649 term.present().expect("present failed");
650
651 term.layer(0).put(0, 0, '.');
652 term.layer(0).put(1, 0, '.');
653 term.layer(1).put(1, 0, '@');
654 term.present().expect("present failed");
655 assert_eq!(term.backend().grid().get(0, 0).glyph(), '.');
656 assert_eq!(term.backend().grid().get(1, 0).glyph(), '@');
657 }
658
659 #[test]
660 fn test_present_untouched_higher_layer_is_transparent() {
661 let mut term = Terminal::new(Headless::new(2, 1));
664 term.layer(0).put(0, 0, 'x');
665 term.layer(1).put(1, 0, 'y');
667 term.present().expect("present failed");
668 assert_eq!(term.backend().grid().get(0, 0).glyph(), 'x');
669 }
670
671 #[test]
672 fn test_terminal_size() {
673 let term = Terminal::new(Headless::new(40, 20));
674 assert_eq!(
675 term.size(),
676 Size {
677 width: 40,
678 height: 20
679 }
680 );
681 }
682
683 #[test]
684 fn test_terminal_area() {
685 let term = Terminal::new(Headless::new(40, 20));
686 assert_eq!(term.area(), Rect::new(0, 0, 40, 20));
687 }
688
689 #[test]
690 fn test_terminal_resize_changes_dimensions() {
691 let mut term = Terminal::new(Headless::new(10, 10));
692 term.resize(30, 15);
693 assert_eq!(
694 term.size(),
695 Size {
696 width: 30,
697 height: 15
698 }
699 );
700 assert_eq!(term.grid().width(), 30);
701 assert_eq!(term.grid().height(), 15);
702 }
703
704 #[test]
705 fn test_terminal_resize_preserves_current_content() {
706 let mut term = Terminal::new(Headless::new(10, 10));
707 term.put(2, 2, 'X');
708 term.resize(20, 20);
709 assert_eq!(term.grid().get(2, 2).glyph(), 'X');
710 assert_eq!(term.grid().get(15, 15).glyph(), ' ');
711 }
712
713 #[test]
714 fn test_terminal_resize_event_auto_applies() {
715 let mut term = Terminal::new(Headless::new(10, 10));
716 term.backend_mut().push_event(Event::Resize(80, 25));
717 let event = term.poll(Duration::ZERO);
718 assert_eq!(event, Some(Event::Resize(80, 25)));
719 assert_eq!(
720 term.size(),
721 Size {
722 width: 80,
723 height: 25
724 }
725 );
726 }
727
728 #[test]
729 fn test_terminal_resize_new_cells_accessible() {
730 let mut term = Terminal::new(Headless::new(3, 3));
732 term.put(0, 0, 'A');
733 term.present();
734
735 term.resize(5, 5);
736
737 term.put(4, 4, 'B');
739 term.present();
740
741 assert_eq!(term.backend().grid().get(4, 4).glyph(), 'B');
742 assert_eq!(term.backend().grid().get(0, 0).glyph(), 'A');
744 }
745
746 #[test]
749 fn test_put_wide_char_sets_continuation() {
750 let mut term = Terminal::new(Headless::new(10, 3));
751 term.put(0, 0, '\u{4e2d}'); assert_eq!(term.grid().get(0, 0).glyph(), '\u{4e2d}');
753 #[cfg(feature = "egc")]
756 {
757 use crate::tile::TileFlags;
758 assert!(
759 term.grid()
760 .get(1, 0)
761 .flags()
762 .contains(TileFlags::WIDE_CHAR_SPACER)
763 );
764 assert_eq!(term.grid().get(1, 0).glyph(), ' ');
765 }
766 #[cfg(not(feature = "egc"))]
767 assert_eq!(term.grid().get(1, 0).glyph(), '\0');
768 assert_eq!(term.grid().get(2, 0).glyph(), ' '); }
770
771 #[test]
772 fn test_print_advances_by_char_width() {
773 let mut term = Terminal::new(Headless::new(10, 3));
774 term.print(0, 0, "\u{4e2d}x"); assert_eq!(term.grid().get(0, 0).glyph(), '\u{4e2d}');
776 #[cfg(feature = "egc")]
777 {
778 use crate::tile::TileFlags;
779 assert!(
780 term.grid()
781 .get(1, 0)
782 .flags()
783 .contains(TileFlags::WIDE_CHAR_SPACER)
784 );
785 }
786 #[cfg(not(feature = "egc"))]
787 assert_eq!(term.grid().get(1, 0).glyph(), '\0');
788 assert_eq!(term.grid().get(2, 0).glyph(), 'x');
789 }
790
791 #[test]
792 fn test_put_at_matches_put() {
793 let mut term = Terminal::new(Headless::new(10, 3));
794 term.put_at(Pos::new(2, 1), 'X');
795 assert_eq!(term.grid().get(2, 1).glyph(), 'X');
796 }
797
798 #[test]
799 fn test_put_wide_char_at_last_column_does_not_overflow() {
800 let mut term = Terminal::new(Headless::new(4, 1));
803 term.put(3, 0, '\u{4e2d}'); assert_eq!(term.grid().get(3, 0).glyph(), ' '); }
806
807 #[test]
810 fn test_print_styled_basic() {
811 use crate::text::{Line, Span};
812 let mut term = Terminal::new(Headless::new(20, 3));
813 let line = Line::from(vec![
814 Span::raw("HP: "),
815 Span::styled("100", Style::new().fg(Color::GREEN)),
816 ]);
817 term.print_styled(0, 0, &line);
818 assert_eq!(term.grid().get(0, 0).glyph(), 'H');
819 assert_eq!(term.grid().get(3, 0).glyph(), ' ');
820 assert_eq!(term.grid().get(4, 0).glyph(), '1');
821 assert_eq!(term.grid().get(4, 0).style.fg, Color::GREEN);
822 assert_eq!(term.grid().get(6, 0).glyph(), '0');
823 }
824
825 #[test]
826 fn test_print_styled_does_not_modify_drawing_style() {
827 use crate::text::{Line, Span};
828 let mut term = Terminal::new(Headless::new(20, 3));
829 term.fg(Color::RED);
830 let line = Line::from(vec![Span::styled("hi", Style::new().fg(Color::BLUE))]);
831 term.print_styled(0, 0, &line);
832 assert_eq!(term.style().fg, Color::RED);
834 }
835
836 #[test]
837 fn test_print_styled_wide_chars() {
838 use crate::text::{Line, Span};
839 let mut term = Terminal::new(Headless::new(10, 3));
840 let line = Line::from(vec![Span::raw("\u{4e2d}x")]);
841 term.print_styled(0, 0, &line);
842 assert_eq!(term.grid().get(0, 0).glyph(), '\u{4e2d}');
843 #[cfg(feature = "egc")]
844 {
845 use crate::tile::TileFlags;
846 assert!(
847 term.grid()
848 .get(1, 0)
849 .flags()
850 .contains(TileFlags::WIDE_CHAR_SPACER)
851 );
852 }
853 #[cfg(not(feature = "egc"))]
854 assert_eq!(term.grid().get(1, 0).glyph(), '\0');
855 assert_eq!(term.grid().get(2, 0).glyph(), 'x');
856 }
857}