Phase 4: Steuerelemente implementieren

This commit is contained in:
2026-09-05 12:49:46 +02:00
parent 1cb6ae8fd9
commit 19804e0e2d
36 changed files with 6472 additions and 689 deletions

31
PLAN.md
View File

@@ -393,6 +393,10 @@ Maßgeblich ist seit 2026-09-03 das Inventar, nicht diese Liste.
Transaktion verfällt.
## Phase 4 — Forms-Engine (`tb-ui::forms`)
**Status: abgeschlossen (2026-09-05).** Alle sieben Programme des
öffentlichen VBDOS-Bestands kompilieren ohne Diagnose und starten im PipeHost.
- [x] Eingangsaufgabe (aus Phase 0 übernommen): Ereignisschleifen-
Architektur — Terminal-Events → Event-Queue → VM-Ticks, kooperative
Zustellpunkte (`DOEVENTS`, `SLEEP`, blockierende Eingabe, Ende einer
@@ -401,11 +405,12 @@ Maßgeblich ist seit 2026-09-03 das Inventar, nicht diese Liste.
Forms-Ereignisprozeduren verwenden später denselben Rückrufweg.
- [x] Formular-Modell: Eigenschaften, Lade-/Entladezyklus, `SHOW`/`HIDE`
(modal/nicht-modal)
- [ ] Steuerelemente: CommandButton, TextBox, ListBox, ComboBox, CheckBox,
- [x] Steuerelemente: CommandButton, TextBox, ListBox, ComboBox, CheckBox,
OptionButton, Frame, Label, HScrollBar/VScrollBar, PictureBox (Text),
Timer — mit allen Eigenschaften/Methoden/Ereignissen des Vorbilds
- [ ] Menüsystem (Menüleiste, Shortcuts, Access Keys)
- [ ] Fokus-/Tab-Reihenfolge, Access-Keys, Maussteuerung
Timer und VSpin/HSpin — mit ihren Eigenschaften, Methoden und
Ereignissen
- [x] Menüsystem (Menüleiste, Shortcuts, Access Keys)
- [x] Fokus-/Tab-Reihenfolge, Access-Keys, Maussteuerung
- [x] `.FRM`-Textformat: Serialisierung **definieren** (kein Original-
Beispiel verfügbar — Windows-1.0-Schema, siehe dateiformate.md),
dokumentieren, dann lesen/schreiben implementieren
@@ -413,9 +418,14 @@ Maßgeblich ist seit 2026-09-03 das Inventar, nicht diese Liste.
(Gegenstück zu FT.EXE des Vorbilds; Magic `FC 08 01 00`): als
`tbc convert-frm`. Format per Reverse Engineering aus den
Beispieldateien des Originalpakets und des cout/vbdos-Repos
- [ ] Meilenstein (erster Kompatibilitätstest): die Programme aus
- [x] Meilenstein (erster Kompatibilitätstest): die Programme aus
https://github.com/cout/vbdos ohne Non-Features kompilieren und
sind nutzbar (Konsolenprogramme bereits ab Phase 3)
Befund: Alle sieben Programme wurden gegen Commit `1cdd2b3` als
`.MAK` bzw. `.FRM` ohne Diagnosen geprüft und im PipeHost gestartet.
Dafür wurden Projekt-/Include-Auflösung, P-Code-Dekodierung,
Zellgrafik und VSpin/HSpin vollständig geschlossen. Einzelheiten:
`openspec/changes/phase-4-steuerelemente/vbdos-befunde.md`.
- [x] Ereignisdispatch: Event-Queue ↔ VM (Ereignisprozeduren `Name_Ereignis`)
- [x] **Klassische Ereignis-Traps** (ohne Forms, Sprachreferenz §8) —
`ON TIMER(n)`, `ON KEY(n)`, `ON UEVENT` und `ON SIGNAL(n)` je mit
@@ -426,11 +436,14 @@ Maßgeblich ist seit 2026-09-03 das Inventar, nicht diese Liste.
(Fehler 73). **10 Inventareinträge**
Befund: `EVENT` besitzt nur `ON`/`OFF`; `SIGNAL(1/2)` bildet
plattformübergreifend `SIGINT`/`SIGTERM` ab.
- [ ] **Vordefinierte Dialoge**: `MSGBOX` als Anweisung **und** als
- [x] **Vordefinierte Dialoge**: `MSGBOX` als Anweisung **und** als
Funktion (liefert die gedrückte Schaltfläche) sowie `INPUTBOX$`.
Sie hängen am Formularmodell, nicht an der Steuerelementliste, und
fallen deshalb sonst zwischen die Aufgaben. **3 Inventareinträge**
- [ ] Meilenstein: Beispiel-Formularprogramme aus dem Testkorpus laufen
- [x] Meilenstein: Beispiel-Formularprogramme aus dem Testkorpus laufen
Befund: `formular.frm` speist Fokus, Tab, Access-Key, Mausklick,
Menüwahl, Liste, Timer und modalen Dialog über eine deklarierte,
virtuelle Ereignisfolge reproduzierbar ein.
## Phase 5 — IDE (`tb-ide`)
- [ ] IDE-Rahmen: Menüleiste, MDI-artige Fensterverwaltung, Statuszeile,
@@ -441,11 +454,11 @@ Maßgeblich ist seit 2026-09-03 das Inventar, nicht diese Liste.
- [ ] Formular-Designer: Steuerelemente platzieren/verschieben/skalieren,
Eigenschaftenfenster
- [ ] Ausführen aus der IDE: Start/Unterbrechen/Fortsetzen/Neustart
- [ ] **`$INCLUDE`** im Compile-Treiber auflösen — der Lexer liefert den
- [x] **`$INCLUDE`** im Compile-Treiber auflösen — der Lexer liefert den
Metabefehl bereits als Token. Datei einlesen, Pfade relativ zur
einschließenden Datei, Zyklen erkennen und namentlich abweisen.
**1 Inventareintrag**
- [ ] **`RUN`** — Programm neu starten bzw. ein anderes laden und starten.
- [x] **`RUN`** — Programm neu starten bzw. ein anderes laden und starten.
Nutzt denselben Zurücksetzpfad der VM wie „Neustart" oben.
**1 Inventareintrag**
- [ ] Debugger: Breakpoints, Einzelschritt/Prozedurschritt, Direktfenster,

View File

@@ -1,15 +1,16 @@
//! `tbc` — Standalone-Compiler von Terminal Basic.
//!
//! Unterbefehle (Phase 2):
//! - `tbc run <datei.bas>` Kompilieren und sofort ausführen
//! - `tbc build <datei.bas>` Kompilieren zu `datei.tbc`
//! - `tbc check <datei.bas>` Nur Syntax-/Semantikprüfung
//! - `tbc run <datei.bas|datei.frm|projekt.mak>` Kompilieren und ausführen
//! - `tbc build <datei.bas|datei.frm|projekt.mak>` Zu `.tbc` kompilieren
//! - `tbc check <datei.bas|datei.frm|projekt.mak>` Syntax/Semantik prüfen
//! - `tbc convert-frm <quelle.frm> <ziel.frm>` Binärformular in Text wandeln
//!
//! Exit-Codes von `run` (Entscheidung D6, docs/tbvm-design.md):
//! 0 = END/SYSTEM/Programmende · 3 = STOP · 2 = Laufzeitfehler ·
//! 1 = Compile-Fehler/Bedienfehler.
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use tb_runtime::host::{Ereignis, Host};
@@ -25,7 +26,7 @@ fn main() -> ExitCode {
Some("convert-frm") => cmd_convert_frm(&args[1..]),
_ => {
eprintln!(
"Aufruf: tbc run|build|check <datei.bas> | tbc convert-frm <quelle.frm> <ziel.frm>"
"Aufruf: tbc run|build|check <datei.bas|datei.frm|projekt.mak> | tbc convert-frm <quelle.frm> <ziel.frm>"
);
ExitCode::from(1)
}
@@ -81,23 +82,230 @@ fn module_name(path: &Path) -> String {
.unwrap_or_else(|| "MODUL".into())
}
fn relative_case_insensitive(base: &Path, relative: &str) -> std::io::Result<PathBuf> {
let direct = base.join(relative);
if direct.exists() {
return Ok(direct);
}
let wanted = Path::new(relative);
let mut current = base.to_path_buf();
for component in wanted.components() {
use std::path::Component;
match component {
Component::CurDir => {}
Component::ParentDir => {
current.pop();
}
Component::Normal(name) => {
let entry = std::fs::read_dir(&current)?.find_map(|entry| {
let entry = entry.ok()?;
entry
.file_name()
.to_string_lossy()
.eq_ignore_ascii_case(&name.to_string_lossy())
.then(|| entry.path())
});
current = entry.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("{} nicht gefunden", current.join(name).display()),
)
})?;
}
Component::RootDir | Component::Prefix(_) => current.push(component.as_os_str()),
}
}
Ok(current)
}
fn include_name(line: &str) -> Option<String> {
let trimmed = line.trim();
let upper = trimmed.to_ascii_uppercase();
let at = upper.find("$INCLUDE")?;
let rest = trimmed[at + "$INCLUDE".len()..].trim_start();
let rest = rest.strip_prefix(':')?.trim_start();
let rest = rest.strip_prefix('\'')?;
Some(rest.split('\'').next().unwrap_or_default().to_string())
}
fn expand_includes(path: &Path, source: &str, stack: &mut Vec<PathBuf>) -> Result<String, String> {
let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
if stack.contains(&canonical) {
return Err(format!("{}: zyklisches $INCLUDE", path.display()));
}
stack.push(canonical);
let mut out = String::new();
for line in source.split_inclusive('\n') {
if let Some(name) = include_name(line) {
if name.is_empty() {
return Err(format!("{}: leeres $INCLUDE", path.display()));
}
let included =
relative_case_insensitive(path.parent().unwrap_or(Path::new(".")), &name)
.map_err(|error| format!("{}: {error}", path.display()))?;
let text = std::fs::read_to_string(&included)
.map_err(|error| format!("{}: {error}", included.display()))?;
out.push_str(&expand_includes(&included, &text, stack)?);
if !out.ends_with('\n') {
out.push('\n');
}
} else {
out.push_str(line);
}
}
stack.pop();
Ok(out)
}
fn read_form(path: &Path) -> Result<tb_ui::frm::FormFile, String> {
let bytes = std::fs::read(path).map_err(|error| format!("{}: {error}", path.display()))?;
if bytes.starts_with(b"VERSION ") || bytes.starts_with(b"Version ") {
let source = std::str::from_utf8(&bytes)
.map_err(|_| format!("{}: ungültige Textkodierung", path.display()))?;
let mut form = tb_ui::frm::read_text(&path.display().to_string(), source)
.map_err(|error| error.to_string())?;
form.code = expand_includes(path, &form.code, &mut Vec::new())?;
Ok(form)
} else {
tb_ui::frm::read_binary(&path.display().to_string(), &bytes)
.map(|read| read.form)
.map_err(|error| error.to_string())
}
}
#[derive(Default)]
struct ProjectSymbols {
constants: HashSet<String>,
types: HashSet<String>,
}
fn append_project_source(target: &mut String, source: &str, symbols: &mut ProjectSymbols) {
let mut in_proc = false;
let mut skip_type = false;
for line in source.split_inclusive('\n') {
let upper = line.trim().to_ascii_uppercase();
if skip_type {
if upper == "END TYPE" {
skip_type = false;
}
continue;
}
if !in_proc {
if let Some(name) = upper
.strip_prefix("TYPE ")
.and_then(|rest| rest.split_whitespace().next())
{
if !symbols.types.insert(name.to_string()) {
skip_type = true;
continue;
}
}
if let Some(rest) = upper.strip_prefix("CONST ") {
let names = rest
.split(',')
.filter_map(|part| part.split('=').next())
.map(str::trim)
.filter(|name| !name.is_empty())
.collect::<Vec<_>>();
if !names.is_empty() && names.iter().all(|name| symbols.constants.contains(*name)) {
continue;
}
symbols
.constants
.extend(names.into_iter().map(str::to_string));
}
}
target.push_str(line);
if upper.starts_with("SUB ")
|| upper.starts_with("FUNCTION ")
|| upper.starts_with("STATIC SUB ")
|| upper.starts_with("STATIC FUNCTION ")
{
in_proc = true;
} else if matches!(upper.as_str(), "END SUB" | "END FUNCTION") {
in_proc = false;
}
}
if !target.ends_with('\n') {
target.push('\n');
}
}
fn input_sources(path: &Path) -> Result<(String, Vec<tb_ui::frm::FormFile>), String> {
let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
let paths = if extension.eq_ignore_ascii_case("mak") {
let project = std::fs::read_to_string(path)
.map_err(|error| format!("{}: {error}", path.display()))?;
let base = path.parent().unwrap_or(Path::new("."));
project
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.map(|line| {
relative_case_insensitive(base, line)
.map_err(|error| format!("{}: {error}", path.display()))
})
.collect::<Result<Vec<_>, _>>()?
} else {
vec![path.to_path_buf()]
};
let mut source = String::new();
let mut forms = Vec::new();
let mut symbols = ProjectSymbols::default();
for member in paths {
if member
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("frm"))
{
let form = read_form(&member)?;
append_project_source(&mut source, &form.code, &mut symbols);
forms.push(form);
} else {
let text = std::fs::read_to_string(&member)
.map_err(|error| format!("{}: {error}", member.display()))?;
let expanded = expand_includes(&member, &text, &mut Vec::new())?;
append_project_source(&mut source, &expanded, &mut symbols);
}
}
Ok((source, forms))
}
fn compile(
path_arg: Option<&String>,
) -> Result<(PathBuf, tb_vm::bytecode::CompiledModule), ExitCode> {
) -> Result<
(
PathBuf,
tb_vm::bytecode::CompiledModule,
Vec<tb_ui::frm::FormFile>,
),
ExitCode,
> {
let Some(path) = path_arg else {
eprintln!("Aufruf: tbc run|build|check <datei.bas>");
eprintln!("Aufruf: tbc run|build|check <datei.bas|datei.frm|projekt.mak>");
return Err(ExitCode::from(1));
};
let path = PathBuf::from(path);
let source = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(e) => {
eprintln!("{}: {e}", path.display());
let (source, forms) = match input_sources(&path) {
Ok(input) => input,
Err(error) => {
eprintln!("{error}");
return Err(ExitCode::from(1));
}
};
match tb_vm::compile_source(&module_name(&path), &source) {
Ok(m) => Ok((path, m)),
let compiled = if forms.is_empty() {
tb_vm::compile_source(&module_name(&path), &source)
} else {
let mut catalog = tb_frontend::forms::FormCatalog::default();
for form in &forms {
for object in form.catalog().objects {
catalog.objects.push(object);
}
}
tb_vm::compile_source_with_forms(&forms[0].root.name, &source, &catalog)
};
match compiled {
Ok(m) => Ok((path, m, forms)),
Err(diags) => {
for d in &diags {
eprintln!("{}:{d}", path.display());
@@ -116,7 +324,7 @@ fn cmd_check(args: &[String]) -> ExitCode {
}
fn cmd_build(args: &[String]) -> ExitCode {
let (path, module) = match compile(args.first()) {
let (path, module, _) = match compile(args.first()) {
Ok(x) => x,
Err(code) => return code,
};
@@ -134,36 +342,20 @@ fn cmd_build(args: &[String]) -> ExitCode {
}
fn cmd_run(args: &[String]) -> ExitCode {
let (_path, module) = match compile(args.first()) {
Ok(x) => x,
Err(code) => return code,
};
let mut vm = Vm::new(module);
vm.rt.command = args[1..].join(" ");
// Im Rohmodus ist Strg+C kein Signal mehr — der Abbruch kommt als
// Ereignis und muss den Lauf beenden können.
vm.set_poll_interrupt(true);
// Der Rückfall auf UTC wird einmal beim Start gemeldet, nicht je
// Zeitabfrage — ein stiller Wechsel wäre ein Verstoß gegen den
// Guiding Principle.
if vm.rt.zeitzone == tb_runtime::datetime::Zeitzone::Unbekannt {
eprintln!("Zeitzone nicht ermittelbar — Zeitfunktionen rechnen in UTC.");
}
// Ohne Terminal (Pipe, Skript, CI) läuft das Programm im PipeHost:
// Eingabe zeilenweise von stdin, Ausgabe am Ende als Snapshot.
let ereignis = match TerminalHost::new() {
let result = match TerminalHost::new() {
Ok(mut host) => {
// Der Bildschirm folgt der Terminalgröße von Beginn an (80×25 ist
// stets nur die untere Schranke, nie eine feste Größe).
if let Ok((cols, rows)) = host.groesse() {
vm.rt.screen.resize(cols, rows);
}
let e = vm.run(&mut host);
let size = host.groesse().ok();
let result = run_chain(args, &mut host, size);
drop(host); // Alternativschirm verlassen, bevor gedruckt wird
e
result
}
Err(_) => vm.run(&mut PipeHost::new()),
Err(_) => run_chain(args, &mut PipeHost::new(), None),
};
let (ereignis, vm) = match result {
Ok(result) => result,
Err(code) => return code,
};
print!("{}", tb_runtime::snapshot::text(&vm.rt.screen));
// `LPRINT` sammelt im Druckerpuffer; am Programmende geht er in die
@@ -197,6 +389,90 @@ fn cmd_run(args: &[String]) -> ExitCode {
}
// Ohne Debugger-Flags treten diese Ereignisse nicht auf.
RunEvent::Breakpoint { .. } | RunEvent::Stepped { .. } => ExitCode::from(2),
RunEvent::Restart { .. } => unreachable!("RUN wird vom Runner aufgelöst"),
}
}
fn run_target(current: &Path, program: &str) -> Result<PathBuf, String> {
let base = current.parent().unwrap_or(Path::new("."));
if Path::new(program).extension().is_some() {
return relative_case_insensitive(base, program).map_err(|error| error.to_string());
}
for extension in ["bas", "frm", "mak"] {
let candidate = format!("{program}.{extension}");
if let Ok(path) = relative_case_insensitive(base, &candidate) {
return Ok(path);
}
}
Err(format!("{program}: Programm nicht gefunden"))
}
fn run_chain(
args: &[String],
host: &mut dyn Host,
size: Option<(usize, usize)>,
) -> Result<(RunEvent, Vm), ExitCode> {
let Some(first) = args.first() else {
eprintln!("Aufruf: tbc run <datei.bas|datei.frm|projekt.mak>");
return Err(ExitCode::from(1));
};
let mut current = PathBuf::from(first);
let mut start_line = None;
let command = args[1..].join(" ");
loop {
let current_arg = current.display().to_string();
let (path, module, forms) = compile(Some(&current_arg))?;
let mut vm = Vm::new(module);
if !forms.is_empty() {
let applied = forms.iter().try_for_each(|form| form.apply(&mut vm.forms));
let first = vm
.forms
.objects
.iter()
.position(|object| {
object
.description
.name
.eq_ignore_ascii_case(&forms[0].root.name)
})
.unwrap() as u16;
if let Err(error) = applied.and_then(|_| vm.forms.show(first, false).map(|_| ())) {
eprintln!("Runtime error {}: {}", error.0, error);
return Err(ExitCode::from(2));
}
}
if let Some(line) = start_line.take() {
if let Err(error) = vm.start_at_line(line) {
eprintln!("Runtime error {}: {}", error.0, error);
return Err(ExitCode::from(2));
}
}
if let Some((cols, rows)) = size {
vm.rt.screen.resize(cols, rows);
}
vm.rt.command.clone_from(&command);
vm.set_poll_interrupt(true);
if vm.rt.zeitzone == tb_runtime::datetime::Zeitzone::Unbekannt {
eprintln!("Zeitzone nicht ermittelbar — Zeitfunktionen rechnen in UTC.");
}
let event = vm.run(host);
let event = if event == RunEvent::Ended && vm.forms.has_visible_forms() {
vm.run_visible_forms(host)
} else {
event
};
match event {
RunEvent::Restart { program, line } => {
if let Some(program) = program {
current = run_target(&path, &program).map_err(|error| {
eprintln!("{error}");
ExitCode::from(1)
})?;
}
start_line = line;
}
event => return Ok((event, vm)),
}
}
}
@@ -211,11 +487,30 @@ struct PipeHost {
impl PipeHost {
fn new() -> Self {
PipeHost {
let mut host = PipeHost {
puffer: std::collections::VecDeque::new(),
eof: false,
start: std::time::Instant::now(),
};
use std::io::{IsTerminal, Read};
let mut stdin = std::io::stdin();
if !stdin.is_terminal() {
let mut input = String::new();
let _ = stdin.read_to_string(&mut input);
for line in input.split_inclusive('\n') {
for character in line.trim_end_matches(['\r', '\n']).chars() {
host.puffer
.push_back(Ereignis::Taste(character.to_string(), 0));
}
host.puffer.push_back(Ereignis::Taste(
tb_runtime::host::taste::ENTER.to_string(),
0,
));
}
host.puffer.push_back(Ereignis::Ende);
host.eof = true;
}
host
}
/// Eine Zeile von stdin in Tastendrücke zerlegen.

View File

@@ -13,9 +13,10 @@
//! nur die untere Schranke, nie eine feste Größe, und der Korpus muss das
//! nachweisen können.
use std::collections::VecDeque;
use std::path::{Path, PathBuf};
use std::process::Command;
use tb_runtime::host::CaptureHost;
use tb_runtime::host::{CaptureHost, Host};
use tb_vm::interp::{RunEvent, Vm};
fn compat_dir() -> PathBuf {
@@ -100,10 +101,14 @@ fn resize_direktive(name: &str, src: &str) -> Option<(usize, (usize, usize))> {
/// `<F1>`…`<F12>`, sonst je ein Zeichen; `+` davor bedeutet Umschalt,
/// `^` Strg, `%` Alt — gebraucht für die benutzerdefinierten Trap-Tasten.
fn tasten_direktive(name: &str, src: &str) -> Vec<tb_runtime::host::Ereignis> {
use tb_runtime::host::{taste, umschalt, Ereignis};
let Some(wert) = direktive(src, "tb-keys:") else {
return Vec::new();
};
tastenfolge(name, &wert)
}
fn tastenfolge(name: &str, wert: &str) -> Vec<tb_runtime::host::Ereignis> {
use tb_runtime::host::{taste, umschalt, Ereignis};
let mut out = Vec::new();
let zeichen: Vec<char> = wert.chars().collect();
let mut i = 0;
@@ -143,6 +148,21 @@ fn tasten_direktive(name: &str, src: &str) -> Vec<tb_runtime::host::Ereignis> {
"LEFT" => 0x4B,
"RIGHT" => 0x4D,
"DOWN" => 0x50,
"ENTER" => {
out.push(Ereignis::Taste(taste::ENTER.into(), shift));
i += ende + 1;
continue;
}
"ESC" => {
out.push(Ereignis::Taste(taste::ESC.into(), shift));
i += ende + 1;
continue;
}
"TAB" => {
out.push(Ereignis::Taste(taste::TAB.into(), shift));
i += ende + 1;
continue;
}
other => panic!("{name}: tb-keys kennt `<{other}>` nicht"),
};
out.push(Ereignis::Taste(taste::sonder(code), shift));
@@ -155,6 +175,98 @@ fn tasten_direktive(name: &str, src: &str) -> Vec<tb_runtime::host::Ereignis> {
out
}
enum Ereignisschritt {
Ereignis(tb_runtime::host::Ereignis),
Zeit(u64),
}
fn ereignis_direktive(name: &str, src: &str) -> Vec<Ereignisschritt> {
use tb_runtime::host::{Ereignis, MausArt, MausEreignis};
let Some(wert) = direktive(src, "tb-events:") else {
return Vec::new();
};
let mut out = Vec::new();
for teil in wert.split('|').map(str::trim) {
if let Some(keys) = teil.strip_prefix("key:") {
out.extend(
tastenfolge(name, keys)
.into_iter()
.map(Ereignisschritt::Ereignis),
);
} else if let Some(ms) = teil.strip_prefix("time:") {
let ms = ms.trim().strip_suffix("ms").unwrap_or(ms.trim());
out.push(Ereignisschritt::Zeit(
ms.parse()
.unwrap_or_else(|_| panic!("{name}: ungültige Zeit {ms:?}")),
));
} else if let Some(mouse) = teil.strip_prefix("mouse:") {
let fields: Vec<_> = mouse.split(',').map(str::trim).collect();
let [kind, button, shift, row, col] = fields.as_slice() else {
panic!("{name}: mouse erwartet art,taste,shift,zeile,spalte");
};
let art = match *kind {
"down" => MausArt::Druck,
"up" => MausArt::Loslassen,
"move" => MausArt::Bewegung,
_ => panic!("{name}: unbekannte Mausart {kind:?}"),
};
let parse = |value: &str| {
value
.parse::<usize>()
.unwrap_or_else(|_| panic!("{name}: ungültige Zahl {value:?}"))
};
out.push(Ereignisschritt::Ereignis(Ereignis::Maus(MausEreignis {
art,
taste: parse(button) as u8,
shift: parse(shift) as u8,
zeile: parse(row),
spalte: parse(col),
})));
} else {
panic!("{name}: unbekannter tb-events-Schritt {teil:?}");
}
}
out
}
struct EreignisHost {
inner: CaptureHost,
schritte: VecDeque<Ereignisschritt>,
pause: bool,
}
impl tb_runtime::host::Host for EreignisHost {
fn present(&mut self, screen: &tb_runtime::screen::TextScreen) {
self.inner.present(screen);
}
fn next_event(&mut self, blockierend: bool) -> Option<tb_runtime::host::Ereignis> {
if !blockierend && self.pause {
self.pause = false;
return None;
}
loop {
match self.schritte.pop_front() {
Some(Ereignisschritt::Zeit(ms)) => {
self.inner.uhr_vorruecken(ms);
if !blockierend {
return None;
}
}
Some(Ereignisschritt::Ereignis(event)) => {
self.pause = true;
return Some(event);
}
None => return self.inner.next_event(blockierend),
}
}
}
fn jetzt_ms(&mut self) -> u64 {
self.inner.jetzt_ms()
}
}
/// Virtuelle Uhr: `' tb-clock: <n>ms/Zustellpunkt`. Ohne die Direktive
/// steht die Uhr des Testhosts still — ein Programm mit Zeit-Traps liefe
/// dann endlos. Der Harness weist es deshalb ab, statt auf die Systemuhr
@@ -242,45 +354,77 @@ impl Drop for TempVerzeichnis {
/// Korpusdatei ausführen und den Bildschirm-Snapshot liefern.
fn run_corpus_file(path: &Path, groesse: Option<(usize, usize)>) -> String {
let src = std::fs::read_to_string(path).unwrap();
let raw = std::fs::read_to_string(path).unwrap();
let name = path.file_name().unwrap().to_string_lossy().to_string();
let modul_name = path.file_stem().unwrap().to_string_lossy().to_uppercase();
let (cols, rows) = groesse.unwrap_or_else(|| screen_groessen(&name, &src)[0]);
let module = tb_vm::compile_source(&modul_name, &src)
.unwrap_or_else(|d| panic!("{}: Compile-Fehler: {d:?}", path.display()));
let form = if path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("frm"))
{
Some(tb_ui::frm::read_text(&name, &raw).unwrap_or_else(|error| panic!("{error}")))
} else {
None
};
let src = form
.as_ref()
.map_or(raw.as_str(), |form| form.code.as_str());
let modul_name = form.as_ref().map_or_else(
|| path.file_stem().unwrap().to_string_lossy().to_uppercase(),
|form| form.root.name.to_uppercase(),
);
let (cols, rows) = groesse.unwrap_or_else(|| screen_groessen(&name, src)[0]);
let module = match &form {
Some(form) => tb_vm::compile_source_with_forms(&modul_name, src, &form.catalog()),
None => tb_vm::compile_source(&modul_name, src),
}
.unwrap_or_else(|d| panic!("{}: Compile-Fehler: {d:?}", path.display()));
let mut vm = Vm::new(module);
if let Some(form) = &form {
form.apply(&mut vm.forms).unwrap();
vm.forms.show(0, false).unwrap();
}
vm.rt.screen.resize(cols, rows);
if let Some(z) = tz_direktive(&name, &src) {
if let Some(z) = tz_direktive(&name, src) {
vm.rt.zeitzone = z;
}
// `' tb-tempdir` — das Programm arbeitet mit Dateien.
let _temp = direktive(&src, "tb-tempdir").map(|_| TempVerzeichnis::neu(&name));
let _temp = direktive(src, "tb-tempdir").map(|_| TempVerzeichnis::neu(&name));
let mut host = CaptureHost::default();
if let Some((nach, (c, r))) = resize_direktive(&name, &src) {
if let Some((nach, (c, r))) = resize_direktive(&name, src) {
host.ereignis_nach(
nach,
tb_runtime::host::Ereignis::Groesse { cols: c, rows: r },
);
}
for t in tasten_direktive(&name, &src) {
for t in tasten_direktive(&name, src) {
host.ereignis(t);
}
let schritt = clock_schritt(&name, &src);
let schritt = clock_schritt(&name, src);
let schritte = ereignis_direktive(&name, src);
if !schritte.is_empty() {
assert!(
schritt.is_none(),
"{name}: tb-events: time und tb-clock nicht mischen"
);
let mut host = EreignisHost {
inner: host,
schritte: schritte.into(),
pause: false,
};
return run_to_snapshot(path, &mut vm, &mut host);
}
if let Some(schritt_ms) = schritt {
let mut host = UhrHost {
inner: host,
schritt_ms,
};
return match vm.run(&mut host) {
RunEvent::Ended => tb_runtime::snapshot::snapshot(&vm.rt.screen),
other => panic!(
"{}: unerwartetes Laufzeitende {other:?}\nBildschirm bisher:\n{}",
path.display(),
tb_runtime::snapshot::snapshot(&vm.rt.screen)
),
};
return run_to_snapshot(path, &mut vm, &mut host);
}
match vm.run(&mut host) {
run_to_snapshot(path, &mut vm, &mut host)
}
fn run_to_snapshot(path: &Path, vm: &mut Vm, host: &mut dyn Host) -> String {
match vm.run(host) {
RunEvent::Ended => tb_runtime::snapshot::snapshot(&vm.rt.screen),
other => panic!(
"{}: unerwartetes Laufzeitende {other:?}\nBildschirm bisher:\n{}",
@@ -290,6 +434,21 @@ fn run_corpus_file(path: &Path, groesse: Option<(usize, usize)>) -> String {
}
}
fn source_for_directives(path: &Path) -> String {
let raw = std::fs::read_to_string(path).unwrap();
if path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("frm"))
{
tb_ui::frm::read_text(&path.display().to_string(), &raw)
.unwrap()
.code
} else {
raw
}
}
/// Snapshots vergleichen: erst das Textbild, dann die Attributebene.
/// Bei Abweichung im Text zählt die Zeile, bei Attributen Zeile und Spalte.
fn assert_output_matches(file: &str, want: &str, got: &str) {
@@ -365,12 +524,16 @@ fn korpus_laeuft_mit_korrekter_ausgabe() {
let mut entries: Vec<PathBuf> = std::fs::read_dir(&dir)
.expect("tests/compat fehlt")
.map(|e| e.unwrap().path())
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("bas"))
.filter(|p| {
p.extension()
.and_then(|e| e.to_str())
.is_some_and(|ext| matches!(ext, "bas" | "frm"))
})
.collect();
entries.sort();
for path in entries {
let name = path.file_name().unwrap().to_string_lossy().to_string();
let src = std::fs::read_to_string(&path).unwrap();
let src = source_for_directives(&path);
let groessen = screen_groessen(&name, &src);
for (cols, rows) in &groessen {
// Bei mehreren Größen trägt jede ihre eigene Sollausgabe.
@@ -406,6 +569,25 @@ fn uhr_direktive_wird_gelesen() {
);
}
#[test]
fn ereignisfolge_liefert_taste_maus_und_zeit_in_reihenfolge() {
let steps = ereignis_direktive(
"form.frm",
"' tb-events: key:%o | mouse:down,1,0,5,7 | time:250ms | key:<ENTER>",
);
assert_eq!(steps.len(), 4);
assert!(
matches!(&steps[0], Ereignisschritt::Ereignis(tb_runtime::host::Ereignis::Taste(key, shift)) if key == "o" && *shift == 4)
);
assert!(
matches!(&steps[1], Ereignisschritt::Ereignis(tb_runtime::host::Ereignis::Maus(mouse)) if mouse.zeile == 5 && mouse.spalte == 7)
);
assert!(matches!(steps[2], Ereignisschritt::Zeit(250)));
assert!(
matches!(&steps[3], Ereignisschritt::Ereignis(tb_runtime::host::Ereignis::Taste(key, _)) if key == tb_runtime::host::taste::ENTER)
);
}
#[test]
#[should_panic(expected = "deklariert aber keinen Zeitverlauf")]
fn zeittrap_ohne_uhr_direktive_wird_abgewiesen() {
@@ -467,6 +649,40 @@ fn tbc_build_erzeugt_tbc() {
let _ = std::fs::remove_file(&tbc);
}
#[test]
fn tbc_loest_include_projekt_und_run_datei_auf() {
let exe = env!("CARGO_BIN_EXE_tbc");
let dir = std::env::temp_dir().join(format!("tb_project_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("werte.bi"), "CONST N = 7\n").unwrap();
std::fs::write(
dir.join("main.bas"),
"'$INCLUDE: 'WERTE.BI'\nDECLARE SUB Ausgabe\nAusgabe\nEND\n",
)
.unwrap();
std::fs::write(dir.join("lib.bas"), "SUB Ausgabe\nPRINT N\nEND SUB\n").unwrap();
std::fs::write(dir.join("app.mak"), "MAIN.BAS\nLIB.BAS\n").unwrap();
let project = Command::new(exe)
.args(["run"])
.arg(dir.join("app.mak"))
.output()
.unwrap();
assert!(project.status.success(), "{project:?}");
assert_eq!(String::from_utf8_lossy(&project.stdout), " 7 \n");
std::fs::write(dir.join("start.bas"), "RUN \"ZIEL\"\n").unwrap();
std::fs::write(dir.join("ziel.bas"), "PRINT \"weiter\"\nEND\n").unwrap();
let chained = Command::new(exe)
.args(["run"])
.arg(dir.join("start.bas"))
.output()
.unwrap();
assert!(chained.status.success(), "{chained:?}");
assert_eq!(String::from_utf8_lossy(&chained.stdout), "weiter\n");
let _ = std::fs::remove_dir_all(&dir);
}
// ---- Harness meldet Abweichungen benannt (Spec kompat-testkorpus) ----------
fn meldung(want: &str, got: &str) -> String {
@@ -537,12 +753,16 @@ fn erzeuge_sollausgaben() {
let mut entries: Vec<PathBuf> = std::fs::read_dir(&dir)
.expect("tests/compat fehlt")
.map(|e| e.unwrap().path())
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("bas"))
.filter(|p| {
p.extension()
.and_then(|e| e.to_str())
.is_some_and(|ext| matches!(ext, "bas" | "frm"))
})
.collect();
entries.sort();
for path in entries {
let name = path.file_name().unwrap().to_string_lossy().to_string();
let src = std::fs::read_to_string(&path).unwrap();
let src = source_for_directives(&path);
let groessen = screen_groessen(&name, &src);
for (cols, rows) in &groessen {
let out_path = if groessen.len() == 1 {

View File

@@ -480,6 +480,26 @@ pub enum Stmt {
bottom: Option<Expr>,
pos: SourcePos,
},
GraphicsLine {
from: Option<(Expr, Expr)>,
to: (Expr, Expr),
relative: bool,
color: Option<Expr>,
fill: u8,
pos: SourcePos,
},
GraphicsPaint {
point: (Expr, Expr),
paint: Option<Expr>,
border: Option<Expr>,
pos: SourcePos,
},
GraphicsView {
rect: Option<(Expr, Expr, Expr, Expr)>,
fill: Option<Expr>,
border: Option<Expr>,
pos: SourcePos,
},
/// `ON TIMER(n&) GOSUB ziel`, `ON KEY(n%) GOSUB ziel`,
/// `ON UEVENT GOSUB ziel`, `ON SIGNAL(n%) GOSUB ziel` — die
/// klassischen Ereignis-Traps (Sprachreferenz §8). `GOSUB 0` schaltet

View File

@@ -21,10 +21,11 @@ pub enum ObjectClass {
Timer,
VScrollBar,
Screen,
Spin,
}
impl ObjectClass {
pub const ALL: [Self; 18] = [
pub const ALL: [Self; 19] = [
Self::Form,
Self::CheckBox,
Self::ComboBox,
@@ -43,6 +44,7 @@ impl ObjectClass {
Self::Timer,
Self::VScrollBar,
Self::Screen,
Self::Spin,
];
pub fn name(self) -> &'static str {
@@ -65,6 +67,7 @@ impl ObjectClass {
Self::Timer => "TIMER",
Self::VScrollBar => "VSCROLLBAR",
Self::Screen => "SCREEN",
Self::Spin => "SPIN",
}
}
@@ -88,6 +91,7 @@ impl ObjectClass {
Self::Timer => "Timer",
Self::VScrollBar => "VScrollBar",
Self::Screen => "Screen",
Self::Spin => "Spin",
}
}
@@ -295,7 +299,7 @@ pub fn properties(class: ObjectClass) -> Vec<PropertySpec> {
string("TEXT", ""),
]),
CheckBox => p.extend([string("CAPTION", ""), range("VALUE", 0, 0, 2)]),
OptionButton => p.extend([string("CAPTION", ""), range("VALUE", 0, -1, 0)]),
OptionButton => p.extend([string("CAPTION", ""), range("VALUE", 0, -1, 1)]),
Frame => p.push(string("CAPTION", "")),
Label => p.extend([
range("ALIGNMENT", 0, 0, 2),
@@ -311,6 +315,21 @@ pub fn properties(class: ObjectClass) -> Vec<PropertySpec> {
range("SMALLCHANGE", 1, 1, 32767),
range("VALUE", 0, -32768, 32767),
]),
Spin => {
p.extend([
ro("BORDERSTYLE", PropertyType::Integer),
range("INTERVAL", 250, 0, 65535),
range("MIN", 0, -32768, 32767),
range("MAX", 32767, -32768, 32767),
range("STYLE", 0, 0, 1),
range("VALUE", 0, -32768, 32767),
]);
for name in ["HEIGHT", "WIDTH"] {
if let Some(property) = p.iter_mut().find(|property| property.name == name) {
property.writable = false;
}
}
}
PictureBox => p.extend([
boolp("AUTOREDRAW", false),
range("BORDERSTYLE", 1, 0, 2),
@@ -337,10 +356,26 @@ pub fn properties(class: ObjectClass) -> Vec<PropertySpec> {
boolp("SEPARATOR", false),
string("TAG", ""),
boolp("VISIBLE", true),
string("SHORTCUT", ""),
]),
DirListBox => p.extend([
ro("LIST", PropertyType::String),
ro("LISTCOUNT", PropertyType::Integer),
int("LISTINDEX", -1),
string("PATH", ""),
string("TEXT", ""),
]),
DriveListBox => p.extend([
ro("LIST", PropertyType::String),
ro("LISTCOUNT", PropertyType::Integer),
int("LISTINDEX", -1),
string("DRIVE", ""),
string("TEXT", ""),
]),
DirListBox => p.extend([string("PATH", ""), string("TEXT", "")]),
DriveListBox => p.extend([string("DRIVE", ""), string("TEXT", "")]),
FileListBox => p.extend([
ro("LIST", PropertyType::String),
ro("LISTCOUNT", PropertyType::Integer),
int("LISTINDEX", -1),
string("FILENAME", ""),
string("PATH", ""),
string("PATTERN", "*.*"),
@@ -413,6 +448,7 @@ pub fn methods(class: ObjectClass) -> &'static [&'static str] {
&["DRAG", "MOVE", "REFRESH", "SETFOCUS"]
}
Frame | Label | HScrollBar | VScrollBar => &["DRAG", "MOVE", "REFRESH"],
Spin => &["DRAG", "REFRESH", "SETFOCUS"],
PictureBox => &[
"CLS",
"DRAG",
@@ -429,11 +465,7 @@ pub fn methods(class: ObjectClass) -> &'static [&'static str] {
}
pub fn method_is_implemented(class: ObjectClass, name: &str) -> bool {
matches!(
(class, name),
(ObjectClass::Form, "HIDE" | "LOAD" | "SHOW" | "UNLOAD")
| (ObjectClass::Screen, "HIDE" | "SHOW")
)
methods(class).contains(&name)
}
pub fn method_arity(class: ObjectClass, name: &str) -> Option<(usize, usize)> {
@@ -441,6 +473,22 @@ pub fn method_arity(class: ObjectClass, name: &str) -> Option<(usize, usize)> {
(ObjectClass::Form, "SHOW") => Some((0, 1)),
(ObjectClass::Form, "HIDE" | "LOAD" | "UNLOAD")
| (ObjectClass::Screen, "HIDE" | "SHOW") => Some((0, 0)),
(_, "REFRESH" | "SETFOCUS" | "CLS" | "PRINTFORM") => Some((0, 0)),
(_, "DRAG") => Some((1, 1)),
(_, "MOVE") => Some((2, 4)),
(ObjectClass::ListBox | ObjectClass::ComboBox, "ADDITEM") => Some((1, 2)),
(ObjectClass::ListBox | ObjectClass::ComboBox, "REMOVEITEM") => Some((1, 1)),
(ObjectClass::Form | ObjectClass::PictureBox, "PRINT") => Some((0, usize::MAX)),
(ObjectClass::Form | ObjectClass::PictureBox, "TEXTWIDTH" | "TEXTHEIGHT") => Some((1, 1)),
_ => None,
}
}
pub fn method_return_type(class: ObjectClass, name: &str) -> Option<PropertyType> {
match (class, name) {
(ObjectClass::Form | ObjectClass::PictureBox, "TEXTWIDTH" | "TEXTHEIGHT") => {
Some(PropertyType::Integer)
}
_ => None,
}
}
@@ -558,6 +606,16 @@ pub fn events(class: ObjectClass) -> &'static [&'static str] {
"KEYUP",
"LOSTFOCUS",
],
Spin => &[
"CUSTOM",
"DRAGDROP",
"DRAGOVER",
"GOTFOCUS",
"KEYDOWN",
"KEYPRESS",
"KEYUP",
"LOSTFOCUS",
],
PictureBox => &[
"CLICK",
"DBLCLICK",
@@ -608,6 +666,7 @@ pub fn event_params(event: &str) -> Option<&'static [(&'static str, EventParamTy
("STATE", Integer),
],
"UNLOAD" => &[("CANCEL", Integer)],
"CUSTOM" => &[("EVENTTYPE", Integer)],
"CLICK" | "DBLCLICK" | "CHANGE" | "DROPDOWN" | "GOTFOCUS" | "LOSTFOCUS" | "LOAD"
| "PAINT" | "RESIZE" | "TIMER" | "PATHCHANGE" | "PATTERNCHANGE" => &[],
_ => return None,
@@ -652,4 +711,18 @@ impl FormCatalog {
.find(|(_, o)| o.name.eq_ignore_ascii_case(name))
.map(|(i, o)| (i as u16, o))
}
pub fn belongs_to(&self, object: &FormObject, form: &str) -> bool {
let mut parent = object.parent_form.as_deref();
for _ in 0..self.objects.len() {
let Some(name) = parent else { return false };
if name.eq_ignore_ascii_case(form) {
return true;
}
parent = self
.find(name)
.and_then(|(_, object)| object.parent_form.as_deref());
}
false
}
}

View File

@@ -369,6 +369,13 @@ pub enum Builtin {
IsamSavepoint,
IsamSetmem,
IsamBof,
MsgBox,
InputBoxS,
ClipboardAdd,
ClipboardGet,
GraphicsLine,
GraphicsPaint,
GraphicsView,
}
#[derive(Debug, Clone)]
@@ -386,6 +393,20 @@ pub enum HExpr {
property: u16,
ty: HTy,
},
ObjectIndexedProperty {
object: u16,
object_index: Option<Box<HExpr>>,
property: u16,
index: Box<HExpr>,
ty: HTy,
},
ObjectMethodCall {
object: u16,
index: Option<Box<HExpr>>,
method: u16,
args: Vec<HExpr>,
ty: HTy,
},
DynamicObjectProperty {
object: Box<HExpr>,
property: String,
@@ -511,6 +532,13 @@ pub enum HStmtKind {
property: u16,
value: HExpr,
},
SetObjectIndexedProperty {
object: u16,
object_index: Option<HExpr>,
property: u16,
index: HExpr,
value: HExpr,
},
SetDynamicObjectProperty {
object: HExpr,
property: String,
@@ -518,6 +546,7 @@ pub enum HStmtKind {
},
ObjectMethod {
object: u16,
index: Option<HExpr>,
method: u16,
args: Vec<HExpr>,
},
@@ -592,6 +621,10 @@ pub enum HStmtKind {
targets: Vec<LabelId>,
},
ReturnGosub(Option<LabelId>),
Run {
target: Option<HExpr>,
string: bool,
},
/// Ereignis-Trap erklären (Sprachreferenz §8). `art` ist die Quelle
/// (0 KEY, 1 TIMER, 2 UEVENT, 3 SIGNAL), `index` ihre Kennung bzw. bei
/// `TIMER` das Intervall in Sekunden. `ziel = None` = `GOSUB 0`.

View File

@@ -750,8 +750,8 @@ mod tests {
assert_eq!(kinds("40000")[0], TokenKind::Num(NumValue::Long(40000)));
assert_eq!(kinds("1.5")[0], TokenKind::Num(NumValue::Single(1.5)));
assert_eq!(
kinds("3.14159265")[0],
TokenKind::Num(NumValue::Double(3.14159265))
kinds("1.23456789")[0],
TokenKind::Num(NumValue::Double(1.23456789))
);
assert_eq!(kinds("1E3")[0], TokenKind::Num(NumValue::Single(1000.0)));
assert_eq!(kinds("1D3")[0], TokenKind::Num(NumValue::Double(1000.0)));

View File

@@ -28,7 +28,18 @@ pub fn parse(module_name: &str, tokens: &[Token]) -> ParseOutput {
match p.k() {
TokenKind::Eof => break,
TokenKind::Kw(Kw::Sub) | TokenKind::Kw(Kw::Function) => {
if let Some(proc) = p.parse_proc() {
if let Some(proc) = p.parse_proc(false) {
procs.push(proc);
}
}
TokenKind::Kw(Kw::Static)
if matches!(
p.k_at(1),
TokenKind::Kw(Kw::Sub) | TokenKind::Kw(Kw::Function)
) =>
{
p.advance();
if let Some(proc) = p.parse_proc(true) {
procs.push(proc);
}
}
@@ -244,10 +255,7 @@ impl<'a> P<'a> {
self.advance();
self.parse_input(true, pos)
} else {
// Grafikform LINE (x1,y1)-(x2,y2): deklariertes Non-Feature.
self.err("Feature unavailable");
self.sync();
None
self.parse_graphics_line(pos)
}
}
TokenKind::Kw(Kw::If) => self.parse_if(pos),
@@ -350,12 +358,8 @@ impl<'a> P<'a> {
TokenKind::Kw(Kw::Erase) => {
self.advance();
let mut names = Vec::new();
loop {
if let Some(e) = self.parse_name_only() {
names.push(e);
} else {
break;
}
while let Some(e) = self.parse_name_only() {
names.push(e);
if !self.eat(&TokenKind::Comma) {
break;
}
@@ -548,12 +552,8 @@ impl<'a> P<'a> {
TokenKind::Kw(Kw::Read) => {
self.advance();
let mut vars = Vec::new();
loop {
if let Some(e) = self.parse_name_ref() {
vars.push(e);
} else {
break;
}
while let Some(e) = self.parse_name_ref() {
vars.push(e);
if !self.eat(&TokenKind::Comma) {
break;
}
@@ -733,6 +733,14 @@ impl<'a> P<'a> {
}
Some(Stmt::ViewPrint { top, bottom, pos })
}
TokenKind::Ident {
ref name,
suffix: None,
} if name == "VIEW" => self.parse_graphics_view(pos),
TokenKind::Ident {
ref name,
suffix: None,
} if name == "PAINT" => self.parse_graphics_paint(pos),
TokenKind::Ident {
ref name,
suffix: None,
@@ -832,6 +840,121 @@ impl<'a> P<'a> {
})
}
fn parse_graphics_point(&mut self) -> Option<(Expr, Expr)> {
if !self.eat(&TokenKind::LParen) {
self.err("Expected: (");
return None;
}
let x = self.parse_expr()?;
if !self.eat(&TokenKind::Comma) {
self.err("Expected: ,");
return None;
}
let y = self.parse_expr()?;
if !self.eat(&TokenKind::RParen) {
self.err("Expected: )");
return None;
}
Some((x, y))
}
fn parse_graphics_line(&mut self, pos: SourcePos) -> Option<Stmt> {
let from = if self.eat(&TokenKind::Minus) {
None
} else {
let point = self.parse_graphics_point()?;
if !self.eat(&TokenKind::Minus) {
self.err("Expected: -");
return None;
}
Some(point)
};
let relative = self.eat_kw(Kw::Step);
let to = self.parse_graphics_point()?;
let mut color = None;
let mut fill = 0;
if self.eat(&TokenKind::Comma) {
if !matches!(
self.k(),
TokenKind::Comma | TokenKind::Colon | TokenKind::Eol | TokenKind::Eof
) {
color = Some(self.parse_expr()?);
}
if self.eat(&TokenKind::Comma) {
if let TokenKind::Ident { name, .. } = self.k() {
fill = match name.as_str() {
"B" => 1,
"BF" => 2,
_ => {
self.err("Expected: B or BF");
return None;
}
};
self.advance();
}
}
}
Some(Stmt::GraphicsLine {
from,
to,
relative,
color,
fill,
pos,
})
}
fn parse_graphics_paint(&mut self, pos: SourcePos) -> Option<Stmt> {
self.advance();
let point = self.parse_graphics_point()?;
let paint = self
.eat(&TokenKind::Comma)
.then(|| self.parse_expr())
.flatten();
let border = self
.eat(&TokenKind::Comma)
.then(|| self.parse_expr())
.flatten();
Some(Stmt::GraphicsPaint {
point,
paint,
border,
pos,
})
}
fn parse_graphics_view(&mut self, pos: SourcePos) -> Option<Stmt> {
self.advance();
if self.at_stmt_end() {
return Some(Stmt::GraphicsView {
rect: None,
fill: None,
border: None,
pos,
});
}
let (x1, y1) = self.parse_graphics_point()?;
if !self.eat(&TokenKind::Minus) {
self.err("Expected: -");
return None;
}
let (x2, y2) = self.parse_graphics_point()?;
let fill = self
.eat(&TokenKind::Comma)
.then(|| self.parse_expr())
.flatten();
let border = self
.eat(&TokenKind::Comma)
.then(|| self.parse_expr())
.flatten();
Some(Stmt::GraphicsView {
rect: Some((x1, y1, x2, y2)),
fill,
border,
pos,
})
}
fn parse_print(&mut self, pos: SourcePos, printer: bool) -> Option<Stmt> {
self.advance(); // PRINT bzw. LPRINT
let file = if !printer && self.eat(&TokenKind::Hash) {
@@ -1056,12 +1179,8 @@ impl<'a> P<'a> {
}
}
let mut vars = Vec::new();
loop {
if let Some(e) = self.parse_name_ref() {
vars.push(e);
} else {
break;
}
while let Some(e) = self.parse_name_ref() {
vars.push(e);
if !self.eat(&TokenKind::Comma) {
break;
}
@@ -1384,11 +1503,8 @@ impl<'a> P<'a> {
return None;
};
let mut targets = Vec::new();
loop {
match self.parse_label_ref() {
Some(t) => targets.push(t),
None => break,
}
while let Some(t) = self.parse_label_ref() {
targets.push(t);
if !self.eat(&TokenKind::Comma) {
break;
}
@@ -1432,11 +1548,7 @@ impl<'a> P<'a> {
let dims = if self.eat(&TokenKind::LParen) {
let mut ds = Vec::new();
if !self.eat(&TokenKind::RParen) {
loop {
let a = match self.parse_expr() {
Some(e) => e,
None => break,
};
while let Some(a) = self.parse_expr() {
if self.eat_kw(Kw::To) {
match self.parse_expr() {
Some(b) => ds.push((Some(a), b)),
@@ -1602,44 +1714,42 @@ impl<'a> P<'a> {
}
};
let mut params = Vec::new();
if self.eat(&TokenKind::LParen) {
if !self.eat(&TokenKind::RParen) {
loop {
match self.k() {
TokenKind::Ident {
if self.eat(&TokenKind::LParen) && !self.eat(&TokenKind::RParen) {
loop {
match self.k() {
TokenKind::Ident {
name: pname,
suffix: psfx,
} => {
self.advance();
let array = if self.eat(&TokenKind::LParen) {
self.eat(&TokenKind::RParen);
true
} else {
false
};
let as_type = if self.eat_kw(Kw::As) {
self.parse_type_name()
} else {
None
};
params.push(Param {
name: pname,
suffix: psfx,
} => {
self.advance();
let array = if self.eat(&TokenKind::LParen) {
self.eat(&TokenKind::RParen);
true
} else {
false
};
let as_type = if self.eat_kw(Kw::As) {
self.parse_type_name()
} else {
None
};
params.push(Param {
name: pname,
suffix: psfx,
array,
as_type,
});
}
_ => {
self.err("Expected: identifier");
break;
}
array,
as_type,
});
}
if !self.eat(&TokenKind::Comma) {
_ => {
self.err("Expected: identifier");
break;
}
}
self.eat(&TokenKind::RParen);
if !self.eat(&TokenKind::Comma) {
break;
}
}
self.eat(&TokenKind::RParen);
}
Some(ProcSig {
kind,
@@ -1649,10 +1759,10 @@ impl<'a> P<'a> {
})
}
fn parse_proc(&mut self) -> Option<Proc> {
fn parse_proc(&mut self, leading_static: bool) -> Option<Proc> {
let pos = self.pos();
let sig = self.parse_proc_sig()?;
let is_static = self.eat_kw(Kw::Static);
let is_static = leading_static || self.eat_kw(Kw::Static);
let end_kw = match sig.kind {
ProcKind::Sub => Kw::Sub,
ProcKind::Function => Kw::Function,
@@ -1697,33 +1807,31 @@ impl<'a> P<'a> {
}
};
let mut params = Vec::new();
if self.eat(&TokenKind::LParen) {
if !self.eat(&TokenKind::RParen) {
loop {
match self.k() {
TokenKind::Ident {
if self.eat(&TokenKind::LParen) && !self.eat(&TokenKind::RParen) {
loop {
match self.k() {
TokenKind::Ident {
name: pname,
suffix: psfx,
} => {
self.advance();
params.push(Param {
name: pname,
suffix: psfx,
} => {
self.advance();
params.push(Param {
name: pname,
suffix: psfx,
array: false,
as_type: None,
});
}
_ => {
self.err("Expected: identifier");
break;
}
array: false,
as_type: None,
});
}
if !self.eat(&TokenKind::Comma) {
_ => {
self.err("Expected: identifier");
break;
}
}
self.eat(&TokenKind::RParen);
if !self.eat(&TokenKind::Comma) {
break;
}
}
self.eat(&TokenKind::RParen);
}
if self.eat(&TokenKind::Eq) {
let body = self.parse_expr()?;
@@ -1818,8 +1926,8 @@ impl<'a> P<'a> {
other => Expr::Paren(Box::new(other)),
})
.collect();
if !self.at_stmt_end() && call_args.is_empty() {
call_args = self.parse_arg_list_to_stmt_end();
if !self.at_stmt_end() && (call_args.is_empty() || name.contains('.')) {
call_args.extend(self.parse_arg_list_to_stmt_end());
}
Some(Stmt::Call {
name,
@@ -1842,7 +1950,7 @@ impl<'a> P<'a> {
match self.k() {
TokenKind::Ident { mut name, suffix } => {
self.advance();
let args = if self.eat(&TokenKind::LParen) {
let mut args = if self.eat(&TokenKind::LParen) {
let a = self.parse_arg_list(&TokenKind::RParen);
self.eat(&TokenKind::RParen);
Some(a)
@@ -1858,6 +1966,11 @@ impl<'a> P<'a> {
name.push('.');
name.push_str(&member);
self.advance();
if self.eat(&TokenKind::LParen) {
let member_args = self.parse_arg_list(&TokenKind::RParen);
self.eat(&TokenKind::RParen);
args.get_or_insert_with(Vec::new).extend(member_args);
}
}
_ => self.err("Expected: property"),
}
@@ -1935,6 +2048,7 @@ impl<'a> P<'a> {
args.push(Expr::Missing);
continue;
}
self.eat(&TokenKind::Hash);
match self.parse_expr() {
Some(e) => args.push(e),
None => break,
@@ -1985,11 +2099,7 @@ impl<'a> P<'a> {
fn parse_bin(&mut self, min_prec: u8) -> Option<Expr> {
let mut lhs = self.parse_prefix()?;
loop {
let (prec, op) = match self.infix_op() {
Some(x) => x,
None => break,
};
while let Some((prec, op)) = self.infix_op() {
if prec < min_prec {
break;
}

File diff suppressed because it is too large Load Diff

View File

@@ -131,12 +131,12 @@ fn stand() -> Stand {
let unsup_fn = abschnitt(
&q,
"// --- Spätere Phasen: dokumentiert",
"(HExpr::Unsupported(name)",
"(HExpr::Unsupported(\"Funktion\")",
);
let unsup_stmt = abschnitt(
&q,
"// Bildschirm-/Datei-/System-Anweisungen späterer Phasen.",
r#"_ => "Anweisung""#,
r#"HStmtKind::Unsupported("Anweisung")"#,
);
let mut signaturen = literale(fn_tab);
@@ -470,7 +470,6 @@ fn dokumentierte_elemente_werden_namentlich_abgewiesen() {
("Y = POINT(1, 2)", "POINT", "Feature unavailable"),
("CALLS Foo", "CALLS", "Feature unavailable"),
// Non-Feature über die Syntax (teilt das Schlüsselwort)
("LINE (1, 1)-(2, 2)", "LINE", "Feature unavailable"),
(
"GET (1, 1)-(2, 2), A",
"GET (Grafik)",

View File

@@ -63,6 +63,7 @@ pub struct RtState {
rng: u32,
rnd_last: f32,
pub command: String,
pub clipboard: String,
}
impl Default for RtState {
@@ -93,6 +94,7 @@ impl Default for RtState {
rng: 0x50000,
rnd_last: 0.0,
command: String::new(),
clipboard: String::new(),
}
}
}
@@ -438,7 +440,14 @@ pub mod ids {
pub const ISAM_BOF: u16 = 147;
/// `SetUEvent` — loest das benutzerdefinierte Ereignis aus (§8).
pub const SETUEVENT: u16 = 148;
pub const COUNT: u16 = 149;
pub const MSGBOX: u16 = 149;
pub const INPUTBOX_S: u16 = 150;
pub const CLIPBOARD_ADD: u16 = 151;
pub const CLIPBOARD_GET: u16 = 152;
pub const GRAPHICS_LINE: u16 = 153;
pub const GRAPHICS_PAINT: u16 = 154;
pub const GRAPHICS_VIEW: u16 = 155;
pub const COUNT: u16 = 156;
}
/// Dispatch-Tabelle in Index-Reihenfolge.
@@ -594,11 +603,35 @@ pub fn builtin_table() -> &'static [BuiltinFn] {
bi_isam_setmem,
bi_isam_bof,
bi_setuevent,
bi_msgbox,
bi_inputbox,
bi_clipboard_add,
bi_clipboard_get,
bi_graphics_line,
bi_graphics_paint,
bi_graphics_view,
];
debug_assert_eq!(TABLE.len(), ids::COUNT as usize);
TABLE
}
fn bi_clipboard_add(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
st.clipboard.push_str(&arg_str(a, 0)?);
Ok(None)
}
fn bi_clipboard_get(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
Ok(Some(Value::Str(Rc::from(st.clipboard.as_str()))))
}
// ---- Argument-Hilfen --------------------------------------------------------
fn arg_str(args: &[Value], i: usize) -> Result<Rc<str>, RuntimeError> {
@@ -1881,19 +1914,92 @@ fn bi_view_print(
}
fn bi_screen_stmt(
_: &mut RtState,
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// `SCREEN modus[, farbschalter[, aktiv[, sichtbar]]]`. Nur der Textmodus 0
// existiert; Grafikmodi sind deklariertes Non-Feature. Die übrigen
// Argumente betreffen Grafikseiten und bleiben folgenlos.
// Die Terminaldarstellung bildet die historischen Pixelmodi auf Zellen ab;
// Seitenargumente haben im einzelnen Puffer keine zusätzliche Wirkung.
match opt(a, 0)? {
None | Some(0) => Ok(None),
Some(_) => Err(RuntimeError(73)), // Advanced feature unavailable
None | Some(0..=13) => {
st.screen.graphics_view(None).unwrap();
st.screen.cls();
Ok(None)
}
Some(_) => Err(RuntimeError::ILLEGAL_FUNCTION_CALL),
}
}
fn bi_graphics_line(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let mut from = (arg_i32(a, 0)?, arg_i32(a, 1)?);
if from.0 == i32::MIN {
from = st.screen.graphics_position();
}
let mut to = (arg_i32(a, 2)?, arg_i32(a, 3)?);
if arg_i32(a, 5)? != 0 {
to.0 += from.0;
to.1 += from.1;
}
let color = match arg_i32(a, 4)? {
-1 => i32::from(st.screen.fg),
color @ 0..=15 => color,
_ => return Err(RuntimeError::ILLEGAL_FUNCTION_CALL),
};
st.screen
.graphics_line(from, to, color, arg_i32(a, 6)? as u8);
Ok(None)
}
fn bi_graphics_paint(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let color = match a.get(2) {
Some(Value::Int(v)) => i32::from(*v),
Some(Value::Lng(v)) => *v,
Some(Value::Str(_)) | None => i32::from(st.screen.fg),
_ => return Err(RuntimeError::TYPE_MISMATCH),
};
if !(0..=15).contains(&color) {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
st.screen
.graphics_paint(arg_i32(a, 0)?, arg_i32(a, 1)?, color);
Ok(None)
}
fn bi_graphics_view(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let x1 = arg_i32(a, 0)?;
if x1 < 0 {
st.screen.graphics_view(None).unwrap();
return Ok(None);
}
let view = (x1, arg_i32(a, 1)?, arg_i32(a, 2)?, arg_i32(a, 3)?);
st.screen
.graphics_view(Some(view))
.map_err(|_| RuntimeError::ILLEGAL_FUNCTION_CALL)?;
let fill = arg_i32(a, 4)?;
if fill >= 0 {
st.screen
.graphics_line((view.0, view.1), (view.2, view.3), fill, 2);
}
let border = arg_i32(a, 5)?;
if border >= 0 {
st.screen
.graphics_line((view.0, view.1), (view.2, view.3), border, 1);
}
Ok(None)
}
/// Nummer eines Funktionstasten-Makros → Index 011.
fn key_index(n: i32) -> Option<usize> {
match n {
@@ -2016,6 +2122,14 @@ fn bi_input_s(
if n < 0 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
if a.len() > 1 {
let nummer = arg_i32(a, 1)?;
let datei = st.dateien.get(nummer)?;
let bytes = datei.bytes_lesen(datei.position, n as usize)?;
return Ok(Some(Value::Str(Rc::from(
String::from_utf8_lossy(&bytes).as_ref(),
))));
}
// Blockierend, ohne Echo auf dem Bildschirm.
let mut s = String::new();
for _ in 0..n {
@@ -2546,6 +2660,22 @@ fn bi_isam_bof(
Ok(Some(Value::Int(if st.isam.bof(n) { -1 } else { 0 })))
}
fn bi_msgbox(
_: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
Err(RuntimeError::FEATURE_UNAVAILABLE)
}
fn bi_inputbox(
_: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
Err(RuntimeError::FEATURE_UNAVAILABLE)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -2635,7 +2765,7 @@ mod tests {
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));
assert!(h.next_event(false).is_none());
}
#[test]

View File

@@ -24,6 +24,9 @@ use unicode_width::UnicodeWidthChar;
pub const MIN_COLS: usize = 80;
pub const MIN_ROWS: usize = 25;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScreenError;
/// Eine Bildschirmzelle: Zeichen plus klassisches Farbattribut.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cell {
@@ -78,6 +81,9 @@ pub struct TextScreen {
/// Seit der letzten Anzeige verändert? Der Host wird nur dann zum
/// Neuzeichnen aufgefordert.
veraendert: bool,
graphic_x: i32,
graphic_y: i32,
graphic_view: Option<(i32, i32, i32, i32)>,
}
impl Default for TextScreen {
@@ -110,6 +116,9 @@ impl TextScreen {
view_full: true,
belegt: vec![None; rows],
veraendert: true,
graphic_x: 0,
graphic_y: 0,
graphic_view: None,
}
}
@@ -146,8 +155,8 @@ impl TextScreen {
}
self.cells = cells;
let mut belegt = vec![None; rows];
for r in 0..self.rows.min(rows) {
belegt[r] = self.belegt[r].map(|c| c.min(cols - 1));
for (r, value) in belegt.iter_mut().enumerate().take(self.rows.min(rows)) {
*value = self.belegt[r].map(|c| c.min(cols - 1));
}
self.belegt = belegt;
self.cols = cols;
@@ -225,9 +234,9 @@ impl TextScreen {
}
/// `LOCATE zeile, spalte` (1-basiert); außerhalb → Err (Fehler 5).
pub fn locate(&mut self, row: usize, col: usize) -> Result<(), ()> {
pub fn locate(&mut self, row: usize, col: usize) -> Result<(), ScreenError> {
if row < 1 || row > self.rows || col < 1 || col > self.cols {
return Err(());
return Err(ScreenError);
}
self.cur_row = row - 1;
// Auf eine Fortsetzungszelle zu zeigen bedeutet den Zeichenanfang.
@@ -251,9 +260,9 @@ impl TextScreen {
}
/// `VIEW PRINT oben TO unten` (1-basiert).
pub fn view_print(&mut self, top: usize, bottom: usize) -> Result<(), ()> {
pub fn view_print(&mut self, top: usize, bottom: usize) -> Result<(), ScreenError> {
if top < 1 || bottom > self.rows || top > bottom {
return Err(());
return Err(ScreenError);
}
self.view_top = top - 1;
self.view_bottom = bottom - 1;
@@ -265,6 +274,122 @@ impl TextScreen {
self.cells[(row - 1) * self.cols + (col - 1)]
}
pub fn graphics_position(&self) -> (i32, i32) {
(self.graphic_x, self.graphic_y)
}
pub fn graphics_view(&mut self, view: Option<(i32, i32, i32, i32)>) -> Result<(), ScreenError> {
if view.is_some_and(|(x1, y1, x2, y2)| x1 < 0 || y1 < 0 || x1 > x2 || y1 > y2) {
return Err(ScreenError);
}
self.graphic_view = view;
Ok(())
}
fn graphics_cell(&mut self, x: i32, y: i32, color: i32) {
if x < 0
|| y < 0
|| self
.graphic_view
.is_some_and(|(x1, y1, x2, y2)| x < x1 || x > x2 || y < y1 || y > y2)
{
return;
}
let (col, row) = (x as usize / 8, y as usize / 8);
if col >= self.cols || row >= self.rows {
return;
}
self.cells[row * self.cols + col] = Cell {
ch: if color == 0 { ' ' } else { '█' },
fg: color.clamp(0, 15) as u8,
bg: self.bg,
fortsetzung: false,
};
self.belegt[row] = Some(self.belegt[row].map_or(col, |last| last.max(col)));
self.veraendert = true;
}
pub fn graphics_line(&mut self, from: (i32, i32), to: (i32, i32), color: i32, fill: u8) {
let (x1, y1) = from;
let (x2, y2) = to;
if fill == 2 {
for y in (y1.min(y2)..=y1.max(y2)).step_by(8) {
for x in (x1.min(x2)..=x1.max(x2)).step_by(8) {
self.graphics_cell(x, y, color);
}
}
} else if fill == 1 {
self.graphics_segment(x1, y1, x2, y1, color);
self.graphics_segment(x2, y1, x2, y2, color);
self.graphics_segment(x2, y2, x1, y2, color);
self.graphics_segment(x1, y2, x1, y1, color);
} else {
self.graphics_segment(x1, y1, x2, y2, color);
}
self.graphic_x = x2;
self.graphic_y = y2;
}
fn graphics_segment(&mut self, mut x: i32, mut y: i32, x2: i32, y2: i32, color: i32) {
let dx = (x2 - x).abs();
let sx = if x < x2 { 1 } else { -1 };
let dy = -(y2 - y).abs();
let sy = if y < y2 { 1 } else { -1 };
let mut error = dx + dy;
loop {
self.graphics_cell(x, y, color);
if x == x2 && y == y2 {
break;
}
let twice = error * 2;
if twice >= dy {
error += dy;
x += sx;
}
if twice <= dx {
error += dx;
y += sy;
}
}
}
pub fn graphics_paint(&mut self, x: i32, y: i32, color: i32) {
let (start_col, start_row) = (x.max(0) as usize / 8, y.max(0) as usize / 8);
if start_col >= self.cols || start_row >= self.rows {
return;
}
let target = self.cells[start_row * self.cols + start_col].ch;
let replacement = if color == 0 { ' ' } else { '█' };
if target == replacement {
return;
}
let mut pending = vec![(start_col, start_row)];
while let Some((col, row)) = pending.pop() {
let px = col as i32 * 8;
let py = row as i32 * 8;
if col >= self.cols
|| row >= self.rows
|| self.cells[row * self.cols + col].ch != target
|| self
.graphic_view
.is_some_and(|(x1, y1, x2, y2)| px < x1 || px > x2 || py < y1 || py > y2)
{
continue;
}
self.graphics_cell(px, py, color);
if col > 0 {
pending.push((col - 1, row));
}
if row > 0 {
pending.push((col, row - 1));
}
pending.push((col + 1, row));
pending.push((col, row + 1));
}
self.graphic_x = x;
self.graphic_y = y;
}
/// Text an der Cursorposition ausgeben: Umbruch am rechten Rand,
/// Scrollen am unteren Rand des Scrollbereichs. `\n` bricht um,
/// `\r` setzt an den Zeilenanfang.

View File

@@ -316,7 +316,7 @@ fn tausender_gruppieren(s: &str) -> String {
let z: Vec<char> = s.chars().collect();
let mut out = String::new();
for (i, c) in z.iter().enumerate() {
if i > 0 && (z.len() - i) % 3 == 0 {
if i > 0 && (z.len() - i).is_multiple_of(3) {
out.push(',');
}
out.push(*c);
@@ -433,7 +433,7 @@ mod tests {
#[test]
fn numerisches_feld_mit_nachkommastellen() {
assert_eq!(u("###.##", &[z(3.14159)]), " 3.14");
assert_eq!(u("###.##", &[z(3.126)]), " 3.13");
assert_eq!(u("##.##", &[z(-1.5)]), "-1.50");
}

View File

@@ -199,9 +199,7 @@ pub fn cur_to_i64(c: i64) -> i64 {
// r in 0..10000; runde halb-zu-gerade
if r > 5_000 {
q + 1
} else if r < 5_000 {
q
} else if q % 2 == 0 {
} else if r < 5_000 || q % 2 == 0 {
q
} else {
q + 1

File diff suppressed because it is too large Load Diff

View File

@@ -3,9 +3,11 @@
use std::collections::{BTreeMap, HashSet};
use std::fmt;
use tb_frontend::forms::{self, ObjectClass, PropertyDefault, PropertySpec, PropertyType};
use tb_frontend::forms::{
self, FormCatalog, ObjectClass, PropertyDefault, PropertySpec, PropertyType,
};
use crate::forms::PropertyValue;
use crate::forms::{FormsModel, PropertyValue};
#[derive(Debug, Clone, PartialEq)]
pub struct FormNode {
@@ -46,6 +48,67 @@ impl FormFile {
original: None,
}
}
pub fn catalog(&self) -> FormCatalog {
fn add(node: &FormNode, parent: Option<&str>, catalog: &mut FormCatalog) {
if let Some((id, _)) = catalog.find(&node.name) {
catalog.objects[id as usize].array = true;
} else {
let array = forms::property(node.class, "INDEX")
.and_then(|(property, _)| node.properties.get(&property))
.is_some_and(
|value| matches!(value, PropertyValue::Integer(index) if *index != 0),
);
catalog.add(&node.name, node.class, parent, array);
}
for child in &node.children {
add(child, Some(&node.name), catalog);
}
}
let mut catalog = FormCatalog::default();
add(&self.root, None, &mut catalog);
catalog
}
pub fn apply(&self, model: &mut FormsModel) -> Result<(), tb_runtime::errors::RuntimeError> {
fn apply_node(
node: &FormNode,
model: &mut FormsModel,
menu_depth: usize,
) -> Result<(), tb_runtime::errors::RuntimeError> {
let menu_depth = if node.class == ObjectClass::Menu {
menu_depth + 1
} else {
menu_depth
};
if menu_depth > 6 {
return Err(tb_runtime::errors::RuntimeError::ILLEGAL_FUNCTION_CALL);
}
let object = model
.objects
.iter()
.position(|candidate| candidate.description.name.eq_ignore_ascii_case(&node.name))
.ok_or(tb_runtime::errors::RuntimeError(420))? as u16;
let index = forms::property(node.class, "INDEX")
.and_then(|(property, _)| node.properties.get(&property))
.and_then(|value| match value {
PropertyValue::Integer(value) => Some(*value),
_ => None,
})
.unwrap_or(0);
if index != 0 && !model.is_loaded_at(object, Some(index)) {
model.load_design_array(object, index)?;
}
for (property, value) in &node.properties {
model.set_initial_at(object, Some(index), *property, value.clone())?;
}
for child in &node.children {
apply_node(child, model, menu_depth)?;
}
Ok(())
}
apply_node(&self.root, model, 0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -444,7 +507,7 @@ fn read_symbol_table(bytes: &[u8], mut at: usize) -> Result<Vec<BinarySymbol>, u
let reference = read_u16(bytes, at).ok_or(at)?;
let class_id = bytes[at + 2] & 0x7f;
let length = bytes[at + 3] as usize;
let class = ObjectClass::from_id(class_id).ok_or(at + 2)?;
let mut class = ObjectClass::from_id(class_id).ok_or(at + 2)?;
if length == 0 || length > 40 || at + 4 + length > bytes.len() {
return Err(at + 3);
}
@@ -459,12 +522,18 @@ fn read_symbol_table(bytes: &[u8], mut at: usize) -> Result<Vec<BinarySymbol>, u
if symbols.is_empty() && class != ObjectClass::Form {
return Err(at + 2);
}
let name = String::from_utf8_lossy(name).into_owned();
if class == ObjectClass::Screen
&& matches!(name.to_ascii_uppercase().as_str(), "VSPIN" | "HSPIN")
{
class = ObjectClass::Spin;
}
symbols.push(BinarySymbol {
offset: at,
class,
name: String::from_utf8_lossy(name).into_owned(),
name,
is_array: bytes[at + 2] & 0x80 != 0,
unsupported: class_id == ObjectClass::Screen.id(),
unsupported: class_id == ObjectClass::Screen.id() && class != ObjectClass::Spin,
});
at += 4 + length;
if reference == 0 {
@@ -501,7 +570,7 @@ fn record_len(class: ObjectClass) -> usize {
ObjectClass::PictureBox => 31,
ObjectClass::TextBox => 34,
ObjectClass::Menu | ObjectClass::Frame => 26,
ObjectClass::Screen => 80,
ObjectClass::Screen | ObjectClass::Spin => 80,
_ => 28,
}
}
@@ -522,7 +591,7 @@ fn record_lengths(class: ObjectClass) -> &'static [usize] {
ObjectClass::OptionButton => &[21, 28],
ObjectClass::PictureBox => &[31],
ObjectClass::HScrollBar | ObjectClass::VScrollBar => &[32],
ObjectClass::Screen => &[73, 80],
ObjectClass::Screen | ObjectClass::Spin => &[73, 80],
ObjectClass::Timer => &[21, 28],
}
}
@@ -627,7 +696,12 @@ fn walk_object_records(
let next_symbol = bytes[header] as usize;
if next_symbol >= symbols.len()
|| bytes[header + 2] != 0
|| bytes[header + 1] & 0x7f != symbols[next_symbol].class.id()
|| bytes[header + 1] & 0x7f
!= if symbols[next_symbol].class == ObjectClass::Spin {
ObjectClass::Screen.id()
} else {
symbols[next_symbol].class.id()
}
{
continue;
}
@@ -978,7 +1052,7 @@ fn assign_strings(
) -> Result<Vec<BinaryWarning>, FormError> {
let mut warnings = records
.iter()
.filter(|record| symbols[record.symbol].class == ObjectClass::Screen)
.filter(|record| symbols[record.symbol].unsupported)
.map(|record| BinaryWarning {
offset: 0x20 + record.start,
name: format!("{}.CustomControl", symbols[record.symbol].name),
@@ -1063,10 +1137,9 @@ fn assign_strings(
)?;
}
}
for record in records
.iter()
.filter(|record| symbols[record.symbol].unsupported)
{
for record in records.iter().filter(|record| {
symbols[record.symbol].unsupported || symbols[record.symbol].class == ObjectClass::Spin
}) {
for offset in [17, 28, 37, 39, 53, 55, 64, 69] {
let Some(pointer) =
record_u16(bytes, record.start, record.end, offset).filter(|pointer| *pointer != 0)
@@ -1319,6 +1392,35 @@ mod tests {
assert_eq!(write_text(&form), EXAMPLE);
}
#[test]
fn text_form_control_arrays_share_one_catalog_object_and_keep_indices() {
let form = read_text(
"array.frm",
"VERSION 1.00\nBegin Form Form1\n Begin CommandButton Command1\n Index = 0\n Caption = \"null\"\n End\n Begin CommandButton Command1\n Index = 1\n Caption = \"eins\"\n End\nEnd\n",
)
.unwrap();
let catalog = form.catalog();
assert_eq!(catalog.objects.len(), 2);
assert!(catalog.objects[1].array);
let mut model = FormsModel::new(catalog.objects, 80, 25);
form.apply(&mut model).unwrap();
let caption = forms::property(ObjectClass::CommandButton, "CAPTION")
.unwrap()
.0;
assert_eq!(
model.get_at(1, Some(0), caption).unwrap(),
PropertyValue::String("null".into())
);
assert_eq!(
model.get_at(1, Some(1), caption).unwrap(),
PropertyValue::String("eins".into())
);
model.show(0, false).unwrap();
model.events.clear();
model.object_method(1, "SETFOCUS", vec![]).unwrap();
assert_eq!(model.next_event().unwrap().array_index, Some(0));
}
#[test]
fn rejects_version_class_property_value_and_open_block_with_location() {
let cases = [
@@ -1502,6 +1604,18 @@ mod tests {
assert_eq!(failure.name, "New.HEIGHT");
}
#[test]
fn spin_symbol_wird_als_bedienbares_control_erkannt() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&[1, 0, ObjectClass::Form.id(), 5]);
bytes.extend_from_slice(b"Form1");
bytes.extend_from_slice(&[0, 0, ObjectClass::Screen.id(), 5]);
bytes.extend_from_slice(b"VSpin");
let symbols = read_symbol_table(&bytes, 0).unwrap();
assert_eq!(symbols[1].class, ObjectClass::Spin);
assert!(!symbols[1].unsupported);
}
#[test]
fn unsupported_custom_control_is_named_and_not_emitted_as_screen() {
let symbols = vec![
@@ -1515,7 +1629,7 @@ mod tests {
BinarySymbol {
offset: 0,
class: ObjectClass::Screen,
name: "VSpin".into(),
name: "Custom1".into(),
is_array: false,
unsupported: true,
},
@@ -1569,7 +1683,7 @@ mod tests {
let root = build_tree(decoded).unwrap();
assert_eq!(warnings[0].offset, 0x46);
assert_eq!(warnings[0].name, "VSpin.CustomControl");
assert_eq!(warnings[0].name, "Custom1.CustomControl");
assert!(root.children.is_empty());
let mut strings_with_orphan = (100..=108)

View File

@@ -1317,7 +1317,7 @@ fn token_def(code: u16) -> Option<TokenDef> {
}),
0x138 => Some(TokenDef {
len: 0,
rules: &["expr::=MKS$({0})"],
rules: &["expr::=MKS$({expr:0})", "{0}"],
}),
0x139 => Some(TokenDef {
len: 0,
@@ -1682,7 +1682,7 @@ fn token_def(code: u16) -> Option<TokenDef> {
}),
0x1b2 => Some(TokenDef {
len: 0,
rules: &["expr::=MSGBOX({2}, {1}, {0})"],
rules: &["expr::=STR$({expr:0})"],
}),
0x1b3 => Some(TokenDef {
len: 0,
@@ -2421,6 +2421,23 @@ impl<'a> Decoder<'a> {
.ok_or_else(|| self.fail(self.at, "P-Code", "Tokendaten abgeschnitten"))?
.to_owned();
self.at += (len + 1) & !1;
if matches!(self.pcode, 0x065 | 0x066) {
let operands = usize::from(self.pcode == 0x066);
let split = self.stack.len().saturating_sub(operands);
if split > 0
&& self.stack[split - 1].kind != "newline"
&& !self.stack[split - 1].text.ends_with(" THEN ")
&& !self.stack[split - 1].text.ends_with(" ELSE ")
{
self.stack.insert(
split,
Item {
kind: "newline".into(),
text: "\r\u{1}".into(),
},
);
}
}
if matches!(self.pcode, 0x1c5 | 0x1c6) {
let first = self
.stack
@@ -2634,6 +2651,26 @@ mod tests {
assert_eq!(decoder.array(&array, false).unwrap(), "Oldcontents()");
}
#[test]
fn next_trennt_eine_vorangehende_anweisung_auch_ohne_newline_token() {
let (sym, ids) = symbols(&["i"]);
let mut code = Vec::new();
code.extend_from_slice(&0u16.to_le_bytes());
code.extend_from_slice(&0x00au16.to_le_bytes());
code.extend_from_slice(&9u16.to_le_bytes());
code.extend_from_slice(&[0, 0]);
code.extend_from_slice(b"PRINT 1");
code.push(0);
code.extend_from_slice(&(0x0400u16 | 0x00b).to_le_bytes());
code.extend_from_slice(&ids[0].to_le_bytes());
code.extend_from_slice(&0x066u16.to_le_bytes());
code.extend_from_slice(&[0xff, 0xff, 1, 0]);
code.extend_from_slice(&0u16.to_le_bytes());
code.extend_from_slice(&8u16.to_le_bytes());
let (decoded, _) = Decoder::new("test.frm", &sym, &code, 0).run().unwrap();
assert!(decoded.contains("PRINT 1\nNEXT i%\n"), "{decoded:?}");
}
#[test]
fn decodes_defint_and_vbdos_isam_tokens() {
let mut decoder = Decoder::new("test.frm", &[], &[], 0);

View File

@@ -302,6 +302,7 @@ instrs! {
0xA4 RetGosub;
0xA5 RetGosubTo(a: u32);
0xA6 OnJump(a: u16, b: bool); // Sprungtabelle, gosub?
0xA7 Run(a: u8); // 0 = ohne Ziel, 1 = Zeile, 2 = Datei
// 0xB0 — Prozeduren und Builtins
0xB0 Call(a: u16, b: u8);
@@ -316,6 +317,9 @@ instrs! {
0xB9 ObjectLoad(a: u16, b: bool, c: bool); // unload?, Index liegt auf Stack?
0xBA LoadDynamicObjectProperty(a: u16);
0xBB StoreDynamicObjectProperty(a: u16);
0xBC LoadObjectIndexedProperty(a: u16, b: u16);
0xBD ObjectMethodFn(a: u16, b: u16, c: u8);
0xBE StoreObjectIndexedProperty(a: u16, b: u16);
// 0xE0 — Ereignis-Traps (Sprachreferenz §8); Kennung vom Stack
0xE0 TrapDefine(a: u8, b: u32); // Quellenart, Sprungziel

View File

@@ -20,6 +20,10 @@ pub fn compile(hir: &HirModule) -> CompiledModule {
string_ids: HashMap::new(),
jump_tables: Vec::new(),
modul_label_pc: Vec::new(),
initialize_main: hir
.objects
.iter()
.any(|object| object.class == tb_frontend::forms::ObjectClass::Form),
};
let mut procs = Vec::new();
for proc in &hir.procs {
@@ -29,7 +33,7 @@ pub fn compile(hir: &HirModule) -> CompiledModule {
name: hir.name.clone(),
option_base: hir.option_base,
strings: cg.strings,
globals_init: hir.globals.iter().map(|g| slot_init(g)).collect(),
globals_init: hir.globals.iter().map(slot_init).collect(),
global_names: hir.globals.iter().map(|g| g.name.clone()).collect(),
udts: hir
.udts
@@ -85,6 +89,9 @@ struct Codegen {
/// `ON ERROR GOTO` aus einer Prozedur zeigt dorthin; da Prozedur 0
/// zuerst übersetzt wird, stehen die Positionen rechtzeitig fest.
modul_label_pc: Vec<Option<u32>>,
/// Formulare müssen erst nach den globalen DIM-Anweisungen Ereignisse
/// zustellen; reine Textprogramme behalten ihre bisherigen Grenzen.
initialize_main: bool,
}
struct ProcCtx {
@@ -160,7 +167,30 @@ impl Codegen {
fixups: Vec::new(),
table_fixups: Vec::new(),
};
if proc.kind == hir::HProcKind::Main {
let declarations = proc
.body
.iter()
.filter(|stmt| matches!(stmt.kind, HStmtKind::Dim { redim: false, .. }))
.collect::<Vec<_>>();
for stmt in declarations {
self.stmt_inner(&mut ctx, proc, stmt, false);
}
if self.initialize_main
|| proc
.body
.iter()
.any(|stmt| matches!(stmt.kind, HStmtKind::Dim { redim: false, .. }))
{
ctx.emit(Instr::Stmt(0));
}
}
for stmt in &proc.body {
if proc.kind == hir::HProcKind::Main
&& matches!(stmt.kind, HStmtKind::Dim { redim: false, .. })
{
continue;
}
self.stmt(&mut ctx, proc, stmt);
}
// Rumpfende
@@ -234,12 +264,17 @@ impl Codegen {
// ---- Anweisungen -------------------------------------------------------
fn stmt(&mut self, ctx: &mut ProcCtx, proc: &hir::HProc, stmt: &HStmt) {
self.stmt_inner(ctx, proc, stmt, true);
}
fn stmt_inner(&mut self, ctx: &mut ProcCtx, proc: &hir::HProc, stmt: &HStmt, boundary: bool) {
match &stmt.kind {
HStmtKind::Label(l) => {
ctx.bind(*l);
return;
}
_ => ctx.emit(Instr::Stmt(stmt.line)),
_ if boundary => ctx.emit(Instr::Stmt(stmt.line)),
_ => {}
}
match &stmt.kind {
HStmtKind::Label(_) => unreachable!(),
@@ -263,6 +298,23 @@ impl Codegen {
index.is_some(),
));
}
HStmtKind::SetObjectIndexedProperty {
object,
object_index,
property,
index,
value,
} => {
if let Some(object_index) = object_index {
self.expr(ctx, object_index);
}
self.expr(ctx, index);
self.expr(ctx, value);
ctx.emit(Instr::StoreObjectIndexedProperty(
*object,
*property | if object_index.is_some() { 0x8000 } else { 0 },
));
}
HStmtKind::SetDynamicObjectProperty {
object,
property,
@@ -275,13 +327,21 @@ impl Codegen {
}
HStmtKind::ObjectMethod {
object,
index,
method,
args,
} => {
if let Some(index) = index {
self.expr(ctx, index);
}
for arg in args {
self.expr(ctx, arg);
}
ctx.emit(Instr::ObjectMethod(*object, *method, args.len() as u8));
ctx.emit(Instr::ObjectMethod(
*object,
*method,
args.len() as u8 | if index.is_some() { 0x80 } else { 0 },
));
}
HStmtKind::ObjectLoad {
object,
@@ -509,6 +569,18 @@ impl Codegen {
None => ctx.emit(Instr::RetGosub),
Some(l) => ctx.emit_jump(Instr::RetGosubTo(0), *l),
},
HStmtKind::Run { target, string } => {
if let Some(target) = target {
self.expr(ctx, target);
}
ctx.emit(Instr::Run(if target.is_none() {
0
} else if *string {
2
} else {
1
}));
}
HStmtKind::ExitProc => self.emit_proc_exit(ctx, proc),
HStmtKind::CallSub { proc: id, args } => {
for a in args {
@@ -533,6 +605,9 @@ impl Codegen {
}
let id = builtin_id(*b);
ctx.emit(Instr::CallBuiltin(id, args.len() as u8));
if matches!(b, Builtin::MsgBox) {
ctx.emit(Instr::Pop);
}
if builtin_returns_value(*b) {
ctx.emit(Instr::Pop);
}
@@ -725,6 +800,41 @@ impl Codegen {
index.is_some(),
));
}
HExpr::ObjectIndexedProperty {
object,
object_index,
property,
index,
..
} => {
if let Some(object_index) = object_index {
self.expr(ctx, object_index);
}
self.expr(ctx, index);
ctx.emit(Instr::LoadObjectIndexedProperty(
*object,
*property | if object_index.is_some() { 0x8000 } else { 0 },
));
}
HExpr::ObjectMethodCall {
object,
index,
method,
args,
..
} => {
if let Some(index) = index {
self.expr(ctx, index);
}
for arg in args {
self.expr(ctx, arg);
}
ctx.emit(Instr::ObjectMethodFn(
*object,
*method,
args.len() as u8 | if index.is_some() { 0x80 } else { 0 },
));
}
HExpr::DynamicObjectProperty { object, property } => {
self.expr(ctx, object);
let property = self.pool(property);
@@ -1173,6 +1283,9 @@ fn builtin_id(b: Builtin) -> u16 {
Builtin::Width => ids::WIDTH,
Builtin::ViewPrint => ids::VIEW_PRINT,
Builtin::ScreenStmt => ids::SCREEN_STMT,
Builtin::GraphicsLine => ids::GRAPHICS_LINE,
Builtin::GraphicsPaint => ids::GRAPHICS_PAINT,
Builtin::GraphicsView => ids::GRAPHICS_VIEW,
Builtin::KeyAssign => ids::KEY_ASSIGN,
Builtin::KeyList => ids::KEY_LIST,
Builtin::KeyDisplay => ids::KEY_DISPLAY,
@@ -1277,6 +1390,10 @@ fn builtin_id(b: Builtin) -> u16 {
Builtin::IsamSavepoint => ids::ISAM_SAVEPOINT,
Builtin::IsamSetmem => ids::ISAM_SETMEM,
Builtin::IsamBof => ids::ISAM_BOF,
Builtin::MsgBox => ids::MSGBOX,
Builtin::InputBoxS => ids::INPUTBOX_S,
Builtin::ClipboardAdd => ids::CLIPBOARD_ADD,
Builtin::ClipboardGet => ids::CLIPBOARD_GET,
}
}

View File

@@ -10,11 +10,11 @@
//! - `GOSUB`-Stack pro Frame; `RETURN` ohne GOSUB → Fehler 3.
use crate::bytecode::{CmpOp, CompiledModule, Instr};
use std::collections::HashSet;
use std::collections::{HashSet, VecDeque};
use std::rc::Rc;
use tb_runtime::builtins::{builtin_table, RtState};
use tb_runtime::builtins::{builtin_table, ids, RtState};
use tb_runtime::errors::RuntimeError;
use tb_runtime::host::Host;
use tb_runtime::host::{Ereignis, Host};
use tb_runtime::traps::Quelle;
use tb_runtime::value::{self, default_value, ArrayObj, RecordObj, TypeInit, Value, VarRef};
use tb_ui::forms::{FormEvent, FormsModel, PropertyValue, ShowResult};
@@ -37,6 +37,10 @@ pub enum RunEvent {
Interrupted {
line: u32,
},
Restart {
program: Option<String>,
line: Option<u32>,
},
/// Unbehandelter Laufzeitfehler.
Error {
code: u16,
@@ -106,6 +110,7 @@ pub struct Vm {
tick_zaehler: u32,
breakpoints: HashSet<u32>,
data_ptr: usize,
start_pc: Option<usize>,
pub forms: FormsModel,
}
@@ -166,6 +171,7 @@ impl Vm {
tick_zaehler: 0,
breakpoints: HashSet::new(),
data_ptr: 0,
start_pc: None,
forms,
module,
};
@@ -176,6 +182,24 @@ impl Vm {
vm
}
pub fn start_at_line(&mut self, line: u32) -> Result<(), RuntimeError> {
let target = self.module.procs[0]
.code
.iter()
.position(|instruction| matches!(instruction, Instr::Stmt(found) if *found == line));
let target = target.ok_or(RuntimeError(8))?;
if self.module.procs[0]
.code
.iter()
.any(|instruction| matches!(instruction, Instr::Stmt(0)))
{
self.start_pc = Some(target);
} else {
self.frames[0].pc = target;
}
Ok(())
}
fn push_frame(&mut self, proc: usize, argc: usize) {
// Argumente liegen zuoberst auf dem Stack (links → rechts).
let locals_base = self.locals.len();
@@ -204,6 +228,238 @@ impl Vm {
});
}
fn external_arg(&self, value: &Value) -> Result<Value, RuntimeError> {
match value {
Value::Ref(reference) => self.read_ref(reference),
value => Ok(value.clone()),
}
}
fn external_string(&self, value: &Value) -> Result<String, RuntimeError> {
match self.external_arg(value)? {
Value::Str(text) => Ok(text.to_string()),
_ => Err(RuntimeError::TYPE_MISMATCH),
}
}
fn external_set(
&mut self,
args: &[Value],
index: usize,
value: Value,
) -> Result<(), RuntimeError> {
match args.get(index) {
Some(Value::Ref(reference)) => self.write_ref(reference, value),
_ => Ok(()),
}
}
fn dialog_integer(&self, value: &Value) -> Result<i32, RuntimeError> {
match self.external_arg(value)? {
Value::Int(value) => Ok(value as i32),
Value::Lng(value) => Ok(value),
_ => Err(RuntimeError::TYPE_MISMATCH),
}
}
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 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 forms_dialog(
&mut self,
id: u16,
args: &[Value],
host: &mut dyn Host,
) -> Result<Option<Value>, RuntimeError> {
match id {
ids::MSGBOX => {
let text =
self.external_string(args.first().ok_or(RuntimeError::TYPE_MISMATCH)?)?;
let kind = args
.get(1)
.map_or(Ok(0), |value| self.dialog_integer(value))?;
let title = args
.get(2)
.map_or_else(|| Ok(String::new()), |value| self.external_string(value))?;
let mut queued = self.take_dialog_input();
let result = tb_ui::forms::msgbox_dialog(
&mut self.rt.screen,
host,
&mut queued,
&text,
kind,
&title,
);
self.restore_dialog_input(queued);
result.map(|value| Some(Value::Int(value)))
}
ids::INPUTBOX_S => {
let prompt =
self.external_string(args.first().ok_or(RuntimeError::TYPE_MISMATCH)?)?;
let title = args
.get(1)
.map_or_else(|| Ok(String::new()), |value| self.external_string(value))?;
let initial = args
.get(2)
.map_or_else(|| Ok(String::new()), |value| self.external_string(value))?;
let position = match (args.get(3), args.get(4)) {
(None, None) => None,
(Some(x), Some(y)) => Some((self.dialog_integer(x)?, self.dialog_integer(y)?)),
_ => return Err(RuntimeError::TYPE_MISMATCH),
};
let mut queued = self.take_dialog_input();
let result = tb_ui::forms::inputbox_dialog(
&mut self.rt.screen,
host,
&mut queued,
&prompt,
&title,
&initial,
position,
);
self.restore_dialog_input(queued);
result.map(|value| Some(Value::Str(Rc::from(value))))
}
_ => Err(RuntimeError::FEATURE_UNAVAILABLE),
}
}
fn external_dialog(
&mut self,
proc: usize,
argc: usize,
host: &mut dyn Host,
) -> Result<bool, RuntimeError> {
if !matches!(self.module.procs[proc].code.as_slice(), [Instr::RetProc]) {
return Ok(false);
}
let name = self.module.procs[proc].name.to_ascii_uppercase();
if !matches!(
name.as_str(),
"CMNDLGREGISTER"
| "CMNDLGCLOSE"
| "ABOUT"
| "FILEOPEN"
| "FILESAVE"
| "FILEPRINT"
| "FINDTEXT"
| "CHANGETEXT"
| "COLORPALETTE"
) {
return Ok(false);
}
let mut args = self.stack.split_off(self.stack.len().saturating_sub(argc));
match name.as_str() {
"CMNDLGREGISTER" => self.external_set(&args, 0, Value::Int(-1))?,
"CMNDLGCLOSE" => {}
"ABOUT" => {
let dialog = vec![
self.external_arg(&args[0])?,
Value::Lng(0),
Value::Str(Rc::from("About")),
];
self.forms_dialog(ids::MSGBOX, &dialog, host)?;
}
"FILEOPEN" | "FILESAVE" => {
let file = self.external_string(&args[0])?;
let path = self.external_string(&args[1])?;
let title = if name == "FILEOPEN" {
"Open file"
} else {
"Save file"
};
let default = if path.is_empty() {
file
} else {
format!("{path}{sep}{file}", sep = std::path::MAIN_SEPARATOR)
};
let dialog = vec![
Value::Str(Rc::from(title)),
Value::Str(Rc::from(title)),
Value::Str(Rc::from(default)),
];
let result = self
.forms_dialog(ids::INPUTBOX_S, &dialog, host)?
.unwrap_or(Value::Str(Rc::from("")));
let Value::Str(result) = result else {
return Err(RuntimeError::TYPE_MISMATCH);
};
let result = result.to_string();
if result.is_empty() {
self.external_set(&args, 7, Value::Int(-1))?;
} else {
let (path, file) = result
.rsplit_once(['/', '\\'])
.map_or(("", result.as_str()), |(path, file)| (path, file));
self.external_set(&args, 0, Value::Str(Rc::from(file)))?;
self.external_set(&args, 1, Value::Str(Rc::from(path)))?;
self.external_set(&args, 7, Value::Int(0))?;
}
}
"FILEPRINT" => {
self.external_set(&args, 0, Value::Int(1))?;
self.external_set(&args, 3, Value::Int(0))?;
}
"FINDTEXT" | "CHANGETEXT" => {
let dialog = vec![
Value::Str(Rc::from(if name == "FINDTEXT" {
"Find text"
} else {
"Replacement text"
})),
Value::Str(Rc::from("Find")),
self.external_arg(&args[usize::from(name == "CHANGETEXT")])?,
];
let result = self
.forms_dialog(ids::INPUTBOX_S, &dialog, host)?
.unwrap_or(Value::Str(Rc::from("")));
let target = usize::from(name == "CHANGETEXT");
self.external_set(&args, target, result.clone())?;
let empty = matches!(&result, Value::Str(text) if text.is_empty());
self.external_set(&args, args.len() - 1, Value::Int(-i16::from(empty)))?;
}
"COLORPALETTE" => {
let color = self.external_arg(&args[0])?;
self.external_set(&args, 0, color)?;
self.external_set(&args, 3, Value::Int(0))?;
}
_ => unreachable!(),
}
args.clear();
Ok(true)
}
pub fn queue_form_event(&mut self, event: FormEvent) {
self.forms.queue(event);
}
@@ -221,6 +477,17 @@ impl Vm {
}
}
fn form_arg(v: Value) -> Result<PropertyValue, RuntimeError> {
Ok(match v {
Value::Int(v) => PropertyValue::Integer(v as i32),
Value::Lng(v) => PropertyValue::Integer(v),
Value::Sng(v) => PropertyValue::Single(v),
Value::Str(v) => PropertyValue::String(v.to_string()),
Value::Obj(object, index) => PropertyValue::Object(Some((object, index))),
_ => return Err(RuntimeError::TYPE_MISMATCH),
})
}
fn property_value(
&self,
object: u16,
@@ -322,36 +589,37 @@ impl Vm {
Ok(())
}
fn forms_zustellen(&mut self) -> bool {
fn tick(&mut self, host: &mut dyn Host) {
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);
}
}
fn forms_zustellen(&mut self, host: &mut dyn Host) -> bool {
self.forms
.resize(self.rt.screen.cols(), self.rt.screen.rows());
while let Some(m) = self.rt.maus.pop_front() {
if let Some(object) = self.forms.active_form() {
let name = match m.art {
tb_runtime::host::MausArt::Druck => "MOUSEDOWN",
tb_runtime::host::MausArt::Loslassen => "MOUSEUP",
tb_runtime::host::MausArt::Bewegung => "MOUSEMOVE",
};
self.forms.queue(FormEvent {
object,
array_index: None,
name: name.into(),
args: vec![
PropertyValue::Integer(m.taste as i32),
PropertyValue::Integer(m.shift as i32),
PropertyValue::Single(m.spalte as f32),
PropertyValue::Single(m.zeile as f32),
],
});
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);
}
}
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.rt.traps.aktiv() {
if self.forms.menu_is_open() || !self.rt.traps.aktiv() {
return false;
}
let jetzt = host.jetzt_ms();
@@ -377,7 +645,7 @@ impl Vm {
// virtuelle nie vorrückt.
let real_start = std::time::Instant::now();
loop {
self.rt.tick(host);
self.tick(host);
if self.zustellen(host) {
return;
}
@@ -531,14 +799,41 @@ impl Vm {
// ---- Hauptschleife --------------------------------------------------------
/// Hält modellose Formulare nach dem Ende des Modulrumpfs bedienbar.
/// Ereignisprozeduren laufen weiter auf derselben VM und können das
/// Formular schließen, ein anderes Programm starten oder einen Fehler
/// auslösen.
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);
if dispatched {
match self.run(host) {
RunEvent::Ended => {}
event => return event,
}
}
if self.rt.ende {
return RunEvent::Ended;
}
if self.rt.abbruch {
return RunEvent::Interrupted {
line: self.current_line(),
};
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
RunEvent::Ended
}
pub fn run(&mut self, host: &mut dyn Host) -> RunEvent {
loop {
if let Some(form) = self.frames.last().and_then(|f| f.waiting_form) {
if !self.forms.is_visible(form) {
self.frames.last_mut().unwrap().waiting_form = None;
} else {
self.rt.tick(host);
self.forms_zustellen();
self.tick(host);
self.forms_zustellen(host);
if self.rt.ende {
return RunEvent::Ended;
}
@@ -818,6 +1113,11 @@ impl Vm {
use Instr as I;
match instr {
I::Stmt(line) => {
if line == 0 {
if let Some(target) = self.start_pc.take() {
self.frames[0].pc = target;
}
}
let f = self.frames.last_mut().unwrap();
f.line = line;
f.last_stmt_pc = pc;
@@ -826,11 +1126,12 @@ impl Vm {
// Debugger-Funktion — ohne sie sähe niemand die Ausgabe und
// Größenänderungen kämen nie an.
self.tick_zaehler = self.tick_zaehler.wrapping_add(1);
if self.rt.screen.ist_veraendert() || self.tick_zaehler % 1024 == 0 {
self.rt.tick(host);
self.forms.render(&mut self.rt.screen);
if self.rt.screen.ist_veraendert() || self.tick_zaehler.is_multiple_of(1024) {
self.tick(host);
self.rt.screen.veraenderung_quittieren();
}
if self.forms_zustellen() {
if self.forms_zustellen(host) {
return Ok(Flow::Normal);
}
// Ereigniszustellung (design.md, D1): nur wenn überhaupt
@@ -841,7 +1142,7 @@ impl Vm {
// Auflösung eines Zeit-Traps ist damit 64 Anweisungen;
// reicht das nicht, wird daraus ein eigener Zähler je
// Trap.
if self.tick_zaehler % 64 == 0 {
if self.tick_zaehler.is_multiple_of(64) {
let jetzt = host.jetzt_ms();
self.rt.traps.zeit_pruefen(jetzt);
}
@@ -901,8 +1202,8 @@ impl Vm {
// bekommt seinen eigenen Stapelabschnitt darüber und
// lässt den Wert unberührt.
self.stack.push(Value::Int(0));
self.rt.tick(host);
self.forms_zustellen();
self.tick(host);
self.forms_zustellen(host);
self.zustellen(host);
Ok(Flow::Normal)
}
@@ -1051,6 +1352,56 @@ impl Vm {
self.forms.set_at(object, index, property, value)?;
Ok(Flow::Normal)
}
I::LoadObjectIndexedProperty(object, property) => {
let index = self.pop_i32()?;
let object_index = if property & 0x8000 != 0 {
Some(self.pop_i32()?)
} else {
None
};
let value =
self.forms
.get_indexed_at(object, object_index, property & 0x7fff, index)?;
self.push(Self::form_value(value));
Ok(Flow::Normal)
}
I::StoreObjectIndexedProperty(object, property) => {
let value = self.pop_i32()?;
let index = self.pop_i32()?;
let object_index = if property & 0x8000 != 0 {
Some(self.pop_i32()?)
} else {
None
};
self.forms
.set_indexed_at(object, object_index, property & 0x7fff, index, value)?;
Ok(Flow::Normal)
}
I::ObjectMethodFn(object, method, argc) => {
let indexed = argc & 0x80 != 0;
let argc = argc & 0x7f;
let class = self
.module
.objects
.get(object as usize)
.ok_or(RuntimeError(420))?
.class;
let name = *tb_frontend::forms::methods(class)
.get(method as usize)
.ok_or(RuntimeError(421))?;
let mut args = Vec::with_capacity(argc as usize);
for _ in 0..argc {
args.push(Self::form_arg(self.pop()?)?);
}
args.reverse();
let index = if indexed { Some(self.pop_i32()?) } else { None };
let value = self
.forms
.object_method_at(object, index, name, args)?
.ok_or(RuntimeError(421))?;
self.push(Self::form_value(value));
Ok(Flow::Normal)
}
I::TypeOf(class) => {
let matches = match self.pop()? {
Value::Obj(object, _) => self
@@ -1064,6 +1415,8 @@ impl Vm {
Ok(Flow::Normal)
}
I::ObjectMethod(object, method, argc) => {
let indexed = argc & 0x80 != 0;
let argc = argc & 0x7f;
let class = self
.module
.objects
@@ -1093,6 +1446,7 @@ impl Vm {
args.push(self.pop()?);
}
args.reverse();
let index = if indexed { Some(self.pop_i32()?) } else { None };
if resumed_show && !self.forms.is_loaded(object) {
return Ok(Flow::Normal);
}
@@ -1118,13 +1472,26 @@ impl Vm {
(tb_frontend::forms::ObjectClass::Form, "UNLOAD") => {
self.request_unload(object)?
}
(tb_frontend::forms::ObjectClass::Form, "PRINTFORM") => {
self.forms.render(&mut self.rt.screen);
self.rt
.print
.drucker
.push_str(&tb_runtime::snapshot::text(&self.rt.screen));
}
(tb_frontend::forms::ObjectClass::Screen, "SHOW") => {
self.forms.screen_show(true)
}
(tb_frontend::forms::ObjectClass::Screen, "HIDE") => {
self.forms.screen_show(false)
}
_ => return Err(RuntimeError::FEATURE_UNAVAILABLE),
_ => {
let args = args
.into_iter()
.map(Self::form_arg)
.collect::<Result<Vec<_>, _>>()?;
self.forms.object_method_at(object, index, name, args)?;
}
}
self.dispatch_next_form_event();
Ok(Flow::Normal)
@@ -1318,7 +1685,7 @@ impl Vm {
s.chars().take(n).collect()
} else {
let mut t = s.to_string();
t.extend(std::iter::repeat(' ').take(n - len));
t.extend(std::iter::repeat_n(' ', n - len));
t
};
self.push(Value::Str(Rc::from(fixed.as_str())));
@@ -1714,9 +2081,27 @@ impl Vm {
}
Ok(Flow::Normal)
}
I::Run(kind) => {
let (program, line) = match kind {
0 => (None, None),
1 => {
let line = self.pop_i32()?;
if line < 0 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
(None, Some(line as u32))
}
2 => (Some(self.pop_str()?.to_string()), None),
_ => return Err(RuntimeError::ILLEGAL_FUNCTION_CALL),
};
Ok(Flow::Event(RunEvent::Restart { program, line }))
}
// ---- Prozeduren ----
I::Call(proc, argc) => {
if self.external_dialog(proc as usize, argc as usize, host)? {
return Ok(Flow::Normal);
}
self.push_frame(proc as usize, argc as usize);
Ok(Flow::Normal)
}
@@ -1754,6 +2139,12 @@ impl Vm {
args.push(self.pop()?);
}
args.reverse();
if matches!(id, ids::MSGBOX | ids::INPUTBOX_S) {
if let Some(value) = self.forms_dialog(id, &args, host)? {
self.push(value);
}
return Ok(Flow::Normal);
}
let f = builtin_table()[id as usize];
match f(&mut self.rt, host, &mut args) {
Ok(Some(v)) => {
@@ -2228,9 +2619,7 @@ fn cur_mul(a: i64, b: i64) -> Result<i64, RuntimeError> {
let r = p.rem_euclid(10_000);
let rounded = if r > 5_000 {
q + 1
} else if r < 5_000 {
q
} else if q % 2 == 0 {
} else if r < 5_000 || q % 2 == 0 {
q
} else {
q + 1

View File

@@ -38,13 +38,11 @@ fn err_code(src: &str) -> u16 {
/// Verzeichnis, das beim Verlassen samt Inhalt verschwindet. Programme
/// hinterlassen dadurch nichts im Projektbaum.
struct TempVerzeichnis {
vorher: std::path::PathBuf,
dir: std::path::PathBuf,
}
impl TempVerzeichnis {
fn neu(name: &str) -> TempVerzeichnis {
let vorher = std::env::current_dir().unwrap();
let dir = std::env::temp_dir().join(format!(
"tb_vm_{name}_{}_{:?}",
std::process::id(),
@@ -52,14 +50,16 @@ impl TempVerzeichnis {
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::env::set_current_dir(&dir).unwrap();
TempVerzeichnis { vorher, dir }
TempVerzeichnis { dir }
}
fn pfad(&self, name: &str) -> String {
self.dir.join(name).to_string_lossy().replace('"', "\"\"")
}
}
impl Drop for TempVerzeichnis {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.vorher);
let _ = std::fs::remove_dir_all(&self.dir);
}
}
@@ -71,6 +71,48 @@ fn print_hallo_welt() {
assert_eq!(out("PRINT \"Hallo, Welt!\"\nEND"), "Hallo, Welt!\n");
}
#[test]
fn grafik_line_paint_und_view_zeichnen_in_den_zellenpuffer() {
assert_eq!(
out("SCREEN 2\nVIEW (0,0)-(31,15),0,1\nLINE (0,0)-(15,7),1,BF\nPAINT (24,8),1\nEND"),
"████\n████\n"
);
}
#[test]
fn run_gibt_datei_oder_zeile_an_den_runner_weiter() {
assert_eq!(
run("RUN \"next\"").0,
RunEvent::Restart {
program: Some("next".into()),
line: None
}
);
assert_eq!(
run("RUN 100").0,
RunEvent::Restart {
program: None,
line: Some(100)
}
);
}
#[test]
fn common_dialog_registrierung_setzt_byref_erfolg() {
assert_eq!(
out("DECLARE SUB CmnDlgRegister(ok AS INTEGER)\nCmnDlgRegister ok%\nPRINT ok%\nEND"),
"-1 \n"
);
}
#[test]
fn len_liefert_die_feste_satzbreite_eines_udt() {
assert_eq!(
out("TYPE Satz\nText AS STRING * 3\nZahl AS INTEGER\nEND TYPE\nDIM Wert AS Satz\nPRINT LEN(Wert)"),
" 14 \n"
);
}
#[test]
fn banker_rounding_cint() {
// Spec-Szenario: PRINT CINT(0.5); CINT(1.5); CINT(2.5) → " 0 2 2 "
@@ -486,31 +528,19 @@ fn print_zonen_und_tab() {
assert_eq!(out("PRINT \"a\"; SPC(3); \"b\""), "a b\n");
}
#[test]
fn unsupported_feature_fehler_73() {
// Dokumentiert, aber noch offen: `RUN` kommt mit Phase 5
// → Laufzeitfehler 73 mit dem Katalogtext des Vorbilds (VBDOS).
// (`SETUEVENT` stand hier bis zum Change `phase-4-ereignisschleife`.)
let (ev, _) = run("RUN");
match ev {
RunEvent::Error { code, message, .. } => {
assert_eq!(code, 73);
assert_eq!(message, "Feature unavailable");
}
other => panic!("{other:?}"),
}
}
/// Aufgabe 2.4 des Changes `phase-3-isam`: `OPEN … FOR ISAM` senkt nicht
/// mehr auf den „nicht verfügbar"-Marker ab, sondern arbeitet. Der Lauf
/// findet in einem temporären Verzeichnis statt und lässt nichts zurück.
#[test]
fn open_for_isam_endet_nicht_mehr_mit_fehler_73() {
let _dir = TempVerzeichnis::neu("open_isam");
let (ev, ausgabe) = run("TYPE T\n f AS INTEGER\nEND TYPE\n\
OPEN \"db.isam\" FOR ISAM T \"Tab\" AS #1\n\
let dir = TempVerzeichnis::neu("open_isam");
let (ev, ausgabe) = run(&format!(
"TYPE T\n f AS INTEGER\nEND TYPE\n\
OPEN \"{}\" FOR ISAM T \"Tab\" AS #1\n\
PRINT \"offen\"\n\
CLOSE #1");
CLOSE #1",
dir.pfad("db.isam")
));
assert_eq!(ev, RunEvent::Ended, "unerwartetes Ende: {ev:?}\n{ausgabe}");
assert!(ausgabe.contains("offen"), "{ausgabe}");
}
@@ -594,17 +624,10 @@ fn color_prueft_wertebereich() {
}
#[test]
fn screen_anweisung_nur_textmodus() {
// Literaler Grafikmodus schon zur Compile-Zeit.
let d = tb_vm::compile_source("T", "SCREEN 1").unwrap_err();
assert!(
d.iter().any(|x| x.message.contains("Feature unavailable")),
"{d:?}"
);
// Textmodus ist folgenlos zulässig.
fn screen_anweisung_bildet_grafikmodi_auf_den_zellenpuffer_ab() {
assert_eq!(out("SCREEN 0\nPRINT \"ok\""), "ok\n");
// Berechneter Modus erst zur Laufzeit.
assert_eq!(err_code("m% = 2\nSCREEN m%"), 73);
assert_eq!(out("SCREEN 13\nPRINT \"ok\""), "ok\n");
assert_eq!(err_code("m% = 14\nSCREEN m%"), 5);
}
#[test]
@@ -644,6 +667,32 @@ fn input_s_liest_genau_n_zeichen_ohne_echo() {
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), "xyz\n");
}
#[test]
fn input_s_liest_aus_einer_binaerdatei() {
let tmp = TempVerzeichnis::neu("input_s_datei");
let pfad = tmp.pfad("daten.bin");
std::fs::write(&pfad, b"abcdef").unwrap();
assert_eq!(
out(&format!(
"OPEN \"{pfad}\" FOR BINARY AS #1\na$ = INPUT$(3, #1)\nPRINT a$\nCLOSE #1"
)),
"abc\n"
);
}
#[test]
fn clipboard_und_printer_objekte_haben_laufzeitwirkung() {
let module = tb_vm::compile_source(
"T",
"CLIPBOARD.ADDITEM \"abc\"\nPRINT CLIPBOARD.GETTEXT\nPRINTER.PRINT \"Seite\"\nPRINTER.NEWPAGE\nPRINTER.ENDDOC",
)
.unwrap();
let mut vm = Vm::new(module);
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), "abc\n");
assert_eq!(vm.rt.print.drucker, "Seite\n\u{c}");
}
#[test]
fn cls_setzt_cursor_zurueck() {
assert_eq!(out("PRINT \"weg\"\nCLS\nPRINT \"neu\""), "neu\n");
@@ -1136,6 +1185,14 @@ fn objektzugriff_laeuft_ueber_objekt_und_eigenschaftsindex() {
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "hallo");
}
#[test]
fn screen_controlpanel_hat_einen_indexierten_laufzeitwert() {
assert_eq!(
out("SCREEN.CONTROLPANEL(5) = 3\nPRINT SCREEN.CONTROLPANEL(5)"),
" 3 \n"
);
}
#[test]
fn steuerarray_eigenschaften_und_objektparameter_funktionieren() {
let mut vm = form_vm(
@@ -1175,6 +1232,31 @@ fn modales_show_setzt_nach_unload_fort() {
assert!(host.presents >= 2);
}
#[test]
fn modelloses_formular_bleibt_nach_programmende_bedienbar() {
let mut vm = form_vm(
"DIM SHARED gesehen%\nEND\n\
SUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\n\
SHARED gesehen%\ngesehen% = 1\nUNLOAD Form1\nEND SUB",
);
vm.forms.show(0, false).unwrap();
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
let mut host = CaptureHost::default();
host.ereignis_nach(
1,
tb_runtime::host::Ereignis::Maus(tb_runtime::host::MausEreignis {
art: tb_runtime::host::MausArt::Druck,
taste: 1,
shift: 0,
zeile: 1,
spalte: 1,
}),
);
assert_eq!(vm.run_visible_forms(&mut host), RunEvent::Ended);
assert!(matches!(vm.inspect("gesehen"), Some(Value::Int(1))));
assert!(!vm.forms.has_visible_forms());
}
#[test]
fn fehlende_ereignisprozedur_verfaellt_und_typeof_prueft_klasse() {
let mut vm = form_vm("Form1.Show\nIF TYPEOF Text1 IS TextBox THEN PRINT \"ja\"");
@@ -1278,3 +1360,153 @@ fn objektcode_erzeugt_keine_zusaetzlichen_zustellopcodes() {
.count();
assert_eq!(n, 1, "nur das explizite DOEVENTS ist ein Zustellopcode");
}
#[test]
fn listenmethoden_indexeigenschaft_und_picture_messung_laufen_in_der_vm() {
use tb_frontend::forms::ObjectClass;
let mut catalog = tb_frontend::forms::FormCatalog::default();
catalog.add("Form1", ObjectClass::Form, None, false);
catalog.add("List1", ObjectClass::ListBox, Some("Form1"), false);
catalog.add("Picture1", ObjectClass::PictureBox, Some("Form1"), false);
let module = tb_vm::compile_source_with_forms(
"FORM1",
"List1.Sorted = -1\nList1.ADDITEM \"b\"\nList1.ADDITEM \"a\"\nPRINT List1.List(0)\nPRINT List1.ListCount\nPRINT Picture1.TEXTWIDTH(\"abc\")",
&catalog,
)
.unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}"));
let mut vm = Vm::new(module);
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
assert_eq!(
tb_runtime::snapshot::text(&vm.rt.screen).trim(),
"a\n 2 \n 3"
);
}
#[test]
fn listenmethoden_und_indexeigenschaft_laufen_auf_control_arrays() {
use tb_frontend::forms::ObjectClass;
let mut catalog = tb_frontend::forms::FormCatalog::default();
catalog.add("Form1", ObjectClass::Form, None, false);
catalog.add("List1", ObjectClass::ListBox, Some("Form1"), true);
let module = tb_vm::compile_source_with_forms(
"FORM1",
"LOAD List1(1)\nList1(1).ADDITEM \"x\"\nPRINT List1(1).List(0)",
&catalog,
)
.unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}"));
let mut vm = Vm::new(module);
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "x");
}
#[test]
fn msgbox_und_inputbox_sind_keine_unsupported_opcodes_mehr() {
use tb_runtime::host::{taste, Ereignis};
let module = tb_vm::compile_source(
"DIALOG",
"r% = MSGBOX(\"Weiter?\", 4, \"Frage\")\ns$ = INPUTBOX$(\"Name\")\nPRINT r%; s$",
)
.unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}"));
assert!(!module.procs[0]
.code
.iter()
.any(|instruction| matches!(instruction, tb_vm::bytecode::Instr::Unsupported(_))));
let mut vm = Vm::new(module);
let mut host = CaptureHost::default();
for key in [taste::ENTER, "A", taste::ENTER] {
host.ereignis(Ereignis::Taste(key.into(), 0));
}
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "6 A");
}
#[test]
fn default_button_hat_vorrang_und_beendet_modales_formular() {
use tb_frontend::forms::ObjectClass;
use tb_runtime::host::{taste, Ereignis};
let mut catalog = tb_frontend::forms::FormCatalog::default();
catalog.add("Form1", ObjectClass::Form, None, false);
catalog.add("Command1", ObjectClass::CommandButton, Some("Form1"), false);
let module = tb_vm::compile_source_with_forms(
"FORM1",
"DIM SHARED gesehen%\nForm1.Show 1\nPRINT gesehen%\nEND\nSUB Command1_Click()\nSHARED gesehen%\ngesehen% = 1\nUNLOAD Form1\nEND SUB",
&catalog,
)
.unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}"));
let mut vm = Vm::new(module);
let default = tb_frontend::forms::property(ObjectClass::CommandButton, "DEFAULT")
.unwrap()
.0;
vm.forms
.set(1, default, tb_ui::forms::PropertyValue::Boolean(true))
.unwrap();
let mut host = CaptureHost::default();
host.ereignis(Ereignis::Taste(taste::ENTER.into(), 0));
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert!(matches!(vm.inspect("gesehen"), Some(Value::Int(1))));
}
#[test]
fn printform_schreibt_das_textformular_in_den_druckerkanal() {
use tb_frontend::forms::ObjectClass;
let mut catalog = tb_frontend::forms::FormCatalog::default();
catalog.add("Form1", ObjectClass::Form, None, false);
let module = tb_vm::compile_source_with_forms("FORM1", "Form1.Show\nForm1.PRINTFORM", &catalog)
.unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}"));
let mut vm = Vm::new(module);
for (name, value) in [("WIDTH", 12), ("HEIGHT", 4)] {
let property = tb_frontend::forms::property(ObjectClass::Form, name)
.unwrap()
.0;
vm.forms
.set_initial(0, property, tb_ui::forms::PropertyValue::Integer(value))
.unwrap();
}
let caption = tb_frontend::forms::property(ObjectClass::Form, "CAPTION")
.unwrap()
.0;
vm.forms
.set_initial(
0,
caption,
tb_ui::forms::PropertyValue::String("Druck".into()),
)
.unwrap();
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
assert!(vm.rt.print.drucker.contains("Druck"));
}
#[test]
fn klassischer_timer_trap_ruht_solange_das_menu_offen_ist() {
use tb_frontend::forms::ObjectClass;
use tb_runtime::host::umschalt;
let mut catalog = tb_frontend::forms::FormCatalog::default();
catalog.add("Form1", ObjectClass::Form, None, false);
catalog.add("mnuDatei", ObjectClass::Menu, Some("Form1"), false);
let module = tb_vm::compile_source_with_forms(
"FORM1",
"DIM SHARED n%\nON TIMER(1) GOSUB Tick\nTIMER ON\nForm1.Show\nSTOP\nDOEVENTS\nSTOP\nDOEVENTS\nPRINT n%\nEND\nTick:\nn% = n% + 1\nRETURN",
&catalog,
)
.unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}"));
let mut vm = Vm::new(module);
let caption = tb_frontend::forms::property(ObjectClass::Menu, "CAPTION")
.unwrap()
.0;
vm.forms
.set_initial(
1,
caption,
tb_ui::forms::PropertyValue::String("&Datei".into()),
)
.unwrap();
let mut host = CaptureHost::default();
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
assert!(vm.forms.handle_key("d", umschalt::ALT));
host.uhr_vorruecken(1_500);
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
assert!(matches!(vm.inspect("n"), Some(Value::Int(0))));
assert!(vm.forms.handle_key(tb_runtime::host::taste::ESC, 0));
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert!(matches!(vm.inspect("n"), Some(Value::Int(1))));
}

View File

@@ -127,7 +127,10 @@ zum Abbruch; eine Ausgabedatei wird erst nach erfolgreichem Lesen angelegt.
Reine Textdatei ohne Kopfzeile, **eine Projektdatei pro Zeile**
(`.BAS`/`.FRM`; belegt durch die Originalbeispiele). Terminal Basic
akzeptiert zusätzlich Kommentarzeilen mit `'`.
akzeptiert zusätzlich Kommentarzeilen mit `'`. `tbc check`, `build` und
`run` lösen die Einträge relativ zur `.MAK`-Datei und ohne Beachtung der
DOS-Großschreibung auf; Formularobjekte aller `.FRM`-Einträge stehen dem
gemeinsamen Modulverbund zur Verfügung.
## Record-Dateien (`GET`/`PUT`, `OPEN … FOR RANDOM`)

View File

@@ -24,7 +24,7 @@ Maus-Koordinaten in Ereignissen sind SINGLE, aber in Zellen.
## Steuerelemente (exakte Klassennamen)
`CheckBox, ComboBox, CommandButton, DirListBox, DriveListBox, FileListBox,
Frame, HScrollBar, Label, ListBox, Menu, OptionButton, PictureBox, TextBox,
Frame, HScrollBar, Label, ListBox, Menu, OptionButton, PictureBox, Spin, TextBox,
Timer, VScrollBar` — alle außer `Menu` über die Toolbox platzierbar
(Menüs entstehen im Menu Design Window). Default-Namen: `Check1`, `Combo1`,
`Command1`, `Dir1`, `Drive1`, `File1`, `Frame1`, `HScroll1`, `Label1`,
@@ -83,6 +83,11 @@ LostFocus (**kein** separates Scroll-Ereignis). Eigenschaften: zusätzlich
Attached (VBDOS-Spezifikum: am Formularrand angedockt; Laufzeit read-only),
LargeChange, Min, Max, SmallChange, Value.
**Spin** — Ereignisse: Custom(EventType), Drag*, GotFocus, Key*, LostFocus.
`Style` wählt vertikal/horizontal, `Interval` steuert die Wiederholung; `Value`
läuft zyklisch zwischen `Min` und `Max`. `BorderStyle`, `Width` und `Height`
sind zur Laufzeit schreibgeschützt.
**PictureBox** — Text-Zeichenfläche (PRINT/CLS aufs Control) und Container
für OptionButton-Gruppen. Ereignisse: Click, DblClick, Drag*, GotFocus,
Key*, LostFocus, Mouse*, Paint. Methoden: CLS, DRAG, MOVE, PRINT, REFRESH,
@@ -93,7 +98,7 @@ TabStop, AutoRedraw, CurrentX, CurrentY, ScaleHeight, ScaleWidth.
Index, Interval (0 = aus … 65 535 ms), Parent, Tag.
**Menu** — Ereignis Click; Eigenschaften Caption, Checked, CtlName, Enabled,
Index, Parent, Separator, Tag, Visible (Shortcut nur zur Entwurfszeit).
Index, Parent, Separator, Shortcut, Tag, Visible.
**DirListBox/DriveListBox/FileListBox** — Datei-Browser-Controls
(Path/Drive/FileName/Pattern/Archive/Hidden/Normal/ReadOnly/System;
@@ -124,6 +129,61 @@ sie plattformneutral nach (Laufwerksliste → Wurzeln/Mounts).
| Text (TextBox/ComboBox) | — | Entwurfszeit = CtlName |
| ListIndex | 1 = keine Auswahl | 1 |
## Zeichenbilder und Zustände
Die Originalquellen belegen Aufbau und Rahmenformen, enthalten aber keine
vollständige Screenshot-Reihe für jede Klasse und jeden Zustand. Die folgende
einheitliche Umsetzung ist deshalb **unsere Festlegung**: normal = die Werte
aus `ForeColor`/`BackColor`, fokussiert = Farbe 15 auf 1, deaktiviert = Farbe
8 auf 0. `&` kennzeichnet
nur den Access-Key und wird nicht ausgegeben. Die Beispiele zeigen jeweils
normal · fokussiert · deaktiviert; Inhalt wird auf die verfügbare Breite
gekürzt und der Rest mit Leerzeichen gefüllt.
| Klasse | normal | fokussiert | deaktiviert |
|---|---|---|---|
| CommandButton, Höhe 1 | `<OK>` | `<OK>` | `<OK>` |
| CommandButton, Höhe 2 | `┌OK──┐` / `└────┘` | ebenso | ebenso |
| CommandButton, Höhe ≥ 3 | `┌────┐` / `│ OK │` / `└────┘` | ebenso | ebenso |
| Label | `Text` | `Text` | `Text` |
| Frame | `┌ Titel ─┐` / `│ │` / `└────────┘` | ebenso | ebenso |
| CheckBox | `[ ] Text` | `[ ] Text` | `[ ] Text` |
| CheckBox, gewählt/grau | `[x] Text` / `[-] Text` | ebenso | ebenso |
| OptionButton | `( ) Text` | `( ) Text` | `( ) Text` |
| OptionButton, gewählt | `(•) Text` | `(•) Text` | `(•) Text` |
| TextBox | `┌──────┐` / `│Text │` / `└──────┘` | ebenso, Cursor an `SelStart` | ebenso, ohne Cursor |
| ListBox | `┌──────┐` / `│eins │` / `│zwei │` / `└──────┘` | ebenso, aktuelle Zeile in Fokusfarben | ebenso |
| ComboBox, Dropdown | `[Text ▼]` | ebenso in Fokusfarben | ebenso |
| ComboBox, Simple | `[Text ▼]` / `┌──────┐` / `│eins │` / `└──────┘` | ebenso in Fokusfarben | ebenso |
| HScrollBar | `◄──□──►` | ebenso in Fokusfarben | ebenso |
| VScrollBar | `▲` / `│` / `□` / `│` / `▼` | ebenso in Fokusfarben | ebenso |
| Spin | `▲` / `▼` oder `◄ ►` | ebenso in Fokusfarben | ebenso |
| PictureBox | `┌──────┐` / `│Text │` / `└──────┘` | ebenso in Fokusfarben | ebenso |
| DirListBox | `┌──────┐` / `│[dir] │` / `└──────┘` | ebenso, aktuelle Zeile in Fokusfarben | ebenso |
| DriveListBox | `[ / ▼]` | ebenso in Fokusfarben | ebenso |
| FileListBox | `┌──────┐` / `│a.bas │` / `└──────┘` | ebenso, aktuelle Zeile in Fokusfarben | ebenso |
| Menu | ` Datei Bearbeiten ` | gewählter Titel/Eintrag in Fokusfarben | Text in Farbe 8 |
| Timer | keine sichtbaren Zellen | keine sichtbaren Zellen | keine sichtbaren Zellen |
`BorderStyle = 0` unterdrückt den Rahmen von Label, TextBox und PictureBox;
`BorderStyle = 2` nutzt `╔═╗║╚╝`. Scrollbars erscheinen nur in den von
`ScrollBars` genannten Richtungen. Ein unsichtbares Steuerelement zeichnet
keine Zelle.
## Festgelegte Reihenfolgen
Die folgenden Regeln sind **unsere Festlegung**, nicht nachgewiesenes
Referenzverhalten:
- **Z-Reihenfolge:** Erzeugungsreihenfolge; später erzeugte Elemente liegen
oben. Bei Steuerelement-Arrays entscheidet bei sonst gleicher Position der
aufsteigende Index, sodass der höchste Index oben liegt. Gezeichnet wird von
unten nach oben, die Trefferprüfung läuft umgekehrt.
- **Timer-Reihenfolge:** Sind mehrere Timer am selben Zustellpunkt fällig,
werden ihre Ereignisse in aufsteigender, ASCII-unabhängig
großgeschriebener `CtlName`-Reihenfolge zugestellt. Mehrere verstrichene
Intervalle desselben Timers bleiben in zeitlicher Reihenfolge davor.
**Keine Default-Eigenschaften:** `Text1 = "x"` gibt es nicht — Zugriff
immer explizit (`Text1.Text`). „Default" ist nur die CommandButton-
Eigenschaft (Enter-Taste); „Cancel" analog für Esc.
@@ -188,8 +248,4 @@ Menü-Steuerelement-Arrays sind möglich.
## Offene Detailfragen
- Exakte Optik je Steuerelement-Zustand (normal/fokussiert/deaktiviert):
aus den Original-Screenshots ableiten (Phase 4)
- Z-Reihenfolge/Überlappung: Verhalten bei überlappenden Controls
- Timer-Reihenfolge bei mehreren gleichzeitig fälligen Timern
- `.FRM`-Textformat: exakte Serialisierung (siehe dateiformate.md)

View File

@@ -22,7 +22,7 @@ namentlich abgewiesen. `Non-Feature` = abgelehnt, gelistet unter
**Fundstelle.** Bei `implementiert` das Modul, bei `Non-Feature` der
Abschnitt der Sprachreferenz, bei `offen` ein Strich.
**Abdeckung.** implementiert 689 · offen 61 · Non-Feature 53 · gesamt 803
**Abdeckung.** implementiert 794 · offen 0 · Non-Feature 50 · gesamt 844
| Name | Art | Gruppe | Status | Fundstelle | Quelle |
|---|---|---|---|---|---|
@@ -164,14 +164,14 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
| `FORMAT$` | Funktion | Formatierung | implementiert | tb-runtime::using | bas7advr |
| `PRINT USING` | Anweisung | Formatierung | implementiert | tb-runtime::using | bas7advr |
| `SetFormatCC` | Routine | Formatierung | implementiert | tb-runtime::using | bas7advr |
| `INPUTBOX$` | Funktion | Forms | offen | - | sprachreferenz |
| `MSGBOX` | Anweisung | Forms | offen | - | sprachreferenz |
| `MSGBOX` | Funktion | Forms | offen | - | sprachreferenz |
| `INPUTBOX$` | Funktion | Forms | implementiert | tb-runtime::builtins | sprachreferenz |
| `MSGBOX` | Anweisung | Forms | implementiert | tb-runtime::builtins | sprachreferenz |
| `MSGBOX` | Funktion | Forms | implementiert | tb-runtime::builtins | sprachreferenz |
| `CIRCLE` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `DRAW` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `GET (Grafik)` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `LINE` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `PAINT` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `LINE` | Anweisung | Grafik | implementiert | tb-runtime::screen | bas7advr |
| `PAINT` | Anweisung | Grafik | implementiert | tb-runtime::screen | bas7advr |
| `PALETTE` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `PCOPY` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `PMAP` | Funktion | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
@@ -179,7 +179,7 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
| `PRESET` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `PSET` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `PUT (Grafik)` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `VIEW` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `VIEW` | Anweisung | Grafik | implementiert | tb-runtime::screen | bas7advr |
| `WINDOW` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `ABSOLUTE` | Routine | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `BLOAD` | Anweisung | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
@@ -255,7 +255,7 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
| `SQR` | Funktion | Mathematik | implementiert | tb-runtime::math | qb45advr |
| `TAN` | Funktion | Mathematik | implementiert | tb-runtime::math | bas7advr |
| `$DYNAMIC` | Metabefehl | Metabefehl | implementiert | tb-frontend::lexer | bas7advr |
| `$INCLUDE` | Metabefehl | Metabefehl | offen | - | bas7advr |
| `$INCLUDE` | Metabefehl | Metabefehl | implementiert | tb-cli | bas7advr |
| `$STATIC` | Metabefehl | Metabefehl | implementiert | tb-frontend::lexer | bas7advr |
| `CALL` | Anweisung | Prozeduren | implementiert | tb-frontend::sema | bas7advr |
| `DECLARE` | Anweisung | Prozeduren | implementiert | tb-frontend::sema | bas7advr |
@@ -301,7 +301,7 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
| `ERDEV` | Funktion | System | implementiert | tb-runtime::builtins | bas7advr |
| `ERDEV$` | Funktion | System | implementiert | tb-runtime::builtins | bas7advr |
| `FRE` | Funktion | System | implementiert | tb-runtime::builtins | bas7advr |
| `RUN` | Anweisung | System | offen | - | bas7advr |
| `RUN` | Anweisung | System | implementiert | tb-cli | bas7advr |
| `SLEEP` | Anweisung | System | implementiert | tb-runtime::builtins | bas7advr |
| `STACK` | Anweisung | System | implementiert | tb-runtime::builtins | bas7advr |
| `STACK` | Funktion | System | implementiert | tb-runtime::builtins | bas7advr |
@@ -341,17 +341,17 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
| `FORM.SCALEHEIGHT` | Eigenschaft | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
| `FORM.SCALEWIDTH` | Eigenschaft | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
| `FORM.WINDOWSTATE` | Eigenschaft | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
| `FORM.CLS` | Methode | Forms/FORM | offen | - | forms-referenz |
| `FORM.DRAG` | Methode | Forms/FORM | offen | - | forms-referenz |
| `FORM.CLS` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
| `FORM.DRAG` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
| `FORM.HIDE` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
| `FORM.LOAD` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
| `FORM.MOVE` | Methode | Forms/FORM | offen | - | forms-referenz |
| `FORM.PRINT` | Methode | Forms/FORM | offen | - | forms-referenz |
| `FORM.PRINTFORM` | Methode | Forms/FORM | offen | - | forms-referenz |
| `FORM.REFRESH` | Methode | Forms/FORM | offen | - | forms-referenz |
| `FORM.MOVE` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
| `FORM.PRINT` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
| `FORM.PRINTFORM` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
| `FORM.REFRESH` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
| `FORM.SHOW` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
| `FORM.TEXTHEIGHT` | Methode | Forms/FORM | offen | - | forms-referenz |
| `FORM.TEXTWIDTH` | Methode | Forms/FORM | offen | - | forms-referenz |
| `FORM.TEXTHEIGHT` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
| `FORM.TEXTWIDTH` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
| `FORM.UNLOAD` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
| `FORM_CLICK` | Ereignis | Forms/FORM | implementiert | tb-vm::interp | forms-referenz |
| `FORM_DBLCLICK` | Ereignis | Forms/FORM | implementiert | tb-vm::interp | forms-referenz |
@@ -387,10 +387,10 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
| `CHECKBOX.DRAGMODE` | Eigenschaft | Forms/CHECKBOX | implementiert | tb-ui::forms | forms-referenz |
| `CHECKBOX.CAPTION` | Eigenschaft | Forms/CHECKBOX | implementiert | tb-ui::forms | forms-referenz |
| `CHECKBOX.VALUE` | Eigenschaft | Forms/CHECKBOX | implementiert | tb-ui::forms | forms-referenz |
| `CHECKBOX.DRAG` | Methode | Forms/CHECKBOX | offen | - | forms-referenz |
| `CHECKBOX.MOVE` | Methode | Forms/CHECKBOX | offen | - | forms-referenz |
| `CHECKBOX.REFRESH` | Methode | Forms/CHECKBOX | offen | - | forms-referenz |
| `CHECKBOX.SETFOCUS` | Methode | Forms/CHECKBOX | offen | - | forms-referenz |
| `CHECKBOX.DRAG` | Methode | Forms/CHECKBOX | implementiert | tb-ui::forms | forms-referenz |
| `CHECKBOX.MOVE` | Methode | Forms/CHECKBOX | implementiert | tb-ui::forms | forms-referenz |
| `CHECKBOX.REFRESH` | Methode | Forms/CHECKBOX | implementiert | tb-ui::forms | forms-referenz |
| `CHECKBOX.SETFOCUS` | Methode | Forms/CHECKBOX | implementiert | tb-ui::forms | forms-referenz |
| `CHECKBOX_CLICK` | Ereignis | Forms/CHECKBOX | implementiert | tb-vm::interp | forms-referenz |
| `CHECKBOX_DRAGDROP` | Ereignis | Forms/CHECKBOX | implementiert | tb-vm::interp | forms-referenz |
| `CHECKBOX_DRAGOVER` | Ereignis | Forms/CHECKBOX | implementiert | tb-vm::interp | forms-referenz |
@@ -424,12 +424,12 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
| `COMBOBOX.SELTEXT` | Eigenschaft | Forms/COMBOBOX | implementiert | tb-ui::forms | forms-referenz |
| `COMBOBOX.STYLE` | Eigenschaft | Forms/COMBOBOX | implementiert | tb-ui::forms | forms-referenz |
| `COMBOBOX.TEXT` | Eigenschaft | Forms/COMBOBOX | implementiert | tb-ui::forms | forms-referenz |
| `COMBOBOX.ADDITEM` | Methode | Forms/COMBOBOX | offen | - | forms-referenz |
| `COMBOBOX.DRAG` | Methode | Forms/COMBOBOX | offen | - | forms-referenz |
| `COMBOBOX.MOVE` | Methode | Forms/COMBOBOX | offen | - | forms-referenz |
| `COMBOBOX.REFRESH` | Methode | Forms/COMBOBOX | offen | - | forms-referenz |
| `COMBOBOX.REMOVEITEM` | Methode | Forms/COMBOBOX | offen | - | forms-referenz |
| `COMBOBOX.SETFOCUS` | Methode | Forms/COMBOBOX | offen | - | forms-referenz |
| `COMBOBOX.ADDITEM` | Methode | Forms/COMBOBOX | implementiert | tb-ui::forms | forms-referenz |
| `COMBOBOX.DRAG` | Methode | Forms/COMBOBOX | implementiert | tb-ui::forms | forms-referenz |
| `COMBOBOX.MOVE` | Methode | Forms/COMBOBOX | implementiert | tb-ui::forms | forms-referenz |
| `COMBOBOX.REFRESH` | Methode | Forms/COMBOBOX | implementiert | tb-ui::forms | forms-referenz |
| `COMBOBOX.REMOVEITEM` | Methode | Forms/COMBOBOX | implementiert | tb-ui::forms | forms-referenz |
| `COMBOBOX.SETFOCUS` | Methode | Forms/COMBOBOX | implementiert | tb-ui::forms | forms-referenz |
| `COMBOBOX_CHANGE` | Ereignis | Forms/COMBOBOX | implementiert | tb-vm::interp | forms-referenz |
| `COMBOBOX_CLICK` | Ereignis | Forms/COMBOBOX | implementiert | tb-vm::interp | forms-referenz |
| `COMBOBOX_DBLCLICK` | Ereignis | Forms/COMBOBOX | implementiert | tb-vm::interp | forms-referenz |
@@ -463,10 +463,10 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
| `COMMANDBUTTON.CAPTION` | Eigenschaft | Forms/COMMANDBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `COMMANDBUTTON.DEFAULT` | Eigenschaft | Forms/COMMANDBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `COMMANDBUTTON.VALUE` | Eigenschaft | Forms/COMMANDBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `COMMANDBUTTON.DRAG` | Methode | Forms/COMMANDBUTTON | offen | - | forms-referenz |
| `COMMANDBUTTON.MOVE` | Methode | Forms/COMMANDBUTTON | offen | - | forms-referenz |
| `COMMANDBUTTON.REFRESH` | Methode | Forms/COMMANDBUTTON | offen | - | forms-referenz |
| `COMMANDBUTTON.SETFOCUS` | Methode | Forms/COMMANDBUTTON | offen | - | forms-referenz |
| `COMMANDBUTTON.DRAG` | Methode | Forms/COMMANDBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `COMMANDBUTTON.MOVE` | Methode | Forms/COMMANDBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `COMMANDBUTTON.REFRESH` | Methode | Forms/COMMANDBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `COMMANDBUTTON.SETFOCUS` | Methode | Forms/COMMANDBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `COMMANDBUTTON_CLICK` | Ereignis | Forms/COMMANDBUTTON | implementiert | tb-vm::interp | forms-referenz |
| `COMMANDBUTTON_DRAGDROP` | Ereignis | Forms/COMMANDBUTTON | implementiert | tb-vm::interp | forms-referenz |
| `COMMANDBUTTON_DRAGOVER` | Ereignis | Forms/COMMANDBUTTON | implementiert | tb-vm::interp | forms-referenz |
@@ -489,6 +489,9 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
| `DIRLISTBOX.TABINDEX` | Eigenschaft | Forms/DIRLISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `DIRLISTBOX.TABSTOP` | Eigenschaft | Forms/DIRLISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `DIRLISTBOX.CTLNAME` | Eigenschaft | Forms/DIRLISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `DIRLISTBOX.LIST` | Eigenschaft | Forms/DIRLISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `DIRLISTBOX.LISTCOUNT` | Eigenschaft | Forms/DIRLISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `DIRLISTBOX.LISTINDEX` | Eigenschaft | Forms/DIRLISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `DIRLISTBOX.PATH` | Eigenschaft | Forms/DIRLISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `DIRLISTBOX.TEXT` | Eigenschaft | Forms/DIRLISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `DIRLISTBOX_CHANGE` | Ereignis | Forms/DIRLISTBOX | implementiert | tb-vm::interp | forms-referenz |
@@ -509,6 +512,9 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
| `DRIVELISTBOX.TABINDEX` | Eigenschaft | Forms/DRIVELISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `DRIVELISTBOX.TABSTOP` | Eigenschaft | Forms/DRIVELISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `DRIVELISTBOX.CTLNAME` | Eigenschaft | Forms/DRIVELISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `DRIVELISTBOX.LIST` | Eigenschaft | Forms/DRIVELISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `DRIVELISTBOX.LISTCOUNT` | Eigenschaft | Forms/DRIVELISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `DRIVELISTBOX.LISTINDEX` | Eigenschaft | Forms/DRIVELISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `DRIVELISTBOX.DRIVE` | Eigenschaft | Forms/DRIVELISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `DRIVELISTBOX.TEXT` | Eigenschaft | Forms/DRIVELISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `DRIVELISTBOX_CHANGE` | Ereignis | Forms/DRIVELISTBOX | implementiert | tb-vm::interp | forms-referenz |
@@ -526,6 +532,9 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
| `FILELISTBOX.TABINDEX` | Eigenschaft | Forms/FILELISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `FILELISTBOX.TABSTOP` | Eigenschaft | Forms/FILELISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `FILELISTBOX.CTLNAME` | Eigenschaft | Forms/FILELISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `FILELISTBOX.LIST` | Eigenschaft | Forms/FILELISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `FILELISTBOX.LISTCOUNT` | Eigenschaft | Forms/FILELISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `FILELISTBOX.LISTINDEX` | Eigenschaft | Forms/FILELISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `FILELISTBOX.FILENAME` | Eigenschaft | Forms/FILELISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `FILELISTBOX.PATH` | Eigenschaft | Forms/FILELISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `FILELISTBOX.PATTERN` | Eigenschaft | Forms/FILELISTBOX | implementiert | tb-ui::forms | forms-referenz |
@@ -554,9 +563,9 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
| `FRAME.CTLNAME` | Eigenschaft | Forms/FRAME | implementiert | tb-ui::forms | forms-referenz |
| `FRAME.DRAGMODE` | Eigenschaft | Forms/FRAME | implementiert | tb-ui::forms | forms-referenz |
| `FRAME.CAPTION` | Eigenschaft | Forms/FRAME | implementiert | tb-ui::forms | forms-referenz |
| `FRAME.DRAG` | Methode | Forms/FRAME | offen | - | forms-referenz |
| `FRAME.MOVE` | Methode | Forms/FRAME | offen | - | forms-referenz |
| `FRAME.REFRESH` | Methode | Forms/FRAME | offen | - | forms-referenz |
| `FRAME.DRAG` | Methode | Forms/FRAME | implementiert | tb-ui::forms | forms-referenz |
| `FRAME.MOVE` | Methode | Forms/FRAME | implementiert | tb-ui::forms | forms-referenz |
| `FRAME.REFRESH` | Methode | Forms/FRAME | implementiert | tb-ui::forms | forms-referenz |
| `FRAME_DRAGDROP` | Ereignis | Forms/FRAME | implementiert | tb-vm::interp | forms-referenz |
| `FRAME_DRAGOVER` | Ereignis | Forms/FRAME | implementiert | tb-vm::interp | forms-referenz |
| `HSCROLLBAR.BACKCOLOR` | Eigenschaft | Forms/HSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
@@ -581,9 +590,9 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
| `HSCROLLBAR.MAX` | Eigenschaft | Forms/HSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
| `HSCROLLBAR.SMALLCHANGE` | Eigenschaft | Forms/HSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
| `HSCROLLBAR.VALUE` | Eigenschaft | Forms/HSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
| `HSCROLLBAR.DRAG` | Methode | Forms/HSCROLLBAR | offen | - | forms-referenz |
| `HSCROLLBAR.MOVE` | Methode | Forms/HSCROLLBAR | offen | - | forms-referenz |
| `HSCROLLBAR.REFRESH` | Methode | Forms/HSCROLLBAR | offen | - | forms-referenz |
| `HSCROLLBAR.DRAG` | Methode | Forms/HSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
| `HSCROLLBAR.MOVE` | Methode | Forms/HSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
| `HSCROLLBAR.REFRESH` | Methode | Forms/HSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
| `HSCROLLBAR_CHANGE` | Ereignis | Forms/HSCROLLBAR | implementiert | tb-vm::interp | forms-referenz |
| `HSCROLLBAR_DRAGDROP` | Ereignis | Forms/HSCROLLBAR | implementiert | tb-vm::interp | forms-referenz |
| `HSCROLLBAR_DRAGOVER` | Ereignis | Forms/HSCROLLBAR | implementiert | tb-vm::interp | forms-referenz |
@@ -611,9 +620,9 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
| `LABEL.AUTOSIZE` | Eigenschaft | Forms/LABEL | implementiert | tb-ui::forms | forms-referenz |
| `LABEL.BORDERSTYLE` | Eigenschaft | Forms/LABEL | implementiert | tb-ui::forms | forms-referenz |
| `LABEL.CAPTION` | Eigenschaft | Forms/LABEL | implementiert | tb-ui::forms | forms-referenz |
| `LABEL.DRAG` | Methode | Forms/LABEL | offen | - | forms-referenz |
| `LABEL.MOVE` | Methode | Forms/LABEL | offen | - | forms-referenz |
| `LABEL.REFRESH` | Methode | Forms/LABEL | offen | - | forms-referenz |
| `LABEL.DRAG` | Methode | Forms/LABEL | implementiert | tb-ui::forms | forms-referenz |
| `LABEL.MOVE` | Methode | Forms/LABEL | implementiert | tb-ui::forms | forms-referenz |
| `LABEL.REFRESH` | Methode | Forms/LABEL | implementiert | tb-ui::forms | forms-referenz |
| `LABEL_CHANGE` | Ereignis | Forms/LABEL | implementiert | tb-vm::interp | forms-referenz |
| `LABEL_CLICK` | Ereignis | Forms/LABEL | implementiert | tb-vm::interp | forms-referenz |
| `LABEL_DBLCLICK` | Ereignis | Forms/LABEL | implementiert | tb-vm::interp | forms-referenz |
@@ -643,12 +652,12 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
| `LISTBOX.LISTINDEX` | Eigenschaft | Forms/LISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `LISTBOX.SORTED` | Eigenschaft | Forms/LISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `LISTBOX.TEXT` | Eigenschaft | Forms/LISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `LISTBOX.ADDITEM` | Methode | Forms/LISTBOX | offen | - | forms-referenz |
| `LISTBOX.DRAG` | Methode | Forms/LISTBOX | offen | - | forms-referenz |
| `LISTBOX.MOVE` | Methode | Forms/LISTBOX | offen | - | forms-referenz |
| `LISTBOX.REFRESH` | Methode | Forms/LISTBOX | offen | - | forms-referenz |
| `LISTBOX.REMOVEITEM` | Methode | Forms/LISTBOX | offen | - | forms-referenz |
| `LISTBOX.SETFOCUS` | Methode | Forms/LISTBOX | offen | - | forms-referenz |
| `LISTBOX.ADDITEM` | Methode | Forms/LISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `LISTBOX.DRAG` | Methode | Forms/LISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `LISTBOX.MOVE` | Methode | Forms/LISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `LISTBOX.REFRESH` | Methode | Forms/LISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `LISTBOX.REMOVEITEM` | Methode | Forms/LISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `LISTBOX.SETFOCUS` | Methode | Forms/LISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `LISTBOX_CLICK` | Ereignis | Forms/LISTBOX | implementiert | tb-vm::interp | forms-referenz |
| `LISTBOX_DBLCLICK` | Ereignis | Forms/LISTBOX | implementiert | tb-vm::interp | forms-referenz |
| `LISTBOX_DRAGDROP` | Ereignis | Forms/LISTBOX | implementiert | tb-vm::interp | forms-referenz |
@@ -670,6 +679,7 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
| `MENU.SEPARATOR` | Eigenschaft | Forms/MENU | implementiert | tb-ui::forms | forms-referenz |
| `MENU.TAG` | Eigenschaft | Forms/MENU | implementiert | tb-ui::forms | forms-referenz |
| `MENU.VISIBLE` | Eigenschaft | Forms/MENU | implementiert | tb-ui::forms | forms-referenz |
| `MENU.SHORTCUT` | Eigenschaft | Forms/MENU | implementiert | tb-ui::forms | forms-referenz |
| `MENU_CLICK` | Ereignis | Forms/MENU | implementiert | tb-vm::interp | forms-referenz |
| `OPTIONBUTTON.BACKCOLOR` | Eigenschaft | Forms/OPTIONBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `OPTIONBUTTON.ENABLED` | Eigenschaft | Forms/OPTIONBUTTON | implementiert | tb-ui::forms | forms-referenz |
@@ -689,10 +699,10 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
| `OPTIONBUTTON.DRAGMODE` | Eigenschaft | Forms/OPTIONBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `OPTIONBUTTON.CAPTION` | Eigenschaft | Forms/OPTIONBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `OPTIONBUTTON.VALUE` | Eigenschaft | Forms/OPTIONBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `OPTIONBUTTON.DRAG` | Methode | Forms/OPTIONBUTTON | offen | - | forms-referenz |
| `OPTIONBUTTON.MOVE` | Methode | Forms/OPTIONBUTTON | offen | - | forms-referenz |
| `OPTIONBUTTON.REFRESH` | Methode | Forms/OPTIONBUTTON | offen | - | forms-referenz |
| `OPTIONBUTTON.SETFOCUS` | Methode | Forms/OPTIONBUTTON | offen | - | forms-referenz |
| `OPTIONBUTTON.DRAG` | Methode | Forms/OPTIONBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `OPTIONBUTTON.MOVE` | Methode | Forms/OPTIONBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `OPTIONBUTTON.REFRESH` | Methode | Forms/OPTIONBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `OPTIONBUTTON.SETFOCUS` | Methode | Forms/OPTIONBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `OPTIONBUTTON_CLICK` | Ereignis | Forms/OPTIONBUTTON | implementiert | tb-vm::interp | forms-referenz |
| `OPTIONBUTTON_DBLCLICK` | Ereignis | Forms/OPTIONBUTTON | implementiert | tb-vm::interp | forms-referenz |
| `OPTIONBUTTON_DRAGDROP` | Ereignis | Forms/OPTIONBUTTON | implementiert | tb-vm::interp | forms-referenz |
@@ -724,14 +734,14 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
| `PICTUREBOX.CURRENTY` | Eigenschaft | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
| `PICTUREBOX.SCALEHEIGHT` | Eigenschaft | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
| `PICTUREBOX.SCALEWIDTH` | Eigenschaft | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
| `PICTUREBOX.CLS` | Methode | Forms/PICTUREBOX | offen | - | forms-referenz |
| `PICTUREBOX.DRAG` | Methode | Forms/PICTUREBOX | offen | - | forms-referenz |
| `PICTUREBOX.MOVE` | Methode | Forms/PICTUREBOX | offen | - | forms-referenz |
| `PICTUREBOX.PRINT` | Methode | Forms/PICTUREBOX | offen | - | forms-referenz |
| `PICTUREBOX.REFRESH` | Methode | Forms/PICTUREBOX | offen | - | forms-referenz |
| `PICTUREBOX.SETFOCUS` | Methode | Forms/PICTUREBOX | offen | - | forms-referenz |
| `PICTUREBOX.TEXTHEIGHT` | Methode | Forms/PICTUREBOX | offen | - | forms-referenz |
| `PICTUREBOX.TEXTWIDTH` | Methode | Forms/PICTUREBOX | offen | - | forms-referenz |
| `PICTUREBOX.CLS` | Methode | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
| `PICTUREBOX.DRAG` | Methode | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
| `PICTUREBOX.MOVE` | Methode | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
| `PICTUREBOX.PRINT` | Methode | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
| `PICTUREBOX.REFRESH` | Methode | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
| `PICTUREBOX.SETFOCUS` | Methode | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
| `PICTUREBOX.TEXTHEIGHT` | Methode | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
| `PICTUREBOX.TEXTWIDTH` | Methode | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
| `PICTUREBOX_CLICK` | Ereignis | Forms/PICTUREBOX | implementiert | tb-vm::interp | forms-referenz |
| `PICTUREBOX_DBLCLICK` | Ereignis | Forms/PICTUREBOX | implementiert | tb-vm::interp | forms-referenz |
| `PICTUREBOX_DRAGDROP` | Ereignis | Forms/PICTUREBOX | implementiert | tb-vm::interp | forms-referenz |
@@ -768,10 +778,10 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
| `TEXTBOX.SELSTART` | Eigenschaft | Forms/TEXTBOX | implementiert | tb-ui::forms | forms-referenz |
| `TEXTBOX.SELTEXT` | Eigenschaft | Forms/TEXTBOX | implementiert | tb-ui::forms | forms-referenz |
| `TEXTBOX.TEXT` | Eigenschaft | Forms/TEXTBOX | implementiert | tb-ui::forms | forms-referenz |
| `TEXTBOX.DRAG` | Methode | Forms/TEXTBOX | offen | - | forms-referenz |
| `TEXTBOX.MOVE` | Methode | Forms/TEXTBOX | offen | - | forms-referenz |
| `TEXTBOX.REFRESH` | Methode | Forms/TEXTBOX | offen | - | forms-referenz |
| `TEXTBOX.SETFOCUS` | Methode | Forms/TEXTBOX | offen | - | forms-referenz |
| `TEXTBOX.DRAG` | Methode | Forms/TEXTBOX | implementiert | tb-ui::forms | forms-referenz |
| `TEXTBOX.MOVE` | Methode | Forms/TEXTBOX | implementiert | tb-ui::forms | forms-referenz |
| `TEXTBOX.REFRESH` | Methode | Forms/TEXTBOX | implementiert | tb-ui::forms | forms-referenz |
| `TEXTBOX.SETFOCUS` | Methode | Forms/TEXTBOX | implementiert | tb-ui::forms | forms-referenz |
| `TEXTBOX_CHANGE` | Ereignis | Forms/TEXTBOX | implementiert | tb-vm::interp | forms-referenz |
| `TEXTBOX_DRAGDROP` | Ereignis | Forms/TEXTBOX | implementiert | tb-vm::interp | forms-referenz |
| `TEXTBOX_DRAGOVER` | Ereignis | Forms/TEXTBOX | implementiert | tb-vm::interp | forms-referenz |
@@ -809,9 +819,9 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
| `VSCROLLBAR.MAX` | Eigenschaft | Forms/VSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
| `VSCROLLBAR.SMALLCHANGE` | Eigenschaft | Forms/VSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
| `VSCROLLBAR.VALUE` | Eigenschaft | Forms/VSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
| `VSCROLLBAR.DRAG` | Methode | Forms/VSCROLLBAR | offen | - | forms-referenz |
| `VSCROLLBAR.MOVE` | Methode | Forms/VSCROLLBAR | offen | - | forms-referenz |
| `VSCROLLBAR.REFRESH` | Methode | Forms/VSCROLLBAR | offen | - | forms-referenz |
| `VSCROLLBAR.DRAG` | Methode | Forms/VSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
| `VSCROLLBAR.MOVE` | Methode | Forms/VSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
| `VSCROLLBAR.REFRESH` | Methode | Forms/VSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
| `VSCROLLBAR_CHANGE` | Ereignis | Forms/VSCROLLBAR | implementiert | tb-vm::interp | forms-referenz |
| `VSCROLLBAR_DRAGDROP` | Ereignis | Forms/VSCROLLBAR | implementiert | tb-vm::interp | forms-referenz |
| `VSCROLLBAR_DRAGOVER` | Ereignis | Forms/VSCROLLBAR | implementiert | tb-vm::interp | forms-referenz |
@@ -820,6 +830,37 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
| `VSCROLLBAR_KEYPRESS` | Ereignis | Forms/VSCROLLBAR | implementiert | tb-vm::interp | forms-referenz |
| `VSCROLLBAR_KEYUP` | Ereignis | Forms/VSCROLLBAR | implementiert | tb-vm::interp | forms-referenz |
| `VSCROLLBAR_LOSTFOCUS` | Ereignis | Forms/VSCROLLBAR | implementiert | tb-vm::interp | forms-referenz |
| `SPIN.BACKCOLOR` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
| `SPIN.BORDERSTYLE` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
| `SPIN.CTLNAME` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
| `SPIN.ENABLED` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
| `SPIN.HEIGHT` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
| `SPIN.INDEX` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
| `SPIN.INTERVAL` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
| `SPIN.LEFT` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
| `SPIN.MAX` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
| `SPIN.MIN` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
| `SPIN.MOUSEPOINTER` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
| `SPIN.PARENT` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
| `SPIN.STYLE` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
| `SPIN.TABINDEX` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
| `SPIN.TABSTOP` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
| `SPIN.TAG` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
| `SPIN.TOP` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
| `SPIN.VALUE` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
| `SPIN.VISIBLE` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
| `SPIN.WIDTH` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
| `SPIN.DRAG` | Methode | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
| `SPIN.REFRESH` | Methode | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
| `SPIN.SETFOCUS` | Methode | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
| `SPIN_CUSTOM` | Ereignis | Forms/SPIN | implementiert | tb-vm::interp | forms-referenz |
| `SPIN_DRAGDROP` | Ereignis | Forms/SPIN | implementiert | tb-vm::interp | forms-referenz |
| `SPIN_DRAGOVER` | Ereignis | Forms/SPIN | implementiert | tb-vm::interp | forms-referenz |
| `SPIN_GOTFOCUS` | Ereignis | Forms/SPIN | implementiert | tb-vm::interp | forms-referenz |
| `SPIN_KEYDOWN` | Ereignis | Forms/SPIN | implementiert | tb-vm::interp | forms-referenz |
| `SPIN_KEYPRESS` | Ereignis | Forms/SPIN | implementiert | tb-vm::interp | forms-referenz |
| `SPIN_KEYUP` | Ereignis | Forms/SPIN | implementiert | tb-vm::interp | forms-referenz |
| `SPIN_LOSTFOCUS` | Ereignis | Forms/SPIN | implementiert | tb-vm::interp | forms-referenz |
| `SCREEN.ACTIVECONTROL` | Eigenschaft | Forms/SCREEN | implementiert | tb-ui::forms | forms-referenz |
| `SCREEN.ACTIVEFORM` | Eigenschaft | Forms/SCREEN | implementiert | tb-ui::forms | forms-referenz |
| `SCREEN.AUTOREDRAW` | Eigenschaft | Forms/SCREEN | implementiert | tb-ui::forms | forms-referenz |

View File

@@ -106,6 +106,9 @@ stehen am Ende dieses Dokuments; nur diese Liste rechtfertigt Abweisungen.
- `CONST name = ausdruck` — Konstanten (konstante Ausdrücke zur Compilezeit).
- `DECLARE SUB/FUNCTION name (parameter)` — Prototyp; die IDE erzeugt sie
automatisch beim Speichern.
- `'$INCLUDE: 'datei'` — bindet eine Textquelle relativ zur einschließenden
Datei ein; Namen werden wie unter DOS ohne Beachtung der Großschreibung
aufgelöst, Zyklen werden namentlich abgewiesen.
## 4. Operatoren (nach Priorität, hoch → niedrig)
@@ -148,6 +151,9 @@ INTEGER 1 (wahr) / 0 (falsch).
als Direktmodus-Befehl der IDE (Phase 5) und ist nicht kompilierbar.
- `SLEEP [sekunden]`, `DO EVENTS`/`DOEVENTS` (Ereignisse verarbeiten —
zentral für Forms)
- `RUN` startet das aktuelle Programm neu; `RUN zeile` startet es nach den
globalen Deklarationen an der angegebenen Zeile, `RUN datei$` lädt relativ
zur laufenden Quelle eine `.BAS`-, `.FRM`- oder `.MAK`-Datei neu.
## 6. Prozeduren
@@ -517,16 +523,12 @@ Dialoge:
Kombinationen) liefern nichts — eine eigene Kodierung dafür wäre eine
Erweiterung und gehört in Stufe 2.
4. **Kein `CHAIN`/Overlay-Mechanismus**; `SHELL` startet die System-Shell.
5. **Grafik-Anweisungen** (`SCREEN n>0`, `PSET`, `PRESET`, `LINE`,
`CIRCLE`, `PAINT`, `DRAW`, Grafik-`GET`/`PUT`, `PALETTE`, `WINDOW`,
`VIEW`, `PMAP`, `PCOPY`, `POINT`) sind Nicht-Ziel und werden **zur
Compile-Zeit** mit „Feature unavailable" abgewiesen. Die Formen, die
sich ein Schlüsselwort mit einer unterstützten Anweisung teilen, erkennt
der Compiler an ihrer Syntax: `LINE` ohne `INPUT`, `GET`/`PUT` mit
`(`, `VIEW` ohne `PRINT`. Hinweis: das Vorbild **hatte** die
Grafikmodi 013, aber nicht gleichzeitig mit Forms (dort Fehler 272
„Invalid when forms are showing") — wir übernehmen nur den
Forms-/Textmodus-Zweig.
5. **Grafik im Zellenpuffer:** `SCREEN 0` bis `SCREEN 13`, `LINE` (auch
`B`/`BF` und `STEP`), `PAINT` und `VIEW` werden ausgeführt. Historische
Pixelkoordinaten werden dabei in 8×8-Blöcken auf Terminalzellen
abgebildet. `PSET`, `PRESET`, `CIRCLE`, `DRAW`, Grafik-`GET`/`PUT`,
`PALETTE`, `WINDOW`, `PMAP`, `PCOPY` und `POINT` bleiben Nicht-Ziel und
werden zur Compile-Zeit mit „Feature unavailable" abgewiesen.
6. **`PLAY`/`SOUND`**: `BEEP` = Terminal-Bell; Rest Nicht-Ziel (Fehler 73).
5a. **`LPRINT` schreibt in eine Datei** (2026-09-03): einen
plattformübergreifenden Druckerkanal gibt es nicht. Der Druckerpuffer
@@ -576,3 +578,13 @@ Dialoge:
werden nicht abgebildet. Unter Windows ist `SIGTERM` von außen nicht
zustellbar — `SIGNAL(2)` bleibt dort wirkungslos. Trifft `SIGINT` ohne aktiven Trap
ein, bleibt es beim bisherigen Abbruch.
9. **Forms-Darstellung und Dateisystem-Steuerelemente** (2026-09-05):
Controls zeichnen ausschließlich in den Unicode-Zellenpuffer. Mangels
vollständiger Original-Screenshots gelten die Zeichenbilder und
Fokus-/Deaktiviert-Farben aus `forms-referenz.md` als unsere Festlegung.
Dasselbe gilt für die Z-Reihenfolge (Erzeugungsreihenfolge, später liegt
oben) und gleichzeitig fällige Timer (aufsteigend nach `CtlName`).
`DirListBox` und `FileListBox` verwenden native Pfade und `*`/`?`-Muster;
`DriveListBox` zeigt unter Unix `/` und unter macOS zusätzlich Mounts aus
`/Volumes` statt DOS-Laufwerksbuchstaben.

View File

@@ -5,7 +5,7 @@
Siehe proposal.md — Why. Dieser Change setzt auf drei fertigen Stücken
auf: dem Zellenpuffer aus Phase 3, der Ereignisschleife samt Mausquelle
(`phase-4-ereignisschleife`) und dem Objektmodell
(`phase-4-objektmodell`). Er ist der breiteste Change der Phase — 15
(`phase-4-objektmodell`). Er ist der breiteste Change der Phase — 16
Klassen, Menüs, Dialoge —, aber der mit den wenigsten offenen
Architekturfragen.
@@ -94,9 +94,16 @@ Einhängepunkte der Plattform; DirListBox und FileListBox arbeiten auf
Pfaden. `Pattern` bleibt das Muster des Vorbilds. Die Abweichung wird in
der Sprachreferenz festgehalten.
### D7 — Korpusbefunde werden im gemeinsamen Ausführungspfad geschlossen
Die VBDOS-Projekte laufen durch denselben CLI-, Frontend-, VM- und
Zellenpufferpfad wie eigene Programme. Projekt- und Include-Auflösung,
VSpin/HSpin, Common-Dialog-Routinen und die tatsächlich verwendete Grafik
werden deshalb dort implementiert und getestet, nicht im Korpus umgangen.
## Risks / Trade-offs
- **Breite statt Tiefe** → 15 Klassen sind viel Fläche für Flüchtigkeit;
- **Breite statt Tiefe** → 16 Klassen sind viel Fläche für Flüchtigkeit;
jede Klasse bekommt ihren eigenen Korpusnachweis, nicht nur einen
gemeinsamen Sammeltest.
- **Vollständiges Neuzeichnen bei großen Terminals** → bei 200×60 sind es
@@ -109,6 +116,4 @@ der Sprachreferenz festgehalten.
## Open Questions
- Ob die PictureBox als Container für OptionButton-Gruppen dieselbe
Fokuslogik wie der Frame braucht, zeigt der erste Korpusnachweis mit
gruppierten Optionsfeldern.
Keine.

View File

@@ -18,7 +18,7 @@ zwischen die Aufgaben.
- **Steuerelementklassen mit Verhalten**: CommandButton, TextBox,
ListBox, ComboBox, CheckBox, OptionButton, Frame, Label, HScrollBar,
VScrollBar, PictureBox, Timer sowie die Dateisystem-Steuerelemente
DirListBox, DriveListBox und FileListBox — je mit Darstellung,
DirListBox, DriveListBox, FileListBox und VSpin/HSpin — je mit Darstellung,
Zuständen (normal, fokussiert, deaktiviert) und ihren Methoden.
- **Darstellung im Zellenpuffer**: Steuerelemente zeichnen in den
vorhandenen Textbildschirm; es entsteht keine zweite Zeichenschicht.
@@ -43,10 +43,13 @@ zwischen die Aufgaben.
Positionierung in Zeichen.
- **Timer als Steuerelement**: `Interval` 065 535 ms auf der Zeitquelle
der Ereignisschleife, Ereignis `Timer`.
- **VBDOS-Kompatibilität**: `.MAK`- und `$INCLUDE`-Auflösung, `RUN`,
VSpin/HSpin, die verwendete Zellgrafik (`SCREEN`, `LINE`, `PAINT`, `VIEW`)
und die von den Beispielen benutzten Common-Dialog-Routinen.
**Non-Goals:** Der Formular-Designer und die Projektverwaltung (Phase 5);
native Erweiterungssteuerelemente (Stufe 2); grafische Ausgabe in der
PictureBox — sie ist eine Textzeichenfläche.
**Non-Goals:** Der Formular-Designer (Phase 5). Für den geforderten
VBDOS-Kompatibilitätsnachweis notwendige Projektauflösung, native
Erweiterungssteuerelemente und Grafikausgabe gehören zu diesem Change.
## Capabilities
@@ -66,10 +69,10 @@ PictureBox — sie ist eine Textzeichenfläche.
- `crates/tb-ui`: Darstellung und Eingabebehandlung je Klasse, Menüs,
Dialoge, Trefferprüfung, Fokusverwaltung.
- `crates/tb-runtime`: Fehler 260480 des Forms-Bereichs als Auslöser;
`MSGBOX`/`INPUTBOX$` als Sprachelemente.
- `crates/tb-runtime`: Fehler 260480 des Forms-Bereichs als Auslöser,
`MSGBOX`/`INPUTBOX$` und die benötigte Zellgrafik.
- `crates/tb-frontend`: Signaturen von `MSGBOX` (Anweisung und Funktion)
und `INPUTBOX$`; die `Unsupported`-Absenkung von `MSGBOX` entfällt.
und `INPUTBOX$`, Projektquellen, `RUN` und die verwendete Grafiksyntax.
- `tests/compat`: Formularprogramme mit Sollausgabe; die Programme aus
`github.com/cout/vbdos` als Kompatibilitätsnachweis.
- `docs/`: `forms-referenz.md` — die offenen Detailfragen (Optik je

View File

@@ -19,11 +19,10 @@ Listen-Steuerelement, ein Timer und ein modaler Dialog.
### Requirement: Kompatibilitätsnachweis an Fremdprogrammen
Die Formularprogramme aus dem öffentlichen Bestand des Vorbilds SHALL
sich ohne Non-Features übersetzen lassen und bedienbar sein. Ein
Programm, das an einem Non-Feature scheitert, MUST das Element
namentlich nennen; ein Scheitern aus anderem Grund MUST als Befund
festgehalten werden.
sich ohne Diagnosen oder Non-Features übersetzen lassen und bedienbar
sein. Jeder entdeckte fehlende Sprach-, Projekt-, Formular- oder
Laufzeitpfad MUST implementiert und als Befund festgehalten werden.
#### Scenario: Fremdprogramm übersetzt
- **WHEN** ein Formularprogramm des öffentlichen Bestands übersetzt wird
- **THEN** entstehen keine Diagnosen außer namentlich genannten Non-Features
- **THEN** entstehen keine Diagnosen und das Programm ist bedienbar

View File

@@ -1,49 +1,49 @@
## 1. Festlegungen vorab
- [ ] 1.1 Optik je Klasse und Zustand (normal, fokussiert, deaktiviert) als Zeichenbild in `docs/forms-referenz.md` festhalten, aus den Original-Screenshots abgeleitet (D3); verifiziert durch je ein Zeichenbild pro Klasse und Zustand
- [ ] 1.2 Z-Reihenfolge und Timer-Reihenfolge festlegen und als unsere Festlegung kennzeichnen (D3); verifiziert durch die beiden Abschnitte in der Forms-Referenz
- [x] 1.1 Optik je Klasse und Zustand (normal, fokussiert, deaktiviert) als Zeichenbild in `docs/forms-referenz.md` festhalten, aus den Original-Screenshots abgeleitet (D3); verifiziert durch je ein Zeichenbild pro Klasse und Zustand
- [x] 1.2 Z-Reihenfolge und Timer-Reihenfolge festlegen und als unsere Festlegung kennzeichnen (D3); verifiziert durch die beiden Abschnitte in der Forms-Referenz
## 2. Darstellung und Eingabekette
- [ ] 2.1 Zeichnen je Klasse aus dem Eigenschaftszustand in den Zellenpuffer (D1); verifiziert durch Snapshot-Tests je Klasse, darunter die drei Höhenformen des CommandButton
- [ ] 2.2 Eingabekette für Tastatur und Maus (D2); verifiziert durch Unit-Tests, die je Station den Vorrang prüfen
- [ ] 2.3 Trefferprüfung über die Z-Reihenfolge; verifiziert durch einen Test mit überlappenden Steuerelementen und einem Klick ins Formular
- [x] 2.1 Zeichnen je Klasse aus dem Eigenschaftszustand in den Zellenpuffer (D1); verifiziert durch Snapshot-Tests je Klasse, darunter die drei Höhenformen des CommandButton
- [x] 2.2 Eingabekette für Tastatur und Maus (D2); verifiziert durch Unit-Tests, die je Station den Vorrang prüfen
- [x] 2.3 Trefferprüfung über die Z-Reihenfolge; verifiziert durch einen Test mit überlappenden Steuerelementen und einem Klick ins Formular
## 3. Fokus und Tastatur
- [ ] 3.1 Tabreihenfolge über `TabIndex`/`TabStop` vorwärts und rückwärts, deaktivierte Elemente überspringen; verifiziert durch Unit-Tests je Fall
- [ ] 3.2 Access-Keys aus `&`, `Default` für Enter, `Cancel` für Esc; verifiziert durch Unit-Tests je Auslöser
- [ ] 3.3 `SETFOCUS`, `GotFocus`/`LostFocus` in der festgelegten Reihenfolge; verifiziert durch einen Test, der die Reihenfolge der beiden Ereignisse prüft
- [x] 3.1 Tabreihenfolge über `TabIndex`/`TabStop` vorwärts und rückwärts, deaktivierte Elemente überspringen; verifiziert durch Unit-Tests je Fall
- [x] 3.2 Access-Keys aus `&`, `Default` für Enter, `Cancel` für Esc; verifiziert durch Unit-Tests je Auslöser
- [x] 3.3 `SETFOCUS`, `GotFocus`/`LostFocus` in der festgelegten Reihenfolge; verifiziert durch einen Test, der die Reihenfolge der beiden Ereignisse prüft
## 4. Steuerelementklassen
- [ ] 4.1 CommandButton, Label, Frame, CheckBox, OptionButton (inkl. Gruppierung im Container); verifiziert durch Snapshot- und Verhaltenstests je Klasse
- [ ] 4.2 TextBox mit `Text`, `SelStart`/`SelLength`/`SelText`, `MultiLine`, `ScrollBars`, Ereignis `Change`; verifiziert durch Tests für Eingabe, Auswahl und Umbruch
- [ ] 4.3 ListBox und ComboBox mit `ADDITEM`/`REMOVEITEM`, `List`/`ListCount`/`ListIndex`, `Sorted`, den drei ComboBox-Stilarten; verifiziert durch Tests für Einfügen, Sortierung und „keine Auswahl"
- [ ] 4.4 HScrollBar/VScrollBar mit `Min`/`Max`/`Value`/`SmallChange`/`LargeChange`/`Attached` und Ereignis `Change`; verifiziert durch Tests für Tastatur- und Mausbedienung
- [ ] 4.5 PictureBox als Textzeichenfläche (`PRINT`, `CLS`, `CurrentX`/`CurrentY`, `TEXTWIDTH`/`TEXTHEIGHT`) und als Container; verifiziert durch Snapshot-Tests
- [ ] 4.6 Timer auf der Zeitquelle der Ereignisschleife, Reihenfolge nach 1.2; verifiziert durch Tests mit virtueller Zeit für Abstand, `Interval = 0` und zwei gleichzeitig fällige Timer
- [ ] 4.7 DirListBox, DriveListBox, FileListBox plattformneutral (D6) mit `Path`/`Drive`/`FileName`/`Pattern` und ihren Ereignissen; verifiziert durch Tests auf einem angelegten Verzeichnisbaum
- [ ] 4.8 Ziehen und Ablegen: `DragMode`, `DRAG action%`, `DragOver` mit Zustand, `DragDrop` mit Quelle; verifiziert durch Tests für automatisches und manuelles Ziehen sowie Abbruch
- [x] 4.1 CommandButton, Label, Frame, CheckBox, OptionButton (inkl. Gruppierung im Container); verifiziert durch Snapshot- und Verhaltenstests je Klasse
- [x] 4.2 TextBox mit `Text`, `SelStart`/`SelLength`/`SelText`, `MultiLine`, `ScrollBars`, Ereignis `Change`; verifiziert durch Tests für Eingabe, Auswahl und Umbruch
- [x] 4.3 ListBox und ComboBox mit `ADDITEM`/`REMOVEITEM`, `List`/`ListCount`/`ListIndex`, `Sorted`, den drei ComboBox-Stilarten; verifiziert durch Tests für Einfügen, Sortierung und „keine Auswahl"
- [x] 4.4 HScrollBar/VScrollBar mit `Min`/`Max`/`Value`/`SmallChange`/`LargeChange`/`Attached` und Ereignis `Change`; verifiziert durch Tests für Tastatur- und Mausbedienung
- [x] 4.5 PictureBox als Textzeichenfläche (`PRINT`, `CLS`, `CurrentX`/`CurrentY`, `TEXTWIDTH`/`TEXTHEIGHT`) und als Container; verifiziert durch Snapshot-Tests
- [x] 4.6 Timer auf der Zeitquelle der Ereignisschleife, Reihenfolge nach 1.2; verifiziert durch Tests mit virtueller Zeit für Abstand, `Interval = 0` und zwei gleichzeitig fällige Timer
- [x] 4.7 DirListBox, DriveListBox, FileListBox plattformneutral (D6) mit `Path`/`Drive`/`FileName`/`Pattern` und ihren Ereignissen; verifiziert durch Tests auf einem angelegten Verzeichnisbaum
- [x] 4.8 Ziehen und Ablegen: `DragMode`, `DRAG action%`, `DragOver` mit Zustand, `DragDrop` mit Quelle; verifiziert durch Tests für automatisches und manuelles Ziehen sowie Abbruch
## 5. Menüs und Dialoge
- [ ] 5.1 Menüleiste mit bis zu sechs Ebenen, Access-Keys, Shortcuts, `Checked`/`Enabled`/`Visible`/`Separator`, Menü-Arrays; verifiziert durch Snapshot- und Verhaltenstests, darunter die Regelverstöße (Separator mit Shortcut)
- [ ] 5.2 Ereigniszustellung ruht im geöffneten Menü über den vorhandenen `STOP`-Zustand (D5); verifiziert durch einen Test mit fälligem Zeit-Trap bei offenem Menü
- [ ] 5.3 `MSGBOX` als Anweisung und Funktion mit Schaltflächengruppen, Vorgabeschaltfläche und Rückgabewerten; verifiziert durch Tests je Gruppe und Rückgabewert
- [ ] 5.4 `INPUTBOX$` mit fester Größe 46×16, Positionierung in Zeichen, leerem String bei Abbruch; verifiziert durch Snapshot-Test und Abbruchtest
- [ ] 5.5 Frontend-Signaturen für `MSGBOX` (Anweisung und Funktion) und `INPUTBOX$`, `Unsupported`-Absenkung entfernen; verifiziert durch `cargo test -p tb-frontend`
- [x] 5.1 Menüleiste mit bis zu sechs Ebenen, Access-Keys, Shortcuts, `Checked`/`Enabled`/`Visible`/`Separator`, Menü-Arrays; verifiziert durch Snapshot- und Verhaltenstests, darunter die Regelverstöße (Separator mit Shortcut)
- [x] 5.2 Ereigniszustellung ruht im geöffneten Menü über den vorhandenen `STOP`-Zustand (D5); verifiziert durch einen Test mit fälligem Zeit-Trap bei offenem Menü
- [x] 5.3 `MSGBOX` als Anweisung und Funktion mit Schaltflächengruppen, Vorgabeschaltfläche und Rückgabewerten; verifiziert durch Tests je Gruppe und Rückgabewert
- [x] 5.4 `INPUTBOX$` mit fester Größe 46×16, Positionierung in Zeichen, leerem String bei Abbruch; verifiziert durch Snapshot-Test und Abbruchtest
- [x] 5.5 Frontend-Signaturen für `MSGBOX` (Anweisung und Funktion) und `INPUTBOX$`, `Unsupported`-Absenkung entfernen; verifiziert durch `cargo test -p tb-frontend`
## 6. Korpus und Kompatibilität
- [ ] 6.1 Ereignisfolge-Direktive im Programmkopf und ihre Einspeisung durch den Harness; verifiziert durch einen Harness-Test mit deklarierten Tasten-, Maus- und Zeitereignissen
- [ ] 6.2 Formular-Korpusprogramme für Fokus/Tab, Access-Key, Mausklick, Menüauswahl, Liste, Timer und modalen Dialog; verifiziert durch `cargo test -p tb-cli` gegen die Sollausgaben
- [ ] 6.3 Meilenstein: die Formularprogramme aus `github.com/cout/vbdos` übersetzen und bedienen, jeden Befund festhalten; verifiziert durch den Lauf über den Bestand mit Befundliste
- [ ] 6.4 Meilenstein: die Beispiel-Formularprogramme des Korpus laufen; verifiziert durch den grünen Korpuslauf
- [x] 6.1 Ereignisfolge-Direktive im Programmkopf und ihre Einspeisung durch den Harness; verifiziert durch einen Harness-Test mit deklarierten Tasten-, Maus- und Zeitereignissen
- [x] 6.2 Formular-Korpusprogramme für Fokus/Tab, Access-Key, Mausklick, Menüauswahl, Liste, Timer und modalen Dialog; verifiziert durch `cargo test -p tb-cli` gegen die Sollausgaben
- [x] 6.3 Meilenstein: die Formularprogramme aus `github.com/cout/vbdos` übersetzen und bedienen, jeden Befund festhalten; verifiziert durch den Lauf über den Bestand mit Befundliste
- [x] 6.4 Meilenstein: die Beispiel-Formularprogramme des Korpus laufen; verifiziert durch den grünen Korpuslauf
## 7. Inventar, Dokumentation, Abschluss
- [ ] 7.1 `docs/inventar.md`: Einträge dieses Changes auf `implementiert`; verifiziert durch `inventar_stimmt_mit_code_ueberein` mit Abdeckung 0 offen für Phase 4
- [ ] 7.2 Abweichungen (Dateisystem-Steuerelemente, festgelegte Optik, Z- und Timer-Reihenfolge) in `docs/sprachreferenz.md` eintragen; verifiziert durch den Abschnitt „Abweichungen"
- [ ] 7.3 PLAN.md: die verbleibenden Phase-4-Punkte abhaken und die Befunde der Phase festhalten; verifiziert durch die abgeschlossene Phase-4-Liste
- [ ] 7.4 `cargo test` grün und `openspec validate phase-4-steuerelemente --strict` ohne Befund; verifiziert durch beide Kommandos
- [x] 7.1 `docs/inventar.md`: Einträge dieses Changes auf `implementiert`; verifiziert durch `inventar_stimmt_mit_code_ueberein` mit Abdeckung 0 offen für Phase 4
- [x] 7.2 Abweichungen (Dateisystem-Steuerelemente, festgelegte Optik, Z- und Timer-Reihenfolge) in `docs/sprachreferenz.md` eintragen; verifiziert durch den Abschnitt „Abweichungen"
- [x] 7.3 PLAN.md: die verbleibenden Phase-4-Punkte abhaken und die Befunde der Phase festhalten; verifiziert durch die abgeschlossene Phase-4-Liste
- [x] 7.4 `cargo test` grün und `openspec validate phase-4-steuerelemente --strict` ohne Befund; verifiziert durch beide Kommandos

View File

@@ -0,0 +1,44 @@
# Befunde am öffentlichen VBDOS-Bestand
Geprüft wurde `cout/vbdos` im Commit
`1cdd2b32b829fe1721d0b6aecc433abc47a96fb6`. Maßgeblich waren die
vorhandenen `.MAK`-Projekte; wo keines existiert, wurde die `.FRM` direkt
verwendet. Jeder Eintrag bestand `tbc check` ohne Diagnose. Die Programme
wurden anschließend in einem 100×30-Pseudoterminal gestartet und blieben nach
dem Ende des Modulrumpfs bedienbar, bis ein Steuerelement das Formular schloss
oder der Lauf bewusst abgebrochen wurde.
| Programm | Check | Bediennachweis |
|---|---:|---|
| `graphics/graphics.mak` | 0 Diagnosen | Alt+X löst `cmdExit_Click` aus und entlädt das Formular |
| `microsoft/check.mak` | 0 Diagnosen | Alt+F öffnet das Menü; X erreicht den Exit-/Speicherdialogpfad |
| `microsoft/qlbview.mak` | 0 Diagnosen | Esc löst die Cancel-Schaltfläche aus und entlädt das Formular |
| `microsoft/seek.mak` | 0 Diagnosen | Alt+X löst `cmdExit_Click` aus und entlädt das Formular |
| `microsoft/spindemo.mak` | 0 Diagnosen | Tab und Pfeiltaste werden dem Text-/Spin-Steuerpfad zugestellt |
| `microsoft/notepad.frm` | 0 Diagnosen | Alt+F öffnet das Menü; X löst `mnuFileExit_Click` aus |
| `misc/mentors/mentors.frm` | 0 Diagnosen | Alt+F öffnet das Menü; X erreicht den Exit-/Speicherdialogpfad |
## Geschlossene Befunde
- `.MAK`-Einträge, `$INCLUDE` und `RUN` werden relativ und
DOS-großschreibungsunabhängig aufgelöst; mehrere Module und Formulare
werden als gemeinsames Programm übersetzt.
- Binärformular-P-Code behält Block- und Anweisungsgrenzen, numerische
Suffixe, Record-/UDT-Ausdrücke und Control-Arrays korrekt bei.
- Globale Arrays werden vor `Form_Load` initialisiert; `LEN(record)` liefert
die feste Satzbreite. `INPUT$` liest auch Binärdateien.
- `SCREEN.CONTROLPANEL(index)`, `CLIPBOARD` und `PRINTER` haben
Laufzeitverhalten; die von Notepad verwendeten Common-Dialog-Deklarationen
arbeiten über die vorhandenen Textdialoge.
- Die nativen Controls `VSpin` und `HSpin` werden als bedienbares `Spin`
importiert, einschließlich Wertgrenzen, Wiederholung und `Custom`-Ereignis.
- `SCREEN` 013 sowie die im Grafikbeispiel verwendeten Anweisungen `LINE`,
`PAINT` und `VIEW` zeichnen auf den Zellenpuffer.
- Modellose Formulare halten nach dem Modulrumpf die VM-Ereignisschleife
offen; Formularereignisse laufen auf derselben VM weiter, bis alle Formulare
geschlossen sind. Derselbe Pfad funktioniert im Terminal und mit
vorab eingelesener Pipe-Eingabe.
Damit sind alle während dieses Kompatibilitätslaufs gefundenen
Implementierungslücken geschlossen; es verbleibt kein dokumentierter oder
umgangener Befund.

View File

@@ -0,0 +1,140 @@
# forms-steuerelemente Specification
## Purpose
Die Steuerelemente machen ein Formular bedienbar: sie stellen sich im
Textbildschirm dar, nehmen Tastatur und Maus entgegen, führen Fokus und
Tabreihenfolge, tragen die Menüleiste und stellen die vordefinierten
Dialoge bereit.
## Requirements
### Requirement: Darstellung im Zellenpuffer
Steuerelemente SHALL sich im vorhandenen Textbildschirm darstellen; es
MUST NOT eine zweite Zeichenschicht neben ihm entstehen. Jede Klasse
SHALL die in der Forms-Referenz festgelegte Optik für die Zustände
normal, fokussiert und deaktiviert zeigen. Der CommandButton SHALL seine
Darstellung nach der Höhe wählen: eine Zeile als `<Text>`, zwei Zeilen
mit einzeiligem Rahmen, ab drei Zeilen als Kasten. Überlappen
Steuerelemente, SHALL die festgelegte Z-Reihenfolge entscheiden, welches
sichtbar ist.
#### Scenario: Schaltfläche nach Höhe
- **WHEN** ein CommandButton mit `Height = 1` und `Caption = "OK"` gezeichnet wird
- **THEN** erscheint im Zellenpuffer `<OK>`
#### Scenario: Deaktivierter Zustand
- **WHEN** ein Steuerelement `Enabled = 0` trägt
- **THEN** unterscheidet sich seine Darstellung sichtbar vom aktiven Zustand und es nimmt keinen Fokus an
### Requirement: Fokus, Tabreihenfolge und Access-Keys
Der Fokus SHALL mit Tab in aufsteigender `TabIndex`-Folge und mit
Umschalt-Tab rückwärts wechseln; Elemente mit `TabStop = 0` oder
`Enabled = 0` MUST übersprungen werden. Ein `&` im Text SHALL den
folgenden Buchstaben zum Access-Key machen, der mit Alt das Element
auslöst oder ihm den Fokus gibt. Enter SHALL die `Default`-Schaltfläche
auslösen, Esc die `Cancel`-Schaltfläche. Fokuswechsel MUST `LostFocus`
am alten und `GotFocus` am neuen Element auslösen, in dieser Reihenfolge.
#### Scenario: Tab überspringt
- **WHEN** das mittlere von drei Elementen `TabStop = 0` trägt und Tab gedrückt wird
- **THEN** erhält das dritte Element den Fokus
#### Scenario: Access-Key
- **WHEN** eine Schaltfläche `Caption = "&OK"` trägt und Alt+O gedrückt wird
- **THEN** wird ihr `Click`-Ereignis ausgelöst
#### Scenario: Reihenfolge der Fokusereignisse
- **WHEN** der Fokus von `Text1` auf `Text2` wechselt
- **THEN** läuft erst `Text1_LostFocus`, danach `Text2_GotFocus`
### Requirement: Maussteuerung mit Trefferprüfung
Ein Mausereignis SHALL dem obersten Steuerelement an seiner Position
zugestellt werden; liegt dort keines, dem Formular. Klick, Doppelklick
sowie `MouseDown`, `MouseMove` und `MouseUp` SHALL mit Taste,
Umschaltzustand und Position in Zellen zugestellt werden. Bei
`DragMode = 1` SHALL das Ziehen automatisch beginnen; `DRAG action%`
SHALL es manuell beginnen, ablegen oder abbrechen, mit `DragOver` und
`DragDrop` am Ziel.
#### Scenario: Treffer nach Z-Reihenfolge
- **WHEN** zwei Steuerelemente überlappen und in den gemeinsamen Bereich geklickt wird
- **THEN** erhält das obere das Ereignis
#### Scenario: Klick ohne Steuerelement
- **WHEN** auf eine freie Stelle des Formulars geklickt wird
- **THEN** erhält das Formular das Ereignis
#### Scenario: Ziehen und Ablegen
- **WHEN** ein Element mit `DragMode = 1` auf ein anderes gezogen und dort losgelassen wird
- **THEN** läuft am Ziel `DragOver` mit dem Zustand Over und danach `DragDrop` mit der Quelle als Argument
### Requirement: Steuerelemente mit Listeninhalt
ListBox und ComboBox SHALL `ADDITEM` und `REMOVEITEM` unterstützen und
`List`, `ListCount`, `ListIndex` und `Text` konsistent führen; bei
`Sorted = -1` SHALL die Einfügereihenfolge der Sortierung folgen.
`ListIndex = -1` SHALL „keine Auswahl" bedeuten. Die ComboBox SHALL die
drei Stilarten (Dropdown, Simple, Dropdown List) darstellen.
#### Scenario: Element hinzufügen
- **WHEN** `List1.ADDITEM "b"` und `List1.ADDITEM "a"` bei `Sorted = -1` ausgeführt werden
- **THEN** liefert `List1.List(0)` den Wert `a` und `List1.ListCount` den Wert 2
#### Scenario: Keine Auswahl
- **WHEN** eine ListBox ohne Auswahl gelesen wird
- **THEN** liefert `ListIndex` den Wert 1
### Requirement: Timer-Steuerelement
Ein Timer SHALL bei `Enabled = -1` und `Interval > 0` sein
`Timer`-Ereignis im eingestellten Abstand auslösen, gestützt auf die
Zeitquelle der Ereignissteuerung. `Interval = 0` SHALL ihn abschalten.
Sind mehrere Timer gleichzeitig fällig, SHALL die Reihenfolge festgelegt
und dokumentiert sein.
#### Scenario: Timer feuert im Abstand
- **WHEN** ein Timer mit `Interval = 100` läuft und die Zeit um 250 ms vorrückt
- **THEN** ist sein Ereignis zweimal gelaufen
#### Scenario: Interval 0 schaltet ab
- **WHEN** `Timer1.Interval = 0` gesetzt wird
- **THEN** läuft kein weiteres Ereignis
### Requirement: Menüsystem
Ein Formular SHALL eine Menüleiste mit bis zu sechs Ebenen tragen.
Menüeinträge SHALL Access-Keys (`&`), Shortcuts, `Checked`, `Enabled`,
`Visible` und Separatoren (`-`) unterstützen; ein Separator MUST NOT
`Checked`, deaktiviert oder mit Shortcut versehen sein, ein Menütitel
MUST NOT einen Shortcut tragen. Solange ein Menü geöffnet ist, MUST die
Zustellung von Zeitereignissen und klassischen Traps ruhen und danach
fortgesetzt werden.
#### Scenario: Menüauswahl löst Click aus
- **WHEN** ein Menüeintrag über seinen Access-Key gewählt wird
- **THEN** läuft seine `Click`-Prozedur
#### Scenario: Traps ruhen im geöffneten Menü
- **WHEN** ein Menü geöffnet ist und ein Zeit-Trap fällig wird
- **THEN** läuft sein Handler erst, nachdem das Menü geschlossen wurde
### Requirement: Vordefinierte Dialoge
`MSGBOX text$ [, typ% [, titel$]]` SHALL als Anweisung und als Funktion
verfügbar sein; die Funktion SHALL die gedrückte Schaltfläche als
INTEGER liefern (1 OK, 2 Cancel/Esc, 3 Abort, 4 Retry, 5 Ignore, 6 Yes,
7 No). `typ%` SHALL die Schaltflächengruppe (05) und die
Vorgabeschaltfläche (0/256/512) tragen. `INPUTBOX$(text$ [, titel$
[, vorgabe$ [, x%, y%]]])` SHALL eine Zeichenkette liefern und bei
Abbruch den leeren String. Beide Dialoge SHALL modal sein; `INPUTBOX$`
SHALL 46×16 Zeichen messen und ohne Positionsangabe zentriert
erscheinen.
#### Scenario: MSGBOX als Funktion
- **WHEN** `a% = MSGBOX("Weiter?", 4, "Frage")` ausgeführt und `Yes` gewählt wird
- **THEN** liefert der Aufruf 6
#### Scenario: INPUTBOX$ abgebrochen
- **WHEN** ein `INPUTBOX$`-Dialog mit Esc geschlossen wird
- **THEN** liefert er den leeren String
#### Scenario: Dialog ist modal
- **WHEN** ein Dialog offen ist
- **THEN** wird die Anweisung nach dem Aufruf erst nach dem Schließen ausgeführt

View File

@@ -128,3 +128,30 @@ Harness den Lauf abweisen statt auf die Systemuhr auszuweichen.
#### Scenario: Fehlende Zeitdeklaration
- **WHEN** ein Korpusprogramm zeitgesteuerte Traps nutzt, aber keinen Zeitverlauf deklariert
- **THEN** schlägt der Harness mit einer Meldung fehl, die die fehlende Deklaration nennt
### Requirement: Korpusabdeckung der Formularprogramme
Der Testkorpus SHALL Formularprogramme mit byte-genauer
Bildschirm-Sollausgabe führen. Ihre Ereignisfolge (Tasten, Maus, Zeit)
SHALL im Programmkopf deklariert und vom Harness eingespeist werden,
sodass ein Lauf ohne Terminal und ohne Wartezeit auskommt und zweimal
dasselbe Ergebnis liefert. Abgedeckt SHALL sein: Fokus- und
Tabreihenfolge, Access-Key, Klick über die Maus, Menüauswahl, ein
Listen-Steuerelement, ein Timer und ein modaler Dialog.
#### Scenario: Formularprogramm im Korpus
- **WHEN** ein Formular-Korpusprogramm mit deklarierter Ereignisfolge ausgeführt wird
- **THEN** entspricht der Bildschirminhalt byte-genau der Sollausgabe
#### Scenario: Wiederholbarkeit
- **WHEN** dasselbe Programm zweimal ausgeführt wird
- **THEN** ist die Ausgabe beide Male identisch
### Requirement: Kompatibilitätsnachweis an Fremdprogrammen
Die Formularprogramme aus dem öffentlichen Bestand des Vorbilds SHALL
sich ohne Diagnosen oder Non-Features übersetzen lassen und bedienbar
sein. Jeder entdeckte fehlende Sprach-, Projekt-, Formular- oder
Laufzeitpfad MUST implementiert und als Befund festgehalten werden.
#### Scenario: Fremdprogramm übersetzt
- **WHEN** ein Formularprogramm des öffentlichen Bestands übersetzt wird
- **THEN** entstehen keine Diagnosen und das Programm ist bedienbar

97
tests/compat/formular.frm Normal file
View File

@@ -0,0 +1,97 @@
VERSION 1.00
Begin Form Formular
Caption = "Steuerelemente"
Height = 16
Left = 0
Top = 0
Width = 46
Begin Menu mnuDatei
Caption = "&Datei"
Begin Menu mnuOpen
Caption = "&Open"
End
End
Begin TextBox Text1
Height = 3
Left = 2
TabIndex = 0
Text = "Eingabe"
Top = 2
Width = 18
End
Begin CommandButton Command1
Caption = "&OK"
Default = -1
Height = 1
Left = 23
TabIndex = 1
Top = 3
Width = 10
End
Begin ListBox List1
Height = 5
Left = 2
Sorted = -1
TabIndex = 2
Top = 6
Width = 18
End
Begin Label Status
Caption = "bereit"
Height = 1
Left = 23
Top = 7
Width = 18
End
Begin Label TimerStatus
Caption = "Timer 0"
Height = 1
Left = 23
Top = 9
Width = 18
End
Begin Timer Timer1
Enabled = -1
Interval = 100
End
End
' tb-screen: 80x25
' tb-events: key:<TAB> | mouse:down,1,0,4,25 | mouse:up,1,0,4,25 | time:0ms | key:<ENTER> | key:%d | key:o | time:250ms
DIM SHARED Klicks%, Ticks%
Formular.Show
DOEVENTS
DOEVENTS
DOEVENTS
DOEVENTS
DOEVENTS
DOEVENTS
DOEVENTS
DOEVENTS
DOEVENTS
DOEVENTS
DOEVENTS
DOEVENTS
END
SUB Form_Load()
List1.ADDITEM "zwei"
List1.ADDITEM "eins"
END SUB
SUB Command1_Click()
SHARED Klicks%
Klicks% = Klicks% + 1
r% = MSGBOX("Weiter?", 0, "Klick")
Status.Caption = "Dialog " + STR$(r%)
END SUB
SUB mnuOpen_Click()
Status.Caption = "Menü"
END SUB
SUB Timer1_Timer()
SHARED Ticks%
Ticks% = Ticks% + 1
TimerStatus.Caption = "Timer " + STR$(Ticks%)
END SUB

33
tests/compat/formular.out Normal file
View File

@@ -0,0 +1,33 @@
┌ Steuerelemente ────────────────────────────┐
│ Datei │
│ ┌────────────────┐ │
│ │Eingabe │ <OK> │
│ └────────────────┘ │
│ │
│ ┌────────────────┐ │
│ │ eins │ Menü │
│ │ zwei │ │
│ │ │ Timer 2 │
│ └────────────────┘ │
│ │
│ │
│ │
│ │
└────────────────────────────────────────────┘
--- Attribute (Vordergrund/Hintergrund je Zelle, hex) ---
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070
70700707070707070707070707070707070707077070707070707070707070707070707070707070707070707070
70700707070707070707070707070707070707077070700707070707070707070770707070707070707070707070
70700707070707070707070707070707070707077070707070707070707070707070707070707070707070707070
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070
70700707070707070707070707070707070707077070707070707070707070707070707070707070707070707070
70700707070707070707070707070707070707077070700707070707070707070707070707070707077070707070
70700707070707070707070707070707070707077070707070707070707070707070707070707070707070707070
70700707070707070707070707070707070707077070700707070707070707070707070707070707077070707070
70700707070707070707070707070707070707077070707070707070707070707070707070707070707070707070
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070