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:
@@ -7,6 +7,7 @@ license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tb-runtime.workspace = true
|
||||
ratatui.workspace = true
|
||||
crossterm.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -9,7 +9,7 @@ use crossterm::event::{self, DisableMouseCapture, EnableMouseCapture, Event, Key
|
||||
use crossterm::execute;
|
||||
use std::io::stdout;
|
||||
use std::time::Duration;
|
||||
use tb_ui::screen::TextScreen;
|
||||
use tb_ui::screen::{ScreenWidget, TextScreen};
|
||||
|
||||
fn testbild(s: &mut TextScreen) {
|
||||
s.set_color(15, 1);
|
||||
@@ -61,7 +61,7 @@ fn main() -> anyhow::Result<()> {
|
||||
status(&mut screen, "Bereit. Tasten/Maus testen, Esc beendet.");
|
||||
|
||||
loop {
|
||||
terminal.draw(|f| f.render_widget(&screen, f.area()))?;
|
||||
terminal.draw(|f| f.render_widget(ScreenWidget(&screen), f.area()))?;
|
||||
if !event::poll(Duration::from_millis(100))? {
|
||||
continue;
|
||||
}
|
||||
|
||||
150
crates/tb-ui/src/host.rs
Normal file
150
crates/tb-ui/src/host.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
//! Terminal-Host: stellt den Zellenpuffer über Ratatui dar und liefert
|
||||
//! Tastatur- und Größenereignisse aus crossterm.
|
||||
//!
|
||||
//! Er lebt hier statt in `tb-runtime`, damit die Ausführungsschicht ohne
|
||||
//! Terminal-Abhängigkeit bleibt (Entscheidung 2026-09-03, D1/D2).
|
||||
|
||||
use std::io::{self, Stdout};
|
||||
use std::time::Duration;
|
||||
|
||||
use crossterm::event::{
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind,
|
||||
KeyModifiers,
|
||||
};
|
||||
use crossterm::execute;
|
||||
use crossterm::terminal::{
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
};
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
use ratatui::Terminal;
|
||||
|
||||
use tb_runtime::host::{taste, Ereignis, Host};
|
||||
use tb_runtime::screen::TextScreen;
|
||||
|
||||
use crate::screen::ScreenWidget;
|
||||
|
||||
/// Host für laufende Programme im Terminal.
|
||||
///
|
||||
/// Der Konstruktor schaltet in den Alternativschirm und den Rohmodus,
|
||||
/// `Drop` stellt beides wieder her — auch bei Panik oder Laufzeitfehler.
|
||||
pub struct TerminalHost {
|
||||
terminal: Terminal<CrosstermBackend<Stdout>>,
|
||||
}
|
||||
|
||||
impl TerminalHost {
|
||||
pub fn new() -> io::Result<Self> {
|
||||
enable_raw_mode()?;
|
||||
let mut out = io::stdout();
|
||||
execute!(out, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let terminal = Terminal::new(CrosstermBackend::new(out))?;
|
||||
Ok(TerminalHost { terminal })
|
||||
}
|
||||
|
||||
/// Aktuelle Terminalgröße, damit der Bildschirm sie von Beginn an nutzt.
|
||||
pub fn groesse(&self) -> io::Result<(usize, usize)> {
|
||||
let s = self.terminal.size()?;
|
||||
Ok((s.width as usize, s.height as usize))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TerminalHost {
|
||||
fn drop(&mut self) {
|
||||
let _ = disable_raw_mode();
|
||||
let _ = execute!(
|
||||
self.terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
);
|
||||
let _ = self.terminal.show_cursor();
|
||||
}
|
||||
}
|
||||
|
||||
impl Host for TerminalHost {
|
||||
fn present(&mut self, screen: &TextScreen) {
|
||||
let _ = self
|
||||
.terminal
|
||||
.draw(|f| f.render_widget(ScreenWidget(screen), f.area()));
|
||||
}
|
||||
|
||||
fn next_event(&mut self, blockierend: bool) -> Option<Ereignis> {
|
||||
loop {
|
||||
if !blockierend && !event::poll(Duration::ZERO).unwrap_or(false) {
|
||||
return None;
|
||||
}
|
||||
match event::read() {
|
||||
Ok(Event::Key(k)) if k.kind == KeyEventKind::Press => {
|
||||
if let Some(e) = taste_zu_ereignis(k) {
|
||||
return Some(e);
|
||||
}
|
||||
}
|
||||
Ok(Event::Resize(cols, rows)) => {
|
||||
return Some(Ereignis::Groesse {
|
||||
cols: cols as usize,
|
||||
rows: rows as usize,
|
||||
})
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(_) => return Some(Ereignis::Ende),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// crossterm-Taste → `INKEY$`-Form. `None` für Tasten ohne Entsprechung.
|
||||
fn taste_zu_ereignis(k: KeyEvent) -> Option<Ereignis> {
|
||||
// Strg+Untbr bzw. Strg+C: Abbruchwunsch.
|
||||
if k.modifiers.contains(KeyModifiers::CONTROL)
|
||||
&& matches!(k.code, KeyCode::Char('c') | KeyCode::Pause)
|
||||
{
|
||||
return Some(Ereignis::Abbruch);
|
||||
}
|
||||
let s = match k.code {
|
||||
KeyCode::Char(c) => c.to_string(),
|
||||
KeyCode::Enter => taste::ENTER.to_string(),
|
||||
KeyCode::Backspace => taste::BACKSPACE.to_string(),
|
||||
KeyCode::Esc => taste::ESC.to_string(),
|
||||
KeyCode::Tab => taste::TAB.to_string(),
|
||||
// Sondertasten in der Kodierung des Vorbilds: Nullzeichen + Kennung.
|
||||
KeyCode::F(n @ 1..=10) => taste::sonder(58 + n),
|
||||
KeyCode::Home => taste::sonder(71),
|
||||
KeyCode::Up => taste::sonder(72),
|
||||
KeyCode::PageUp => taste::sonder(73),
|
||||
KeyCode::Left => taste::sonder(75),
|
||||
KeyCode::Right => taste::sonder(77),
|
||||
KeyCode::End => taste::sonder(79),
|
||||
KeyCode::Down => taste::sonder(80),
|
||||
KeyCode::PageDown => taste::sonder(81),
|
||||
KeyCode::Insert => taste::sonder(82),
|
||||
KeyCode::Delete => taste::sonder(83),
|
||||
_ => return None,
|
||||
};
|
||||
Some(Ereignis::Taste(s))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn zeichentaste_wird_zu_inkey_form() {
|
||||
let k = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
|
||||
assert_eq!(taste_zu_ereignis(k), Some(Ereignis::Taste("a".into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sondertaste_traegt_fuehrendes_nullzeichen() {
|
||||
let k = KeyEvent::new(KeyCode::F(1), KeyModifiers::NONE);
|
||||
let Some(Ereignis::Taste(s)) = taste_zu_ereignis(k) else {
|
||||
panic!("keine Taste");
|
||||
};
|
||||
assert_eq!(s.chars().count(), 2);
|
||||
assert_eq!(s.chars().next(), Some('\0'));
|
||||
assert_eq!(s.chars().nth(1), Some(59 as char)); // F1 = 59
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strg_c_ist_abbruch() {
|
||||
let k = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
|
||||
assert_eq!(taste_zu_ereignis(k), Some(Ereignis::Abbruch));
|
||||
}
|
||||
}
|
||||
@@ -9,5 +9,6 @@
|
||||
//! Tastatur- und Mausereignisse.
|
||||
|
||||
pub mod screen;
|
||||
pub mod host; // Terminal-Host: Anzeige + Tastatur-/Größenereignisse
|
||||
pub mod forms;
|
||||
pub mod events;
|
||||
|
||||
@@ -1,40 +1,17 @@
|
||||
//! Textbildschirm-Emulation: Zellenpuffer (Unicode-Zeichen + Farbattribut),
|
||||
//! Cursor, Scrollen — gerendert als Ratatui-Widget.
|
||||
//! Darstellung des Textbildschirms als Ratatui-Widget.
|
||||
//!
|
||||
//! 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.
|
||||
//!
|
||||
//! Das Zellenmodell ist strikt 1 Zeichen = 1 Zelle; Zeichen mit
|
||||
//! Darstellungsbreite ≠ 1 sind eine offene Frage (PLAN.md).
|
||||
//! Der Zellenpuffer selbst lebt in `tb_runtime::screen` (Entscheidung
|
||||
//! 2026-09-03) — hier bleibt nur die Anbindung an das Terminal: die
|
||||
//! Farbabbildung und das Widget. Da `Widget` und `TextScreen` beide fremd
|
||||
//! sind, trägt das Widget ein Newtype.
|
||||
|
||||
pub use tb_runtime::screen::{Cell, TextScreen, MIN_COLS, MIN_ROWS};
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::widgets::Widget;
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
impl Default for Cell {
|
||||
fn default() -> Self {
|
||||
Cell { ch: ' ', fg: 7, bg: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
/// Abbildung der klassischen 16-Farben-Palette auf ANSI-Indexfarben.
|
||||
/// (Klassisch: 1 = Blau, 4 = Rot — ANSI: 1 = Rot, 4 = Blau usw.)
|
||||
pub fn basic_color(n: u8) -> Color {
|
||||
@@ -42,200 +19,14 @@ pub fn basic_color(n: u8) -> Color {
|
||||
Color::Indexed(MAP[(n & 0x0F) as usize])
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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 }; 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;
|
||||
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 = 0;
|
||||
self.view_bottom = rows - 1;
|
||||
} else {
|
||||
self.view_top = self.view_top.min(rows - 1);
|
||||
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) {
|
||||
let blank = Cell { ch: ' ', fg: self.fg, bg: self.bg };
|
||||
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;
|
||||
}
|
||||
|
||||
/// `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) {
|
||||
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;
|
||||
self.cur_col = col - 1;
|
||||
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) {
|
||||
for ch in text.chars() {
|
||||
match ch {
|
||||
'\n' => self.newline(),
|
||||
'\r' => self.cur_col = 0,
|
||||
_ => {
|
||||
self.cells[self.cur_row * self.cols + self.cur_col] =
|
||||
Cell { ch, fg: self.fg, bg: self.bg };
|
||||
self.cur_col += 1;
|
||||
if self.cur_col >= self.cols {
|
||||
self.newline();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// Scrollbereich um eine Zeile nach oben schieben; unterste Zeile leeren.
|
||||
pub fn scroll_up(&mut self) {
|
||||
let blank = Cell { ch: ' ', fg: self.fg, bg: self.bg };
|
||||
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);
|
||||
}
|
||||
}
|
||||
/// Zeichenbares Gegenstück zu [`TextScreen`].
|
||||
pub struct ScreenWidget<'a>(pub &'a TextScreen);
|
||||
|
||||
/// Rendert den Bildschirm oben links in die verfügbare Fläche. Ist das
|
||||
/// Terminal kleiner als die Mindestgröße, erscheint nur ein Hinweis
|
||||
/// (btop-artig); die Anwendung ruft bei Resize `TextScreen::resize` auf,
|
||||
/// damit Puffer und Terminal deckungsgleich bleiben.
|
||||
impl Widget for &TextScreen {
|
||||
impl Widget for ScreenWidget<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
if (area.width as usize) < MIN_COLS || (area.height as usize) < MIN_ROWS {
|
||||
let msg = format!(
|
||||
@@ -245,11 +36,16 @@ impl Widget for &TextScreen {
|
||||
buf.set_string(area.x, area.y, msg, Style::default().fg(Color::Red));
|
||||
return;
|
||||
}
|
||||
let cols = self.cols.min(area.width as usize);
|
||||
let rows = self.rows.min(area.height as usize);
|
||||
let cols = self.0.cols().min(area.width as usize);
|
||||
let rows = self.0.rows().min(area.height as usize);
|
||||
for row in 0..rows {
|
||||
for col in 0..cols {
|
||||
let c = self.cells[row * self.cols + col];
|
||||
let c = self.0.cell(row + 1, col + 1);
|
||||
if c.fortsetzung {
|
||||
// Die Zelle gehört zum breiten Zeichen davor; das Terminal
|
||||
// belegt sie beim Zeichnen selbst mit.
|
||||
continue;
|
||||
}
|
||||
if let Some(cell) = buf.cell_mut((area.x + col as u16, area.y + row as u16)) {
|
||||
let mut s = String::new();
|
||||
s.push(c.ch);
|
||||
@@ -262,8 +58,8 @@ impl Widget for &TextScreen {
|
||||
}
|
||||
// Cursor als invertierte Zelle darstellen (Terminal-Cursor wird in
|
||||
// der Forms-/Runtime-Schicht später gezielt gesteuert).
|
||||
if self.cursor_visible && self.cur_row < rows && self.cur_col < cols {
|
||||
let (cx, cy) = (area.x + self.cur_col as u16, area.y + self.cur_row as u16);
|
||||
if self.0.cursor_visible && self.0.csrlin() <= rows && self.0.pos() <= cols {
|
||||
let (cx, cy) = (area.x + (self.0.pos() - 1) as u16, area.y + (self.0.csrlin() - 1) as u16);
|
||||
if let Some(cell) = buf.cell_mut((cx, cy)) {
|
||||
cell.set_style(
|
||||
cell.style().add_modifier(ratatui::style::Modifier::REVERSED),
|
||||
@@ -277,104 +73,6 @@ impl Widget for &TextScreen {
|
||||
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 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_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');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn farbabbildung() {
|
||||
assert_eq!(basic_color(1), Color::Indexed(4)); // klassisch Blau
|
||||
|
||||
Reference in New Issue
Block a user