Phase 3 (Kern): Laufzeitbibliothek, Bildschirm und Datei-E/A
Setzt den OpenSpec-Change phase-3-runtime-bildschirm um (77/77 Aufgaben). Abdeckung laut Inventar: 195 implementiert, 37 offen, 53 Non-Feature. Vollstaendigkeits-Inventar - docs/inventar.md mit 285 Eintraegen aus den Themenlisten von bas7advr.hlp und qb45advr.hlp, je mit Status und Fundstelle - crates/tb-frontend/tests/inventar.rs haelt die Tabelle in beide Richtungen gegen den Code; der Abdeckungsstand kann nicht veralten Bruchschritt (Puffer, Host, Korpus) - Cell/TextScreen ziehen ratatui-frei nach tb-runtime::screen; tb-ui behaelt Farbabbildung, Widget und den neuen Terminal-Host - Host wechselt vom Zeichenstrom auf Anzeige des Zellenpuffers plus Ereignisse (Taste, Groessenaenderung, Abbruch) - Korpusvergleich auf getrimmten Bildschirm-Snapshot; die acht bestehenden Sollausgaben blieben dabei unveraendert Groessenunabhaengigkeit - 80x25 ist nirgends mehr eine feste Grenze; Groessenaenderungen waehrend der Ausfuehrung werden zugestellt (Inhalt oben links erhalten, Cursor und VIEW PRINT geklemmt) - Korpusnachweis: dasselbe Programm bei 80x25 und 120x40 mit je eigener Sollausgabe, plus ein Programm mit Groessenwechsel mitten im Lauf Bibliothek - Breite Unicode-Zeichen belegen zwei Zellen (Cursor, POS, Randumbruch) - Bildschirm: CLS, COLOR, LOCATE, WIDTH, VIEW PRINT, SCREEN, CSRLIN, POS - Tastatur: INKEY$, INPUT$, Funktionstasten-Makros (KEY n / LIST / ON) - PRINT USING, LPRINT USING, FORMAT$, SetFormatCC - Mathematik mit kompatiblem PRNG (gleiche Saat, gleiche Folge) - Datum und Zeit mit Serienwerten ab 1899 - Finanzmathematik: FV#, PV#, Pmt#, IPmt#, PPmt#, NPer#, Rate#, NPV#, IRR#, MIRR#, SLN#, SYD#, DDB# - Datei-E/A: sequenziell, RANDOM (Recordpuffer und UDT-Variablen), BINARY, FIELD/LSET/RSET, Statusfunktionen, Dateisystem, MK$/CV - System: ENVIRON, FRE, CLEAR, TRON/TROFF, STACK, ERDEV, ERR-Anweisung Altlasten aus Phase 2 - ON ERROR GOTO auf Modulebene ist aus Prozeduren ansprechbar (prozeduruebergreifender Fixup im Codegenerator) - DATA behaelt seinen Rohtext (Gross-/Kleinschreibung, innerer Leerraum) - Die vier TODO-verify-Zellen der Konvertierungsmatrix sind aufgeloest Vom Inventar aufgedeckte Fehler - Zwoelf Non-Features wies der Compiler entgegen der Phase-1-Spec nicht ab (CALLS, SSEG, POINT, VIEW, COM, PEN, STRIG, STICK, die String*-Routinen, LINE und OPEN "COMn:") -- geschlossen - Drei Gruppen fehlten im urspruenglichen Umfang: Finanzmathematik, Record-Konvertierung, Rest der Dateisystemfunktionen - bas7advr.hlp allein ist keine vollstaendige Quelle; das Inventar bildet die Vereinigung mit qb45advr.hlp Neue Changes - phase-3-isam: schliesst Phase 3 ab (Speicherschicht redb) - phase-3-ortszeit: zieht die UTC-Abweichung zurueck Neue Abhaengigkeit: unicode-width. Dokumentiert: sprachreferenz.md und tbvm-design.md sind TODO-frei, docs/bibliothek.md neu, PLAN.md fortgeschrieben. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
557
crates/tb-runtime/src/screen.rs
Normal file
557
crates/tb-runtime/src/screen.rs
Normal file
@@ -0,0 +1,557 @@
|
||||
//! 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;
|
||||
|
||||
/// Eine Bildschirmzelle: Zeichen plus klassisches Farbattribut.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Cell {
|
||||
pub ch: char,
|
||||
/// Vordergrund 0–15 (klassische Palette).
|
||||
pub fg: u8,
|
||||
/// Hintergrund 0–7.
|
||||
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,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
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 in 0..self.rows.min(rows) {
|
||||
belegt[r] = 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 0–31, hg 0–7). Blinkende Vordergrundfarben
|
||||
/// (16–31) 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<(), ()> {
|
||||
if row < 1 || row > self.rows || col < 1 || col > self.cols {
|
||||
return Err(());
|
||||
}
|
||||
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<(), ()> {
|
||||
if top < 1 || bottom > self.rows || top > bottom {
|
||||
return Err(());
|
||||
}
|
||||
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)]
|
||||
}
|
||||
|
||||
/// 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
// 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 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 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 3–5
|
||||
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');
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user