1use crate::backend::{Backend, Output};
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 present_count: u64,
46}
47
48impl<B: Backend> Terminal<B> {
49 #[must_use]
52 pub fn new(backend: B) -> Self {
53 let size = backend.size();
54 let current = Grid::new(size.width, size.height);
55 let previous = Grid::new(size.width, size.height);
56 let flattened_current = Grid::new(size.width, size.height);
57 let flattened_previous = Grid::new(size.width, size.height);
58 Self {
59 current,
60 previous,
61 flattened_current,
62 flattened_previous,
63 backend,
64 drawing_style: Style::default(),
65 queued_event: None,
66 active_layer: 0,
67 flattened_stale: false,
68 present_count: 0,
69 }
70 }
71
72 pub const fn layer(&mut self, layer: u8) -> &mut Self {
77 self.active_layer = layer;
78 self
79 }
80
81 pub const fn fg(&mut self, color: Color) -> &mut Self {
83 self.drawing_style.fg = color;
84 self
85 }
86
87 pub const fn bg(&mut self, color: Color) -> &mut Self {
89 self.drawing_style.bg = color;
90 self
91 }
92
93 pub fn reset_style(&mut self) -> &mut Self {
95 self.drawing_style = Style::default();
96 self
97 }
98
99 #[must_use]
101 pub const fn style(&self) -> Style {
102 self.drawing_style
103 }
104
105 #[must_use]
107 pub const fn size(&self) -> Size {
108 Size {
109 width: self.current.width(),
110 height: self.current.height(),
111 }
112 }
113
114 #[must_use]
119 pub const fn area(&self) -> Rect {
120 Rect::new(0, 0, self.current.width(), self.current.height())
121 }
122
123 pub fn resize(&mut self, width: u16, height: u16) {
129 self.current.resize(width, height);
130 self.previous.resize(width, height);
131 self.flattened_current.resize(width, height);
132 self.flattened_previous.resize(width, height);
133 self.previous.clear_all();
136 self.flattened_previous.clear_all();
137 self.backend.resize(Size { width, height });
138 }
139
140 pub fn put(&mut self, x: u16, y: u16, ch: char) {
149 let style = self.drawing_style;
150 #[cfg(feature = "egc")]
151 {
152 let mut buf = [0u8; 4];
153 let s = ch.encode_utf8(&mut buf);
154 self.current
155 .write_grapheme(self.active_layer, x, y, s, style);
156 }
157 #[cfg(not(feature = "egc"))]
158 {
159 let tile = Tile::new(ch, style);
160 self.current.put_tile(self.active_layer, x, y, tile);
161 }
162 }
163
164 pub fn put_at(&mut self, pos: Pos, ch: char) {
169 self.put(pos.x, pos.y, ch);
170 }
171
172 #[must_use]
174 pub const fn grid(&self) -> &Grid {
175 &self.current
176 }
177
178 pub const fn grid_mut(&mut self) -> &mut Grid {
180 &mut self.current
181 }
182
183 #[must_use]
185 pub const fn backend(&self) -> &B {
186 &self.backend
187 }
188
189 pub const fn backend_mut(&mut self) -> &mut B {
191 &mut self.backend
192 }
193
194 pub fn clear(&mut self) {
196 self.current.clear(self.active_layer);
197 }
198
199 pub fn clear_all(&mut self) {
201 self.current.clear_all();
202 }
203
204 pub fn clear_region(&mut self, rect: Rect) {
206 for y in rect.top()..rect.bottom() {
207 for x in rect.left()..rect.right() {
208 if let Some(cell) = self.current.checked_get_mut(x, y) {
209 *cell = Tile::default();
210 }
211 }
212 }
213 }
214
215 pub fn put_styled(&mut self, x: u16, y: u16, ch: char, style: Style) {
217 #[cfg(feature = "egc")]
218 {
219 let mut buf = [0u8; 4];
220 let s = ch.encode_utf8(&mut buf);
221 self.current
222 .write_grapheme(self.active_layer, x, y, s, style);
223 }
224 #[cfg(not(feature = "egc"))]
225 {
226 let tile = Tile::new(ch, style);
227 self.current.put_tile(self.active_layer, x, y, tile);
228 }
229 }
230
231 pub fn put_offset(&mut self, x: u16, y: u16, dx: i16, dy: i16, ch: char) {
237 let tile = Tile::new(ch, self.drawing_style).with_offset(dx, dy);
238 self.current.put_tile(self.active_layer, x, y, tile);
239 }
240
241 pub fn print(&mut self, x: u16, y: u16, text: &str) {
247 let style = self.drawing_style;
248 #[cfg(feature = "egc")]
249 self.print_str_egc(x, y, text, style);
250 #[cfg(not(feature = "egc"))]
251 self.print_str_chars(x, y, text, style);
252 }
253
254 pub fn print_styled(&mut self, x: u16, y: u16, line: &Line) {
260 #[cfg(feature = "egc")]
261 {
262 use unicode_segmentation::UnicodeSegmentation;
263 use unicode_width::UnicodeWidthStr;
264 let mut cur_x = x;
265 for span in &line.spans {
266 for grapheme in span.content.graphemes(true) {
267 if grapheme == "\n" {
268 break;
269 }
270 #[allow(clippy::cast_possible_truncation)]
271 let w = grapheme.width() as u16;
272 if w == 0 {
273 continue;
274 }
275 if cur_x >= self.current.width() {
276 break;
277 }
278 self.current
279 .write_grapheme(self.active_layer, cur_x, y, grapheme, span.style);
280 cur_x += w;
281 }
282 }
283 }
284 #[cfg(not(feature = "egc"))]
285 {
286 use unicode_width::UnicodeWidthChar;
287 let mut cur_x = x;
288 for span in &line.spans {
289 for ch in span.content.chars() {
290 if ch == '\n' {
291 break;
292 }
293 #[allow(clippy::cast_possible_truncation)]
294 let w = UnicodeWidthChar::width(ch).unwrap_or(1) as u16;
295 if usize::from(cur_x) >= usize::from(self.current.width()) {
296 break;
297 }
298 let tile = Tile::new(ch, span.style);
299 self.current.put_tile(self.active_layer, cur_x, y, tile);
300 cur_x += w;
301 }
302 }
303 }
304 }
305
306 #[cfg(feature = "egc")]
316 pub fn print_box(
317 &mut self,
318 rect: Rect,
319 line: &Line,
320 h_align: crate::layout::HAlign,
321 v_align: crate::layout::VAlign,
322 ) {
323 crate::layout::TextLayout::new(line)
324 .rect(rect)
325 .h_align(h_align)
326 .v_align(v_align)
327 .render(self);
328 }
329
330 #[must_use]
339 pub const fn present_count(&self) -> u64 {
340 self.present_count
341 }
342
343 pub fn present(&mut self) -> Result<(), <B as Output>::Error> {
382 self.present_count = self.present_count.wrapping_add(1);
383 if self.backend.composites_layers() {
384 if self.backend.needs_full_frame() {
386 let all = self.current.layers();
387 self.backend.draw_layers(all)?;
388 } else {
389 let diff = self.current.diff(&self.previous);
390 self.backend.draw_layers(diff)?;
391 }
392 } else if self.current.max_layer() == 0 && self.previous.max_layer() == 0 {
393 let diff = self.current.diff(&self.previous);
397 self.backend.draw_layers(diff)?;
398 self.flattened_stale = true;
399 } else {
400 if self.flattened_stale {
403 self.flattened_previous.clear_all();
406 self.flattened_stale = false;
407 }
408 self.current.flatten_into(&mut self.flattened_current);
409 let diff = self.flattened_current.diff(&self.flattened_previous);
410 self.backend.draw_layers(diff)?;
411 core::mem::swap(&mut self.flattened_current, &mut self.flattened_previous);
412 }
413 self.backend.flush()?;
414 core::mem::swap(&mut self.current, &mut self.previous);
415 self.current.clear_all();
416 Ok(())
417 }
418
419 pub fn poll(&mut self, timeout: Duration) -> Option<Event> {
428 let event = self
429 .queued_event
430 .take()
431 .or_else(|| self.backend.poll_event(timeout))?;
432 if let Event::Resize(w, h) = event {
433 self.resize(w, h);
434 }
435 Some(event)
436 }
437
438 pub fn read_blocking(&mut self) -> Event {
451 self.poll(Duration::MAX)
452 .expect("read_blocking() called but no events available")
453 }
454
455 pub fn drain_events(&mut self) -> impl Iterator<Item = Event> + use<'_, B> {
469 struct DrainEvents<'a, B: Backend> {
470 terminal: &'a mut Terminal<B>,
471 }
472
473 impl<B: Backend> Iterator for DrainEvents<'_, B> {
474 type Item = Event;
475
476 fn next(&mut self) -> Option<Event> {
477 self.terminal.poll(Duration::ZERO)
478 }
479 }
480
481 impl<B: Backend> core::iter::FusedIterator for DrainEvents<'_, B> {}
482
483 DrainEvents { terminal: self }
484 }
485
486 pub fn has_input(&mut self) -> bool {
492 if self.queued_event.is_some() {
493 true
494 } else if let Some(event) = self.backend.poll_event(Duration::ZERO) {
495 self.queued_event = Some(event);
496 true
497 } else {
498 false
499 }
500 }
501
502 #[cfg(feature = "egc")]
504 fn print_str_egc(&mut self, x: u16, y: u16, text: &str, style: Style) {
505 use unicode_segmentation::UnicodeSegmentation;
506 use unicode_width::UnicodeWidthStr;
507 let layer = self.active_layer;
508 let mut cur_x = x;
509 let mut cur_y = y;
510 for grapheme in text.graphemes(true) {
511 if grapheme == "\n" {
512 cur_x = x;
513 cur_y += 1;
514 continue;
515 }
516 #[allow(clippy::cast_possible_truncation)]
517 let w = grapheme.width() as u16;
518 if w == 0 {
519 continue;
520 }
521 self.current
522 .write_grapheme(layer, cur_x, cur_y, grapheme, style);
523 cur_x += w;
524 if cur_x >= self.current.width() {
525 cur_x = x;
526 cur_y += 1;
527 }
528 }
529 }
530
531 #[cfg(not(feature = "egc"))]
533 fn print_str_chars(&mut self, x: u16, y: u16, text: &str, style: Style) {
534 let mut cur_x = x;
535 let mut cur_y = y;
536 for c in text.chars() {
537 if c == '\n' {
538 cur_x = x;
539 cur_y += 1;
540 } else {
541 #[allow(clippy::cast_possible_truncation)]
542 let w = UnicodeWidthChar::width(c).unwrap_or(1) as u16;
543 let tile = Tile::new(c, style);
544 self.current.put_tile(self.active_layer, cur_x, cur_y, tile);
545 cur_x += w;
546 if usize::from(cur_x) >= usize::from(self.current.width()) {
547 cur_x = x;
548 cur_y += 1;
549 }
550 }
551 }
552 }
553}
554
555#[cfg(test)]
556mod tests {
557 use super::*;
558 use crate::backend::Headless;
559 use crate::tile::Tile;
560
561 #[test]
562 fn test_terminal_grid_mut() {
563 let backend = Headless::new(10, 10);
564 let mut terminal = Terminal::new(backend);
565
566 assert_eq!(terminal.grid().get(0, 0).glyph(), ' ');
567
568 terminal
569 .grid_mut()
570 .put(0, 0, Tile::new('X', Style::default()));
571
572 assert_eq!(terminal.grid().get(0, 0).glyph(), 'X');
573 }
574
575 #[test]
576 fn test_terminal_poll_and_read() {
577 let backend = Headless::new(10, 10);
578 let mut terminal = Terminal::new(backend);
579
580 assert_eq!(terminal.poll(Duration::ZERO), None);
581
582 terminal.backend_mut().push_event(Event::Close);
583 assert_eq!(terminal.poll(Duration::ZERO), Some(Event::Close));
584
585 terminal.backend_mut().push_event(Event::Resize(80, 25));
586 assert_eq!(terminal.read_blocking(), Event::Resize(80, 25));
587 }
588
589 #[test]
590 fn test_terminal_has_input() {
591 let backend = Headless::new(10, 10);
592 let mut terminal = Terminal::new(backend);
593
594 assert!(!terminal.has_input());
595
596 terminal.backend_mut().push_event(Event::Close);
597 assert!(terminal.has_input());
598 assert!(terminal.has_input()); assert_eq!(terminal.poll(Duration::ZERO), Some(Event::Close));
602
603 assert!(!terminal.has_input());
605 }
606
607 #[test]
608 #[should_panic(expected = "read_blocking() called but no events available")]
609 fn test_terminal_read_panic() {
610 let backend = Headless::new(10, 10);
611 let mut terminal = Terminal::new(backend);
612 let _ = terminal.read_blocking();
613 }
614
615 #[test]
618 fn test_present_composites_layers_for_cell_backend() {
619 let mut term = Terminal::new(Headless::new(3, 1));
622 term.layer(0).put(0, 0, '.');
623 term.layer(0).put(1, 0, '.');
624 term.layer(1).put(1, 0, '@');
625 term.present().expect("present failed");
626 assert_eq!(term.backend().grid().get(0, 0).glyph(), '.');
627 assert_eq!(term.backend().grid().get(1, 0).glyph(), '@');
629 }
630
631 #[test]
632 fn test_present_explicit_space_on_higher_layer_erases_and_sets_bg() {
633 let mut term = Terminal::new(Headless::new(2, 1));
637 term.layer(0).put(0, 0, 'x');
638 term.layer(1)
639 .put_styled(0, 0, ' ', Style::new().bg(Color::RED));
640 term.present().expect("present failed");
641 let cell = term.backend().grid().get(0, 0);
642 assert_eq!(cell.glyph(), ' ');
643 assert_eq!(cell.style().background(), Color::RED);
644 }
645
646 #[test]
647 fn test_present_single_layer_fast_path_matches_backend() {
648 let mut term = Terminal::new(Headless::new(3, 1));
651 term.put(0, 0, 'a');
652 term.present().expect("present failed");
653 assert_eq!(term.backend().grid().get(0, 0).glyph(), 'a');
654
655 term.put(0, 0, 'a');
658 term.put(2, 0, 'c');
659 term.present().expect("present failed");
660 assert_eq!(term.backend().grid().get(0, 0).glyph(), 'a');
661 assert_eq!(term.backend().grid().get(2, 0).glyph(), 'c');
662
663 term.put(0, 0, 'a');
665 term.present().expect("present failed");
666 assert_eq!(term.backend().grid().get(0, 0).glyph(), 'a');
667 assert_eq!(term.backend().grid().get(2, 0).glyph(), ' ');
668 }
669
670 #[test]
671 fn test_present_transition_single_to_multi_layer() {
672 let mut term = Terminal::new(Headless::new(2, 1));
676 term.layer(0).put(0, 0, '.');
677 term.layer(0).put(1, 0, '.');
678 term.present().expect("present failed");
679
680 term.layer(0).put(0, 0, '.');
681 term.layer(0).put(1, 0, '.');
682 term.layer(1).put(1, 0, '@');
683 term.present().expect("present failed");
684 assert_eq!(term.backend().grid().get(0, 0).glyph(), '.');
685 assert_eq!(term.backend().grid().get(1, 0).glyph(), '@');
686 }
687
688 #[test]
689 fn test_present_untouched_higher_layer_is_transparent() {
690 let mut term = Terminal::new(Headless::new(2, 1));
693 term.layer(0).put(0, 0, 'x');
694 term.layer(1).put(1, 0, 'y');
696 term.present().expect("present failed");
697 assert_eq!(term.backend().grid().get(0, 0).glyph(), 'x');
698 }
699
700 #[test]
701 fn test_terminal_size() {
702 let term = Terminal::new(Headless::new(40, 20));
703 assert_eq!(
704 term.size(),
705 Size {
706 width: 40,
707 height: 20
708 }
709 );
710 }
711
712 #[test]
713 fn test_terminal_area() {
714 let term = Terminal::new(Headless::new(40, 20));
715 assert_eq!(term.area(), Rect::new(0, 0, 40, 20));
716 }
717
718 #[test]
719 fn test_terminal_resize_changes_dimensions() {
720 let mut term = Terminal::new(Headless::new(10, 10));
721 term.resize(30, 15);
722 assert_eq!(
723 term.size(),
724 Size {
725 width: 30,
726 height: 15
727 }
728 );
729 assert_eq!(term.grid().width(), 30);
730 assert_eq!(term.grid().height(), 15);
731 }
732
733 #[test]
734 fn test_terminal_resize_preserves_current_content() {
735 let mut term = Terminal::new(Headless::new(10, 10));
736 term.put(2, 2, 'X');
737 term.resize(20, 20);
738 assert_eq!(term.grid().get(2, 2).glyph(), 'X');
739 assert_eq!(term.grid().get(15, 15).glyph(), ' ');
740 }
741
742 #[test]
743 fn test_terminal_resize_event_auto_applies() {
744 let mut term = Terminal::new(Headless::new(10, 10));
745 term.backend_mut().push_event(Event::Resize(80, 25));
746 let event = term.poll(Duration::ZERO);
747 assert_eq!(event, Some(Event::Resize(80, 25)));
748 assert_eq!(
749 term.size(),
750 Size {
751 width: 80,
752 height: 25
753 }
754 );
755 }
756
757 #[test]
758 fn test_terminal_resize_new_cells_accessible() {
759 let mut term = Terminal::new(Headless::new(3, 3));
761 term.put(0, 0, 'A');
762 term.present();
763
764 term.resize(5, 5);
765
766 term.put(4, 4, 'B');
768 term.present();
769
770 assert_eq!(term.backend().grid().get(4, 4).glyph(), 'B');
771 assert_eq!(term.backend().grid().get(0, 0).glyph(), 'A');
773 }
774
775 #[test]
778 fn test_put_wide_char_sets_continuation() {
779 let mut term = Terminal::new(Headless::new(10, 3));
780 term.put(0, 0, '\u{4e2d}'); assert_eq!(term.grid().get(0, 0).glyph(), '\u{4e2d}');
782 #[cfg(feature = "egc")]
785 {
786 use crate::tile::TileFlags;
787 assert!(
788 term.grid()
789 .get(1, 0)
790 .flags()
791 .contains(TileFlags::WIDE_CHAR_SPACER)
792 );
793 assert_eq!(term.grid().get(1, 0).glyph(), ' ');
794 }
795 #[cfg(not(feature = "egc"))]
796 assert_eq!(term.grid().get(1, 0).glyph(), '\0');
797 assert_eq!(term.grid().get(2, 0).glyph(), ' '); }
799
800 #[test]
801 fn test_print_advances_by_char_width() {
802 let mut term = Terminal::new(Headless::new(10, 3));
803 term.print(0, 0, "\u{4e2d}x"); assert_eq!(term.grid().get(0, 0).glyph(), '\u{4e2d}');
805 #[cfg(feature = "egc")]
806 {
807 use crate::tile::TileFlags;
808 assert!(
809 term.grid()
810 .get(1, 0)
811 .flags()
812 .contains(TileFlags::WIDE_CHAR_SPACER)
813 );
814 }
815 #[cfg(not(feature = "egc"))]
816 assert_eq!(term.grid().get(1, 0).glyph(), '\0');
817 assert_eq!(term.grid().get(2, 0).glyph(), 'x');
818 }
819
820 #[test]
821 fn test_put_at_matches_put() {
822 let mut term = Terminal::new(Headless::new(10, 3));
823 term.put_at(Pos::new(2, 1), 'X');
824 assert_eq!(term.grid().get(2, 1).glyph(), 'X');
825 }
826
827 #[test]
828 fn test_put_wide_char_at_last_column_does_not_overflow() {
829 let mut term = Terminal::new(Headless::new(4, 1));
832 term.put(3, 0, '\u{4e2d}'); assert_eq!(term.grid().get(3, 0).glyph(), ' '); }
835
836 #[test]
839 fn test_print_styled_basic() {
840 use crate::text::{Line, Span};
841 let mut term = Terminal::new(Headless::new(20, 3));
842 let line = Line::from(vec![
843 Span::raw("HP: "),
844 Span::styled("100", Style::new().fg(Color::GREEN)),
845 ]);
846 term.print_styled(0, 0, &line);
847 assert_eq!(term.grid().get(0, 0).glyph(), 'H');
848 assert_eq!(term.grid().get(3, 0).glyph(), ' ');
849 assert_eq!(term.grid().get(4, 0).glyph(), '1');
850 assert_eq!(term.grid().get(4, 0).style.fg, Color::GREEN);
851 assert_eq!(term.grid().get(6, 0).glyph(), '0');
852 }
853
854 #[test]
855 fn test_print_styled_does_not_modify_drawing_style() {
856 use crate::text::{Line, Span};
857 let mut term = Terminal::new(Headless::new(20, 3));
858 term.fg(Color::RED);
859 let line = Line::from(vec![Span::styled("hi", Style::new().fg(Color::BLUE))]);
860 term.print_styled(0, 0, &line);
861 assert_eq!(term.style().fg, Color::RED);
863 }
864
865 #[test]
866 fn test_print_styled_wide_chars() {
867 use crate::text::{Line, Span};
868 let mut term = Terminal::new(Headless::new(10, 3));
869 let line = Line::from(vec![Span::raw("\u{4e2d}x")]);
870 term.print_styled(0, 0, &line);
871 assert_eq!(term.grid().get(0, 0).glyph(), '\u{4e2d}');
872 #[cfg(feature = "egc")]
873 {
874 use crate::tile::TileFlags;
875 assert!(
876 term.grid()
877 .get(1, 0)
878 .flags()
879 .contains(TileFlags::WIDE_CHAR_SPACER)
880 );
881 }
882 #[cfg(not(feature = "egc"))]
883 assert_eq!(term.grid().get(1, 0).glyph(), '\0');
884 assert_eq!(term.grid().get(2, 0).glyph(), 'x');
885 }
886}