Ereigniszustellung und Hostgrenzen korrigieren und Change archivieren
This commit is contained in:
@@ -14,5 +14,5 @@ path = "src/main.rs"
|
||||
tb-frontend.workspace = true
|
||||
tb-vm.workspace = true
|
||||
tb-runtime.workspace = true
|
||||
tb-ui.workspace = true
|
||||
tb-ui = { workspace = true, features = ["terminal"] }
|
||||
anyhow.workspace = true
|
||||
|
||||
@@ -551,6 +551,20 @@ impl Host for PipeHost {
|
||||
self.puffer.pop_front()
|
||||
}
|
||||
|
||||
fn warten(&mut self, deadline_ms: Option<u64>) -> Option<Ereignis> {
|
||||
if let Some(event) = self.next_event(false) {
|
||||
return Some(event);
|
||||
}
|
||||
if let Some(deadline) = deadline_ms {
|
||||
std::thread::sleep(std::time::Duration::from_millis(
|
||||
deadline.saturating_sub(self.jetzt_ms()),
|
||||
));
|
||||
None
|
||||
} else {
|
||||
self.next_event(true).or(Some(Ereignis::Ende))
|
||||
}
|
||||
}
|
||||
|
||||
fn jetzt_ms(&mut self) -> u64 {
|
||||
self.start.elapsed().as_millis() as u64
|
||||
}
|
||||
|
||||
@@ -265,6 +265,12 @@ impl tb_runtime::host::Host for EreignisHost {
|
||||
}
|
||||
}
|
||||
|
||||
fn warten(&mut self, deadline_ms: Option<u64>) -> Option<tb_runtime::host::Ereignis> {
|
||||
if let Some(event) = self.next_event(false) {
|
||||
return Some(event);
|
||||
}
|
||||
self.inner.warten(deadline_ms)
|
||||
}
|
||||
fn jetzt_ms(&mut self) -> u64 {
|
||||
self.inner.jetzt_ms()
|
||||
}
|
||||
@@ -318,6 +324,12 @@ impl tb_runtime::host::Host for UhrHost {
|
||||
fn next_event(&mut self, blockierend: bool) -> Option<tb_runtime::host::Ereignis> {
|
||||
self.inner.next_event(blockierend)
|
||||
}
|
||||
fn warten(&mut self, deadline_ms: Option<u64>) -> Option<tb_runtime::host::Ereignis> {
|
||||
if let Some(event) = self.next_event(false) {
|
||||
return Some(event);
|
||||
}
|
||||
self.inner.warten(deadline_ms)
|
||||
}
|
||||
fn jetzt_ms(&mut self) -> u64 {
|
||||
let jetzt = self.inner.jetzt_ms();
|
||||
self.inner.uhr_vorruecken(self.schritt_ms);
|
||||
|
||||
@@ -1882,7 +1882,7 @@ impl<'a> P<'a> {
|
||||
{
|
||||
if matches!(
|
||||
name.as_str(),
|
||||
"TIMER" | "KEY" | "COM" | "PEN" | "PLAY" | "STRIG" | "UEVENT" | "EVENT"
|
||||
"TIMER" | "KEY" | "COM" | "PEN" | "PLAY" | "STRIG" | "UEVENT" | "SIGNAL" | "EVENT"
|
||||
) {
|
||||
let action = match self.k() {
|
||||
TokenKind::Kw(Kw::On) => Some(EventAction::On),
|
||||
|
||||
@@ -28,14 +28,8 @@ pub struct RtState {
|
||||
/// ISAM-Zustand: Tabellenbindungen, Cursor, laufende Transaktion.
|
||||
pub isam: crate::isam::Isam,
|
||||
pub print: PrintState,
|
||||
/// Noch nicht abgeholte Tastendrücke in `INKEY$`-Form, je mit
|
||||
/// Umschaltzustand (Bitfeld 1 Umschalt, 2 Strg, 4 Alt). Der Zustand
|
||||
/// wird nur für die benutzerdefinierten Trap-Tasten gebraucht;
|
||||
/// `INKEY$` und `INPUT` sehen ihn nicht.
|
||||
pub tasten: VecDeque<(String, u8)>,
|
||||
/// Mausereignisse für den Forms-Dispatch; sie werden erst an den
|
||||
/// bestehenden Zustellpunkten von der VM entnommen.
|
||||
pub maus: VecDeque<crate::host::MausEreignis>,
|
||||
/// Hostreihenfolge bis zur Annahme durch Trap, Formular oder Konsole.
|
||||
pub eingaben: VecDeque<Eingabe>,
|
||||
/// Abbruchwunsch (Strg+Untbr) wurde gemeldet.
|
||||
pub abbruch: bool,
|
||||
/// Eingabeende wurde gemeldet.
|
||||
@@ -66,6 +60,23 @@ pub struct RtState {
|
||||
pub clipboard: String,
|
||||
}
|
||||
|
||||
/// Eine noch nicht verbrauchte Eingabe. Unter KEY STOP wird der Trap
|
||||
/// einmal gemerkt; die Taste bleibt danach unabhängig vom Trap lesbar.
|
||||
#[derive(Debug)]
|
||||
pub struct Eingabe {
|
||||
pub ereignis: Ereignis,
|
||||
trap_gemerkt: bool,
|
||||
}
|
||||
|
||||
impl From<Ereignis> for Eingabe {
|
||||
fn from(ereignis: Ereignis) -> Self {
|
||||
Self {
|
||||
ereignis,
|
||||
trap_gemerkt: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RtState {
|
||||
fn default() -> Self {
|
||||
RtState {
|
||||
@@ -73,8 +84,7 @@ impl Default for RtState {
|
||||
dateien: Default::default(),
|
||||
isam: Default::default(),
|
||||
print: PrintState::default(),
|
||||
tasten: VecDeque::new(),
|
||||
maus: VecDeque::new(),
|
||||
eingaben: VecDeque::new(),
|
||||
abbruch: false,
|
||||
ende: false,
|
||||
// Das Vorbild belegt die Makros im Interpreter vor; ein
|
||||
@@ -111,24 +121,28 @@ impl RtState {
|
||||
let Some(e) = host.next_event(blockierend) else {
|
||||
return;
|
||||
};
|
||||
match e {
|
||||
Ereignis::Taste(t, shift) => self.tasten.push_back((t, shift)),
|
||||
Ereignis::Groesse { cols, rows } => self.screen.resize(cols, rows),
|
||||
Ereignis::Abbruch => self.abbruch = true,
|
||||
Ereignis::Ende => self.ende = true,
|
||||
Ereignis::Signal(n) => {
|
||||
if n == 1 && !self.traps.melden(crate::traps::Quelle::Signal(n)) {
|
||||
self.abbruch = true;
|
||||
}
|
||||
}
|
||||
Ereignis::Maus(m) => self.maus.push_back(m),
|
||||
}
|
||||
self.ereignis(e);
|
||||
if blockierend {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ereignis(&mut self, e: Ereignis) {
|
||||
match e {
|
||||
Ereignis::Taste(..) | Ereignis::Maus(_) => self.eingaben.push_back(e.into()),
|
||||
Ereignis::Groesse { cols, rows } => self.screen.resize(cols, rows),
|
||||
Ereignis::Abbruch => self.abbruch = true,
|
||||
Ereignis::Ende => self.ende = true,
|
||||
Ereignis::Signal(n) => {
|
||||
let gemeldet = self.traps.melden(crate::traps::Quelle::Signal(n));
|
||||
if n == 1 && !gemeldet {
|
||||
self.abbruch = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bildschirm anzeigen und anstehende Ereignisse einsammeln.
|
||||
/// Zustellpunkt an Tick-Grenzen und vor blockierender Eingabe.
|
||||
pub fn tick(&mut self, host: &mut dyn Host) {
|
||||
@@ -147,45 +161,80 @@ impl RtState {
|
||||
if !self.traps.aktiv() {
|
||||
return;
|
||||
}
|
||||
let jetzt = host.jetzt_ms();
|
||||
self.traps.zeit_pruefen(jetzt);
|
||||
if self.traps.naechste_frist().is_some() {
|
||||
self.traps.zeit_pruefen(host.jetzt_ms());
|
||||
}
|
||||
self.tasten_traps_pruefen();
|
||||
}
|
||||
|
||||
/// Getrappte Tasten aus der Schlange nehmen. Ohne Host, damit die VM
|
||||
/// das an jeder Anweisungsgrenze tun kann, ohne die Uhr zu lesen.
|
||||
pub fn tasten_traps_pruefen(&mut self) {
|
||||
// Nur der Kopf der Schlange: die Reihenfolge der Eingabe bleibt
|
||||
// erhalten, und eine getrappte Taste hinter einer ungetrappten
|
||||
// wartet, bis diese gelesen ist.
|
||||
while let Some((s, shift)) = self.tasten.front() {
|
||||
let Some(code) = crate::host::taste::scancode_von(s) else {
|
||||
break;
|
||||
};
|
||||
let Some(n) = self.traps.taste_faengt(code, *shift) else {
|
||||
break;
|
||||
};
|
||||
self.tasten.pop_front();
|
||||
self.traps.melden(crate::traps::Quelle::Key(n));
|
||||
while self.taste_trappen(0) {}
|
||||
}
|
||||
|
||||
/// Prüft eine wartende Taste genau bis zu ihrer Annahme. ON verbraucht
|
||||
/// sie, STOP merkt den Trap einmal und belässt sie im Eingabestrom.
|
||||
pub fn taste_trappen(&mut self, index: usize) -> bool {
|
||||
if !self.traps.aktiv() {
|
||||
return false;
|
||||
}
|
||||
let Some(eingabe) = self.eingaben.get_mut(index) else {
|
||||
return false;
|
||||
};
|
||||
let Ereignis::Taste(s, shift) = &eingabe.ereignis else {
|
||||
return false;
|
||||
};
|
||||
if eingabe.trap_gemerkt {
|
||||
return false;
|
||||
}
|
||||
let Some(code) = crate::host::taste::scancode_von(s) else {
|
||||
return false;
|
||||
};
|
||||
let Some(n) = self.traps.taste_faengt(code, *shift) else {
|
||||
return false;
|
||||
};
|
||||
self.traps.melden(crate::traps::Quelle::Key(n));
|
||||
if self.traps.ist_an(crate::traps::Quelle::Key(n)) {
|
||||
self.eingaben.remove(index);
|
||||
true
|
||||
} else {
|
||||
eingabe.trap_gemerkt = true;
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Nächster Tastendruck in `INKEY$`-Form; blockiert bei `blockierend`.
|
||||
pub fn naechste_taste(&mut self, host: &mut dyn Host, blockierend: bool) -> Option<String> {
|
||||
loop {
|
||||
if let Some((t, _)) = self.tasten.pop_front() {
|
||||
/// Nur bereits gepufferte Tasten annehmen, ohne weitere Hostereignisse
|
||||
/// abzuholen. Die VM kann zwischen Abholen und Blockieren zustellen.
|
||||
pub fn gepufferte_taste(&mut self) -> Option<String> {
|
||||
while let Some(i) = self
|
||||
.eingaben
|
||||
.iter()
|
||||
.position(|e| matches!(e.ereignis, Ereignis::Taste(..)))
|
||||
{
|
||||
if self.taste_trappen(i) {
|
||||
continue;
|
||||
}
|
||||
if let Ereignis::Taste(t, _) = self.eingaben.remove(i).unwrap().ereignis {
|
||||
return Some(t);
|
||||
}
|
||||
if self.ende {
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Nächster Tastendruck; auch während blockierender Eingabe gelten
|
||||
/// ON/OFF/STOP. Mausereignisse bleiben dem VM-Zustellpunkt vorbehalten.
|
||||
pub fn naechste_taste(&mut self, host: &mut dyn Host, blockierend: bool) -> Option<String> {
|
||||
let mut abgeholt = false;
|
||||
loop {
|
||||
if let Some(t) = self.gepufferte_taste() {
|
||||
return Some(t);
|
||||
}
|
||||
if self.ende || self.abbruch || (!blockierend && abgeholt) {
|
||||
return None;
|
||||
}
|
||||
self.pump(host, blockierend);
|
||||
if !blockierend && self.tasten.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if blockierend && self.tasten.is_empty() && self.ende {
|
||||
return None;
|
||||
}
|
||||
abgeholt = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2367,12 +2416,24 @@ fn bi_sleep(
|
||||
a: &mut [Value],
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
let secs = if a.is_empty() { 0.0 } else { arg_f64(a, 0)? };
|
||||
// SLEEP n wartet n Sekunden; SLEEP ohne Argument bis zum Tastendruck.
|
||||
// Der Bytecode nutzt Instr::Sleep für Handlerzustellung. Auch direkte
|
||||
// Bibliotheksaufrufe dürfen keine eigene reale Uhr verwenden.
|
||||
let deadline = (secs > 0.0).then(|| host.jetzt_ms().saturating_add((secs * 1000.0) as u64));
|
||||
st.tick(host);
|
||||
if secs > 0.0 {
|
||||
std::thread::sleep(std::time::Duration::from_secs_f64(secs));
|
||||
} else {
|
||||
st.naechste_taste(host, true);
|
||||
loop {
|
||||
if st.ende || st.abbruch {
|
||||
break;
|
||||
}
|
||||
if let Some(deadline) = deadline {
|
||||
if host.jetzt_ms() >= deadline {
|
||||
break;
|
||||
}
|
||||
} else if st.naechste_taste(host, false).is_some() {
|
||||
break;
|
||||
}
|
||||
if let Some(event) = host.warten(deadline) {
|
||||
st.ereignis(event);
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
@@ -2777,6 +2838,60 @@ mod tests {
|
||||
assert!(h.next_event(false).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signal_stop_merkt_beide_quellen_und_off_verwirft() {
|
||||
use crate::host::{CaptureHost, Ereignis};
|
||||
use crate::traps::{Quelle, Zustand};
|
||||
for n in [1, 2] {
|
||||
let mut st = RtState::default();
|
||||
let mut host = CaptureHost::default();
|
||||
st.traps.definieren(Quelle::Signal(n), Some(22));
|
||||
st.traps.setzen(Quelle::Signal(n), Zustand::Gestoppt, 0);
|
||||
for _ in 0..3 {
|
||||
host.ereignis(Ereignis::Signal(n));
|
||||
}
|
||||
st.pump(&mut host, false);
|
||||
assert!(!st.abbruch);
|
||||
assert_eq!(st.traps.naechstes(), None);
|
||||
st.traps.setzen(Quelle::Signal(n), Zustand::An, 0);
|
||||
assert_eq!(st.traps.naechstes(), Some((Quelle::Signal(n), 22)));
|
||||
st.traps.handler_beendet(Quelle::Signal(n));
|
||||
assert_eq!(st.traps.naechstes(), None);
|
||||
st.traps.setzen(Quelle::Signal(n), Zustand::Gestoppt, 0);
|
||||
host.ereignis(Ereignis::Signal(n));
|
||||
st.pump(&mut host, false);
|
||||
st.traps.setzen(Quelle::Signal(n), Zustand::Aus, 0);
|
||||
st.traps.setzen(Quelle::Signal(n), Zustand::An, 0);
|
||||
assert_eq!(st.traps.naechstes(), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_stop_merkt_einmal_und_laesst_alle_tasten_auch_nach_on_lesbar() {
|
||||
use crate::host::{CaptureHost, Ereignis};
|
||||
use crate::traps::{Quelle, Zustand};
|
||||
let mut st = RtState::default();
|
||||
let mut host = CaptureHost::default();
|
||||
st.traps.definieren(Quelle::Key(15), Some(22));
|
||||
st.traps.benutzertaste(15, 0, 30);
|
||||
st.traps.setzen(Quelle::Key(15), Zustand::Gestoppt, 0);
|
||||
for _ in 0..3 {
|
||||
host.ereignis(Ereignis::Taste("a".into(), 0));
|
||||
}
|
||||
st.pump(&mut host, false);
|
||||
for i in 0..3 {
|
||||
assert!(!st.taste_trappen(i));
|
||||
}
|
||||
assert_eq!(st.naechste_taste(&mut host, false).as_deref(), Some("a"));
|
||||
st.traps.setzen(Quelle::Key(15), Zustand::An, 0);
|
||||
assert_eq!(st.traps.naechstes(), Some((Quelle::Key(15), 22)));
|
||||
st.traps.handler_beendet(Quelle::Key(15));
|
||||
for _ in 0..2 {
|
||||
assert_eq!(st.naechste_taste(&mut host, false).as_deref(), Some("a"));
|
||||
}
|
||||
assert_eq!(st.traps.naechstes(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sigint_ohne_aktiven_trap_bleibt_abbruch() {
|
||||
use crate::host::Ereignis;
|
||||
@@ -2802,7 +2917,10 @@ mod tests {
|
||||
}));
|
||||
}
|
||||
st.pump(&mut h, false);
|
||||
assert!(st.tasten.is_empty(), "Maus landet nicht im Tastenpuffer");
|
||||
assert!(st
|
||||
.eingaben
|
||||
.iter()
|
||||
.all(|e| matches!(e.ereignis, Ereignis::Maus(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
//! oder das Zurücklesen per `SCREEN(z, s)` nicht transportieren
|
||||
//! (Entscheidung 2026-09-03, D2).
|
||||
//!
|
||||
//! Host-Aufrufe dürfen blockieren; kooperative Zustellpunkte bringt erst die
|
||||
//! Ereignisschleife der Forms-Engine (Phase 4).
|
||||
//! Der Host besitzt Zeit und Warten; die VM liefert nur an kooperativen
|
||||
//! Zustellpunkten aus.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
@@ -165,6 +165,12 @@ pub trait Host {
|
||||
/// wenn nichts anliegt (Grundlage von `INKEY$`).
|
||||
fn next_event(&mut self, blockierend: bool) -> Option<Ereignis>;
|
||||
|
||||
/// Wartet auf Eingabe oder eine absolute Deadline in `jetzt_ms`-Zeit.
|
||||
/// `None` bedeutet: Deadline erreicht. Ohne Deadline wird bis zu einem
|
||||
/// Ereignis (gegebenenfalls explizitem Eingabeende) gewartet. Virtuelle
|
||||
/// Hosts steuern den Zeitfortschritt selbst; keine reale Rückfalluhr.
|
||||
fn warten(&mut self, deadline_ms: Option<u64>) -> Option<Ereignis>;
|
||||
|
||||
/// Monotone Zeit in Millisekunden seit Programmstart.
|
||||
///
|
||||
/// Die Fälligkeit der Zeit-Traps rechnet die Laufzeit daraus aus; sie
|
||||
@@ -260,6 +266,19 @@ impl Host for CaptureHost {
|
||||
self.ereignisse.pop_front()
|
||||
}
|
||||
|
||||
fn warten(&mut self, deadline_ms: Option<u64>) -> Option<Ereignis> {
|
||||
if let Some(event) = self.next_event(false) {
|
||||
return Some(event);
|
||||
}
|
||||
match deadline_ms {
|
||||
Some(deadline) => {
|
||||
self.uhr_ms = self.uhr_ms.max(deadline);
|
||||
None
|
||||
}
|
||||
None => Some(Ereignis::Ende),
|
||||
}
|
||||
}
|
||||
|
||||
fn jetzt_ms(&mut self) -> u64 {
|
||||
self.uhr_ms
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@ pub struct Traps {
|
||||
/// `EVENT OFF` klammert einen Abschnitt ohne Ereignisprüfung. Kein
|
||||
/// dritter Zustand — den kennt das Vorbild nicht.
|
||||
event_an: bool,
|
||||
/// Menüsperre ohne Änderung der expliziten ON/OFF/STOP-Zustände.
|
||||
suspendiert: bool,
|
||||
/// Fälligkeit des Zeit-Traps: Intervall und der Zeitpunkt, an dem er
|
||||
/// das nächste Mal fällig wird (beides in Millisekunden).
|
||||
timer_intervall_ms: u64,
|
||||
@@ -178,6 +180,27 @@ impl Traps {
|
||||
self.aktiv
|
||||
}
|
||||
|
||||
pub fn suspendieren(&mut self, gesperrt: bool) {
|
||||
self.suspendiert = gesperrt;
|
||||
}
|
||||
|
||||
pub fn ist_an(&self, q: Quelle) -> bool {
|
||||
self.traps
|
||||
.iter()
|
||||
.any(|t| t.quelle == q && t.zustand == Zustand::An)
|
||||
}
|
||||
|
||||
/// Nächste Hostfrist, sofern der Zeit-Trap überhaupt Zeit benötigt.
|
||||
pub fn naechste_frist(&self) -> Option<u64> {
|
||||
(self.event_an
|
||||
&& !self.suspendiert
|
||||
&& self.timer_intervall_ms > 0
|
||||
&& self.traps.iter().any(|t| {
|
||||
t.quelle == Quelle::Timer && t.ziel.is_some() && t.zustand != Zustand::Aus
|
||||
}))
|
||||
.then_some(self.timer_faellig_ms)
|
||||
}
|
||||
|
||||
/// Benutzerdefinierte Taste erklären (`KEY n, CHR$(flag)+CHR$(scan)`,
|
||||
/// n = 15–25). `flag` ist das Tastaturflagbyte des Vorbilds.
|
||||
pub fn benutzertaste(&mut self, n: u8, flag: u8, scancode: u8) {
|
||||
@@ -193,7 +216,7 @@ impl Traps {
|
||||
/// Liefert die Trap-Nummer, nicht nur „ja/nein": der Aufrufer muss
|
||||
/// wissen, welche Quelle er melden soll.
|
||||
pub fn taste_faengt(&self, scancode: u8, shift: u8) -> Option<u8> {
|
||||
if !self.event_an {
|
||||
if !self.event_an || self.suspendiert {
|
||||
return None;
|
||||
}
|
||||
let treffer = |k: u8| -> bool {
|
||||
@@ -218,7 +241,7 @@ impl Traps {
|
||||
/// Nächstes zuzustellendes Ereignis: Quelle und Sprungziel. Liefert
|
||||
/// höchstens eines je Aufruf; die übrigen bleiben anstehend.
|
||||
pub fn naechstes(&mut self) -> Option<(Quelle, u32)> {
|
||||
if !self.event_an {
|
||||
if !self.event_an || self.suspendiert {
|
||||
return None;
|
||||
}
|
||||
let mut kandidaten: Vec<usize> = (0..self.traps.len())
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
[package]
|
||||
name = "tb-ui"
|
||||
description = "Terminal Basic: Bildschirm-Runtime und Forms-Engine auf Ratatui"
|
||||
description = "Terminal Basic: Forms-Engine mit optionalem Terminal-Backend"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
terminal = ["dep:ratatui", "dep:crossterm", "dep:signal-hook"]
|
||||
|
||||
[dependencies]
|
||||
tb-runtime.workspace = true
|
||||
tb-frontend.workspace = true
|
||||
ratatui.workspace = true
|
||||
crossterm.workspace = true
|
||||
signal-hook.workspace = true
|
||||
ratatui = { workspace = true, optional = true }
|
||||
crossterm = { workspace = true, optional = true }
|
||||
signal-hook = { workspace = true, optional = true }
|
||||
thiserror.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
|
||||
@@ -1777,6 +1777,49 @@ impl FormsModel {
|
||||
}
|
||||
}
|
||||
|
||||
fn timer_enabled(&self, key: ObjectKey) -> bool {
|
||||
self.instance(key)
|
||||
.is_ok_and(|obj| obj.description.class == ObjectClass::Timer)
|
||||
&& self.boolean(key, "ENABLED")
|
||||
&& self.integer(key, "INTERVAL").unwrap_or(0) > 0
|
||||
&& self
|
||||
.root_form(key)
|
||||
.is_some_and(|form| self.is_visible(form))
|
||||
}
|
||||
|
||||
/// Zeitbedarf für Timer und gedrückt gehaltene Spin-Schaltflächen.
|
||||
pub fn next_deadline(&self) -> Option<u64> {
|
||||
if self.menu_is_open() || (!self.has_visible_forms() && self.spin_repeat.is_none()) {
|
||||
return None;
|
||||
}
|
||||
let timers = self
|
||||
.keys()
|
||||
.into_iter()
|
||||
.filter(|key| self.timer_enabled(*key))
|
||||
.map(|key| {
|
||||
self.timer_last
|
||||
.get(&key)
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
.saturating_add(self.integer(key, "INTERVAL").unwrap() as u64)
|
||||
});
|
||||
let spin = self.spin_repeat.and_then(|(key, _)| {
|
||||
let interval = self.integer(key, "INTERVAL").unwrap_or(250).max(0) as u64;
|
||||
(interval > 0).then(|| {
|
||||
self.spin_last
|
||||
.map_or(0, |last| last.saturating_add(interval))
|
||||
})
|
||||
});
|
||||
timers.chain(spin).min()
|
||||
}
|
||||
|
||||
pub fn mouse_needs_time(&self, event: MausEreignis) -> bool {
|
||||
self.active_form.is_some()
|
||||
&& !self.menu_is_open()
|
||||
&& event.art == MausArt::Loslassen
|
||||
&& self.pressed.is_some()
|
||||
}
|
||||
|
||||
pub fn timers(&mut self, now_ms: u64) {
|
||||
if !self.menu_path.is_empty() {
|
||||
return;
|
||||
@@ -1784,15 +1827,7 @@ impl FormsModel {
|
||||
let mut due: Vec<_> = self
|
||||
.keys()
|
||||
.into_iter()
|
||||
.filter(|key| {
|
||||
self.instance(*key)
|
||||
.is_ok_and(|obj| obj.description.class == ObjectClass::Timer)
|
||||
&& self.boolean(*key, "ENABLED")
|
||||
&& self.integer(*key, "INTERVAL").unwrap_or(0) > 0
|
||||
&& self
|
||||
.root_form(*key)
|
||||
.is_some_and(|form| self.is_visible(form))
|
||||
})
|
||||
.filter(|key| self.timer_enabled(*key))
|
||||
.collect();
|
||||
due.sort_by_key(|key| {
|
||||
self.instance(*key)
|
||||
@@ -2395,13 +2430,16 @@ fn dialog_event(
|
||||
model: &mut FormsModel,
|
||||
screen: &mut TextScreen,
|
||||
host: &mut dyn Host,
|
||||
queued: &mut VecDeque<Ereignis>,
|
||||
queued: &mut VecDeque<tb_runtime::builtins::Eingabe>,
|
||||
plain_access_key: bool,
|
||||
) -> Option<FormEvent> {
|
||||
loop {
|
||||
model.render(screen);
|
||||
host.present(screen);
|
||||
let event = queued.pop_front().or_else(|| host.next_event(true))?;
|
||||
let event = queued
|
||||
.pop_front()
|
||||
.map(|e| e.ereignis)
|
||||
.or_else(|| host.next_event(true))?;
|
||||
match event {
|
||||
Ereignis::Taste(key, shift) => {
|
||||
let shift = if plain_access_key && key.chars().count() == 1 {
|
||||
@@ -2412,14 +2450,19 @@ fn dialog_event(
|
||||
model.handle_key(&key, shift);
|
||||
}
|
||||
Ereignis::Maus(event) => {
|
||||
model.handle_mouse_at(event, host.jetzt_ms());
|
||||
let now = if model.mouse_needs_time(event) {
|
||||
host.jetzt_ms()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
model.handle_mouse_at(event, now);
|
||||
}
|
||||
Ereignis::Groesse { cols, rows } => {
|
||||
screen.resize(cols, rows);
|
||||
model.resize(cols, rows);
|
||||
}
|
||||
event @ (Ereignis::Abbruch | Ereignis::Ende | Ereignis::Signal(_)) => {
|
||||
queued.push_front(event);
|
||||
queued.push_front(event.into());
|
||||
return None;
|
||||
}
|
||||
}
|
||||
@@ -2435,7 +2478,7 @@ fn dialog_event(
|
||||
pub fn msgbox_dialog(
|
||||
screen: &mut TextScreen,
|
||||
host: &mut dyn Host,
|
||||
queued: &mut VecDeque<Ereignis>,
|
||||
queued: &mut VecDeque<tb_runtime::builtins::Eingabe>,
|
||||
text: &str,
|
||||
kind: i32,
|
||||
title: &str,
|
||||
@@ -2549,7 +2592,7 @@ pub fn msgbox_dialog(
|
||||
pub fn inputbox_dialog(
|
||||
screen: &mut TextScreen,
|
||||
host: &mut dyn Host,
|
||||
queued: &mut VecDeque<Ereignis>,
|
||||
queued: &mut VecDeque<tb_runtime::builtins::Eingabe>,
|
||||
prompt: &str,
|
||||
title: &str,
|
||||
initial: &str,
|
||||
|
||||
@@ -86,7 +86,33 @@ impl Host for TerminalHost {
|
||||
self.start.elapsed().as_millis() as u64
|
||||
}
|
||||
|
||||
fn warten(&mut self, deadline_ms: Option<u64>) -> Option<Ereignis> {
|
||||
loop {
|
||||
if let Some(e) = self.next_event(false) {
|
||||
return Some(e);
|
||||
}
|
||||
let timeout = match deadline_ms {
|
||||
Some(deadline) => {
|
||||
let rest = deadline.saturating_sub(self.jetzt_ms());
|
||||
if rest == 0 {
|
||||
return None;
|
||||
}
|
||||
Duration::from_millis(rest.min(10))
|
||||
}
|
||||
None => Duration::from_millis(10),
|
||||
};
|
||||
// Begrenzt warten, damit auch SIGINT/SIGTERM ohne Terminaltaste
|
||||
// abgeholt werden. Die VM kennt dieses Backend-Intervall nicht.
|
||||
if event::poll(timeout).is_err() {
|
||||
return Some(Ereignis::Ende);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn next_event(&mut self, blockierend: bool) -> Option<Ereignis> {
|
||||
if blockierend {
|
||||
return self.warten(None);
|
||||
}
|
||||
loop {
|
||||
// Signale vor dem Terminal: sie liegen schon an, wenn wir hier
|
||||
// ankommen, und ein blockierendes `read` wuerde sie liegenlassen.
|
||||
@@ -181,7 +207,7 @@ fn taste_zu_ereignis(k: KeyEvent) -> Option<Ereignis> {
|
||||
KeyCode::Enter => taste::ENTER.to_string(),
|
||||
KeyCode::Backspace => taste::BACKSPACE.to_string(),
|
||||
KeyCode::Esc => taste::ESC.to_string(),
|
||||
KeyCode::Tab => taste::TAB.to_string(),
|
||||
KeyCode::Tab | KeyCode::BackTab => 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),
|
||||
@@ -197,7 +223,7 @@ fn taste_zu_ereignis(k: KeyEvent) -> Option<Ereignis> {
|
||||
_ => return None,
|
||||
};
|
||||
let mut shift = 0u8;
|
||||
if k.modifiers.contains(KeyModifiers::SHIFT) {
|
||||
if k.modifiers.contains(KeyModifiers::SHIFT) || k.code == KeyCode::BackTab {
|
||||
shift |= umschalt::SHIFT;
|
||||
}
|
||||
if k.modifiers.contains(KeyModifiers::CONTROL) {
|
||||
@@ -213,6 +239,16 @@ fn taste_zu_ereignis(k: KeyEvent) -> Option<Ereignis> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn backtab_ist_tab_mit_shift_auch_ohne_modifier() {
|
||||
for modifiers in [KeyModifiers::NONE, KeyModifiers::SHIFT, KeyModifiers::ALT] {
|
||||
assert_eq!(
|
||||
taste_zu_ereignis(KeyEvent::new(KeyCode::BackTab, modifiers)),
|
||||
taste_zu_ereignis(KeyEvent::new(KeyCode::Tab, modifiers | KeyModifiers::SHIFT))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zeichentaste_wird_zu_inkey_form() {
|
||||
let k = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
//! Bildschirm-Runtime und Forms-Engine von Terminal Basic, aufbauend auf Ratatui.
|
||||
//! Terminalunabhängiges Forms-Modell, FRM-Reader und Dialoge.
|
||||
//!
|
||||
//! Zwei Schichten:
|
||||
//! 1. `screen`: Emulation des 80×25-Textbildschirms (Unicode/UTF-8, 16 Farben,
|
||||
//! Cursor) als Zeichenpuffer, gerendert über Ratatui. Darauf setzen die
|
||||
//! klassischen Anweisungen `PRINT`, `LOCATE`, `COLOR`, `CLS`, `INPUT` auf.
|
||||
//! 2. `forms`: ereignisgesteuerte Forms-Engine — Fenster, Steuerelemente
|
||||
//! (Schaltflächen, Textfelder, Listen …), Menüs, Fokusreihenfolge,
|
||||
//! Tastatur- und Mausereignisse.
|
||||
//! Die Ausführung verwendet `tb_runtime::host::Host` und dessen Zellenpuffer.
|
||||
//! Das optionale Feature `terminal` ergänzt Anzeige über Ratatui,
|
||||
//! crossterm-Eingaben und Betriebssystemsignale.
|
||||
|
||||
pub mod events;
|
||||
pub mod forms;
|
||||
pub mod frm;
|
||||
mod frm_pcode;
|
||||
#[cfg(feature = "terminal")]
|
||||
pub mod host; // Terminal-Host: Anzeige + Tastatur-/Größenereignisse
|
||||
#[cfg(feature = "terminal")]
|
||||
pub mod screen;
|
||||
#[cfg(feature = "terminal")]
|
||||
pub mod signale; // Betriebssystemsignale als Ereignisquelle (SIGNAL)
|
||||
|
||||
@@ -78,11 +78,27 @@ struct Frame {
|
||||
waiting_form: Option<u16>,
|
||||
pending_show: Option<u16>,
|
||||
form_event: Option<FormEventReturn>,
|
||||
handler_start: bool,
|
||||
eingabe: Option<KonsolenEingabe>,
|
||||
}
|
||||
|
||||
struct KonsolenEingabe {
|
||||
text: String,
|
||||
/// None = Zeile bis Enter; Some = noch erwartete Tasten für INPUT$.
|
||||
rest: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Zustellpunkt {
|
||||
Anweisung,
|
||||
Kooperativ,
|
||||
Konsole,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum FormEventReturn {
|
||||
Normal,
|
||||
Load,
|
||||
Unload(u16),
|
||||
}
|
||||
|
||||
@@ -225,6 +241,8 @@ impl Vm {
|
||||
waiting_form: None,
|
||||
pending_show: None,
|
||||
form_event: None,
|
||||
handler_start: false,
|
||||
eingabe: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -262,36 +280,15 @@ impl Vm {
|
||||
}
|
||||
}
|
||||
|
||||
fn take_dialog_input(&mut self) -> VecDeque<Ereignis> {
|
||||
let mut events = VecDeque::new();
|
||||
events.extend(
|
||||
self.rt
|
||||
.tasten
|
||||
.drain(..)
|
||||
.map(|(key, shift)| Ereignis::Taste(key, shift)),
|
||||
);
|
||||
events.extend(self.rt.maus.drain(..).map(Ereignis::Maus));
|
||||
events
|
||||
fn take_dialog_input(&mut self) -> VecDeque<tb_runtime::builtins::Eingabe> {
|
||||
std::mem::take(&mut self.rt.eingaben)
|
||||
}
|
||||
|
||||
fn restore_dialog_input(&mut self, events: VecDeque<Ereignis>) {
|
||||
for event in events {
|
||||
match event {
|
||||
Ereignis::Taste(key, shift) => self.rt.tasten.push_back((key, shift)),
|
||||
Ereignis::Maus(event) => self.rt.maus.push_back(event),
|
||||
Ereignis::Abbruch => self.rt.abbruch = true,
|
||||
Ereignis::Ende => self.rt.ende = true,
|
||||
Ereignis::Signal(number) => {
|
||||
if number == 1
|
||||
&& !self
|
||||
.rt
|
||||
.traps
|
||||
.melden(tb_runtime::traps::Quelle::Signal(number))
|
||||
{
|
||||
self.rt.abbruch = true;
|
||||
}
|
||||
}
|
||||
Ereignis::Groesse { cols, rows } => self.rt.screen.resize(cols, rows),
|
||||
fn restore_dialog_input(&mut self, events: VecDeque<tb_runtime::builtins::Eingabe>) {
|
||||
for input in events {
|
||||
match input.ereignis {
|
||||
Ereignis::Taste(..) | Ereignis::Maus(_) => self.rt.eingaben.push_back(input),
|
||||
event => self.rt.ereignis(event),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -563,13 +560,47 @@ impl Vm {
|
||||
);
|
||||
if let Some(frame) = self.frames.last_mut() {
|
||||
frame.form_event = Some(on_return);
|
||||
frame.handler_start = true;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn dispatch_next_form_event(&mut self) -> bool {
|
||||
while let Some(event) = self.forms.next_event() {
|
||||
if self.dispatch_form_event(event, FormEventReturn::Normal) {
|
||||
fn dispatch_next_form_event(&mut self, load_only: bool) -> bool {
|
||||
let mut i = 0;
|
||||
while let Some(event) = self.forms.events.get(i) {
|
||||
if load_only && !event.name.eq_ignore_ascii_case("LOAD") {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let laeuft = self
|
||||
.module
|
||||
.event_procs
|
||||
.iter()
|
||||
.find(|binding| {
|
||||
binding.object == event.object
|
||||
&& binding.event.eq_ignore_ascii_case(&event.name)
|
||||
})
|
||||
.is_some_and(|binding| {
|
||||
self.frames.iter().any(|frame| {
|
||||
frame.form_event.is_some() && frame.proc == binding.proc as usize
|
||||
})
|
||||
});
|
||||
// Gesperrte Timer und laufende Prozeduren bleiben anstehend.
|
||||
// Menübedienung und explizite modale Kooperation müssen trotzdem
|
||||
// andere Handler erreichen können.
|
||||
if laeuft || (self.forms.menu_is_open() && event.name.eq_ignore_ascii_case("TIMER")) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let event = self.forms.events.remove(i).unwrap();
|
||||
if self.dispatch_form_event(
|
||||
event,
|
||||
if load_only {
|
||||
FormEventReturn::Load
|
||||
} else {
|
||||
FormEventReturn::Normal
|
||||
},
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -577,6 +608,12 @@ impl Vm {
|
||||
}
|
||||
|
||||
fn request_unload(&mut self, object: u16) -> Result<(), RuntimeError> {
|
||||
if self.frames.iter().any(|frame| {
|
||||
matches!(frame.form_event,
|
||||
Some(FormEventReturn::Unload(running)) if running == object)
|
||||
}) {
|
||||
return Ok(());
|
||||
}
|
||||
let event = FormEvent {
|
||||
object,
|
||||
array_index: None,
|
||||
@@ -590,80 +627,136 @@ impl Vm {
|
||||
}
|
||||
|
||||
fn tick(&mut self, host: &mut dyn Host) {
|
||||
self.rt.traps.suspendieren(self.forms.menu_is_open());
|
||||
self.forms.render(&mut self.rt.screen);
|
||||
if self.forms.menu_is_open() {
|
||||
host.present(&self.rt.screen);
|
||||
self.rt.pump(host, false);
|
||||
} else {
|
||||
self.rt.tick(host);
|
||||
host.present(&self.rt.screen);
|
||||
self.rt.pump(host, false);
|
||||
if !self.forms.menu_is_open() && self.rt.traps.naechste_frist().is_some() {
|
||||
self.rt.traps.zeit_pruefen(host.jetzt_ms());
|
||||
}
|
||||
}
|
||||
|
||||
fn forms_zustellen(&mut self, host: &mut dyn Host) -> bool {
|
||||
self.forms
|
||||
.resize(self.rt.screen.cols(), self.rt.screen.rows());
|
||||
self.forms.timers(host.jetzt_ms());
|
||||
if self.forms.active_form().is_some() {
|
||||
while let Some((key, shift)) = self.rt.tasten.pop_front() {
|
||||
self.forms.handle_key(&key, shift);
|
||||
/// Ein gemeinsamer Zustellpfad für Statements, DOEVENTS, SLEEP und
|
||||
/// blockierende Eingabe. Nur explizite Kooperation verschachtelt Forms.
|
||||
fn zustellen(&mut self, host: &mut dyn Host, punkt: Zustellpunkt) -> bool {
|
||||
let kooperativ = punkt != Zustellpunkt::Anweisung;
|
||||
'zustellung: loop {
|
||||
self.rt.traps.suspendieren(self.forms.menu_is_open());
|
||||
self.forms
|
||||
.resize(self.rt.screen.cols(), self.rt.screen.rows());
|
||||
if self.forms.next_deadline().is_some() {
|
||||
self.forms.timers(host.jetzt_ms());
|
||||
}
|
||||
}
|
||||
let now_ms = host.jetzt_ms();
|
||||
while let Some(m) = self.rt.maus.pop_front() {
|
||||
self.forms.handle_mouse_at(m, now_ms);
|
||||
}
|
||||
self.forms.render(&mut self.rt.screen);
|
||||
self.dispatch_next_form_event()
|
||||
}
|
||||
|
||||
/// Anstehendes Ereignis zustellen, falls eines fällig ist. Liefert
|
||||
/// `true`, wenn ein Handler aufgesetzt wurde.
|
||||
fn zustellen(&mut self, host: &mut dyn Host) -> bool {
|
||||
if self.forms.menu_is_open() || !self.rt.traps.aktiv() {
|
||||
let forms_frei = kooperativ || !self.frames.iter().any(|f| f.form_event.is_some());
|
||||
if forms_frei && self.dispatch_next_form_event(false) {
|
||||
return true;
|
||||
}
|
||||
if self.rt.traps.aktiv() {
|
||||
self.rt.tasten_traps_pruefen();
|
||||
if (kooperativ || self.tick_zaehler.is_multiple_of(64))
|
||||
&& self.rt.traps.naechste_frist().is_some()
|
||||
{
|
||||
self.rt.traps.zeit_pruefen(host.jetzt_ms());
|
||||
}
|
||||
if let Some((q, ziel)) = self.rt.traps.naechstes() {
|
||||
self.trap_frame_aufsetzen(q, ziel);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Unverbrauchte Konsolentasten bleiben liegen. Maus ohne Formular
|
||||
// wird verworfen; mit Formular wird jede Eingabe samt ihrer Handler
|
||||
// abgearbeitet, bevor die nächste Eingabe angenommen wird.
|
||||
let mut i = 0;
|
||||
while i < self.rt.eingaben.len() {
|
||||
if !self.forms.menu_is_open() && self.rt.taste_trappen(i) {
|
||||
if let Some((q, ziel)) = self.rt.traps.naechstes() {
|
||||
self.trap_frame_aufsetzen(q, ziel);
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if punkt == Zustellpunkt::Konsole
|
||||
&& matches!(self.rt.eingaben[i].ereignis, Ereignis::Taste(..))
|
||||
{
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if self.forms.active_form().is_some() {
|
||||
if !forms_frei {
|
||||
break;
|
||||
}
|
||||
match self.rt.eingaben.remove(i).unwrap().ereignis {
|
||||
Ereignis::Taste(key, shift) => {
|
||||
let menu_war_offen = self.forms.menu_is_open();
|
||||
self.forms.handle_key(&key, shift);
|
||||
self.rt.traps.suspendieren(self.forms.menu_is_open());
|
||||
if menu_war_offen && !self.forms.menu_is_open() {
|
||||
continue 'zustellung;
|
||||
}
|
||||
}
|
||||
Ereignis::Maus(m) => {
|
||||
if m.zeile > 0
|
||||
&& m.zeile <= self.rt.screen.rows()
|
||||
&& m.spalte > 0
|
||||
&& m.spalte <= self.rt.screen.cols()
|
||||
{
|
||||
let now = if self.forms.mouse_needs_time(m) {
|
||||
host.jetzt_ms()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
self.forms.handle_mouse_at(m, now);
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
if self.dispatch_next_form_event(false) {
|
||||
return true;
|
||||
}
|
||||
} else if matches!(self.rt.eingaben[i].ereignis, Ereignis::Maus(_)) {
|
||||
self.rt.eingaben.remove(i);
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
self.forms.render(&mut self.rt.screen);
|
||||
return false;
|
||||
}
|
||||
let jetzt = host.jetzt_ms();
|
||||
self.rt.traps.zeit_pruefen(jetzt);
|
||||
self.rt.tasten_traps_pruefen();
|
||||
if let Some((q, ziel)) = self.rt.traps.naechstes() {
|
||||
self.trap_frame_aufsetzen(q, ziel);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// `SLEEP [n]`: wartet `n` Sekunden, ohne Argument bis zum nächsten
|
||||
/// Tastendruck. Ein zugestelltes Ereignis beendet das Warten
|
||||
/// (Sprachreferenz §8) — der Handler läuft, und danach geht es hinter
|
||||
/// dem `SLEEP` weiter.
|
||||
fn warten(&mut self, host: &mut dyn Host, deadline: Option<u64>) {
|
||||
let deadline = deadline
|
||||
.into_iter()
|
||||
.chain(self.forms.next_deadline())
|
||||
.chain(if self.forms.menu_is_open() {
|
||||
None
|
||||
} else {
|
||||
self.rt.traps.naechste_frist()
|
||||
})
|
||||
.min();
|
||||
if let Some(event) = host.warten(deadline) {
|
||||
self.rt.ereignis(event);
|
||||
}
|
||||
}
|
||||
|
||||
/// Ein zugestelltes Ereignis beendet SLEEP; Fristen und Warten gehören
|
||||
/// ausschließlich dem Host, einschließlich einer virtuellen Uhr.
|
||||
fn schlafen(&mut self, host: &mut dyn Host, sekunden: f64) {
|
||||
let start = host.jetzt_ms();
|
||||
let dauer_ms = (sekunden.max(0.0) * 1000.0) as u64;
|
||||
// ponytail: zusätzlich eine echte Zeitschranke, damit ein Host mit
|
||||
// stehender Uhr (Testhost) nicht ewig wartet. Ceiling: bei einer
|
||||
// virtuellen Uhr endet `SLEEP` nach der realen Dauer, wenn die
|
||||
// virtuelle nie vorrückt.
|
||||
let real_start = std::time::Instant::now();
|
||||
let deadline =
|
||||
(sekunden > 0.0).then(|| host.jetzt_ms().saturating_add((sekunden * 1000.0) as u64));
|
||||
loop {
|
||||
self.tick(host);
|
||||
if self.zustellen(host) {
|
||||
if self.zustellen(host, Zustellpunkt::Kooperativ) || self.rt.ende || self.rt.abbruch {
|
||||
return;
|
||||
}
|
||||
if self.rt.ende || self.rt.abbruch {
|
||||
if let Some(deadline) = deadline {
|
||||
if host.jetzt_ms() >= deadline {
|
||||
return;
|
||||
}
|
||||
} else if self.rt.naechste_taste(host, false).is_some() {
|
||||
return;
|
||||
}
|
||||
if sekunden <= 0.0 {
|
||||
// Ohne Argument: bis zum Tastendruck.
|
||||
if self.rt.naechste_taste(host, false).is_some() {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
let vergangen = host.jetzt_ms().saturating_sub(start);
|
||||
if vergangen >= dauer_ms || real_start.elapsed().as_millis() as u64 >= dauer_ms {
|
||||
return;
|
||||
}
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
self.warten(host, deadline);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -686,6 +779,8 @@ impl Vm {
|
||||
waiting_form: None,
|
||||
pending_show: None,
|
||||
form_event: None,
|
||||
handler_start: true,
|
||||
eingabe: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -699,7 +794,11 @@ impl Vm {
|
||||
|
||||
fn pop_frame(&mut self) {
|
||||
let f = self.frames.pop().expect("Frame-Unterlauf");
|
||||
self.locals.truncate(f.locals_base);
|
||||
if let Some(q) = f.trap {
|
||||
self.rt.traps.handler_beendet(q);
|
||||
} else {
|
||||
self.locals.truncate(f.locals_base);
|
||||
}
|
||||
self.stack.truncate(f.stack_base);
|
||||
}
|
||||
|
||||
@@ -806,14 +905,14 @@ impl Vm {
|
||||
pub fn run_visible_forms(&mut self, host: &mut dyn Host) -> RunEvent {
|
||||
while self.forms.has_visible_forms() {
|
||||
self.tick(host);
|
||||
let dispatched = self.forms_zustellen(host);
|
||||
let dispatched = self.zustellen(host, Zustellpunkt::Kooperativ);
|
||||
if dispatched {
|
||||
match self.run(host) {
|
||||
RunEvent::Ended => {}
|
||||
event => return event,
|
||||
}
|
||||
}
|
||||
if self.rt.ende {
|
||||
if !dispatched && self.rt.ende {
|
||||
return RunEvent::Ended;
|
||||
}
|
||||
if self.rt.abbruch {
|
||||
@@ -821,7 +920,9 @@ impl Vm {
|
||||
line: self.current_line(),
|
||||
};
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(1));
|
||||
if !dispatched && self.forms.has_visible_forms() {
|
||||
self.warten(host, None);
|
||||
}
|
||||
}
|
||||
RunEvent::Ended
|
||||
}
|
||||
@@ -833,11 +934,18 @@ impl Vm {
|
||||
self.frames.last_mut().unwrap().waiting_form = None;
|
||||
} else {
|
||||
self.tick(host);
|
||||
self.forms_zustellen(host);
|
||||
if self.rt.ende {
|
||||
let dispatched = self.zustellen(host, Zustellpunkt::Kooperativ);
|
||||
if !dispatched && self.rt.ende {
|
||||
return RunEvent::Ended;
|
||||
}
|
||||
std::thread::yield_now();
|
||||
if self.rt.abbruch {
|
||||
return RunEvent::Interrupted {
|
||||
line: self.current_line(),
|
||||
};
|
||||
}
|
||||
if !dispatched {
|
||||
self.warten(host, None);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -898,6 +1006,7 @@ impl Vm {
|
||||
(f.stack_base, f.last_stmt_pc, f.proc)
|
||||
};
|
||||
self.stack.truncate(stack_base);
|
||||
self.frames.last_mut().unwrap().eingabe = None;
|
||||
self.err = code;
|
||||
match handler {
|
||||
Handler::Goto(pc) => {
|
||||
@@ -1131,27 +1240,11 @@ impl Vm {
|
||||
self.tick(host);
|
||||
self.rt.screen.veraenderung_quittieren();
|
||||
}
|
||||
if self.forms_zustellen(host) {
|
||||
let erster_eintritt =
|
||||
std::mem::take(&mut self.frames.last_mut().unwrap().handler_start);
|
||||
if !erster_eintritt && self.zustellen(host, Zustellpunkt::Anweisung) {
|
||||
return Ok(Flow::Normal);
|
||||
}
|
||||
// Ereigniszustellung (design.md, D1): nur wenn überhaupt
|
||||
// ein Trap definiert ist — sonst kostet die Grenze nichts.
|
||||
if self.rt.traps.aktiv() {
|
||||
// ponytail: die Uhr nur alle 64 Anweisungen lesen, die
|
||||
// Tastenschlange dagegen jedes Mal. Ceiling: die
|
||||
// Auflösung eines Zeit-Traps ist damit 64 Anweisungen;
|
||||
// reicht das nicht, wird daraus ein eigener Zähler je
|
||||
// Trap.
|
||||
if self.tick_zaehler.is_multiple_of(64) {
|
||||
let jetzt = host.jetzt_ms();
|
||||
self.rt.traps.zeit_pruefen(jetzt);
|
||||
}
|
||||
self.rt.tasten_traps_pruefen();
|
||||
if let Some((q, ziel)) = self.rt.traps.naechstes() {
|
||||
self.trap_frame_aufsetzen(q, ziel);
|
||||
return Ok(Flow::Normal);
|
||||
}
|
||||
}
|
||||
if self.flags != 0 {
|
||||
if self.flags & F_STEP != 0 {
|
||||
return Ok(Flow::Event(RunEvent::Stepped { line }));
|
||||
@@ -1193,7 +1286,7 @@ impl Vm {
|
||||
1 => tb_runtime::traps::Zustand::Aus,
|
||||
_ => tb_runtime::traps::Zustand::Gestoppt,
|
||||
};
|
||||
let jetzt = host.jetzt_ms();
|
||||
let jetzt = if art == ART_TIMER { host.jetzt_ms() } else { 0 };
|
||||
self.rt.traps.setzen(q, z, jetzt);
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
@@ -1203,8 +1296,7 @@ impl Vm {
|
||||
// lässt den Wert unberührt.
|
||||
self.stack.push(Value::Int(0));
|
||||
self.tick(host);
|
||||
self.forms_zustellen(host);
|
||||
self.zustellen(host);
|
||||
self.zustellen(host, Zustellpunkt::Kooperativ);
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::Sleep(mit_arg) => {
|
||||
@@ -1258,7 +1350,7 @@ impl Vm {
|
||||
};
|
||||
if !self.forms.is_loaded_at(object, index) {
|
||||
self.forms.ensure_loaded_at(object, index)?;
|
||||
if index.is_none() && self.dispatch_next_form_event() {
|
||||
if index.is_none() && self.dispatch_next_form_event(true) {
|
||||
let caller = self.frames.len() - 2;
|
||||
self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1);
|
||||
return Ok(Flow::Normal);
|
||||
@@ -1280,7 +1372,7 @@ impl Vm {
|
||||
if index.is_none() {
|
||||
self.push(value.clone());
|
||||
}
|
||||
if index.is_none() && self.dispatch_next_form_event() {
|
||||
if index.is_none() && self.dispatch_next_form_event(true) {
|
||||
let caller = self.frames.len() - 2;
|
||||
self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1);
|
||||
return Ok(Flow::Normal);
|
||||
@@ -1304,7 +1396,7 @@ impl Vm {
|
||||
};
|
||||
self.forms.ensure_loaded_at(object, index)?;
|
||||
self.push(Value::Obj(object, index));
|
||||
self.dispatch_next_form_event();
|
||||
self.dispatch_next_form_event(true);
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::LoadDynamicObjectProperty(property) => {
|
||||
@@ -1434,7 +1526,7 @@ impl Vm {
|
||||
&& !self.forms.is_loaded(object)
|
||||
{
|
||||
self.forms.ensure_loaded(object)?;
|
||||
if self.dispatch_next_form_event() {
|
||||
if self.dispatch_next_form_event(true) {
|
||||
let caller = self.frames.len() - 2;
|
||||
self.frames[caller].pending_show = Some(object);
|
||||
self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1);
|
||||
@@ -1493,7 +1585,7 @@ impl Vm {
|
||||
self.forms.object_method_at(object, index, name, args)?;
|
||||
}
|
||||
}
|
||||
self.dispatch_next_form_event();
|
||||
self.dispatch_next_form_event(true);
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::ObjectLoad(object, unload, has_index) => {
|
||||
@@ -1508,7 +1600,7 @@ impl Vm {
|
||||
self.request_unload(object)?;
|
||||
} else {
|
||||
self.forms.ensure_loaded(object)?;
|
||||
self.dispatch_next_form_event();
|
||||
self.dispatch_next_form_event(true);
|
||||
}
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
@@ -2058,6 +2150,8 @@ impl Vm {
|
||||
self.pop_frame();
|
||||
}
|
||||
let f = self.frames.last_mut().unwrap();
|
||||
f.eingabe = None;
|
||||
self.stack.truncate(f.stack_base);
|
||||
f.pc = t as usize;
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
@@ -2122,8 +2216,10 @@ impl Vm {
|
||||
if let Some(FormEventReturn::Unload(object)) = event {
|
||||
self.forms.unload_with(object, |c| *c = cancel)?;
|
||||
}
|
||||
if event.is_some() {
|
||||
self.dispatch_next_form_event();
|
||||
if matches!(event, Some(FormEventReturn::Normal))
|
||||
&& !self.frames.iter().any(|f| f.form_event.is_some())
|
||||
{
|
||||
self.dispatch_next_form_event(false);
|
||||
}
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
@@ -2134,6 +2230,38 @@ impl Vm {
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::CallBuiltin(id, argc) => {
|
||||
if id == ids::RANDOMIZE && argc == 0 {
|
||||
if self.frames.last().unwrap().eingabe.is_none() {
|
||||
self.rt
|
||||
.screen
|
||||
.print("Random Number Seed (-32768 to 32767)? ");
|
||||
}
|
||||
let caller = self.frames.len() - 1;
|
||||
if let Some(text) = self.konsoleneingabe(host, None)? {
|
||||
self.rt.saat_setzen(tb_runtime::format::val(&text));
|
||||
} else {
|
||||
self.frames[caller].pc = pc;
|
||||
}
|
||||
return Ok(Flow::Normal);
|
||||
}
|
||||
if id == ids::INPUT_S && argc == 1 {
|
||||
let n = match self.stack.last() {
|
||||
Some(Value::Int(n)) => i32::from(*n),
|
||||
Some(Value::Lng(n)) => *n,
|
||||
_ => return Err(RuntimeError::TYPE_MISMATCH),
|
||||
};
|
||||
if n < 0 {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
let caller = self.frames.len() - 1;
|
||||
if let Some(text) = self.konsoleneingabe(host, Some(n as usize))? {
|
||||
self.pop()?;
|
||||
self.push(Value::Str(Rc::from(text)));
|
||||
} else {
|
||||
self.frames[caller].pc = pc;
|
||||
}
|
||||
return Ok(Flow::Normal);
|
||||
}
|
||||
let mut args = Vec::with_capacity(argc as usize);
|
||||
for _ in 0..argc {
|
||||
args.push(self.pop()?);
|
||||
@@ -2223,7 +2351,10 @@ impl Vm {
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::Input(argc, line_mode, prompt, question) => {
|
||||
self.do_input(argc, line_mode, prompt, question, host)?;
|
||||
let caller = self.frames.len() - 1;
|
||||
if !self.do_input(argc, line_mode, prompt, question, host)? {
|
||||
self.frames[caller].pc = pc;
|
||||
}
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::InputFile(argc, line_mode) => {
|
||||
@@ -2272,6 +2403,64 @@ impl Vm {
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
|
||||
/// Fortsetzbarer Leser: Text und Restlänge gehören zum unterbrochenen
|
||||
/// Frame; Referenzen/Argumente bleiben bis zum Abschluss auf dessen Stack.
|
||||
fn konsoleneingabe(
|
||||
&mut self,
|
||||
host: &mut dyn Host,
|
||||
rest: Option<usize>,
|
||||
) -> Result<Option<String>, RuntimeError> {
|
||||
let caller = self.frames.len() - 1;
|
||||
self.frames[caller]
|
||||
.eingabe
|
||||
.get_or_insert_with(|| KonsolenEingabe {
|
||||
text: String::new(),
|
||||
rest,
|
||||
});
|
||||
loop {
|
||||
if self.frames[caller].eingabe.as_ref().unwrap().rest == Some(0) {
|
||||
return Ok(Some(self.frames[caller].eingabe.take().unwrap().text));
|
||||
}
|
||||
self.tick(host);
|
||||
if self.zustellen(host, Zustellpunkt::Konsole) {
|
||||
return Ok(None);
|
||||
}
|
||||
if let Some(taste) = self.rt.gepufferte_taste() {
|
||||
let eingabe = self.frames[caller].eingabe.as_mut().unwrap();
|
||||
if let Some(rest) = &mut eingabe.rest {
|
||||
eingabe.text.push_str(&taste);
|
||||
*rest -= 1;
|
||||
} else {
|
||||
match taste.as_str() {
|
||||
tb_runtime::host::taste::ENTER => {
|
||||
self.rt.screen.print("\n");
|
||||
return Ok(Some(self.frames[caller].eingabe.take().unwrap().text));
|
||||
}
|
||||
tb_runtime::host::taste::BACKSPACE => {
|
||||
if eingabe.text.pop().is_some() {
|
||||
self.rt.screen.rueckschritt();
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if taste.chars().count() == 1
|
||||
&& taste.chars().next().is_some_and(|c| c as u32 >= 0x20)
|
||||
{
|
||||
eingabe.text.push_str(&taste);
|
||||
self.rt.screen.print(&taste);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if self.rt.ende || self.rt.abbruch {
|
||||
self.frames[caller].eingabe = None;
|
||||
return Err(RuntimeError(62));
|
||||
}
|
||||
self.warten(host, None);
|
||||
}
|
||||
}
|
||||
|
||||
fn do_input(
|
||||
&mut self,
|
||||
argc: u8,
|
||||
@@ -2279,38 +2468,41 @@ impl Vm {
|
||||
prompt: u16,
|
||||
question: bool,
|
||||
host: &mut dyn Host,
|
||||
) -> Result<(), RuntimeError> {
|
||||
// Referenzen in Reihenfolge einsammeln.
|
||||
let mut refs = Vec::with_capacity(argc as usize);
|
||||
for _ in 0..argc {
|
||||
match self.pop()? {
|
||||
Value::Ref(r) => refs.push(r),
|
||||
_ => return Err(RuntimeError::TYPE_MISMATCH),
|
||||
}
|
||||
}
|
||||
refs.reverse();
|
||||
|
||||
) -> Result<bool, RuntimeError> {
|
||||
let start = self
|
||||
.stack
|
||||
.len()
|
||||
.checked_sub(argc as usize)
|
||||
.ok_or(RuntimeError(51))?;
|
||||
let refs = self.stack[start..]
|
||||
.iter()
|
||||
.map(|v| match v {
|
||||
Value::Ref(r) => Ok(r.clone()),
|
||||
_ => Err(RuntimeError::TYPE_MISMATCH),
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
loop {
|
||||
if prompt != 0xFFFF {
|
||||
let p = self.module.strings[prompt as usize].clone();
|
||||
self.rt.screen.print(&p);
|
||||
if self.frames.last().unwrap().eingabe.is_none() {
|
||||
if prompt != 0xFFFF {
|
||||
self.rt.screen.print(&self.module.strings[prompt as usize]);
|
||||
}
|
||||
if question {
|
||||
self.rt.screen.print("? ");
|
||||
}
|
||||
}
|
||||
if question {
|
||||
self.rt.screen.print("? ");
|
||||
}
|
||||
let Some(mut line) = self.rt.read_line(host) else {
|
||||
return Err(RuntimeError(62)); // Input past end of file
|
||||
let Some(line) = self.konsoleneingabe(host, None)? else {
|
||||
return Ok(false);
|
||||
};
|
||||
if line_mode {
|
||||
let r = refs.first().ok_or(RuntimeError(51))?;
|
||||
self.write_ref(r, Value::Str(Rc::from(line.as_str())))?;
|
||||
return Ok(());
|
||||
self.write_ref(r, Value::Str(Rc::from(line)))?;
|
||||
self.stack.truncate(start);
|
||||
return Ok(true);
|
||||
}
|
||||
// Felder trennen (Quotes respektieren).
|
||||
let fields = split_input_fields(&line);
|
||||
if fields.len() != refs.len() {
|
||||
self.rt.screen.print("Redo from start\n");
|
||||
line.clear();
|
||||
continue;
|
||||
}
|
||||
// Konvertieren nach aktuellem Zieltyp; Fehler → Redo.
|
||||
@@ -2333,7 +2525,8 @@ impl Vm {
|
||||
for (r, v) in refs.iter().zip(vals) {
|
||||
self.write_ref(r, v)?;
|
||||
}
|
||||
return Ok(());
|
||||
self.stack.truncate(self.stack.len() - argc as usize);
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
584
crates/tb-vm/tests/events.rs
Normal file
584
crates/tb-vm/tests/events.rs
Normal file
@@ -0,0 +1,584 @@
|
||||
//! Regressionen zum Change ereigniszustellung-und-hostgrenzen.
|
||||
use tb_frontend::forms::{FormCatalog, ObjectClass};
|
||||
use tb_runtime::host::{CaptureHost, Ereignis, Host, MausArt, MausEreignis};
|
||||
use tb_runtime::screen::TextScreen;
|
||||
use tb_runtime::value::Value;
|
||||
use tb_vm::interp::{RunEvent, Vm};
|
||||
|
||||
#[path = "../../../tests/support/prozessfrist.rs"]
|
||||
mod prozessfrist;
|
||||
|
||||
fn vm(src: &str) -> Vm {
|
||||
let mut catalog = FormCatalog::default();
|
||||
catalog.add("Form1", ObjectClass::Form, None, false);
|
||||
catalog.add("Text1", ObjectClass::TextBox, Some("Form1"), false);
|
||||
catalog.add("Text2", ObjectClass::TextBox, Some("Form1"), false);
|
||||
catalog.add("Timer1", ObjectClass::Timer, Some("Form1"), false);
|
||||
catalog.add("Menu1", ObjectClass::Menu, Some("Form1"), false);
|
||||
catalog.add("Item1", ObjectClass::Menu, Some("Menu1"), false);
|
||||
Vm::new(
|
||||
tb_vm::compile_source_with_forms("FORM1", src, &catalog)
|
||||
.unwrap_or_else(|d| panic!("{d:?}")),
|
||||
)
|
||||
}
|
||||
|
||||
fn stop(vm: &mut Vm, host: &mut dyn Host) {
|
||||
assert!(matches!(vm.run(host), RunEvent::Stopped { .. }));
|
||||
}
|
||||
|
||||
fn string(vm: &Vm, name: &str) -> String {
|
||||
match vm.inspect(name) {
|
||||
Some(Value::Str(s)) => s.to_string(),
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn integer(vm: &Vm, name: &str) -> i16 {
|
||||
match vm.inspect(name) {
|
||||
Some(Value::Int(n)) => n,
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn key(s: &str) -> Ereignis {
|
||||
Ereignis::Taste(s.into(), 0)
|
||||
}
|
||||
fn mouse() -> Ereignis {
|
||||
Ereignis::Maus(MausEreignis {
|
||||
art: MausArt::Druck,
|
||||
taste: 1,
|
||||
shift: 0,
|
||||
zeile: 1,
|
||||
spalte: 1,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn beide_signale_werden_zugestellt_und_beginnen_in_quellreihenfolge() {
|
||||
let mut vm = vm("ON SIGNAL(1) GOSUB IntHandler\nON SIGNAL(2) GOSUB TermHandler\nSIGNAL(1) ON\nSIGNAL(2) ON\nSTOP\nDOEVENTS\nEND\nIntHandler:\ns$ = s$ + \"1\"\nRETURN\nTermHandler:\ns$ = s$ + \"2\"\nRETURN");
|
||||
let mut host = CaptureHost::default();
|
||||
stop(&mut vm, &mut host);
|
||||
host.ereignis(Ereignis::Signal(2));
|
||||
host.ereignis(Ereignis::Signal(1));
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!(string(&vm, "s"), "12");
|
||||
for n in [0, 3] {
|
||||
assert!(
|
||||
tb_vm::compile_source("TEST", &format!("ON SIGNAL({n}) GOSUB H\nEND\nH:\nRETURN"))
|
||||
.is_err()
|
||||
);
|
||||
let mut bad = vm_for_source(&format!("n% = {n}\nON SIGNAL(n%) GOSUB H\nEND\nH:\nRETURN"));
|
||||
assert!(matches!(
|
||||
bad.run(&mut host),
|
||||
RunEvent::Error { code: 5, .. }
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn vm_for_source(src: &str) -> Vm {
|
||||
Vm::new(tb_vm::compile_source("TEST", src).unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_on_off_stop_gelten_fuer_alle_konsoleneingaben() {
|
||||
for state in ["ON", "OFF", "STOP"] {
|
||||
for input in ["s$ = INKEY$", "INPUT s$", "LINE INPUT s$", "s$ = INPUT$(1)"] {
|
||||
let mut vm = vm_for_source(&format!("ON KEY(15) GOSUB H\nKEY 15, CHR$(0) + CHR$(30)\nKEY(15) {state}\nSTOP\n{input}\nKEY(15) ON\nDOEVENTS\nEND\nH:\nn% = n% + 1\nRETURN"));
|
||||
let mut host = CaptureHost::default();
|
||||
stop(&mut vm, &mut host);
|
||||
host.ereignis(key("a"));
|
||||
if state == "ON" {
|
||||
host.ereignis(key("b"));
|
||||
}
|
||||
host.ereignis(key("\r"));
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended, "{state}: {input}");
|
||||
assert_eq!(
|
||||
string(&vm, "s"),
|
||||
if state == "ON" { "b" } else { "a" },
|
||||
"{state}: {input}"
|
||||
);
|
||||
assert_eq!(
|
||||
integer(&vm, "n"),
|
||||
if state == "OFF" { 0 } else { 1 },
|
||||
"{state}: {input}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn taste_maus_und_trap_behalten_die_annahmereihenfolge() {
|
||||
for (events, expected) in [
|
||||
(vec![key("a"), mouse()], "aM"),
|
||||
(vec![mouse(), key("a")], "Ma"),
|
||||
(
|
||||
vec![key("a"), mouse(), key("b"), key("t"), mouse()],
|
||||
"aMbTM",
|
||||
),
|
||||
] {
|
||||
let mut vm = vm("DIM SHARED s$\nON KEY(15) GOSUB H\nKEY 15, CHR$(0) + CHR$(20)\nKEY(15) ON\nForm1.Show\nText1.SetFocus\nSTOP\nn% = DOEVENTS + LEN(s$)\nDOEVENTS\nDOEVENTS\nDOEVENTS\nDOEVENTS\nEND\nH:\ns$ = s$ + \"T\"\nRETURN\nSUB Text1_KeyPress(KeyAscii AS INTEGER)\nSHARED s$\ns$ = s$ + CHR$(KeyAscii)\nEND SUB\nSUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\nSHARED s$\ns$ = s$ + \"M\"\nEND SUB");
|
||||
let mut host = CaptureHost::default();
|
||||
stop(&mut vm, &mut host);
|
||||
for event in events {
|
||||
host.ereignis(event);
|
||||
}
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!(string(&vm, "s"), expected);
|
||||
assert!(
|
||||
matches!(vm.inspect("n"), Some(Value::Int(n)) if n > 0),
|
||||
"DOEVENTS muss vor der Auswertung von LEN zustellen"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fokus_handler_laufen_geordnet_bis_zur_rueckkehr() {
|
||||
let mut vm = vm("DIM SHARED s$\nText1.TabIndex = 0\nText2.TabIndex = 1\nForm1.Show\nText1.SetFocus\nSTOP\nDOEVENTS\nEND\nSUB Text1_LostFocus()\nSHARED s$\ns$ = s$ + \"L\"\ns$ = s$ + \"l\"\nEND SUB\nSUB Text2_GotFocus()\nSHARED s$\ns$ = s$ + \"G\"\ns$ = s$ + \"g\"\nEND SUB");
|
||||
let mut host = CaptureHost::default();
|
||||
stop(&mut vm, &mut host);
|
||||
host.ereignis(key("\t"));
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!(string(&vm, "s"), "LlGg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doevents_verhindert_rekursion_derselben_forms_prozedur() {
|
||||
let mut vm = vm("DIM SHARED s$\nForm1.Show\nText1.SetFocus\nSTOP\nDOEVENTS\nEND\nSUB Text1_KeyPress(KeyAscii AS INTEGER)\nSHARED s$\ns$ = s$ + CHR$(KeyAscii)\nDOEVENTS\ns$ = s$ + \"r\"\nEND SUB");
|
||||
let mut host = CaptureHost::default();
|
||||
stop(&mut vm, &mut host);
|
||||
host.ereignis(key("a"));
|
||||
host.ereignis(key("b"));
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!(string(&vm, "s"), "arbr");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offenes_menu_sperrt_traps_und_wartende_forms_timer_an_allen_zustellpunkten() {
|
||||
let mut vm = vm("DIM SHARED n%\nON UEVENT GOSUB H\nUEVENT ON\nMenu1.Caption = \"&Datei\"\nItem1.Caption = \"&Ende\"\nForm1.Show\nSTOP\nCALL SetUEvent\nn% = n% + 1\nDOEVENTS\nSLEEP .001\nLINE INPUT s$\nSTOP\nDOEVENTS\nEND\nH:\nn% = n% + 10\nRETURN\nSUB Timer1_Timer()\nSHARED n%\nn% = n% + 100\nEND SUB");
|
||||
let mut host = CaptureHost::default();
|
||||
stop(&mut vm, &mut host);
|
||||
vm.forms.handle_key("d", 4);
|
||||
assert!(vm.forms.menu_is_open());
|
||||
vm.queue_form_event(tb_ui::forms::FormEvent {
|
||||
object: 3,
|
||||
array_index: None,
|
||||
name: "TIMER".into(),
|
||||
args: vec![],
|
||||
});
|
||||
// Erst die blockierende Konsole holt Enter ab, damit das Menü offen bleibt.
|
||||
struct InputHost(CaptureHost);
|
||||
impl Host for InputHost {
|
||||
fn present(&mut self, s: &TextScreen) {
|
||||
self.0.present(s);
|
||||
}
|
||||
fn next_event(&mut self, block: bool) -> Option<Ereignis> {
|
||||
block.then(|| key("\r"))
|
||||
}
|
||||
fn jetzt_ms(&mut self) -> u64 {
|
||||
self.0.jetzt_ms()
|
||||
}
|
||||
fn warten(&mut self, deadline: Option<u64>) -> Option<Ereignis> {
|
||||
if deadline.is_none() {
|
||||
self.next_event(true)
|
||||
} else {
|
||||
self.0.warten(deadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
stop(&mut vm, &mut InputHost(CaptureHost::default()));
|
||||
assert_eq!(integer(&vm, "n"), 1);
|
||||
assert!(vm.forms.menu_is_open());
|
||||
host.ereignis(key("\u{1b}"));
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!(integer(&vm, "n"), 111);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ohne_zeitbedarf_keine_uhrabfrage_und_keine_mausansammlung() {
|
||||
struct OhneUhr(CaptureHost);
|
||||
impl Host for OhneUhr {
|
||||
fn present(&mut self, s: &TextScreen) {
|
||||
self.0.present(s);
|
||||
}
|
||||
fn next_event(&mut self, b: bool) -> Option<Ereignis> {
|
||||
self.0.next_event(b)
|
||||
}
|
||||
fn jetzt_ms(&mut self) -> u64 {
|
||||
panic!("unerwartete Uhrabfrage")
|
||||
}
|
||||
fn warten(&mut self, _: Option<u64>) -> Option<Ereignis> {
|
||||
panic!("unerwartetes Warten")
|
||||
}
|
||||
}
|
||||
for src in ["a% = 1\na% = 2\nEND", "Form1.Show\na% = 1\nDOEVENTS\nEND"] {
|
||||
let mut vm = vm(src);
|
||||
let mut host = OhneUhr(CaptureHost::default());
|
||||
for _ in 0..500 {
|
||||
host.0.ereignis(mouse());
|
||||
}
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert!(vm.rt.eingaben.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sleep_folgt_nur_der_hostfrist_und_forms_timer_beendet_es() {
|
||||
let _frist = prozessfrist::Prozessfrist::neu("Host-SLEEP", std::time::Duration::from_secs(5));
|
||||
struct WarteHost {
|
||||
now: u64,
|
||||
deadlines: Vec<Option<u64>>,
|
||||
timer: bool,
|
||||
}
|
||||
impl Host for WarteHost {
|
||||
fn present(&mut self, _: &TextScreen) {}
|
||||
fn next_event(&mut self, _: bool) -> Option<Ereignis> {
|
||||
None
|
||||
}
|
||||
fn jetzt_ms(&mut self) -> u64 {
|
||||
self.now
|
||||
}
|
||||
fn warten(&mut self, deadline: Option<u64>) -> Option<Ereignis> {
|
||||
self.deadlines.push(deadline);
|
||||
if self.deadlines.len() == 1 && !self.timer {
|
||||
// Reale Zeit verstreicht bei eingefrorener Hostuhr. Erst die
|
||||
// folgende Host-Wartephase darf die Deadline erreichen.
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
return Some(Ereignis::Groesse { cols: 80, rows: 25 });
|
||||
}
|
||||
self.now = deadline.expect("Deadline fehlt");
|
||||
None
|
||||
}
|
||||
}
|
||||
let mut host = WarteHost {
|
||||
now: 0,
|
||||
deadlines: vec![],
|
||||
timer: false,
|
||||
};
|
||||
let mut plain = vm_for_source("SLEEP .001\nEND");
|
||||
assert_eq!(plain.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!(host.deadlines, [Some(1), Some(1)]);
|
||||
let mut vm = vm("DIM SHARED n%\nForm1.Show\nTimer1.Interval = 100\nTimer1.Enabled = -1\nSLEEP 60\nEND\nSUB Timer1_Timer()\nSHARED n%\nn% = n% + 1\nEND SUB");
|
||||
let mut host = WarteHost {
|
||||
now: 0,
|
||||
deadlines: vec![],
|
||||
timer: true,
|
||||
};
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!(host.deadlines, [Some(100)]);
|
||||
assert_eq!(integer(&vm, "n"), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dialog_erhaelt_stop_markierung_unverbrauchter_tasten() {
|
||||
let mut vm = vm_for_source("ON KEY(15) GOSUB H\nKEY 15, CHR$(0) + CHR$(30)\nKEY(15) STOP\nSTOP\nx% = MSGBOX(\"Weiter\")\nKEY(15) ON\nDOEVENTS\ns$ = INKEY$\nEND\nH:\nn% = n% + 1\nRETURN");
|
||||
let mut host = CaptureHost::default();
|
||||
stop(&mut vm, &mut host);
|
||||
host.ereignis(key("\r"));
|
||||
host.ereignis(key("a"));
|
||||
vm.rt.pump(&mut host, false);
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!(string(&vm, "s"), "a");
|
||||
assert_eq!(integer(&vm, "n"), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unload_aus_unload_handler_ruft_dieselbe_prozedur_nicht_rekursiv_auf() {
|
||||
let mut vm = vm("DIM SHARED s$\nForm1.Show\nUNLOAD Form1\nEND\nSUB Form_Unload(Cancel AS INTEGER)\nSHARED s$\ns$ = s$ + \"u\"\nUNLOAD Form1\ns$ = s$ + \"r\"\nEND SUB");
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
assert_eq!(string(&vm, "s"), "ur");
|
||||
assert!(!vm.forms.is_loaded(0));
|
||||
}
|
||||
|
||||
// Nachverifikation V1–V5: unveränderte Sollwerte aus den Review-Proben.
|
||||
#[test]
|
||||
fn review_signal_waehrend_blockierender_eingabe() {
|
||||
struct Input {
|
||||
reads: usize,
|
||||
}
|
||||
impl Host for Input {
|
||||
fn present(&mut self, _: &TextScreen) {}
|
||||
fn next_event(&mut self, block: bool) -> Option<Ereignis> {
|
||||
if !block {
|
||||
return None;
|
||||
}
|
||||
self.reads += 1;
|
||||
Some(if self.reads == 1 {
|
||||
Ereignis::Signal(2)
|
||||
} else {
|
||||
Ereignis::Ende
|
||||
})
|
||||
}
|
||||
fn jetzt_ms(&mut self) -> u64 {
|
||||
0
|
||||
}
|
||||
fn warten(&mut self, _: Option<u64>) -> Option<Ereignis> {
|
||||
self.next_event(true)
|
||||
}
|
||||
}
|
||||
let mut results = vec![];
|
||||
for statement in ["INPUT s$", "LINE INPUT s$", "s$ = INPUT$(1)"] {
|
||||
let src =
|
||||
format!("ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\n{statement}\nEND\nH:\nPRINT \"H\"\nEND");
|
||||
let mut vm = Vm::new(tb_vm::compile_source("TEST", &src).unwrap());
|
||||
let result = vm.run(&mut Input { reads: 0 });
|
||||
println!(
|
||||
"{statement}: {result:?}; Ausgabe={:?}",
|
||||
tb_runtime::snapshot::text(&vm.rt.screen)
|
||||
);
|
||||
results.push(result);
|
||||
}
|
||||
assert!(results.iter().all(|result| *result == RunEvent::Ended));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_objektargument_darf_gotfocus_nicht_vorziehen() {
|
||||
for action in [
|
||||
"CALL Mark(Text1)",
|
||||
"n% = Text1.TabIndex\nCALL Mark(Text1)",
|
||||
"Text1.Text = \"neu\"\nCALL Mark(Text1)",
|
||||
"Text1.Refresh\nCALL Mark(Text1)",
|
||||
"LOAD Form1\nCALL Mark(Text1)",
|
||||
] {
|
||||
let mut vm = vm(&format!("DIM SHARED s$\nText1.TabIndex = 0\nText2.TabIndex = 1\nForm1.Show\nText1.SetFocus\nSTOP\nDOEVENTS\nEND\nSUB Text1_LostFocus()\nSHARED s$\n{action}\ns$ = s$ + \"l\"\nEND SUB\nSUB Mark(c AS CONTROL)\nSHARED s$\ns$ = s$ + \"L\"\nEND SUB\nSUB Text2_GotFocus()\nSHARED s$\ns$ = s$ + \"G\"\ns$ = s$ + \"g\"\nEND SUB"));
|
||||
let mut host = CaptureHost::default();
|
||||
stop(&mut vm, &mut host);
|
||||
host.ereignis(key("\t"));
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended, "{action}");
|
||||
assert_eq!(string(&vm, "s"), "LlGg", "{action}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_gleichzeitig_key_und_uevent_behalten_prioritaet() {
|
||||
let mut vm = vm("ON KEY(15) GOSUB K\nKEY 15, CHR$(0) + CHR$(20)\nKEY(15) ON\nON UEVENT GOSUB U\nUEVENT ON\nSTOP\nDOEVENTS\nEND\nK:\ns$ = s$ + \"K\"\nRETURN\nU:\ns$ = s$ + \"U\"\nRETURN");
|
||||
let mut host = CaptureHost::default();
|
||||
stop(&mut vm, &mut host);
|
||||
host.ereignis(key("t"));
|
||||
vm.rt.pump(&mut host, false);
|
||||
vm.rt.traps.melden(tb_runtime::traps::Quelle::UEvent);
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!(string(&vm, "s"), "KU");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_modale_eingabe_vor_eof_wird_zugestellt() {
|
||||
let mut vm = vm("DIM SHARED s$\nForm1.Show 1\nEND\nSUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\nSHARED s$\ns$ = \"M\"\nUNLOAD Form1\nEND SUB");
|
||||
let mut host = CaptureHost::default();
|
||||
host.ereignis_nach(2, mouse());
|
||||
host.ereignis_nach(2, Ereignis::Ende);
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!(string(&vm, "s"), "M");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eof_leert_angenommene_eingaben_bei_modalen_und_modellosen_forms() {
|
||||
for modal in [false, true] {
|
||||
let show = if modal { "Form1.Show 1" } else { "Form1.Show" };
|
||||
let mut vm = vm(&format!("DIM SHARED s$\n{show}\nEND\nSUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\nSHARED s$\ns$ = s$ + \"M\"\nEND SUB"));
|
||||
let mut host = CaptureHost::default();
|
||||
if !modal {
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
}
|
||||
host.ereignis_nach(2, mouse());
|
||||
host.ereignis_nach(2, mouse());
|
||||
host.ereignis_nach(2, Ereignis::Ende);
|
||||
let result = if modal {
|
||||
vm.run(&mut host)
|
||||
} else {
|
||||
vm.run_visible_forms(&mut host)
|
||||
};
|
||||
assert_eq!(result, RunEvent::Ended, "modal={modal}");
|
||||
assert_eq!(string(&vm, "s"), "MM", "modal={modal}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_menu_schliessen_stellt_pending_trap_vor_erneutem_warten_zu() {
|
||||
struct MenuHost {
|
||||
time: u64,
|
||||
waits: usize,
|
||||
}
|
||||
impl Host for MenuHost {
|
||||
fn present(&mut self, _: &TextScreen) {}
|
||||
fn next_event(&mut self, _: bool) -> Option<Ereignis> {
|
||||
None
|
||||
}
|
||||
fn jetzt_ms(&mut self) -> u64 {
|
||||
self.time
|
||||
}
|
||||
fn warten(&mut self, deadline: Option<u64>) -> Option<Ereignis> {
|
||||
self.waits += 1;
|
||||
if self.waits == 1 {
|
||||
return Some(key("\u{1b}"));
|
||||
}
|
||||
self.time = deadline.unwrap();
|
||||
None
|
||||
}
|
||||
}
|
||||
let mut vm = vm("ON UEVENT GOSUB H\nUEVENT ON\nMenu1.Caption = \"&Datei\"\nItem1.Caption = \"&Ende\"\nForm1.Show\nSTOP\nSLEEP 60\nEND\nH:\ns$ = \"U\"\nRETURN");
|
||||
let mut host = CaptureHost::default();
|
||||
stop(&mut vm, &mut host);
|
||||
vm.forms.handle_key("d", 4);
|
||||
vm.rt.traps.melden(tb_runtime::traps::Quelle::UEvent);
|
||||
let mut host = MenuHost { time: 0, waits: 0 };
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
println!(
|
||||
"Menü/SLEEP: Zeit={}, Warteaufrufe={}, Handler={}",
|
||||
host.time,
|
||||
host.waits,
|
||||
string(&vm, "s")
|
||||
);
|
||||
assert_eq!(host.waits, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_maus_vor_getrappter_taste() {
|
||||
let mut vm = vm("DIM SHARED s$\nON KEY(15) GOSUB H\nKEY 15, CHR$(0) + CHR$(20)\nKEY(15) ON\nForm1.Show\nSTOP\nDOEVENTS\nDOEVENTS\nEND\nH:\ns$ = s$ + \"K\"\nRETURN\nSUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\nSHARED s$\ns$ = s$ + \"M\"\nEND SUB");
|
||||
let mut host = CaptureHost::default();
|
||||
stop(&mut vm, &mut host);
|
||||
host.ereignis(mouse());
|
||||
host.ereignis(key("t"));
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!(string(&vm, "s"), "MK");
|
||||
}
|
||||
|
||||
/// Ereignisse treffen erst beim Warten ein, auch mitten in einer Eingabe.
|
||||
struct EingabeHost {
|
||||
events: std::collections::VecDeque<Ereignis>,
|
||||
now: u64,
|
||||
deadlines: Vec<Option<u64>>,
|
||||
}
|
||||
impl Host for EingabeHost {
|
||||
fn present(&mut self, _: &TextScreen) {}
|
||||
fn next_event(&mut self, _: bool) -> Option<Ereignis> {
|
||||
None
|
||||
}
|
||||
fn jetzt_ms(&mut self) -> u64 {
|
||||
self.now
|
||||
}
|
||||
fn warten(&mut self, deadline: Option<u64>) -> Option<Ereignis> {
|
||||
self.deadlines.push(deadline);
|
||||
if let Some(deadline) = deadline {
|
||||
self.now = deadline;
|
||||
None
|
||||
} else {
|
||||
Some(self.events.pop_front().unwrap_or(Ereignis::Ende))
|
||||
}
|
||||
}
|
||||
}
|
||||
fn eingabe_host(events: Vec<Ereignis>) -> EingabeHost {
|
||||
EingabeHost {
|
||||
events: events.into(),
|
||||
now: 0,
|
||||
deadlines: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_fortsetzung_erhaelt_teiltext_ausdruck_und_byref_ziel() {
|
||||
for (statement, expected, enter) in [
|
||||
("INPUT s$", "ab", true),
|
||||
("LINE INPUT s$", "ab", true),
|
||||
("s$ = \"x\" + INPUT$(2) + \"y\"", "xaby", false),
|
||||
] {
|
||||
let src = format!("ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\nCALL Lesen(s$)\nEND\nH:\nn% = n% + 1\nRETURN\nSUB Lesen(s$)\n{statement}\nEND SUB");
|
||||
let mut vm = vm_for_source(&src);
|
||||
let mut events = vec![key("a"), Ereignis::Signal(2), key("b")];
|
||||
if enter {
|
||||
events.push(key("\r"));
|
||||
}
|
||||
let mut host = eingabe_host(events);
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended, "{statement}");
|
||||
assert_eq!(string(&vm, "s"), expected);
|
||||
assert_eq!(integer(&vm, "n"), 1);
|
||||
assert_eq!(host.deadlines.len(), if enter { 4 } else { 3 });
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handler_eingabe_und_stop_bewahren_die_aeussere_eingabe() {
|
||||
let mut vm = vm_for_source(
|
||||
"ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\nINPUT s$\nEND\nH:\nSTOP\nINPUT t$\nRETURN",
|
||||
);
|
||||
let mut host = eingabe_host(vec![
|
||||
key("a"),
|
||||
Ereignis::Signal(2),
|
||||
key("h"),
|
||||
key("\r"),
|
||||
key("b"),
|
||||
key("\r"),
|
||||
]);
|
||||
stop(&mut vm, &mut host);
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!(string(&vm, "s"), "ab");
|
||||
assert_eq!(string(&vm, "t"), "h");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timer_waehrend_input_wird_vor_erneutem_warten_zugestellt() {
|
||||
let mut vm = vm_for_source(
|
||||
"ON TIMER(1) GOSUB H\nTIMER ON\nINPUT s$\nEND\nH:\nn% = n% + 1\nTIMER OFF\nRETURN",
|
||||
);
|
||||
let mut host = eingabe_host(vec![key("a"), key("\r")]);
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!(string(&vm, "s"), "a");
|
||||
assert_eq!(integer(&vm, "n"), 1);
|
||||
assert_eq!(host.deadlines, [Some(1000), None, None]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_redo_verliert_weder_referenzen_noch_teiltext_beim_trap() {
|
||||
let mut vm =
|
||||
vm_for_source("ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\nINPUT \"Zahl:\"; n%\nEND\nH:\nRETURN");
|
||||
let mut host = eingabe_host(vec![
|
||||
key("x"),
|
||||
key("\r"),
|
||||
key("4"),
|
||||
Ereignis::Signal(2),
|
||||
key("2"),
|
||||
key("\r"),
|
||||
]);
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!(integer(&vm, "n"), 42);
|
||||
assert_eq!(
|
||||
tb_runtime::snapshot::text(&vm.rt.screen)
|
||||
.matches("Zahl:")
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lokaler_error_handler_nach_trap_verwirft_abgebrochene_teileingabe() {
|
||||
let mut vm = vm_for_source("ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\nCALL Lesen(s$)\nEND\nH:\nERROR 5\nRETURN\nSUB Lesen(s$)\nON LOCAL ERROR GOTO Fehler\nINPUT s$\nEXIT SUB\nFehler:\nINPUT s$\nRESUME NEXT\nEND SUB");
|
||||
let mut host = eingabe_host(vec![key("a"), Ereignis::Signal(2), key("b"), key("\r")]);
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!(string(&vm, "s"), "b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn randomize_eingabe_teilt_den_fortsetzbaren_konsolenpfad() {
|
||||
let mut vm = vm_for_source(
|
||||
"ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\nRANDOMIZE\nEND\nH:\nn% = n% + 1\nRETURN",
|
||||
);
|
||||
let mut host = eingabe_host(vec![key("4"), Ereignis::Signal(2), key("2"), key("\r")]);
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!(integer(&vm, "n"), 1);
|
||||
assert_eq!(
|
||||
tb_runtime::snapshot::text(&vm.rt.screen)
|
||||
.matches("Random Number Seed")
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn return_label_verwirft_abgebrochene_eingabe_und_ausdruck() {
|
||||
for statement in ["INPUT s$", "s$ = \"alt\" + INPUT$(2)"] {
|
||||
let mut vm = vm_for_source(&format!("ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\n{statement}\nEND\nWeiter:\nINPUT t$\nEND\nH:\nRETURN Weiter"));
|
||||
let mut host = eingabe_host(vec![key("a"), Ereignis::Signal(2), key("b"), key("\r")]);
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended, "{statement}");
|
||||
assert_eq!(string(&vm, "s"), "");
|
||||
assert_eq!(string(&vm, "t"), "b");
|
||||
}
|
||||
}
|
||||
@@ -859,6 +859,12 @@ fn run_mit_uhr(src: &str, schritt_ms: u64) -> (RunEvent, String) {
|
||||
fn next_event(&mut self, b: bool) -> Option<tb_runtime::host::Ereignis> {
|
||||
self.inner.next_event(b)
|
||||
}
|
||||
fn warten(&mut self, deadline_ms: Option<u64>) -> Option<tb_runtime::host::Ereignis> {
|
||||
if let Some(event) = self.next_event(false) {
|
||||
return Some(event);
|
||||
}
|
||||
self.inner.warten(deadline_ms)
|
||||
}
|
||||
fn jetzt_ms(&mut self) -> u64 {
|
||||
let j = self.inner.jetzt_ms();
|
||||
self.inner.uhr_vorruecken(self.schritt);
|
||||
|
||||
Reference in New Issue
Block a user