1use crate::backend::Backend;
4use crate::color::Color;
5use crate::event::Event;
6use crate::grid::{Grid, Rect, Size};
7use crate::style::{CellModifier, 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> {
19 current: Grid,
20 previous: Grid,
21 backend: B,
22 drawing_style: Style,
23 queued_event: Option<Event>,
24 active_layer: u8,
26}
27
28impl<B: Backend> Terminal<B> {
29 #[must_use]
32 pub fn new(backend: B) -> Self {
33 let size = backend.size();
34 let current = Grid::new(size.width, size.height);
35 let previous = Grid::new(size.width, size.height);
36 Self {
37 current,
38 previous,
39 backend,
40 drawing_style: Style::default(),
41 queued_event: None,
42 active_layer: 0,
43 }
44 }
45
46 pub const fn layer(&mut self, layer: u8) -> &mut Self {
53 self.active_layer = layer;
54 self
55 }
56
57 pub const fn fg(&mut self, color: Color) -> &mut Self {
59 self.drawing_style.fg = color;
60 self
61 }
62
63 pub const fn bg(&mut self, color: Color) -> &mut Self {
65 self.drawing_style.bg = color;
66 self
67 }
68
69 pub const fn modifier(&mut self, modifier: CellModifier) -> &mut Self {
71 self.drawing_style.modifiers = modifier;
72 self
73 }
74
75 pub fn reset_style(&mut self) -> &mut Self {
77 self.drawing_style = Style::default();
78 self
79 }
80
81 #[must_use]
83 pub const fn style(&self) -> Style {
84 self.drawing_style
85 }
86
87 #[must_use]
89 pub const fn size(&self) -> Size {
90 Size {
91 width: self.current.width(),
92 height: self.current.height(),
93 }
94 }
95
96 pub fn resize(&mut self, width: u16, height: u16) {
102 self.current.resize(width, height);
103 self.previous.resize(width, height);
104 self.previous.clear_all();
107 self.backend.resize(Size { width, height });
108 }
109
110 pub fn put(&mut self, x: u16, y: u16, ch: char) {
121 let style = self.drawing_style;
122 #[cfg(feature = "egc")]
123 {
124 if self.active_layer == 0 {
125 let mut buf = [0u8; 4];
126 let s = ch.encode_utf8(&mut buf);
127 self.current.write_grapheme(x, y, s, style);
128 return;
129 }
130 }
131 let tile = Tile {
132 glyph: ch,
133 style,
134 ..Tile::default()
135 };
136 self.current.put_tile(self.active_layer, x, y, tile);
137 }
138
139 #[must_use]
141 pub const fn grid(&self) -> &Grid {
142 &self.current
143 }
144
145 pub const fn grid_mut(&mut self) -> &mut Grid {
147 &mut self.current
148 }
149
150 #[must_use]
152 pub const fn backend(&self) -> &B {
153 &self.backend
154 }
155
156 pub const fn backend_mut(&mut self) -> &mut B {
158 &mut self.backend
159 }
160
161 pub fn clear(&mut self) {
163 self.current.clear(self.active_layer);
164 }
165
166 pub fn clear_all(&mut self) {
168 self.current.clear_all();
169 }
170
171 pub fn clear_region(&mut self, rect: Rect) {
173 for y in rect.top()..rect.bottom() {
174 for x in rect.left()..rect.right() {
175 if let Some(cell) = self.current.checked_get_mut(x, y) {
176 *cell = Tile::default();
177 }
178 }
179 }
180 }
181
182 pub fn put_styled(&mut self, x: u16, y: u16, ch: char, style: Style) {
186 #[cfg(feature = "egc")]
187 {
188 if self.active_layer == 0 {
189 let mut buf = [0u8; 4];
190 let s = ch.encode_utf8(&mut buf);
191 self.current.write_grapheme(x, y, s, style);
192 return;
193 }
194 }
195 let tile = Tile {
196 glyph: ch,
197 style,
198 ..Tile::default()
199 };
200 self.current.put_tile(self.active_layer, x, y, tile);
201 }
202
203 pub fn put_offset(&mut self, x: u16, y: u16, dx: i16, dy: i16, ch: char) {
209 let tile = Tile {
210 glyph: ch,
211 style: self.drawing_style,
212 dx,
213 dy,
214 ..Tile::default()
215 };
216 self.current.put_tile(self.active_layer, x, y, tile);
217 }
218
219 pub fn print(&mut self, x: u16, y: u16, text: &str) {
225 let style = self.drawing_style;
226 #[cfg(feature = "egc")]
227 self.print_str_egc(x, y, text, style);
228 #[cfg(not(feature = "egc"))]
229 self.print_str_chars(x, y, text, style);
230 }
231
232 pub fn print_styled(&mut self, x: u16, y: u16, line: &Line) {
238 #[cfg(feature = "egc")]
239 {
240 use unicode_segmentation::UnicodeSegmentation;
241 use unicode_width::UnicodeWidthStr;
242 let mut cur_x = x;
243 for span in &line.spans {
244 for grapheme in span.content.graphemes(true) {
245 if grapheme == "\n" {
246 break;
247 }
248 #[allow(clippy::cast_possible_truncation)]
249 let w = grapheme.width() as u16;
250 if w == 0 {
251 continue;
252 }
253 if cur_x >= self.current.width() {
254 break;
255 }
256 self.current.write_grapheme(cur_x, y, grapheme, span.style);
257 cur_x += w;
258 }
259 }
260 }
261 #[cfg(not(feature = "egc"))]
262 {
263 use unicode_width::UnicodeWidthChar;
264 let mut cur_x = x;
265 for span in &line.spans {
266 for ch in span.content.chars() {
267 if ch == '\n' {
268 break;
269 }
270 #[allow(clippy::cast_possible_truncation)]
271 let w = UnicodeWidthChar::width(ch).unwrap_or(1) as u16;
272 if usize::from(cur_x) >= usize::from(self.current.width()) {
273 break;
274 }
275 let tile = Tile {
276 glyph: ch,
277 style: span.style,
278 ..Tile::default()
279 };
280 self.current.put_tile(self.active_layer, cur_x, y, tile);
281 cur_x += w;
282 }
283 }
284 }
285 }
286
287 #[cfg(feature = "egc")]
297 pub fn print_box(
298 &mut self,
299 rect: Rect,
300 line: &Line,
301 h_align: crate::layout::HAlign,
302 v_align: crate::layout::VAlign,
303 ) {
304 crate::layout::TextLayout::new(line)
305 .rect(rect)
306 .h_align(h_align)
307 .v_align(v_align)
308 .render(self);
309 }
310
311 pub fn present(&mut self) {
324 if self.backend.needs_full_frame() {
325 let all = self.current.layers();
326 self.backend.draw_layers(all);
327 } else {
328 let diff = self.current.diff(&self.previous);
329 self.backend.draw_layers(diff);
330 }
331 self.backend.flush();
332 core::mem::swap(&mut self.current, &mut self.previous);
333 }
334
335 pub fn poll(&mut self, timeout: Duration) -> Option<Event> {
344 let event = self
345 .queued_event
346 .take()
347 .or_else(|| self.backend.poll_event(timeout))?;
348 if let Event::Resize(w, h) = event {
349 self.resize(w, h);
350 }
351 Some(event)
352 }
353
354 pub fn read(&mut self) -> Event {
361 self.poll(Duration::MAX)
362 .expect("read() called but no events available")
363 }
364
365 pub fn has_input(&mut self) -> bool {
371 if self.queued_event.is_some() {
372 true
373 } else if let Some(event) = self.backend.poll_event(Duration::ZERO) {
374 self.queued_event = Some(event);
375 true
376 } else {
377 false
378 }
379 }
380
381 #[cfg(feature = "egc")]
383 fn print_str_egc(&mut self, x: u16, y: u16, text: &str, style: Style) {
384 use unicode_segmentation::UnicodeSegmentation;
385 use unicode_width::UnicodeWidthStr;
386 let mut cur_x = x;
387 let mut cur_y = y;
388 for grapheme in text.graphemes(true) {
389 if grapheme == "\n" {
390 cur_x = x;
391 cur_y += 1;
392 continue;
393 }
394 #[allow(clippy::cast_possible_truncation)]
395 let w = grapheme.width() as u16;
396 if w == 0 {
397 continue;
398 }
399 self.current.write_grapheme(cur_x, cur_y, grapheme, style);
400 cur_x += w;
401 if cur_x >= self.current.width() {
402 cur_x = x;
403 cur_y += 1;
404 }
405 }
406 }
407
408 #[cfg(not(feature = "egc"))]
410 fn print_str_chars(&mut self, x: u16, y: u16, text: &str, style: Style) {
411 let mut cur_x = x;
412 let mut cur_y = y;
413 for c in text.chars() {
414 if c == '\n' {
415 cur_x = x;
416 cur_y += 1;
417 } else {
418 #[allow(clippy::cast_possible_truncation)]
419 let w = UnicodeWidthChar::width(c).unwrap_or(1) as u16;
420 let tile = Tile {
421 glyph: c,
422 style,
423 ..Tile::default()
424 };
425 self.current.put_tile(self.active_layer, cur_x, cur_y, tile);
426 cur_x += w;
427 if usize::from(cur_x) >= usize::from(self.current.width()) {
428 cur_x = x;
429 cur_y += 1;
430 }
431 }
432 }
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use crate::backend::Headless;
440 use crate::tile::Tile;
441
442 #[test]
443 fn test_terminal_grid_mut() {
444 let backend = Headless::new(10, 10);
445 let mut terminal = Terminal::new(backend);
446
447 assert_eq!(terminal.grid().get(0, 0).glyph(), ' ');
448
449 terminal
450 .grid_mut()
451 .put(0, 0, Tile::new('X', Style::default()));
452
453 assert_eq!(terminal.grid().get(0, 0).glyph(), 'X');
454 }
455
456 #[test]
457 fn test_terminal_poll_and_read() {
458 let backend = Headless::new(10, 10);
459 let mut terminal = Terminal::new(backend);
460
461 assert_eq!(terminal.poll(Duration::ZERO), None);
462
463 terminal.backend_mut().push_event(Event::Close);
464 assert_eq!(terminal.poll(Duration::ZERO), Some(Event::Close));
465
466 terminal.backend_mut().push_event(Event::Resize(80, 25));
467 assert_eq!(terminal.read(), Event::Resize(80, 25));
468 }
469
470 #[test]
471 fn test_terminal_has_input() {
472 let backend = Headless::new(10, 10);
473 let mut terminal = Terminal::new(backend);
474
475 assert!(!terminal.has_input());
476
477 terminal.backend_mut().push_event(Event::Close);
478 assert!(terminal.has_input());
479 assert!(terminal.has_input()); assert_eq!(terminal.poll(Duration::ZERO), Some(Event::Close));
483
484 assert!(!terminal.has_input());
486 }
487
488 #[test]
489 #[should_panic(expected = "read() called but no events available")]
490 fn test_terminal_read_panic() {
491 let backend = Headless::new(10, 10);
492 let mut terminal = Terminal::new(backend);
493 let _ = terminal.read();
494 }
495
496 #[test]
499 fn test_terminal_size() {
500 let term = Terminal::new(Headless::new(40, 20));
501 assert_eq!(
502 term.size(),
503 Size {
504 width: 40,
505 height: 20
506 }
507 );
508 }
509
510 #[test]
511 fn test_terminal_resize_changes_dimensions() {
512 let mut term = Terminal::new(Headless::new(10, 10));
513 term.resize(30, 15);
514 assert_eq!(
515 term.size(),
516 Size {
517 width: 30,
518 height: 15
519 }
520 );
521 assert_eq!(term.grid().width(), 30);
522 assert_eq!(term.grid().height(), 15);
523 }
524
525 #[test]
526 fn test_terminal_resize_preserves_current_content() {
527 let mut term = Terminal::new(Headless::new(10, 10));
528 term.put(2, 2, 'X');
529 term.resize(20, 20);
530 assert_eq!(term.grid().get(2, 2).glyph(), 'X');
531 assert_eq!(term.grid().get(15, 15).glyph(), ' ');
532 }
533
534 #[test]
535 fn test_terminal_resize_event_auto_applies() {
536 let mut term = Terminal::new(Headless::new(10, 10));
537 term.backend_mut().push_event(Event::Resize(80, 25));
538 let event = term.poll(Duration::ZERO);
539 assert_eq!(event, Some(Event::Resize(80, 25)));
540 assert_eq!(
541 term.size(),
542 Size {
543 width: 80,
544 height: 25
545 }
546 );
547 }
548
549 #[test]
550 fn test_terminal_resize_new_cells_accessible() {
551 let mut term = Terminal::new(Headless::new(3, 3));
553 term.put(0, 0, 'A');
554 term.present();
555
556 term.resize(5, 5);
557
558 term.put(4, 4, 'B');
560 term.present();
561
562 assert_eq!(term.backend().grid().get(4, 4).glyph(), 'B');
563 assert_eq!(term.backend().grid().get(0, 0).glyph(), 'A');
565 }
566
567 #[test]
570 fn test_put_wide_char_sets_continuation() {
571 let mut term = Terminal::new(Headless::new(10, 3));
572 term.put(0, 0, '\u{4e2d}'); assert_eq!(term.grid().get(0, 0).glyph(), '\u{4e2d}');
574 #[cfg(feature = "egc")]
577 {
578 use crate::tile::TileFlags;
579 assert!(
580 term.grid()
581 .get(1, 0)
582 .flags()
583 .contains(TileFlags::WIDE_CHAR_SPACER)
584 );
585 assert_eq!(term.grid().get(1, 0).glyph(), ' ');
586 }
587 #[cfg(not(feature = "egc"))]
588 assert_eq!(term.grid().get(1, 0).glyph(), '\0');
589 assert_eq!(term.grid().get(2, 0).glyph(), ' '); }
591
592 #[test]
593 fn test_print_advances_by_char_width() {
594 let mut term = Terminal::new(Headless::new(10, 3));
595 term.print(0, 0, "\u{4e2d}x"); assert_eq!(term.grid().get(0, 0).glyph(), '\u{4e2d}');
597 #[cfg(feature = "egc")]
598 {
599 use crate::tile::TileFlags;
600 assert!(
601 term.grid()
602 .get(1, 0)
603 .flags()
604 .contains(TileFlags::WIDE_CHAR_SPACER)
605 );
606 }
607 #[cfg(not(feature = "egc"))]
608 assert_eq!(term.grid().get(1, 0).glyph(), '\0');
609 assert_eq!(term.grid().get(2, 0).glyph(), 'x');
610 }
611
612 #[test]
613 fn test_put_wide_char_at_last_column_does_not_overflow() {
614 let mut term = Terminal::new(Headless::new(4, 1));
617 term.put(3, 0, '\u{4e2d}'); assert_eq!(term.grid().get(3, 0).glyph(), ' '); }
620
621 #[test]
624 fn test_print_styled_basic() {
625 use crate::text::{Line, Span};
626 let mut term = Terminal::new(Headless::new(20, 3));
627 let line = Line::from(vec![
628 Span::raw("HP: "),
629 Span::styled("100", Style::new().fg(Color::GREEN)),
630 ]);
631 term.print_styled(0, 0, &line);
632 assert_eq!(term.grid().get(0, 0).glyph(), 'H');
633 assert_eq!(term.grid().get(3, 0).glyph(), ' ');
634 assert_eq!(term.grid().get(4, 0).glyph(), '1');
635 assert_eq!(term.grid().get(4, 0).style.fg, Color::GREEN);
636 assert_eq!(term.grid().get(6, 0).glyph(), '0');
637 }
638
639 #[test]
640 fn test_print_styled_does_not_modify_drawing_style() {
641 use crate::text::{Line, Span};
642 let mut term = Terminal::new(Headless::new(20, 3));
643 term.fg(Color::RED);
644 let line = Line::from(vec![Span::styled("hi", Style::new().fg(Color::BLUE))]);
645 term.print_styled(0, 0, &line);
646 assert_eq!(term.style().fg, Color::RED);
648 }
649
650 #[test]
651 fn test_print_styled_wide_chars() {
652 use crate::text::{Line, Span};
653 let mut term = Terminal::new(Headless::new(10, 3));
654 let line = Line::from(vec![Span::raw("\u{4e2d}x")]);
655 term.print_styled(0, 0, &line);
656 assert_eq!(term.grid().get(0, 0).glyph(), '\u{4e2d}');
657 #[cfg(feature = "egc")]
658 {
659 use crate::tile::TileFlags;
660 assert!(
661 term.grid()
662 .get(1, 0)
663 .flags()
664 .contains(TileFlags::WIDE_CHAR_SPACER)
665 );
666 }
667 #[cfg(not(feature = "egc"))]
668 assert_eq!(term.grid().get(1, 0).glyph(), '\0');
669 assert_eq!(term.grid().get(2, 0).glyph(), 'x');
670 }
671}