Phase 4: Ereignisschleife und klassische Traps
This commit is contained in:
@@ -7,13 +7,13 @@
|
||||
|
||||
use crate::console::PrintState;
|
||||
use crate::datetime as dt;
|
||||
use crate::finanz as fin;
|
||||
use crate::fileio as fio;
|
||||
use crate::value;
|
||||
use crate::errors::RuntimeError;
|
||||
use crate::fileio as fio;
|
||||
use crate::finanz as fin;
|
||||
use crate::format;
|
||||
use crate::host::{Ereignis, Host};
|
||||
use crate::screen::TextScreen;
|
||||
use crate::value;
|
||||
use crate::value::{as_f64, cur_to_f64, f64_to_cur, Value};
|
||||
use std::collections::VecDeque;
|
||||
use std::rc::Rc;
|
||||
@@ -28,8 +28,11 @@ 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.
|
||||
pub tasten: VecDeque<String>,
|
||||
/// 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)>,
|
||||
/// Abbruchwunsch (Strg+Untbr) wurde gemeldet.
|
||||
pub abbruch: bool,
|
||||
/// Eingabeende wurde gemeldet.
|
||||
@@ -42,6 +45,8 @@ pub struct RtState {
|
||||
pub waehrung: crate::using::Waehrung,
|
||||
/// Ablaufverfolgung (`TRON`/`TROFF`) eingeschaltet?
|
||||
pub trace: bool,
|
||||
/// Zustände der klassischen Ereignis-Traps (Sprachreferenz §8).
|
||||
pub traps: crate::traps::Traps,
|
||||
/// Trefferliste und Zeiger für `DIR$`.
|
||||
pub dir_treffer: Vec<String>,
|
||||
pub dir_index: usize,
|
||||
@@ -73,6 +78,7 @@ impl Default for RtState {
|
||||
key_zeile: false,
|
||||
waehrung: Default::default(),
|
||||
trace: false,
|
||||
traps: crate::traps::Traps::neu(),
|
||||
dir_treffer: Vec::new(),
|
||||
dir_index: 0,
|
||||
uhr_offset: 0,
|
||||
@@ -100,10 +106,19 @@ impl RtState {
|
||||
return;
|
||||
};
|
||||
match e {
|
||||
Ereignis::Taste(t) => self.tasten.push_back(t),
|
||||
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;
|
||||
}
|
||||
}
|
||||
// ponytail: Mausereignisse bekommen ihren Verbraucher erst
|
||||
// mit dem Forms-Objektmodell. Bis dahin werden sie hier
|
||||
// verworfen, damit die Warteschlange nicht wächst.
|
||||
Ereignis::Maus(_) => {}
|
||||
}
|
||||
if blockierend {
|
||||
return;
|
||||
@@ -116,12 +131,46 @@ impl RtState {
|
||||
pub fn tick(&mut self, host: &mut dyn Host) {
|
||||
host.present(&self.screen);
|
||||
self.pump(host, false);
|
||||
self.traps_pruefen(host);
|
||||
}
|
||||
|
||||
/// Was am Zustellpunkt vor der eigentlichen Zustellung zu tun ist:
|
||||
/// Zeit prüfen und getrappte Tasten aus der Schlange nehmen.
|
||||
///
|
||||
/// Die Tasten werden **hier** aussortiert, nicht beim Einsortieren
|
||||
/// (design.md, D5): so wirkt ein `KEY(n) OFF` sofort und auch auf
|
||||
/// Tasten, die schon in der Schlange liegen.
|
||||
pub fn traps_pruefen(&mut self, host: &mut dyn Host) {
|
||||
if !self.traps.aktiv() {
|
||||
return;
|
||||
}
|
||||
let jetzt = host.jetzt_ms();
|
||||
self.traps.zeit_pruefen(jetzt);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
if let Some((t, _)) = self.tasten.pop_front() {
|
||||
return Some(t);
|
||||
}
|
||||
if self.ende {
|
||||
@@ -386,7 +435,9 @@ pub mod ids {
|
||||
pub const ISAM_SAVEPOINT: u16 = 145;
|
||||
pub const ISAM_SETMEM: u16 = 146;
|
||||
pub const ISAM_BOF: u16 = 147;
|
||||
pub const COUNT: u16 = 148;
|
||||
/// `SetUEvent` — loest das benutzerdefinierte Ereignis aus (§8).
|
||||
pub const SETUEVENT: u16 = 148;
|
||||
pub const COUNT: u16 = 149;
|
||||
}
|
||||
|
||||
/// Dispatch-Tabelle in Index-Reihenfolge.
|
||||
@@ -541,6 +592,7 @@ pub fn builtin_table() -> &'static [BuiltinFn] {
|
||||
bi_isam_savepoint,
|
||||
bi_isam_setmem,
|
||||
bi_isam_bof,
|
||||
bi_setuevent,
|
||||
];
|
||||
debug_assert_eq!(TABLE.len(), ids::COUNT as usize);
|
||||
TABLE
|
||||
@@ -1439,9 +1491,26 @@ fn key_index(n: i32) -> Option<usize> {
|
||||
}
|
||||
}
|
||||
|
||||
fn bi_key_assign(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
fn bi_key_assign(
|
||||
st: &mut RtState,
|
||||
_: &mut dyn Host,
|
||||
a: &mut [Value],
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
let n = arg_i32(a, 0)?;
|
||||
let text = arg_str(a, 1)?;
|
||||
// 15–25 weisen kein Makro zu, sondern erklären eine Trap-Taste über
|
||||
// Tastaturflagbyte und Scancode (Sprachreferenz §8).
|
||||
if (15..=25).contains(&n) {
|
||||
let mut it = text.chars();
|
||||
let (Some(flag), Some(scan)) = (it.next(), it.next()) else {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
};
|
||||
if it.next().is_some() || flag as u32 > 255 || scan as u32 > 255 {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
st.traps.benutzertaste(n as u8, flag as u8, scan as u8);
|
||||
return Ok(None);
|
||||
}
|
||||
let i = key_index(n).ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
|
||||
// Höchstens 15 Zeichen; überzählige werden verworfen (Vorbild).
|
||||
st.key_makros[i] = text.chars().take(15).collect();
|
||||
@@ -1651,7 +1720,21 @@ fn bi_sleep(st: &mut RtState, host: &mut dyn Host, a: &mut [Value]) -> Result<Op
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn bi_beep(_: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
fn bi_setuevent(
|
||||
st: &mut RtState,
|
||||
_: &mut dyn Host,
|
||||
_: &mut [Value],
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
// „sets a flag that BASIC checks before executing the next statement"
|
||||
st.traps.melden(crate::traps::Quelle::UEvent);
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn bi_beep(
|
||||
_: &mut RtState,
|
||||
_: &mut dyn Host,
|
||||
_: &mut [Value],
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
// ponytail: Der Signalton geht direkt an das Terminal, nicht durch den
|
||||
// Zellenpuffer — er hinterlässt dort nichts. Ceiling: sobald der Host
|
||||
// mehr als Anzeigen kann, wird daraus ein eigener Host-Aufruf.
|
||||
@@ -1885,4 +1968,155 @@ mod tests {
|
||||
};
|
||||
assert_eq!(n, 3);
|
||||
}
|
||||
#[test]
|
||||
fn setuevent_meldet_das_benutzerereignis() {
|
||||
use crate::traps::{Quelle, Zustand};
|
||||
let mut st = RtState::default();
|
||||
let mut h = crate::host::CaptureHost::default();
|
||||
st.traps.definieren(Quelle::UEvent, Some(11));
|
||||
st.traps.setzen(Quelle::UEvent, Zustand::An, 0);
|
||||
assert_eq!(st.traps.naechstes(), None);
|
||||
bi_setuevent(&mut st, &mut h, &mut []).unwrap();
|
||||
assert_eq!(st.traps.naechstes(), Some((Quelle::UEvent, 11)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signal_aus_dem_host_meldet_den_trap() {
|
||||
use crate::host::{Ereignis, Host};
|
||||
use crate::traps::{Quelle, Zustand};
|
||||
let mut st = RtState::default();
|
||||
let mut h = crate::host::CaptureHost::default();
|
||||
st.traps.definieren(Quelle::Signal(1), Some(22));
|
||||
st.traps.setzen(Quelle::Signal(1), Zustand::An, 0);
|
||||
h.ereignis(Ereignis::Signal(1));
|
||||
st.pump(&mut h, false);
|
||||
assert_eq!(st.traps.naechstes(), Some((Quelle::Signal(1), 22)));
|
||||
assert!(matches!(h.next_event(false), None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sigint_ohne_aktiven_trap_bleibt_abbruch() {
|
||||
use crate::host::Ereignis;
|
||||
let mut st = RtState::default();
|
||||
let mut h = crate::host::CaptureHost::default();
|
||||
h.ereignis(Ereignis::Signal(1));
|
||||
st.pump(&mut h, false);
|
||||
assert!(st.abbruch);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mausereignisse_haeufen_sich_nicht_an() {
|
||||
use crate::host::{Ereignis, MausArt, MausEreignis};
|
||||
let mut st = RtState::default();
|
||||
let mut h = crate::host::CaptureHost::default();
|
||||
for i in 0..500 {
|
||||
h.ereignis(Ereignis::Maus(MausEreignis {
|
||||
art: MausArt::Bewegung,
|
||||
taste: 0,
|
||||
shift: 0,
|
||||
zeile: 1 + i % 20,
|
||||
spalte: 1 + i % 70,
|
||||
}));
|
||||
}
|
||||
st.pump(&mut h, false);
|
||||
assert!(st.tasten.is_empty(), "Maus landet nicht im Tastenpuffer");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn getrappte_taste_verlaesst_den_eingabestrom() {
|
||||
use crate::host::{taste, CaptureHost, Ereignis};
|
||||
use crate::traps::{Quelle, Zustand};
|
||||
let mut st = RtState::default();
|
||||
let mut h = CaptureHost::default();
|
||||
st.traps.definieren(Quelle::Key(1), Some(77));
|
||||
st.traps.setzen(Quelle::Key(1), Zustand::An, 0);
|
||||
h.ereignis(Ereignis::Taste(taste::sonder(0x3B), 0)); // F1
|
||||
st.pump(&mut h, false);
|
||||
st.traps_pruefen(&mut h);
|
||||
assert_eq!(st.traps.naechstes(), Some((Quelle::Key(1), 77)));
|
||||
assert_eq!(st.naechste_taste(&mut h, false), None, "F1 ist weg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nicht_getrappte_taste_bleibt_im_strom() {
|
||||
use crate::host::{taste, CaptureHost, Ereignis};
|
||||
use crate::traps::{Quelle, Zustand};
|
||||
let mut st = RtState::default();
|
||||
let mut h = CaptureHost::default();
|
||||
st.traps.definieren(Quelle::Key(1), Some(77));
|
||||
st.traps.setzen(Quelle::Key(1), Zustand::Aus, 0);
|
||||
h.ereignis(Ereignis::Taste(taste::sonder(0x3B), 0));
|
||||
st.pump(&mut h, false);
|
||||
st.traps_pruefen(&mut h);
|
||||
assert_eq!(st.traps.naechstes(), None);
|
||||
assert_eq!(
|
||||
st.naechste_taste(&mut h, false),
|
||||
Some(taste::sonder(0x3B)),
|
||||
"bei OFF bleibt F1 lesbar"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn off_wirkt_auf_bereits_wartende_tasten() {
|
||||
use crate::host::{taste, CaptureHost, Ereignis};
|
||||
use crate::traps::{Quelle, Zustand};
|
||||
let mut st = RtState::default();
|
||||
let mut h = CaptureHost::default();
|
||||
st.traps.definieren(Quelle::Key(1), Some(77));
|
||||
st.traps.setzen(Quelle::Key(1), Zustand::An, 0);
|
||||
h.ereignis(Ereignis::Taste(taste::sonder(0x3B), 0));
|
||||
st.pump(&mut h, false); // Taste liegt in der Schlange
|
||||
st.traps.setzen(Quelle::Key(1), Zustand::Aus, 0);
|
||||
st.traps_pruefen(&mut h);
|
||||
assert_eq!(
|
||||
st.naechste_taste(&mut h, false),
|
||||
Some(taste::sonder(0x3B)),
|
||||
"das OFF wirkt auch auf schon Wartendes (design.md D5)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn benutzertaste_ueber_key_anweisung() {
|
||||
use crate::host::{umschalt, CaptureHost};
|
||||
use crate::traps::{Quelle, Zustand};
|
||||
let mut st = RtState::default();
|
||||
let mut h = CaptureHost::default();
|
||||
st.traps.definieren(Quelle::Key(15), Some(88));
|
||||
st.traps.setzen(Quelle::Key(15), Zustand::An, 0);
|
||||
// KEY 15, CHR$(4) + CHR$(30) — Strg+A
|
||||
let def = format!("{}{}", 4u8 as char, 0x1Eu8 as char);
|
||||
bi_key_assign(
|
||||
&mut st,
|
||||
&mut h,
|
||||
&mut [Value::Int(15), Value::Str(def.into())],
|
||||
)
|
||||
.unwrap();
|
||||
h.taste_mit("a", umschalt::CTRL);
|
||||
st.pump(&mut h, false);
|
||||
st.traps_pruefen(&mut h);
|
||||
assert_eq!(st.traps.naechstes(), Some((Quelle::Key(15), 88)));
|
||||
assert_eq!(st.naechste_taste(&mut h, false), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn benutzertaste_ohne_umschalt_bleibt_im_strom() {
|
||||
use crate::host::CaptureHost;
|
||||
use crate::traps::{Quelle, Zustand};
|
||||
let mut st = RtState::default();
|
||||
let mut h = CaptureHost::default();
|
||||
st.traps.definieren(Quelle::Key(15), Some(88));
|
||||
st.traps.setzen(Quelle::Key(15), Zustand::An, 0);
|
||||
let def = format!("{}{}", 4u8 as char, 0x1Eu8 as char);
|
||||
bi_key_assign(
|
||||
&mut st,
|
||||
&mut h,
|
||||
&mut [Value::Int(15), Value::Str(def.into())],
|
||||
)
|
||||
.unwrap();
|
||||
h.taste_mit("a", 0); // ohne Strg
|
||||
st.pump(&mut h, false);
|
||||
st.traps_pruefen(&mut h);
|
||||
assert_eq!(st.traps.naechstes(), None);
|
||||
assert_eq!(st.naechste_taste(&mut h, false), Some("a".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user