Ereigniszustellung und Hostgrenzen korrigieren und Change archivieren
This commit is contained in:
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user