Files
TerminalBasic/crates/tb-runtime/src/screen.rs

775 lines
25 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Textbildschirm-Emulation: Zellenpuffer (Unicode-Zeichen + Farbattribut),
//! Cursor, Scrollen.
//!
//! Der Puffer liegt in der Laufzeitbibliothek und ist frei von jeder
//! Terminal-Abhängigkeit: `PRINT`, `LOCATE`, `CLS`, `COLOR` und `SCREEN()`
//! sind Bibliothekselemente und müssen ihn erreichen, und jedes
//! Bildschirmverhalten bleibt ohne Terminal prüfbar. Die Darstellung liegt in
//! `tb-ui` (`ScreenWidget`).
//!
//! Entscheidungen (siehe PLAN.md):
//! - durchgängig Unicode, keine CP437-Emulation
//! - **dynamische Größe**: der Bildschirm folgt der Terminalgröße; unterhalb
//! der Mindestgröße (80×25) wird nur ein Hinweis gerendert (btop-artig),
//! größere Terminals werden voll genutzt.
//!
//! Zeichen belegen so viele Zellen, wie ihre Darstellungsbreite verlangt
//! (Entscheidung 2026-09-02): schmale Zeichen eine, breite (Emoji, CJK)
//! zwei. Die zweite Zelle ist als Fortsetzung markiert und wird nie
//! eigenständig beschrieben.
use unicode_width::UnicodeWidthChar;
/// Mindestgröße; darunter wird nur ein Hinweis angezeigt.
pub const MIN_COLS: usize = 80;
pub const MIN_ROWS: usize = 25;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScreenError;
/// Eine Bildschirmzelle: Zeichen plus klassisches Farbattribut.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cell {
pub ch: char,
/// Vordergrund 015 (klassische Palette).
pub fg: u8,
/// Hintergrund 07.
pub bg: u8,
/// Zweite Hälfte eines breiten Zeichens. Solche Zellen tragen kein
/// eigenes Zeichen und werden bei der Darstellung übersprungen.
pub fortsetzung: bool,
}
impl Default for Cell {
fn default() -> Self {
Cell {
ch: ' ',
fg: 7,
bg: 0,
fortsetzung: false,
}
}
}
/// Der emulierte Textbildschirm mit dynamischer Größe.
///
/// Koordinaten in der öffentlichen API sind 1-basiert (Zeile, Spalte),
/// wie bei `LOCATE`/`CSRLIN`/`POS` des Dialekts.
#[derive(Debug, Clone)]
pub struct TextScreen {
cols: usize,
rows: usize,
cells: Vec<Cell>,
/// Cursorposition, 0-basiert intern.
cur_row: usize,
cur_col: usize,
pub cursor_visible: bool,
/// Aktuelle Ausgabefarben (`COLOR`).
pub fg: u8,
pub bg: u8,
/// Scrollbereich (`VIEW PRINT`), 0-basiert inklusiv.
view_top: usize,
view_bottom: usize,
/// true, solange kein eigenes `VIEW PRINT` gesetzt ist — der
/// Scrollbereich folgt dann der Bildschirmgröße.
view_full: bool,
/// Je Zeile die größte beschriebene Spalte (0-basiert), `None` für
/// unberührte Zeilen. Grundlage des getrimmten Snapshots: nachgestellte
/// Leerzeichen, die `PRINT` selbst ausgibt, zählen als belegt, ein nie
/// beschriebener Zeilenrest nicht.
belegt: Vec<Option<usize>>,
/// Seit der letzten Anzeige verändert? Der Host wird nur dann zum
/// Neuzeichnen aufgefordert.
veraendert: bool,
graphic_x: i32,
graphic_y: i32,
graphic_view: Option<(i32, i32, i32, i32)>,
}
impl Default for TextScreen {
fn default() -> Self {
Self::new()
}
}
impl TextScreen {
/// Bildschirm in Mindestgröße (80×25).
pub fn new() -> Self {
Self::with_size(MIN_COLS, MIN_ROWS)
}
/// Bildschirm in gegebener Größe (wird auf die Mindestgröße angehoben).
pub fn with_size(cols: usize, rows: usize) -> Self {
let cols = cols.max(MIN_COLS);
let rows = rows.max(MIN_ROWS);
TextScreen {
cols,
rows,
cells: vec![Cell::default(); cols * rows],
cur_row: 0,
cur_col: 0,
cursor_visible: true,
fg: 7,
bg: 0,
view_top: 0,
view_bottom: rows - 1,
view_full: true,
belegt: vec![None; rows],
veraendert: true,
graphic_x: 0,
graphic_y: 0,
graphic_view: None,
}
}
pub fn cols(&self) -> usize {
self.cols
}
pub fn rows(&self) -> usize {
self.rows
}
/// An neue Terminalgröße anpassen: Inhalt bleibt oben links erhalten,
/// neue Zellen sind leer. Unterhalb der Mindestgröße bleibt der Puffer
/// bei 80×25 (das Widget zeigt dann den Zu-klein-Hinweis).
pub fn resize(&mut self, cols: usize, rows: usize) {
self.veraendert = true;
let cols = cols.max(MIN_COLS);
let rows = rows.max(MIN_ROWS);
if cols == self.cols && rows == self.rows {
return;
}
let mut cells = vec![
Cell {
ch: ' ',
fg: self.fg,
bg: self.bg,
fortsetzung: false
};
cols * rows
];
for row in 0..self.rows.min(rows) {
for col in 0..self.cols.min(cols) {
cells[row * cols + col] = self.cells[row * self.cols + col];
}
}
self.cells = cells;
let mut belegt = vec![None; rows];
for (r, value) in belegt.iter_mut().enumerate().take(self.rows.min(rows)) {
*value = self.belegt[r].map(|c| c.min(cols - 1));
}
self.belegt = belegt;
self.cols = cols;
self.rows = rows;
self.cur_row = self.cur_row.min(rows - 1);
self.cur_col = self.cur_col.min(cols - 1);
if self.view_full || self.view_top > rows - 1 {
// Ohne eigenes VIEW PRINT folgt der Bereich dem Bildschirm; ein
// Bereich, der ganz vom Schirm gefallen ist, wäre leer und fällt
// auf den vollen Bildschirm zurück.
self.view_top = 0;
self.view_bottom = rows - 1;
self.view_full = true;
} else {
self.view_bottom = self.view_bottom.min(rows - 1);
}
}
/// `CLS`: Scrollbereich mit aktueller Hintergrundfarbe löschen,
/// Cursor an den Anfang des Bereichs.
pub fn cls(&mut self) {
self.veraendert = true;
let blank = Cell {
ch: ' ',
fg: self.fg,
bg: self.bg,
fortsetzung: false,
};
for row in self.view_top..=self.view_bottom {
self.cells[row * self.cols..(row + 1) * self.cols].fill(blank);
}
self.cur_row = self.view_top;
self.cur_col = 0;
for r in self.view_top..=self.view_bottom {
self.belegt[r] = None;
}
}
/// Hat sich seit der letzten Quittung etwas geändert?
pub fn ist_veraendert(&self) -> bool {
self.veraendert
}
/// Änderung quittieren (nach dem Anzeigen).
pub fn veraenderung_quittieren(&mut self) {
self.veraendert = false;
}
/// Anzahl belegter Zeilen (1-basiert bis zur letzten beschriebenen),
/// 0 wenn nichts ausgegeben wurde.
pub fn belegte_zeilen(&self) -> usize {
self.belegt
.iter()
.rposition(|b| b.is_some())
.map_or(0, |i| i + 1)
}
/// Breite der belegten Zeile `z` (1-basiert); 0 für unberührte Zeilen.
pub fn belegte_breite(&self, z: usize) -> usize {
self.belegt
.get(z - 1)
.copied()
.flatten()
.map_or(0, |c| c + 1)
}
/// `COLOR vg, hg` (vg 031, hg 07). Blinkende Vordergrundfarben
/// (1631) werden als „hell" simuliert (Entscheidung 2026-09-02):
/// echtes Terminal-Blinken ist nicht überall verfügbar.
pub fn set_color(&mut self, fg: u8, bg: u8) {
self.veraendert = true;
let fg = if fg >= 16 { (fg & 0x0F) | 8 } else { fg };
self.fg = fg & 0x0F;
self.bg = bg & 0x07;
}
/// `LOCATE zeile, spalte` (1-basiert); außerhalb → Err (Fehler 5).
pub fn locate(&mut self, row: usize, col: usize) -> Result<(), ScreenError> {
if row < 1 || row > self.rows || col < 1 || col > self.cols {
return Err(ScreenError);
}
self.cur_row = row - 1;
// Auf eine Fortsetzungszelle zu zeigen bedeutet den Zeichenanfang.
let mut c = col - 1;
if c > 0 && self.cells[self.cur_row * self.cols + c].fortsetzung {
c -= 1;
}
self.cur_col = c;
self.veraendert = true;
Ok(())
}
/// `CSRLIN` (1-basiert).
pub fn csrlin(&self) -> usize {
self.cur_row + 1
}
/// `POS(0)` (1-basiert).
pub fn pos(&self) -> usize {
self.cur_col + 1
}
/// `VIEW PRINT oben TO unten` (1-basiert).
pub fn view_print(&mut self, top: usize, bottom: usize) -> Result<(), ScreenError> {
if top < 1 || bottom > self.rows || top > bottom {
return Err(ScreenError);
}
self.view_top = top - 1;
self.view_bottom = bottom - 1;
self.view_full = top == 1 && bottom == self.rows;
Ok(())
}
pub fn cell(&self, row: usize, col: usize) -> Cell {
self.cells[(row - 1) * self.cols + (col - 1)]
}
pub fn graphics_position(&self) -> (i32, i32) {
(self.graphic_x, self.graphic_y)
}
pub fn graphics_view(&mut self, view: Option<(i32, i32, i32, i32)>) -> Result<(), ScreenError> {
if view.is_some_and(|(x1, y1, x2, y2)| x1 < 0 || y1 < 0 || x1 > x2 || y1 > y2) {
return Err(ScreenError);
}
self.graphic_view = view;
Ok(())
}
fn graphics_cell(&mut self, x: i32, y: i32, color: i32) {
if x < 0
|| y < 0
|| self
.graphic_view
.is_some_and(|(x1, y1, x2, y2)| x < x1 || x > x2 || y < y1 || y > y2)
{
return;
}
let (col, row) = (x as usize / 8, y as usize / 8);
if col >= self.cols || row >= self.rows {
return;
}
self.cells[row * self.cols + col] = Cell {
ch: if color == 0 { ' ' } else { '█' },
fg: color.clamp(0, 15) as u8,
bg: self.bg,
fortsetzung: false,
};
self.belegt[row] = Some(self.belegt[row].map_or(col, |last| last.max(col)));
self.veraendert = true;
}
pub fn graphics_line(&mut self, from: (i32, i32), to: (i32, i32), color: i32, fill: u8) {
let (x1, y1) = from;
let (x2, y2) = to;
if fill == 2 {
for y in (y1.min(y2)..=y1.max(y2)).step_by(8) {
for x in (x1.min(x2)..=x1.max(x2)).step_by(8) {
self.graphics_cell(x, y, color);
}
}
} else if fill == 1 {
self.graphics_segment(x1, y1, x2, y1, color);
self.graphics_segment(x2, y1, x2, y2, color);
self.graphics_segment(x2, y2, x1, y2, color);
self.graphics_segment(x1, y2, x1, y1, color);
} else {
self.graphics_segment(x1, y1, x2, y2, color);
}
self.graphic_x = x2;
self.graphic_y = y2;
}
fn graphics_segment(&mut self, mut x: i32, mut y: i32, x2: i32, y2: i32, color: i32) {
let dx = (x2 - x).abs();
let sx = if x < x2 { 1 } else { -1 };
let dy = -(y2 - y).abs();
let sy = if y < y2 { 1 } else { -1 };
let mut error = dx + dy;
loop {
self.graphics_cell(x, y, color);
if x == x2 && y == y2 {
break;
}
let twice = error * 2;
if twice >= dy {
error += dy;
x += sx;
}
if twice <= dx {
error += dx;
y += sy;
}
}
}
pub fn graphics_paint(&mut self, x: i32, y: i32, color: i32) {
let (start_col, start_row) = (x.max(0) as usize / 8, y.max(0) as usize / 8);
if start_col >= self.cols || start_row >= self.rows {
return;
}
let target = self.cells[start_row * self.cols + start_col].ch;
let replacement = if color == 0 { ' ' } else { '█' };
if target == replacement {
return;
}
let mut pending = vec![(start_col, start_row)];
while let Some((col, row)) = pending.pop() {
let px = col as i32 * 8;
let py = row as i32 * 8;
if col >= self.cols
|| row >= self.rows
|| self.cells[row * self.cols + col].ch != target
|| self
.graphic_view
.is_some_and(|(x1, y1, x2, y2)| px < x1 || px > x2 || py < y1 || py > y2)
{
continue;
}
self.graphics_cell(px, py, color);
if col > 0 {
pending.push((col - 1, row));
}
if row > 0 {
pending.push((col, row - 1));
}
pending.push((col + 1, row));
pending.push((col, row + 1));
}
self.graphic_x = x;
self.graphic_y = y;
}
/// Text an der Cursorposition ausgeben: Umbruch am rechten Rand,
/// Scrollen am unteren Rand des Scrollbereichs. `\n` bricht um,
/// `\r` setzt an den Zeilenanfang.
pub fn print(&mut self, text: &str) {
self.veraendert = true;
for ch in text.chars() {
match ch {
'\n' => self.newline(),
'\r' => self.cur_col = 0,
_ => self.zeichen_setzen(ch, true),
}
}
}
/// Eine UI-Zeile ohne Umbruch oder Scrollen bis zum Bildschirmrand zeichnen.
/// Steuerzeichen werden als Leerzeichen dargestellt; breite Zeichen passen
/// nur vollständig. Der Cursor bleibt spätestens in der letzten Spalte.
pub fn print_line(&mut self, text: &str) {
self.veraendert = true;
for ch in text.chars() {
let ch = if ch.is_control() { ' ' } else { ch };
if self.cur_col + ch.width().unwrap_or(1).max(1) > self.cols {
break;
}
self.zeichen_setzen(ch, false);
}
self.cur_col = self.cur_col.min(self.cols - 1);
}
/// Ein Zeichen an der Cursorposition setzen und den Cursor um seine
/// Darstellungsbreite vorrücken. Ein breites Zeichen, das nicht mehr
/// vollständig in die Zeile passt, wandert als Ganzes in die nächste;
/// die letzte Spalte bleibt dann leer.
fn zeichen_setzen(&mut self, ch: char, wrap: bool) {
// ponytail: Breite 0 (kombinierende Zeichen) wird wie 1 behandelt —
// sie bekommen eine eigene Zelle statt sich an die vorige zu hängen.
// Ceiling: echte Kombination erst, wenn ein Korpusprogramm sie braucht.
let breite = ch.width().unwrap_or(1).max(1);
if self.cur_col + breite > self.cols {
self.newline();
}
let attr = (self.fg, self.bg);
let start = self.cur_row * self.cols + self.cur_col;
self.haelften_freimachen(self.cur_col, breite);
self.cells[start] = Cell {
ch,
fg: attr.0,
bg: attr.1,
fortsetzung: false,
};
for i in 1..breite {
self.cells[start + i] = Cell {
ch: ' ',
fg: attr.0,
bg: attr.1,
fortsetzung: true,
};
}
let letzte = self.cur_col + breite - 1;
let b = &mut self.belegt[self.cur_row];
*b = Some(b.map_or(letzte, |c| c.max(letzte)));
self.cur_col += breite;
if wrap && self.cur_col >= self.cols {
self.newline();
}
}
/// Vor dem Schreiben angeschnittene breite Zeichen auflösen: wird auf
/// eine Fortsetzung geschrieben, verliert die Hälfte davor ihr Zeichen;
/// überschreibt der neue Text den Anfang eines breiten Zeichens, wird
/// dessen Fortsetzung geleert. Sonst blieben Geisterzeichen stehen.
fn haelften_freimachen(&mut self, col: usize, breite: usize) {
let zeilenanfang = self.cur_row * self.cols;
let leer = Cell {
ch: ' ',
fg: self.fg,
bg: self.bg,
fortsetzung: false,
};
if col > 0 && self.cells[zeilenanfang + col].fortsetzung {
self.cells[zeilenanfang + col - 1] = leer;
}
let hinter = col + breite;
if hinter < self.cols && self.cells[zeilenanfang + hinter].fortsetzung {
self.cells[zeilenanfang + hinter] = leer;
}
}
/// Zeilenumbruch inkl. Scrollen im Scrollbereich.
fn newline(&mut self) {
self.cur_col = 0;
if self.cur_row >= self.view_bottom {
self.scroll_up();
self.cur_row = self.view_bottom;
} else {
self.cur_row += 1;
}
}
/// Rückschritt: Cursor eine Spalte zurück und die Zelle leeren.
/// Am Zeilenanfang passiert nichts (Vorbild).
pub fn rueckschritt(&mut self) {
if self.cur_col == 0 {
return;
}
self.cur_col -= 1;
// War das gelöschte eine Fortsetzung, gehört die Hälfte davor dazu.
if self.cells[self.cur_row * self.cols + self.cur_col].fortsetzung && self.cur_col > 0 {
self.cells[self.cur_row * self.cols + self.cur_col] = Cell {
ch: ' ',
fg: self.fg,
bg: self.bg,
fortsetzung: false,
};
self.cur_col -= 1;
}
self.cells[self.cur_row * self.cols + self.cur_col] = Cell {
ch: ' ',
fg: self.fg,
bg: self.bg,
fortsetzung: false,
};
// Die gelöschte Zelle gilt nicht mehr als belegt.
let b = &mut self.belegt[self.cur_row];
*b = match (self.cur_col, *b) {
(0, _) => None,
(c, Some(alt)) => Some(alt.min(c - 1)),
(c, None) => Some(c - 1),
};
self.veraendert = true;
}
/// Scrollbereich um eine Zeile nach oben schieben; unterste Zeile leeren.
pub fn scroll_up(&mut self) {
self.veraendert = true;
let blank = Cell {
ch: ' ',
fg: self.fg,
bg: self.bg,
fortsetzung: false,
};
for row in self.view_top..self.view_bottom {
let (a, b) = self.cells.split_at_mut((row + 1) * self.cols);
a[row * self.cols..].copy_from_slice(&b[..self.cols]);
}
self.cells[self.view_bottom * self.cols..(self.view_bottom + 1) * self.cols].fill(blank);
for row in self.view_top..self.view_bottom {
self.belegt[row] = self.belegt[row + 1];
}
self.belegt[self.view_bottom] = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn print_schreibt_und_bewegt_cursor() {
let mut s = TextScreen::new();
s.print("AB");
assert_eq!(s.cell(1, 1).ch, 'A');
assert_eq!(s.cell(1, 2).ch, 'B');
assert_eq!((s.csrlin(), s.pos()), (1, 3));
}
#[test]
fn ui_zeile_bleibt_am_rand_ohne_umbruch_oder_scrollen() {
let mut s = TextScreen::new();
s.print("oben");
s.view_print(3, 10).unwrap();
s.set_color(2, 4);
for row in [10, 25] {
s.locate(row, 78).unwrap();
s.print_line("中xREST");
assert_eq!(s.cell(row, 78).ch, '中');
assert!(s.cell(row, 79).fortsetzung);
assert_eq!(s.cell(row, 80).ch, 'x');
assert_eq!((s.csrlin(), s.pos()), (row, 80));
assert_eq!((s.cell(row, 80).fg, s.cell(row, 80).bg), (2, 4));
}
assert_eq!(s.cell(1, 1).ch, 'o');
assert_eq!(s.cell(9, 78).ch, ' ');
assert_eq!(s.cell(24, 78).ch, ' ');
s.locate(2, 1).unwrap();
s.print_line("a\nb\rc");
assert_eq!(
(1..=5).map(|col| s.cell(2, col).ch).collect::<String>(),
"a b c"
);
s.locate(2, 80).unwrap();
s.print_line("");
assert_eq!(s.cell(2, 80).ch, ' ');
assert_eq!(s.cell(3, 1).ch, ' ');
assert_eq!((s.csrlin(), s.pos()), (2, 80));
}
#[test]
fn unicode_zeichen_belegen_eine_zelle() {
let mut s = TextScreen::new();
s.print("Ä☃");
assert_eq!(s.cell(1, 1).ch, 'Ä');
assert_eq!(s.cell(1, 2).ch, '☃');
}
#[test]
fn breites_zeichen_belegt_zwei_zellen() {
let mut s = TextScreen::new();
s.print("中a");
assert_eq!(s.cell(1, 1).ch, '中');
assert!(s.cell(1, 2).fortsetzung);
assert_eq!(s.cell(1, 3).ch, 'a');
assert_eq!(s.pos(), 4);
}
#[test]
fn breites_zeichen_am_rechten_rand_bricht_ganz_um() {
let mut s = TextScreen::new();
s.print(&"x".repeat(79)); // Cursor auf Spalte 80
assert_eq!(s.pos(), 80);
s.print("");
// Letzte Spalte bleibt leer, das Zeichen steht in Zeile 2.
assert_eq!(s.cell(1, 80).ch, ' ');
assert_eq!(s.cell(2, 1).ch, '中');
assert!(s.cell(2, 2).fortsetzung);
}
#[test]
fn ueberschreiben_einer_haelfte_leert_die_andere() {
let mut s = TextScreen::new();
s.print("");
// Auf den Anfang schreiben → Fortsetzung muss weg.
s.locate(1, 1).unwrap();
s.print("a");
assert_eq!(s.cell(1, 1).ch, 'a');
assert!(!s.cell(1, 2).fortsetzung);
assert_eq!(s.cell(1, 2).ch, ' ');
// Auf die Fortsetzung schreiben → Anfang muss weg.
let mut s2 = TextScreen::new();
s2.print("");
s2.locate(1, 2).unwrap(); // klemmt auf den Zeichenanfang
assert_eq!(s2.pos(), 1);
s2.locate(1, 3).unwrap();
s2.print("b");
assert_eq!(s2.cell(1, 3).ch, 'b');
}
#[test]
fn locate_auf_fortsetzung_trifft_den_zeichenanfang() {
let mut s = TextScreen::new();
s.print("");
s.locate(1, 2).unwrap();
assert_eq!(s.pos(), 1);
}
#[test]
fn rueckschritt_loescht_ein_breites_zeichen_ganz() {
let mut s = TextScreen::new();
s.print("a中");
assert_eq!(s.pos(), 4);
s.rueckschritt();
assert_eq!(s.pos(), 2);
assert_eq!(s.cell(1, 2).ch, ' ');
assert!(!s.cell(1, 2).fortsetzung);
assert_eq!(s.cell(1, 3).ch, ' ');
}
#[test]
fn umbruch_am_rechten_rand() {
let mut s = TextScreen::new();
s.print(&"x".repeat(81));
assert_eq!(s.cell(1, 80).ch, 'x');
assert_eq!(s.cell(2, 1).ch, 'x');
assert_eq!((s.csrlin(), s.pos()), (2, 2));
}
#[test]
fn scrollen_am_unteren_rand() {
let mut s = TextScreen::new();
s.locate(25, 1).unwrap();
s.print("unten\n"); // erzwingt Scroll
assert_eq!(s.cell(24, 1).ch, 'u');
assert_eq!(s.cell(25, 1).ch, ' ');
assert_eq!(s.csrlin(), 25);
}
#[test]
fn view_print_begrenzt_scrollen() {
let mut s = TextScreen::new();
s.locate(1, 1).unwrap();
s.print("kopf");
s.view_print(3, 5).unwrap();
s.locate(5, 1).unwrap();
s.print("a\nb"); // scrollt nur Zeilen 35
assert_eq!(s.cell(1, 1).ch, 'k');
assert_eq!(s.cell(4, 1).ch, 'a');
assert_eq!(s.cell(5, 1).ch, 'b');
}
#[test]
fn locate_prueft_grenzen() {
let mut s = TextScreen::new();
assert!(s.locate(0, 1).is_err());
assert!(s.locate(26, 1).is_err());
assert!(s.locate(25, 80).is_ok());
}
#[test]
fn dynamische_groesse_und_resize() {
let mut s = TextScreen::with_size(120, 40);
assert_eq!((s.cols(), s.rows()), (120, 40));
s.locate(40, 1).unwrap();
s.print("!");
assert_eq!(s.cell(40, 1).ch, '!');
// Verkleinern: Inhalt oben links bleibt, Cursor wird geklemmt
s.locate(1, 1).unwrap();
s.print("K");
s.resize(100, 30);
assert_eq!((s.cols(), s.rows()), (100, 30));
assert_eq!(s.cell(1, 1).ch, 'K');
// Unter Minimum wird auf 80×25 geklemmt
s.resize(10, 5);
assert_eq!((s.cols(), s.rows()), (80, 25));
}
#[test]
fn blink_wird_als_hell_simuliert() {
let mut s = TextScreen::new();
s.set_color(17, 0); // blinkend Blau → helles Blau
assert_eq!(s.fg, 9);
s.set_color(31, 0); // blinkend Hellweiß → Hellweiß
assert_eq!(s.fg, 15);
s.set_color(7, 0);
assert_eq!(s.fg, 7);
}
#[test]
fn resize_klemmt_cursor_und_scrollbereich() {
let mut s = TextScreen::with_size(120, 40);
s.locate(1, 1).unwrap();
s.print("K");
s.view_print(30, 38).unwrap();
s.locate(38, 5).unwrap();
s.resize(80, 25);
// Inhalt oben links bleibt.
assert_eq!(s.cell(1, 1).ch, 'K');
// Cursor liegt in den neuen Grenzen.
assert!(s.csrlin() <= s.rows() && s.pos() <= s.cols());
// Der Bereich lag ganz außerhalb → voller Bildschirm.
s.locate(25, 1).unwrap();
s.print("u\n");
assert_eq!(s.cell(24, 1).ch, 'u');
}
#[test]
fn resize_klemmt_nur_den_unteren_rand_wenn_der_bereich_bleibt() {
let mut s = TextScreen::with_size(100, 40);
s.view_print(5, 38).unwrap();
s.resize(100, 30);
// Oberer Rand bleibt bei 5, unterer klemmt auf 30.
s.locate(30, 1).unwrap();
s.print("a\nb");
assert_eq!(s.cell(29, 1).ch, 'a');
assert_eq!(s.cell(30, 1).ch, 'b');
// Zeile 4 liegt außerhalb und bleibt unberührt.
assert_eq!(s.cell(4, 1).ch, ' ');
}
#[test]
fn resize_folgt_vollem_scrollbereich() {
let mut s = TextScreen::new();
s.resize(90, 40);
s.locate(40, 1).unwrap();
s.print("a\nb"); // Scroll am neuen unteren Rand
assert_eq!(s.cell(39, 1).ch, 'a');
assert_eq!(s.cell(40, 1).ch, 'b');
}
}