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. Transaktion verfällt.
## Phase 4 — Forms-Engine (`tb-ui::forms`) ## 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- - [x] Eingangsaufgabe (aus Phase 0 übernommen): Ereignisschleifen-
Architektur — Terminal-Events → Event-Queue → VM-Ticks, kooperative Architektur — Terminal-Events → Event-Queue → VM-Ticks, kooperative
Zustellpunkte (`DOEVENTS`, `SLEEP`, blockierende Eingabe, Ende einer 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. Forms-Ereignisprozeduren verwenden später denselben Rückrufweg.
- [x] Formular-Modell: Eigenschaften, Lade-/Entladezyklus, `SHOW`/`HIDE` - [x] Formular-Modell: Eigenschaften, Lade-/Entladezyklus, `SHOW`/`HIDE`
(modal/nicht-modal) (modal/nicht-modal)
- [ ] Steuerelemente: CommandButton, TextBox, ListBox, ComboBox, CheckBox, - [x] Steuerelemente: CommandButton, TextBox, ListBox, ComboBox, CheckBox,
OptionButton, Frame, Label, HScrollBar/VScrollBar, PictureBox (Text), OptionButton, Frame, Label, HScrollBar/VScrollBar, PictureBox (Text),
Timer — mit allen Eigenschaften/Methoden/Ereignissen des Vorbilds Timer und VSpin/HSpin — mit ihren Eigenschaften, Methoden und
- [ ] Menüsystem (Menüleiste, Shortcuts, Access Keys) Ereignissen
- [ ] Fokus-/Tab-Reihenfolge, Access-Keys, Maussteuerung - [x] Menüsystem (Menüleiste, Shortcuts, Access Keys)
- [x] Fokus-/Tab-Reihenfolge, Access-Keys, Maussteuerung
- [x] `.FRM`-Textformat: Serialisierung **definieren** (kein Original- - [x] `.FRM`-Textformat: Serialisierung **definieren** (kein Original-
Beispiel verfügbar — Windows-1.0-Schema, siehe dateiformate.md), Beispiel verfügbar — Windows-1.0-Schema, siehe dateiformate.md),
dokumentieren, dann lesen/schreiben implementieren 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 (Gegenstück zu FT.EXE des Vorbilds; Magic `FC 08 01 00`): als
`tbc convert-frm`. Format per Reverse Engineering aus den `tbc convert-frm`. Format per Reverse Engineering aus den
Beispieldateien des Originalpakets und des cout/vbdos-Repos 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 https://github.com/cout/vbdos ohne Non-Features kompilieren und
sind nutzbar (Konsolenprogramme bereits ab Phase 3) 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] Ereignisdispatch: Event-Queue ↔ VM (Ereignisprozeduren `Name_Ereignis`)
- [x] **Klassische Ereignis-Traps** (ohne Forms, Sprachreferenz §8) — - [x] **Klassische Ereignis-Traps** (ohne Forms, Sprachreferenz §8) —
`ON TIMER(n)`, `ON KEY(n)`, `ON UEVENT` und `ON SIGNAL(n)` je mit `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** (Fehler 73). **10 Inventareinträge**
Befund: `EVENT` besitzt nur `ON`/`OFF`; `SIGNAL(1/2)` bildet Befund: `EVENT` besitzt nur `ON`/`OFF`; `SIGNAL(1/2)` bildet
plattformübergreifend `SIGINT`/`SIGTERM` ab. 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$`. Funktion (liefert die gedrückte Schaltfläche) sowie `INPUTBOX$`.
Sie hängen am Formularmodell, nicht an der Steuerelementliste, und Sie hängen am Formularmodell, nicht an der Steuerelementliste, und
fallen deshalb sonst zwischen die Aufgaben. **3 Inventareinträge** 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`) ## Phase 5 — IDE (`tb-ide`)
- [ ] IDE-Rahmen: Menüleiste, MDI-artige Fensterverwaltung, Statuszeile, - [ ] 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, - [ ] Formular-Designer: Steuerelemente platzieren/verschieben/skalieren,
Eigenschaftenfenster Eigenschaftenfenster
- [ ] Ausführen aus der IDE: Start/Unterbrechen/Fortsetzen/Neustart - [ ] 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 Metabefehl bereits als Token. Datei einlesen, Pfade relativ zur
einschließenden Datei, Zyklen erkennen und namentlich abweisen. einschließenden Datei, Zyklen erkennen und namentlich abweisen.
**1 Inventareintrag** **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. Nutzt denselben Zurücksetzpfad der VM wie „Neustart" oben.
**1 Inventareintrag** **1 Inventareintrag**
- [ ] Debugger: Breakpoints, Einzelschritt/Prozedurschritt, Direktfenster, - [ ] Debugger: Breakpoints, Einzelschritt/Prozedurschritt, Direktfenster,

View File

@@ -1,15 +1,16 @@
//! `tbc` — Standalone-Compiler von Terminal Basic. //! `tbc` — Standalone-Compiler von Terminal Basic.
//! //!
//! Unterbefehle (Phase 2): //! Unterbefehle (Phase 2):
//! - `tbc run <datei.bas>` Kompilieren und sofort ausführen //! - `tbc run <datei.bas|datei.frm|projekt.mak>` Kompilieren und ausführen
//! - `tbc build <datei.bas>` Kompilieren zu `datei.tbc` //! - `tbc build <datei.bas|datei.frm|projekt.mak>` Zu `.tbc` kompilieren
//! - `tbc check <datei.bas>` Nur Syntax-/Semantikprüfung //! - `tbc check <datei.bas|datei.frm|projekt.mak>` Syntax/Semantik prüfen
//! - `tbc convert-frm <quelle.frm> <ziel.frm>` Binärformular in Text wandeln //! - `tbc convert-frm <quelle.frm> <ziel.frm>` Binärformular in Text wandeln
//! //!
//! Exit-Codes von `run` (Entscheidung D6, docs/tbvm-design.md): //! Exit-Codes von `run` (Entscheidung D6, docs/tbvm-design.md):
//! 0 = END/SYSTEM/Programmende · 3 = STOP · 2 = Laufzeitfehler · //! 0 = END/SYSTEM/Programmende · 3 = STOP · 2 = Laufzeitfehler ·
//! 1 = Compile-Fehler/Bedienfehler. //! 1 = Compile-Fehler/Bedienfehler.
use std::collections::HashSet;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::ExitCode; use std::process::ExitCode;
use tb_runtime::host::{Ereignis, Host}; use tb_runtime::host::{Ereignis, Host};
@@ -25,7 +26,7 @@ fn main() -> ExitCode {
Some("convert-frm") => cmd_convert_frm(&args[1..]), Some("convert-frm") => cmd_convert_frm(&args[1..]),
_ => { _ => {
eprintln!( 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) ExitCode::from(1)
} }
@@ -81,23 +82,230 @@ fn module_name(path: &Path) -> String {
.unwrap_or_else(|| "MODUL".into()) .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( fn compile(
path_arg: Option<&String>, 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 { 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)); return Err(ExitCode::from(1));
}; };
let path = PathBuf::from(path); let path = PathBuf::from(path);
let source = match std::fs::read_to_string(&path) { let (source, forms) = match input_sources(&path) {
Ok(s) => s, Ok(input) => input,
Err(e) => { Err(error) => {
eprintln!("{}: {e}", path.display()); eprintln!("{error}");
return Err(ExitCode::from(1)); return Err(ExitCode::from(1));
} }
}; };
match tb_vm::compile_source(&module_name(&path), &source) { let compiled = if forms.is_empty() {
Ok(m) => Ok((path, m)), 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) => { Err(diags) => {
for d in &diags { for d in &diags {
eprintln!("{}:{d}", path.display()); eprintln!("{}:{d}", path.display());
@@ -116,7 +324,7 @@ fn cmd_check(args: &[String]) -> ExitCode {
} }
fn cmd_build(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, Ok(x) => x,
Err(code) => return code, Err(code) => return code,
}; };
@@ -134,36 +342,20 @@ fn cmd_build(args: &[String]) -> ExitCode {
} }
fn cmd_run(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: // Ohne Terminal (Pipe, Skript, CI) läuft das Programm im PipeHost:
// Eingabe zeilenweise von stdin, Ausgabe am Ende als Snapshot. // Eingabe zeilenweise von stdin, Ausgabe am Ende als Snapshot.
let ereignis = match TerminalHost::new() { let result = match TerminalHost::new() {
Ok(mut host) => { Ok(mut host) => {
// Der Bildschirm folgt der Terminalgröße von Beginn an (80×25 ist let size = host.groesse().ok();
// stets nur die untere Schranke, nie eine feste Größe). let result = run_chain(args, &mut host, size);
if let Ok((cols, rows)) = host.groesse() {
vm.rt.screen.resize(cols, rows);
}
let e = vm.run(&mut host);
drop(host); // Alternativschirm verlassen, bevor gedruckt wird 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)); print!("{}", tb_runtime::snapshot::text(&vm.rt.screen));
// `LPRINT` sammelt im Druckerpuffer; am Programmende geht er in die // `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. // Ohne Debugger-Flags treten diese Ereignisse nicht auf.
RunEvent::Breakpoint { .. } | RunEvent::Stepped { .. } => ExitCode::from(2), 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 { impl PipeHost {
fn new() -> Self { fn new() -> Self {
PipeHost { let mut host = PipeHost {
puffer: std::collections::VecDeque::new(), puffer: std::collections::VecDeque::new(),
eof: false, eof: false,
start: std::time::Instant::now(), 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. /// 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 //! nur die untere Schranke, nie eine feste Größe, und der Korpus muss das
//! nachweisen können. //! nachweisen können.
use std::collections::VecDeque;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use tb_runtime::host::CaptureHost; use tb_runtime::host::{CaptureHost, Host};
use tb_vm::interp::{RunEvent, Vm}; use tb_vm::interp::{RunEvent, Vm};
fn compat_dir() -> PathBuf { 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, /// `<F1>`…`<F12>`, sonst je ein Zeichen; `+` davor bedeutet Umschalt,
/// `^` Strg, `%` Alt — gebraucht für die benutzerdefinierten Trap-Tasten. /// `^` Strg, `%` Alt — gebraucht für die benutzerdefinierten Trap-Tasten.
fn tasten_direktive(name: &str, src: &str) -> Vec<tb_runtime::host::Ereignis> { 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 { let Some(wert) = direktive(src, "tb-keys:") else {
return Vec::new(); 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 mut out = Vec::new();
let zeichen: Vec<char> = wert.chars().collect(); let zeichen: Vec<char> = wert.chars().collect();
let mut i = 0; let mut i = 0;
@@ -143,6 +148,21 @@ fn tasten_direktive(name: &str, src: &str) -> Vec<tb_runtime::host::Ereignis> {
"LEFT" => 0x4B, "LEFT" => 0x4B,
"RIGHT" => 0x4D, "RIGHT" => 0x4D,
"DOWN" => 0x50, "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"), other => panic!("{name}: tb-keys kennt `<{other}>` nicht"),
}; };
out.push(Ereignis::Taste(taste::sonder(code), shift)); 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 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 /// Virtuelle Uhr: `' tb-clock: <n>ms/Zustellpunkt`. Ohne die Direktive
/// steht die Uhr des Testhosts still — ein Programm mit Zeit-Traps liefe /// steht die Uhr des Testhosts still — ein Programm mit Zeit-Traps liefe
/// dann endlos. Der Harness weist es deshalb ab, statt auf die Systemuhr /// 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. /// Korpusdatei ausführen und den Bildschirm-Snapshot liefern.
fn run_corpus_file(path: &Path, groesse: Option<(usize, usize)>) -> String { 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 name = path.file_name().unwrap().to_string_lossy().to_string();
let modul_name = path.file_stem().unwrap().to_string_lossy().to_uppercase(); let form = if path
let (cols, rows) = groesse.unwrap_or_else(|| screen_groessen(&name, &src)[0]); .extension()
let module = tb_vm::compile_source(&modul_name, &src) .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())); .unwrap_or_else(|d| panic!("{}: Compile-Fehler: {d:?}", path.display()));
let mut vm = Vm::new(module); 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); 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; vm.rt.zeitzone = z;
} }
// `' tb-tempdir` — das Programm arbeitet mit Dateien. // `' 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(); 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( host.ereignis_nach(
nach, nach,
tb_runtime::host::Ereignis::Groesse { cols: c, rows: r }, 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); 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 { if let Some(schritt_ms) = schritt {
let mut host = UhrHost { let mut host = UhrHost {
inner: host, inner: host,
schritt_ms, schritt_ms,
}; };
return match vm.run(&mut host) { return run_to_snapshot(path, &mut vm, &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)
),
};
} }
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), RunEvent::Ended => tb_runtime::snapshot::snapshot(&vm.rt.screen),
other => panic!( other => panic!(
"{}: unerwartetes Laufzeitende {other:?}\nBildschirm bisher:\n{}", "{}: 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. /// Snapshots vergleichen: erst das Textbild, dann die Attributebene.
/// Bei Abweichung im Text zählt die Zeile, bei Attributen Zeile und Spalte. /// Bei Abweichung im Text zählt die Zeile, bei Attributen Zeile und Spalte.
fn assert_output_matches(file: &str, want: &str, got: &str) { 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) let mut entries: Vec<PathBuf> = std::fs::read_dir(&dir)
.expect("tests/compat fehlt") .expect("tests/compat fehlt")
.map(|e| e.unwrap().path()) .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(); .collect();
entries.sort(); entries.sort();
for path in entries { for path in entries {
let name = path.file_name().unwrap().to_string_lossy().to_string(); 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); let groessen = screen_groessen(&name, &src);
for (cols, rows) in &groessen { for (cols, rows) in &groessen {
// Bei mehreren Größen trägt jede ihre eigene Sollausgabe. // 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] #[test]
#[should_panic(expected = "deklariert aber keinen Zeitverlauf")] #[should_panic(expected = "deklariert aber keinen Zeitverlauf")]
fn zeittrap_ohne_uhr_direktive_wird_abgewiesen() { fn zeittrap_ohne_uhr_direktive_wird_abgewiesen() {
@@ -467,6 +649,40 @@ fn tbc_build_erzeugt_tbc() {
let _ = std::fs::remove_file(&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) ---------- // ---- Harness meldet Abweichungen benannt (Spec kompat-testkorpus) ----------
fn meldung(want: &str, got: &str) -> String { fn meldung(want: &str, got: &str) -> String {
@@ -537,12 +753,16 @@ fn erzeuge_sollausgaben() {
let mut entries: Vec<PathBuf> = std::fs::read_dir(&dir) let mut entries: Vec<PathBuf> = std::fs::read_dir(&dir)
.expect("tests/compat fehlt") .expect("tests/compat fehlt")
.map(|e| e.unwrap().path()) .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(); .collect();
entries.sort(); entries.sort();
for path in entries { for path in entries {
let name = path.file_name().unwrap().to_string_lossy().to_string(); 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); let groessen = screen_groessen(&name, &src);
for (cols, rows) in &groessen { for (cols, rows) in &groessen {
let out_path = if groessen.len() == 1 { let out_path = if groessen.len() == 1 {

View File

@@ -480,6 +480,26 @@ pub enum Stmt {
bottom: Option<Expr>, bottom: Option<Expr>,
pos: SourcePos, 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 TIMER(n&) GOSUB ziel`, `ON KEY(n%) GOSUB ziel`,
/// `ON UEVENT GOSUB ziel`, `ON SIGNAL(n%) GOSUB ziel` — die /// `ON UEVENT GOSUB ziel`, `ON SIGNAL(n%) GOSUB ziel` — die
/// klassischen Ereignis-Traps (Sprachreferenz §8). `GOSUB 0` schaltet /// klassischen Ereignis-Traps (Sprachreferenz §8). `GOSUB 0` schaltet

View File

@@ -21,10 +21,11 @@ pub enum ObjectClass {
Timer, Timer,
VScrollBar, VScrollBar,
Screen, Screen,
Spin,
} }
impl ObjectClass { impl ObjectClass {
pub const ALL: [Self; 18] = [ pub const ALL: [Self; 19] = [
Self::Form, Self::Form,
Self::CheckBox, Self::CheckBox,
Self::ComboBox, Self::ComboBox,
@@ -43,6 +44,7 @@ impl ObjectClass {
Self::Timer, Self::Timer,
Self::VScrollBar, Self::VScrollBar,
Self::Screen, Self::Screen,
Self::Spin,
]; ];
pub fn name(self) -> &'static str { pub fn name(self) -> &'static str {
@@ -65,6 +67,7 @@ impl ObjectClass {
Self::Timer => "TIMER", Self::Timer => "TIMER",
Self::VScrollBar => "VSCROLLBAR", Self::VScrollBar => "VSCROLLBAR",
Self::Screen => "SCREEN", Self::Screen => "SCREEN",
Self::Spin => "SPIN",
} }
} }
@@ -88,6 +91,7 @@ impl ObjectClass {
Self::Timer => "Timer", Self::Timer => "Timer",
Self::VScrollBar => "VScrollBar", Self::VScrollBar => "VScrollBar",
Self::Screen => "Screen", Self::Screen => "Screen",
Self::Spin => "Spin",
} }
} }
@@ -295,7 +299,7 @@ pub fn properties(class: ObjectClass) -> Vec<PropertySpec> {
string("TEXT", ""), string("TEXT", ""),
]), ]),
CheckBox => p.extend([string("CAPTION", ""), range("VALUE", 0, 0, 2)]), 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", "")), Frame => p.push(string("CAPTION", "")),
Label => p.extend([ Label => p.extend([
range("ALIGNMENT", 0, 0, 2), range("ALIGNMENT", 0, 0, 2),
@@ -311,6 +315,21 @@ pub fn properties(class: ObjectClass) -> Vec<PropertySpec> {
range("SMALLCHANGE", 1, 1, 32767), range("SMALLCHANGE", 1, 1, 32767),
range("VALUE", 0, -32768, 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([ PictureBox => p.extend([
boolp("AUTOREDRAW", false), boolp("AUTOREDRAW", false),
range("BORDERSTYLE", 1, 0, 2), range("BORDERSTYLE", 1, 0, 2),
@@ -337,10 +356,26 @@ pub fn properties(class: ObjectClass) -> Vec<PropertySpec> {
boolp("SEPARATOR", false), boolp("SEPARATOR", false),
string("TAG", ""), string("TAG", ""),
boolp("VISIBLE", true), 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([ FileListBox => p.extend([
ro("LIST", PropertyType::String),
ro("LISTCOUNT", PropertyType::Integer),
int("LISTINDEX", -1),
string("FILENAME", ""), string("FILENAME", ""),
string("PATH", ""), string("PATH", ""),
string("PATTERN", "*.*"), string("PATTERN", "*.*"),
@@ -413,6 +448,7 @@ pub fn methods(class: ObjectClass) -> &'static [&'static str] {
&["DRAG", "MOVE", "REFRESH", "SETFOCUS"] &["DRAG", "MOVE", "REFRESH", "SETFOCUS"]
} }
Frame | Label | HScrollBar | VScrollBar => &["DRAG", "MOVE", "REFRESH"], Frame | Label | HScrollBar | VScrollBar => &["DRAG", "MOVE", "REFRESH"],
Spin => &["DRAG", "REFRESH", "SETFOCUS"],
PictureBox => &[ PictureBox => &[
"CLS", "CLS",
"DRAG", "DRAG",
@@ -429,11 +465,7 @@ pub fn methods(class: ObjectClass) -> &'static [&'static str] {
} }
pub fn method_is_implemented(class: ObjectClass, name: &str) -> bool { pub fn method_is_implemented(class: ObjectClass, name: &str) -> bool {
matches!( methods(class).contains(&name)
(class, name),
(ObjectClass::Form, "HIDE" | "LOAD" | "SHOW" | "UNLOAD")
| (ObjectClass::Screen, "HIDE" | "SHOW")
)
} }
pub fn method_arity(class: ObjectClass, name: &str) -> Option<(usize, usize)> { 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, "SHOW") => Some((0, 1)),
(ObjectClass::Form, "HIDE" | "LOAD" | "UNLOAD") (ObjectClass::Form, "HIDE" | "LOAD" | "UNLOAD")
| (ObjectClass::Screen, "HIDE" | "SHOW") => Some((0, 0)), | (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, _ => None,
} }
} }
@@ -558,6 +606,16 @@ pub fn events(class: ObjectClass) -> &'static [&'static str] {
"KEYUP", "KEYUP",
"LOSTFOCUS", "LOSTFOCUS",
], ],
Spin => &[
"CUSTOM",
"DRAGDROP",
"DRAGOVER",
"GOTFOCUS",
"KEYDOWN",
"KEYPRESS",
"KEYUP",
"LOSTFOCUS",
],
PictureBox => &[ PictureBox => &[
"CLICK", "CLICK",
"DBLCLICK", "DBLCLICK",
@@ -608,6 +666,7 @@ pub fn event_params(event: &str) -> Option<&'static [(&'static str, EventParamTy
("STATE", Integer), ("STATE", Integer),
], ],
"UNLOAD" => &[("CANCEL", Integer)], "UNLOAD" => &[("CANCEL", Integer)],
"CUSTOM" => &[("EVENTTYPE", Integer)],
"CLICK" | "DBLCLICK" | "CHANGE" | "DROPDOWN" | "GOTFOCUS" | "LOSTFOCUS" | "LOAD" "CLICK" | "DBLCLICK" | "CHANGE" | "DROPDOWN" | "GOTFOCUS" | "LOSTFOCUS" | "LOAD"
| "PAINT" | "RESIZE" | "TIMER" | "PATHCHANGE" | "PATTERNCHANGE" => &[], | "PAINT" | "RESIZE" | "TIMER" | "PATHCHANGE" | "PATTERNCHANGE" => &[],
_ => return None, _ => return None,
@@ -652,4 +711,18 @@ impl FormCatalog {
.find(|(_, o)| o.name.eq_ignore_ascii_case(name)) .find(|(_, o)| o.name.eq_ignore_ascii_case(name))
.map(|(i, o)| (i as u16, o)) .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, IsamSavepoint,
IsamSetmem, IsamSetmem,
IsamBof, IsamBof,
MsgBox,
InputBoxS,
ClipboardAdd,
ClipboardGet,
GraphicsLine,
GraphicsPaint,
GraphicsView,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -386,6 +393,20 @@ pub enum HExpr {
property: u16, property: u16,
ty: HTy, 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 { DynamicObjectProperty {
object: Box<HExpr>, object: Box<HExpr>,
property: String, property: String,
@@ -511,6 +532,13 @@ pub enum HStmtKind {
property: u16, property: u16,
value: HExpr, value: HExpr,
}, },
SetObjectIndexedProperty {
object: u16,
object_index: Option<HExpr>,
property: u16,
index: HExpr,
value: HExpr,
},
SetDynamicObjectProperty { SetDynamicObjectProperty {
object: HExpr, object: HExpr,
property: String, property: String,
@@ -518,6 +546,7 @@ pub enum HStmtKind {
}, },
ObjectMethod { ObjectMethod {
object: u16, object: u16,
index: Option<HExpr>,
method: u16, method: u16,
args: Vec<HExpr>, args: Vec<HExpr>,
}, },
@@ -592,6 +621,10 @@ pub enum HStmtKind {
targets: Vec<LabelId>, targets: Vec<LabelId>,
}, },
ReturnGosub(Option<LabelId>), ReturnGosub(Option<LabelId>),
Run {
target: Option<HExpr>,
string: bool,
},
/// Ereignis-Trap erklären (Sprachreferenz §8). `art` ist die Quelle /// Ereignis-Trap erklären (Sprachreferenz §8). `art` ist die Quelle
/// (0 KEY, 1 TIMER, 2 UEVENT, 3 SIGNAL), `index` ihre Kennung bzw. bei /// (0 KEY, 1 TIMER, 2 UEVENT, 3 SIGNAL), `index` ihre Kennung bzw. bei
/// `TIMER` das Intervall in Sekunden. `ziel = None` = `GOSUB 0`. /// `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("40000")[0], TokenKind::Num(NumValue::Long(40000)));
assert_eq!(kinds("1.5")[0], TokenKind::Num(NumValue::Single(1.5))); assert_eq!(kinds("1.5")[0], TokenKind::Num(NumValue::Single(1.5)));
assert_eq!( assert_eq!(
kinds("3.14159265")[0], kinds("1.23456789")[0],
TokenKind::Num(NumValue::Double(3.14159265)) TokenKind::Num(NumValue::Double(1.23456789))
); );
assert_eq!(kinds("1E3")[0], TokenKind::Num(NumValue::Single(1000.0))); assert_eq!(kinds("1E3")[0], TokenKind::Num(NumValue::Single(1000.0)));
assert_eq!(kinds("1D3")[0], TokenKind::Num(NumValue::Double(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() { match p.k() {
TokenKind::Eof => break, TokenKind::Eof => break,
TokenKind::Kw(Kw::Sub) | TokenKind::Kw(Kw::Function) => { 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); procs.push(proc);
} }
} }
@@ -244,10 +255,7 @@ impl<'a> P<'a> {
self.advance(); self.advance();
self.parse_input(true, pos) self.parse_input(true, pos)
} else { } else {
// Grafikform LINE (x1,y1)-(x2,y2): deklariertes Non-Feature. self.parse_graphics_line(pos)
self.err("Feature unavailable");
self.sync();
None
} }
} }
TokenKind::Kw(Kw::If) => self.parse_if(pos), TokenKind::Kw(Kw::If) => self.parse_if(pos),
@@ -350,12 +358,8 @@ impl<'a> P<'a> {
TokenKind::Kw(Kw::Erase) => { TokenKind::Kw(Kw::Erase) => {
self.advance(); self.advance();
let mut names = Vec::new(); let mut names = Vec::new();
loop { while let Some(e) = self.parse_name_only() {
if let Some(e) = self.parse_name_only() {
names.push(e); names.push(e);
} else {
break;
}
if !self.eat(&TokenKind::Comma) { if !self.eat(&TokenKind::Comma) {
break; break;
} }
@@ -548,12 +552,8 @@ impl<'a> P<'a> {
TokenKind::Kw(Kw::Read) => { TokenKind::Kw(Kw::Read) => {
self.advance(); self.advance();
let mut vars = Vec::new(); let mut vars = Vec::new();
loop { while let Some(e) = self.parse_name_ref() {
if let Some(e) = self.parse_name_ref() {
vars.push(e); vars.push(e);
} else {
break;
}
if !self.eat(&TokenKind::Comma) { if !self.eat(&TokenKind::Comma) {
break; break;
} }
@@ -733,6 +733,14 @@ impl<'a> P<'a> {
} }
Some(Stmt::ViewPrint { top, bottom, pos }) 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 { TokenKind::Ident {
ref name, ref name,
suffix: None, 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> { fn parse_print(&mut self, pos: SourcePos, printer: bool) -> Option<Stmt> {
self.advance(); // PRINT bzw. LPRINT self.advance(); // PRINT bzw. LPRINT
let file = if !printer && self.eat(&TokenKind::Hash) { let file = if !printer && self.eat(&TokenKind::Hash) {
@@ -1056,12 +1179,8 @@ impl<'a> P<'a> {
} }
} }
let mut vars = Vec::new(); let mut vars = Vec::new();
loop { while let Some(e) = self.parse_name_ref() {
if let Some(e) = self.parse_name_ref() {
vars.push(e); vars.push(e);
} else {
break;
}
if !self.eat(&TokenKind::Comma) { if !self.eat(&TokenKind::Comma) {
break; break;
} }
@@ -1384,11 +1503,8 @@ impl<'a> P<'a> {
return None; return None;
}; };
let mut targets = Vec::new(); let mut targets = Vec::new();
loop { while let Some(t) = self.parse_label_ref() {
match self.parse_label_ref() { targets.push(t);
Some(t) => targets.push(t),
None => break,
}
if !self.eat(&TokenKind::Comma) { if !self.eat(&TokenKind::Comma) {
break; break;
} }
@@ -1432,11 +1548,7 @@ impl<'a> P<'a> {
let dims = if self.eat(&TokenKind::LParen) { let dims = if self.eat(&TokenKind::LParen) {
let mut ds = Vec::new(); let mut ds = Vec::new();
if !self.eat(&TokenKind::RParen) { if !self.eat(&TokenKind::RParen) {
loop { while let Some(a) = self.parse_expr() {
let a = match self.parse_expr() {
Some(e) => e,
None => break,
};
if self.eat_kw(Kw::To) { if self.eat_kw(Kw::To) {
match self.parse_expr() { match self.parse_expr() {
Some(b) => ds.push((Some(a), b)), Some(b) => ds.push((Some(a), b)),
@@ -1602,8 +1714,7 @@ impl<'a> P<'a> {
} }
}; };
let mut params = Vec::new(); let mut params = Vec::new();
if self.eat(&TokenKind::LParen) { if self.eat(&TokenKind::LParen) && !self.eat(&TokenKind::RParen) {
if !self.eat(&TokenKind::RParen) {
loop { loop {
match self.k() { match self.k() {
TokenKind::Ident { TokenKind::Ident {
@@ -1640,7 +1751,6 @@ impl<'a> P<'a> {
} }
self.eat(&TokenKind::RParen); self.eat(&TokenKind::RParen);
} }
}
Some(ProcSig { Some(ProcSig {
kind, kind,
name, name,
@@ -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 pos = self.pos();
let sig = self.parse_proc_sig()?; 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 { let end_kw = match sig.kind {
ProcKind::Sub => Kw::Sub, ProcKind::Sub => Kw::Sub,
ProcKind::Function => Kw::Function, ProcKind::Function => Kw::Function,
@@ -1697,8 +1807,7 @@ impl<'a> P<'a> {
} }
}; };
let mut params = Vec::new(); let mut params = Vec::new();
if self.eat(&TokenKind::LParen) { if self.eat(&TokenKind::LParen) && !self.eat(&TokenKind::RParen) {
if !self.eat(&TokenKind::RParen) {
loop { loop {
match self.k() { match self.k() {
TokenKind::Ident { TokenKind::Ident {
@@ -1724,7 +1833,6 @@ impl<'a> P<'a> {
} }
self.eat(&TokenKind::RParen); self.eat(&TokenKind::RParen);
} }
}
if self.eat(&TokenKind::Eq) { if self.eat(&TokenKind::Eq) {
let body = self.parse_expr()?; let body = self.parse_expr()?;
Some(Stmt::DefFn { Some(Stmt::DefFn {
@@ -1818,8 +1926,8 @@ impl<'a> P<'a> {
other => Expr::Paren(Box::new(other)), other => Expr::Paren(Box::new(other)),
}) })
.collect(); .collect();
if !self.at_stmt_end() && call_args.is_empty() { if !self.at_stmt_end() && (call_args.is_empty() || name.contains('.')) {
call_args = self.parse_arg_list_to_stmt_end(); call_args.extend(self.parse_arg_list_to_stmt_end());
} }
Some(Stmt::Call { Some(Stmt::Call {
name, name,
@@ -1842,7 +1950,7 @@ impl<'a> P<'a> {
match self.k() { match self.k() {
TokenKind::Ident { mut name, suffix } => { TokenKind::Ident { mut name, suffix } => {
self.advance(); 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); let a = self.parse_arg_list(&TokenKind::RParen);
self.eat(&TokenKind::RParen); self.eat(&TokenKind::RParen);
Some(a) Some(a)
@@ -1858,6 +1966,11 @@ impl<'a> P<'a> {
name.push('.'); name.push('.');
name.push_str(&member); name.push_str(&member);
self.advance(); 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"), _ => self.err("Expected: property"),
} }
@@ -1935,6 +2048,7 @@ impl<'a> P<'a> {
args.push(Expr::Missing); args.push(Expr::Missing);
continue; continue;
} }
self.eat(&TokenKind::Hash);
match self.parse_expr() { match self.parse_expr() {
Some(e) => args.push(e), Some(e) => args.push(e),
None => break, None => break,
@@ -1985,11 +2099,7 @@ impl<'a> P<'a> {
fn parse_bin(&mut self, min_prec: u8) -> Option<Expr> { fn parse_bin(&mut self, min_prec: u8) -> Option<Expr> {
let mut lhs = self.parse_prefix()?; let mut lhs = self.parse_prefix()?;
loop { while let Some((prec, op)) = self.infix_op() {
let (prec, op) = match self.infix_op() {
Some(x) => x,
None => break,
};
if prec < min_prec { if prec < min_prec {
break; break;
} }

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -63,6 +63,7 @@ pub struct RtState {
rng: u32, rng: u32,
rnd_last: f32, rnd_last: f32,
pub command: String, pub command: String,
pub clipboard: String,
} }
impl Default for RtState { impl Default for RtState {
@@ -93,6 +94,7 @@ impl Default for RtState {
rng: 0x50000, rng: 0x50000,
rnd_last: 0.0, rnd_last: 0.0,
command: String::new(), command: String::new(),
clipboard: String::new(),
} }
} }
} }
@@ -438,7 +440,14 @@ pub mod ids {
pub const ISAM_BOF: u16 = 147; pub const ISAM_BOF: u16 = 147;
/// `SetUEvent` — loest das benutzerdefinierte Ereignis aus (§8). /// `SetUEvent` — loest das benutzerdefinierte Ereignis aus (§8).
pub const SETUEVENT: u16 = 148; 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. /// Dispatch-Tabelle in Index-Reihenfolge.
@@ -594,11 +603,35 @@ pub fn builtin_table() -> &'static [BuiltinFn] {
bi_isam_setmem, bi_isam_setmem,
bi_isam_bof, bi_isam_bof,
bi_setuevent, 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); debug_assert_eq!(TABLE.len(), ids::COUNT as usize);
TABLE 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 -------------------------------------------------------- // ---- Argument-Hilfen --------------------------------------------------------
fn arg_str(args: &[Value], i: usize) -> Result<Rc<str>, RuntimeError> { fn arg_str(args: &[Value], i: usize) -> Result<Rc<str>, RuntimeError> {
@@ -1881,17 +1914,90 @@ fn bi_view_print(
} }
fn bi_screen_stmt( fn bi_screen_stmt(
_: &mut RtState, st: &mut RtState,
_: &mut dyn Host, _: &mut dyn Host,
a: &mut [Value], a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> { ) -> Result<Option<Value>, RuntimeError> {
// `SCREEN modus[, farbschalter[, aktiv[, sichtbar]]]`. Nur der Textmodus 0 // Die Terminaldarstellung bildet die historischen Pixelmodi auf Zellen ab;
// existiert; Grafikmodi sind deklariertes Non-Feature. Die übrigen // Seitenargumente haben im einzelnen Puffer keine zusätzliche Wirkung.
// Argumente betreffen Grafikseiten und bleiben folgenlos.
match opt(a, 0)? { match opt(a, 0)? {
None | Some(0) => Ok(None), None | Some(0..=13) => {
Some(_) => Err(RuntimeError(73)), // Advanced feature unavailable 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. /// Nummer eines Funktionstasten-Makros → Index 011.
@@ -2016,6 +2122,14 @@ fn bi_input_s(
if n < 0 { if n < 0 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL); 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. // Blockierend, ohne Echo auf dem Bildschirm.
let mut s = String::new(); let mut s = String::new();
for _ in 0..n { for _ in 0..n {
@@ -2546,6 +2660,22 @@ fn bi_isam_bof(
Ok(Some(Value::Int(if st.isam.bof(n) { -1 } else { 0 }))) 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -2635,7 +2765,7 @@ mod tests {
h.ereignis(Ereignis::Signal(1)); h.ereignis(Ereignis::Signal(1));
st.pump(&mut h, false); st.pump(&mut h, false);
assert_eq!(st.traps.naechstes(), Some((Quelle::Signal(1), 22))); 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] #[test]

View File

@@ -24,6 +24,9 @@ use unicode_width::UnicodeWidthChar;
pub const MIN_COLS: usize = 80; pub const MIN_COLS: usize = 80;
pub const MIN_ROWS: usize = 25; pub const MIN_ROWS: usize = 25;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScreenError;
/// Eine Bildschirmzelle: Zeichen plus klassisches Farbattribut. /// Eine Bildschirmzelle: Zeichen plus klassisches Farbattribut.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cell { pub struct Cell {
@@ -78,6 +81,9 @@ pub struct TextScreen {
/// Seit der letzten Anzeige verändert? Der Host wird nur dann zum /// Seit der letzten Anzeige verändert? Der Host wird nur dann zum
/// Neuzeichnen aufgefordert. /// Neuzeichnen aufgefordert.
veraendert: bool, veraendert: bool,
graphic_x: i32,
graphic_y: i32,
graphic_view: Option<(i32, i32, i32, i32)>,
} }
impl Default for TextScreen { impl Default for TextScreen {
@@ -110,6 +116,9 @@ impl TextScreen {
view_full: true, view_full: true,
belegt: vec![None; rows], belegt: vec![None; rows],
veraendert: true, veraendert: true,
graphic_x: 0,
graphic_y: 0,
graphic_view: None,
} }
} }
@@ -146,8 +155,8 @@ impl TextScreen {
} }
self.cells = cells; self.cells = cells;
let mut belegt = vec![None; rows]; let mut belegt = vec![None; rows];
for r in 0..self.rows.min(rows) { for (r, value) in belegt.iter_mut().enumerate().take(self.rows.min(rows)) {
belegt[r] = self.belegt[r].map(|c| c.min(cols - 1)); *value = self.belegt[r].map(|c| c.min(cols - 1));
} }
self.belegt = belegt; self.belegt = belegt;
self.cols = cols; self.cols = cols;
@@ -225,9 +234,9 @@ impl TextScreen {
} }
/// `LOCATE zeile, spalte` (1-basiert); außerhalb → Err (Fehler 5). /// `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 { if row < 1 || row > self.rows || col < 1 || col > self.cols {
return Err(()); return Err(ScreenError);
} }
self.cur_row = row - 1; self.cur_row = row - 1;
// Auf eine Fortsetzungszelle zu zeigen bedeutet den Zeichenanfang. // Auf eine Fortsetzungszelle zu zeigen bedeutet den Zeichenanfang.
@@ -251,9 +260,9 @@ impl TextScreen {
} }
/// `VIEW PRINT oben TO unten` (1-basiert). /// `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 { if top < 1 || bottom > self.rows || top > bottom {
return Err(()); return Err(ScreenError);
} }
self.view_top = top - 1; self.view_top = top - 1;
self.view_bottom = bottom - 1; self.view_bottom = bottom - 1;
@@ -265,6 +274,122 @@ impl TextScreen {
self.cells[(row - 1) * self.cols + (col - 1)] 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, /// Text an der Cursorposition ausgeben: Umbruch am rechten Rand,
/// Scrollen am unteren Rand des Scrollbereichs. `\n` bricht um, /// Scrollen am unteren Rand des Scrollbereichs. `\n` bricht um,
/// `\r` setzt an den Zeilenanfang. /// `\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 z: Vec<char> = s.chars().collect();
let mut out = String::new(); let mut out = String::new();
for (i, c) in z.iter().enumerate() { 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(',');
} }
out.push(*c); out.push(*c);
@@ -433,7 +433,7 @@ mod tests {
#[test] #[test]
fn numerisches_feld_mit_nachkommastellen() { 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"); 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 // r in 0..10000; runde halb-zu-gerade
if r > 5_000 { if r > 5_000 {
q + 1 q + 1
} else if r < 5_000 { } else if r < 5_000 || q % 2 == 0 {
q
} else if q % 2 == 0 {
q q
} else { } else {
q + 1 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::collections::{BTreeMap, HashSet};
use std::fmt; 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)] #[derive(Debug, Clone, PartialEq)]
pub struct FormNode { pub struct FormNode {
@@ -46,6 +48,67 @@ impl FormFile {
original: None, 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)] #[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 reference = read_u16(bytes, at).ok_or(at)?;
let class_id = bytes[at + 2] & 0x7f; let class_id = bytes[at + 2] & 0x7f;
let length = bytes[at + 3] as usize; 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() { if length == 0 || length > 40 || at + 4 + length > bytes.len() {
return Err(at + 3); 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 { if symbols.is_empty() && class != ObjectClass::Form {
return Err(at + 2); 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 { symbols.push(BinarySymbol {
offset: at, offset: at,
class, class,
name: String::from_utf8_lossy(name).into_owned(), name,
is_array: bytes[at + 2] & 0x80 != 0, 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; at += 4 + length;
if reference == 0 { if reference == 0 {
@@ -501,7 +570,7 @@ fn record_len(class: ObjectClass) -> usize {
ObjectClass::PictureBox => 31, ObjectClass::PictureBox => 31,
ObjectClass::TextBox => 34, ObjectClass::TextBox => 34,
ObjectClass::Menu | ObjectClass::Frame => 26, ObjectClass::Menu | ObjectClass::Frame => 26,
ObjectClass::Screen => 80, ObjectClass::Screen | ObjectClass::Spin => 80,
_ => 28, _ => 28,
} }
} }
@@ -522,7 +591,7 @@ fn record_lengths(class: ObjectClass) -> &'static [usize] {
ObjectClass::OptionButton => &[21, 28], ObjectClass::OptionButton => &[21, 28],
ObjectClass::PictureBox => &[31], ObjectClass::PictureBox => &[31],
ObjectClass::HScrollBar | ObjectClass::VScrollBar => &[32], ObjectClass::HScrollBar | ObjectClass::VScrollBar => &[32],
ObjectClass::Screen => &[73, 80], ObjectClass::Screen | ObjectClass::Spin => &[73, 80],
ObjectClass::Timer => &[21, 28], ObjectClass::Timer => &[21, 28],
} }
} }
@@ -627,7 +696,12 @@ fn walk_object_records(
let next_symbol = bytes[header] as usize; let next_symbol = bytes[header] as usize;
if next_symbol >= symbols.len() if next_symbol >= symbols.len()
|| bytes[header + 2] != 0 || 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; continue;
} }
@@ -978,7 +1052,7 @@ fn assign_strings(
) -> Result<Vec<BinaryWarning>, FormError> { ) -> Result<Vec<BinaryWarning>, FormError> {
let mut warnings = records let mut warnings = records
.iter() .iter()
.filter(|record| symbols[record.symbol].class == ObjectClass::Screen) .filter(|record| symbols[record.symbol].unsupported)
.map(|record| BinaryWarning { .map(|record| BinaryWarning {
offset: 0x20 + record.start, offset: 0x20 + record.start,
name: format!("{}.CustomControl", symbols[record.symbol].name), name: format!("{}.CustomControl", symbols[record.symbol].name),
@@ -1063,10 +1137,9 @@ fn assign_strings(
)?; )?;
} }
} }
for record in records for record in records.iter().filter(|record| {
.iter() symbols[record.symbol].unsupported || symbols[record.symbol].class == ObjectClass::Spin
.filter(|record| symbols[record.symbol].unsupported) }) {
{
for offset in [17, 28, 37, 39, 53, 55, 64, 69] { for offset in [17, 28, 37, 39, 53, 55, 64, 69] {
let Some(pointer) = let Some(pointer) =
record_u16(bytes, record.start, record.end, offset).filter(|pointer| *pointer != 0) record_u16(bytes, record.start, record.end, offset).filter(|pointer| *pointer != 0)
@@ -1319,6 +1392,35 @@ mod tests {
assert_eq!(write_text(&form), EXAMPLE); 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] #[test]
fn rejects_version_class_property_value_and_open_block_with_location() { fn rejects_version_class_property_value_and_open_block_with_location() {
let cases = [ let cases = [
@@ -1502,6 +1604,18 @@ mod tests {
assert_eq!(failure.name, "New.HEIGHT"); 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] #[test]
fn unsupported_custom_control_is_named_and_not_emitted_as_screen() { fn unsupported_custom_control_is_named_and_not_emitted_as_screen() {
let symbols = vec![ let symbols = vec![
@@ -1515,7 +1629,7 @@ mod tests {
BinarySymbol { BinarySymbol {
offset: 0, offset: 0,
class: ObjectClass::Screen, class: ObjectClass::Screen,
name: "VSpin".into(), name: "Custom1".into(),
is_array: false, is_array: false,
unsupported: true, unsupported: true,
}, },
@@ -1569,7 +1683,7 @@ mod tests {
let root = build_tree(decoded).unwrap(); let root = build_tree(decoded).unwrap();
assert_eq!(warnings[0].offset, 0x46); 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()); assert!(root.children.is_empty());
let mut strings_with_orphan = (100..=108) let mut strings_with_orphan = (100..=108)

View File

@@ -1317,7 +1317,7 @@ fn token_def(code: u16) -> Option<TokenDef> {
}), }),
0x138 => Some(TokenDef { 0x138 => Some(TokenDef {
len: 0, len: 0,
rules: &["expr::=MKS$({0})"], rules: &["expr::=MKS$({expr:0})", "{0}"],
}), }),
0x139 => Some(TokenDef { 0x139 => Some(TokenDef {
len: 0, len: 0,
@@ -1682,7 +1682,7 @@ fn token_def(code: u16) -> Option<TokenDef> {
}), }),
0x1b2 => Some(TokenDef { 0x1b2 => Some(TokenDef {
len: 0, len: 0,
rules: &["expr::=MSGBOX({2}, {1}, {0})"], rules: &["expr::=STR$({expr:0})"],
}), }),
0x1b3 => Some(TokenDef { 0x1b3 => Some(TokenDef {
len: 0, len: 0,
@@ -2421,6 +2421,23 @@ impl<'a> Decoder<'a> {
.ok_or_else(|| self.fail(self.at, "P-Code", "Tokendaten abgeschnitten"))? .ok_or_else(|| self.fail(self.at, "P-Code", "Tokendaten abgeschnitten"))?
.to_owned(); .to_owned();
self.at += (len + 1) & !1; 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) { if matches!(self.pcode, 0x1c5 | 0x1c6) {
let first = self let first = self
.stack .stack
@@ -2634,6 +2651,26 @@ mod tests {
assert_eq!(decoder.array(&array, false).unwrap(), "Oldcontents()"); 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] #[test]
fn decodes_defint_and_vbdos_isam_tokens() { fn decodes_defint_and_vbdos_isam_tokens() {
let mut decoder = Decoder::new("test.frm", &[], &[], 0); let mut decoder = Decoder::new("test.frm", &[], &[], 0);

View File

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

View File

@@ -20,6 +20,10 @@ pub fn compile(hir: &HirModule) -> CompiledModule {
string_ids: HashMap::new(), string_ids: HashMap::new(),
jump_tables: Vec::new(), jump_tables: Vec::new(),
modul_label_pc: 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(); let mut procs = Vec::new();
for proc in &hir.procs { for proc in &hir.procs {
@@ -29,7 +33,7 @@ pub fn compile(hir: &HirModule) -> CompiledModule {
name: hir.name.clone(), name: hir.name.clone(),
option_base: hir.option_base, option_base: hir.option_base,
strings: cg.strings, 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(), global_names: hir.globals.iter().map(|g| g.name.clone()).collect(),
udts: hir udts: hir
.udts .udts
@@ -85,6 +89,9 @@ struct Codegen {
/// `ON ERROR GOTO` aus einer Prozedur zeigt dorthin; da Prozedur 0 /// `ON ERROR GOTO` aus einer Prozedur zeigt dorthin; da Prozedur 0
/// zuerst übersetzt wird, stehen die Positionen rechtzeitig fest. /// zuerst übersetzt wird, stehen die Positionen rechtzeitig fest.
modul_label_pc: Vec<Option<u32>>, 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 { struct ProcCtx {
@@ -160,7 +167,30 @@ impl Codegen {
fixups: Vec::new(), fixups: Vec::new(),
table_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 { 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); self.stmt(&mut ctx, proc, stmt);
} }
// Rumpfende // Rumpfende
@@ -234,12 +264,17 @@ impl Codegen {
// ---- Anweisungen ------------------------------------------------------- // ---- Anweisungen -------------------------------------------------------
fn stmt(&mut self, ctx: &mut ProcCtx, proc: &hir::HProc, stmt: &HStmt) { 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 { match &stmt.kind {
HStmtKind::Label(l) => { HStmtKind::Label(l) => {
ctx.bind(*l); ctx.bind(*l);
return; return;
} }
_ => ctx.emit(Instr::Stmt(stmt.line)), _ if boundary => ctx.emit(Instr::Stmt(stmt.line)),
_ => {}
} }
match &stmt.kind { match &stmt.kind {
HStmtKind::Label(_) => unreachable!(), HStmtKind::Label(_) => unreachable!(),
@@ -263,6 +298,23 @@ impl Codegen {
index.is_some(), 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 { HStmtKind::SetDynamicObjectProperty {
object, object,
property, property,
@@ -275,13 +327,21 @@ impl Codegen {
} }
HStmtKind::ObjectMethod { HStmtKind::ObjectMethod {
object, object,
index,
method, method,
args, args,
} => { } => {
if let Some(index) = index {
self.expr(ctx, index);
}
for arg in args { for arg in args {
self.expr(ctx, arg); 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 { HStmtKind::ObjectLoad {
object, object,
@@ -509,6 +569,18 @@ impl Codegen {
None => ctx.emit(Instr::RetGosub), None => ctx.emit(Instr::RetGosub),
Some(l) => ctx.emit_jump(Instr::RetGosubTo(0), *l), 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::ExitProc => self.emit_proc_exit(ctx, proc),
HStmtKind::CallSub { proc: id, args } => { HStmtKind::CallSub { proc: id, args } => {
for a in args { for a in args {
@@ -533,6 +605,9 @@ impl Codegen {
} }
let id = builtin_id(*b); let id = builtin_id(*b);
ctx.emit(Instr::CallBuiltin(id, args.len() as u8)); ctx.emit(Instr::CallBuiltin(id, args.len() as u8));
if matches!(b, Builtin::MsgBox) {
ctx.emit(Instr::Pop);
}
if builtin_returns_value(*b) { if builtin_returns_value(*b) {
ctx.emit(Instr::Pop); ctx.emit(Instr::Pop);
} }
@@ -725,6 +800,41 @@ impl Codegen {
index.is_some(), 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 } => { HExpr::DynamicObjectProperty { object, property } => {
self.expr(ctx, object); self.expr(ctx, object);
let property = self.pool(property); let property = self.pool(property);
@@ -1173,6 +1283,9 @@ fn builtin_id(b: Builtin) -> u16 {
Builtin::Width => ids::WIDTH, Builtin::Width => ids::WIDTH,
Builtin::ViewPrint => ids::VIEW_PRINT, Builtin::ViewPrint => ids::VIEW_PRINT,
Builtin::ScreenStmt => ids::SCREEN_STMT, 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::KeyAssign => ids::KEY_ASSIGN,
Builtin::KeyList => ids::KEY_LIST, Builtin::KeyList => ids::KEY_LIST,
Builtin::KeyDisplay => ids::KEY_DISPLAY, Builtin::KeyDisplay => ids::KEY_DISPLAY,
@@ -1277,6 +1390,10 @@ fn builtin_id(b: Builtin) -> u16 {
Builtin::IsamSavepoint => ids::ISAM_SAVEPOINT, Builtin::IsamSavepoint => ids::ISAM_SAVEPOINT,
Builtin::IsamSetmem => ids::ISAM_SETMEM, Builtin::IsamSetmem => ids::ISAM_SETMEM,
Builtin::IsamBof => ids::ISAM_BOF, 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. //! - `GOSUB`-Stack pro Frame; `RETURN` ohne GOSUB → Fehler 3.
use crate::bytecode::{CmpOp, CompiledModule, Instr}; use crate::bytecode::{CmpOp, CompiledModule, Instr};
use std::collections::HashSet; use std::collections::{HashSet, VecDeque};
use std::rc::Rc; 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::errors::RuntimeError;
use tb_runtime::host::Host; use tb_runtime::host::{Ereignis, Host};
use tb_runtime::traps::Quelle; use tb_runtime::traps::Quelle;
use tb_runtime::value::{self, default_value, ArrayObj, RecordObj, TypeInit, Value, VarRef}; use tb_runtime::value::{self, default_value, ArrayObj, RecordObj, TypeInit, Value, VarRef};
use tb_ui::forms::{FormEvent, FormsModel, PropertyValue, ShowResult}; use tb_ui::forms::{FormEvent, FormsModel, PropertyValue, ShowResult};
@@ -37,6 +37,10 @@ pub enum RunEvent {
Interrupted { Interrupted {
line: u32, line: u32,
}, },
Restart {
program: Option<String>,
line: Option<u32>,
},
/// Unbehandelter Laufzeitfehler. /// Unbehandelter Laufzeitfehler.
Error { Error {
code: u16, code: u16,
@@ -106,6 +110,7 @@ pub struct Vm {
tick_zaehler: u32, tick_zaehler: u32,
breakpoints: HashSet<u32>, breakpoints: HashSet<u32>,
data_ptr: usize, data_ptr: usize,
start_pc: Option<usize>,
pub forms: FormsModel, pub forms: FormsModel,
} }
@@ -166,6 +171,7 @@ impl Vm {
tick_zaehler: 0, tick_zaehler: 0,
breakpoints: HashSet::new(), breakpoints: HashSet::new(),
data_ptr: 0, data_ptr: 0,
start_pc: None,
forms, forms,
module, module,
}; };
@@ -176,6 +182,24 @@ impl Vm {
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) { fn push_frame(&mut self, proc: usize, argc: usize) {
// Argumente liegen zuoberst auf dem Stack (links → rechts). // Argumente liegen zuoberst auf dem Stack (links → rechts).
let locals_base = self.locals.len(); 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) { pub fn queue_form_event(&mut self, event: FormEvent) {
self.forms.queue(event); 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( fn property_value(
&self, &self,
object: u16, object: u16,
@@ -322,36 +589,37 @@ impl Vm {
Ok(()) 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 self.forms
.resize(self.rt.screen.cols(), self.rt.screen.rows()); .resize(self.rt.screen.cols(), self.rt.screen.rows());
self.forms.timers(host.jetzt_ms());
if self.forms.active_form().is_some() {
while let Some((key, shift)) = self.rt.tasten.pop_front() {
self.forms.handle_key(&key, shift);
}
}
let now_ms = host.jetzt_ms();
while let Some(m) = self.rt.maus.pop_front() { while let Some(m) = self.rt.maus.pop_front() {
if let Some(object) = self.forms.active_form() { self.forms.handle_mouse_at(m, now_ms);
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.render(&mut self.rt.screen);
self.dispatch_next_form_event() self.dispatch_next_form_event()
} }
/// Anstehendes Ereignis zustellen, falls eines fällig ist. Liefert /// Anstehendes Ereignis zustellen, falls eines fällig ist. Liefert
/// `true`, wenn ein Handler aufgesetzt wurde. /// `true`, wenn ein Handler aufgesetzt wurde.
fn zustellen(&mut self, host: &mut dyn Host) -> bool { 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; return false;
} }
let jetzt = host.jetzt_ms(); let jetzt = host.jetzt_ms();
@@ -377,7 +645,7 @@ impl Vm {
// virtuelle nie vorrückt. // virtuelle nie vorrückt.
let real_start = std::time::Instant::now(); let real_start = std::time::Instant::now();
loop { loop {
self.rt.tick(host); self.tick(host);
if self.zustellen(host) { if self.zustellen(host) {
return; return;
} }
@@ -531,14 +799,41 @@ impl Vm {
// ---- Hauptschleife -------------------------------------------------------- // ---- 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 { pub fn run(&mut self, host: &mut dyn Host) -> RunEvent {
loop { loop {
if let Some(form) = self.frames.last().and_then(|f| f.waiting_form) { if let Some(form) = self.frames.last().and_then(|f| f.waiting_form) {
if !self.forms.is_visible(form) { if !self.forms.is_visible(form) {
self.frames.last_mut().unwrap().waiting_form = None; self.frames.last_mut().unwrap().waiting_form = None;
} else { } else {
self.rt.tick(host); self.tick(host);
self.forms_zustellen(); self.forms_zustellen(host);
if self.rt.ende { if self.rt.ende {
return RunEvent::Ended; return RunEvent::Ended;
} }
@@ -818,6 +1113,11 @@ impl Vm {
use Instr as I; use Instr as I;
match instr { match instr {
I::Stmt(line) => { 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(); let f = self.frames.last_mut().unwrap();
f.line = line; f.line = line;
f.last_stmt_pc = pc; f.last_stmt_pc = pc;
@@ -826,11 +1126,12 @@ impl Vm {
// Debugger-Funktion — ohne sie sähe niemand die Ausgabe und // Debugger-Funktion — ohne sie sähe niemand die Ausgabe und
// Größenänderungen kämen nie an. // Größenänderungen kämen nie an.
self.tick_zaehler = self.tick_zaehler.wrapping_add(1); self.tick_zaehler = self.tick_zaehler.wrapping_add(1);
if self.rt.screen.ist_veraendert() || self.tick_zaehler % 1024 == 0 { self.forms.render(&mut self.rt.screen);
self.rt.tick(host); if self.rt.screen.ist_veraendert() || self.tick_zaehler.is_multiple_of(1024) {
self.tick(host);
self.rt.screen.veraenderung_quittieren(); self.rt.screen.veraenderung_quittieren();
} }
if self.forms_zustellen() { if self.forms_zustellen(host) {
return Ok(Flow::Normal); return Ok(Flow::Normal);
} }
// Ereigniszustellung (design.md, D1): nur wenn überhaupt // Ereigniszustellung (design.md, D1): nur wenn überhaupt
@@ -841,7 +1142,7 @@ impl Vm {
// Auflösung eines Zeit-Traps ist damit 64 Anweisungen; // Auflösung eines Zeit-Traps ist damit 64 Anweisungen;
// reicht das nicht, wird daraus ein eigener Zähler je // reicht das nicht, wird daraus ein eigener Zähler je
// Trap. // Trap.
if self.tick_zaehler % 64 == 0 { if self.tick_zaehler.is_multiple_of(64) {
let jetzt = host.jetzt_ms(); let jetzt = host.jetzt_ms();
self.rt.traps.zeit_pruefen(jetzt); self.rt.traps.zeit_pruefen(jetzt);
} }
@@ -901,8 +1202,8 @@ impl Vm {
// bekommt seinen eigenen Stapelabschnitt darüber und // bekommt seinen eigenen Stapelabschnitt darüber und
// lässt den Wert unberührt. // lässt den Wert unberührt.
self.stack.push(Value::Int(0)); self.stack.push(Value::Int(0));
self.rt.tick(host); self.tick(host);
self.forms_zustellen(); self.forms_zustellen(host);
self.zustellen(host); self.zustellen(host);
Ok(Flow::Normal) Ok(Flow::Normal)
} }
@@ -1051,6 +1352,56 @@ impl Vm {
self.forms.set_at(object, index, property, value)?; self.forms.set_at(object, index, property, value)?;
Ok(Flow::Normal) 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) => { I::TypeOf(class) => {
let matches = match self.pop()? { let matches = match self.pop()? {
Value::Obj(object, _) => self Value::Obj(object, _) => self
@@ -1064,6 +1415,8 @@ impl Vm {
Ok(Flow::Normal) Ok(Flow::Normal)
} }
I::ObjectMethod(object, method, argc) => { I::ObjectMethod(object, method, argc) => {
let indexed = argc & 0x80 != 0;
let argc = argc & 0x7f;
let class = self let class = self
.module .module
.objects .objects
@@ -1093,6 +1446,7 @@ impl Vm {
args.push(self.pop()?); args.push(self.pop()?);
} }
args.reverse(); args.reverse();
let index = if indexed { Some(self.pop_i32()?) } else { None };
if resumed_show && !self.forms.is_loaded(object) { if resumed_show && !self.forms.is_loaded(object) {
return Ok(Flow::Normal); return Ok(Flow::Normal);
} }
@@ -1118,13 +1472,26 @@ impl Vm {
(tb_frontend::forms::ObjectClass::Form, "UNLOAD") => { (tb_frontend::forms::ObjectClass::Form, "UNLOAD") => {
self.request_unload(object)? 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") => { (tb_frontend::forms::ObjectClass::Screen, "SHOW") => {
self.forms.screen_show(true) self.forms.screen_show(true)
} }
(tb_frontend::forms::ObjectClass::Screen, "HIDE") => { (tb_frontend::forms::ObjectClass::Screen, "HIDE") => {
self.forms.screen_show(false) 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(); self.dispatch_next_form_event();
Ok(Flow::Normal) Ok(Flow::Normal)
@@ -1318,7 +1685,7 @@ impl Vm {
s.chars().take(n).collect() s.chars().take(n).collect()
} else { } else {
let mut t = s.to_string(); let mut t = s.to_string();
t.extend(std::iter::repeat(' ').take(n - len)); t.extend(std::iter::repeat_n(' ', n - len));
t t
}; };
self.push(Value::Str(Rc::from(fixed.as_str()))); self.push(Value::Str(Rc::from(fixed.as_str())));
@@ -1714,9 +2081,27 @@ impl Vm {
} }
Ok(Flow::Normal) 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 ---- // ---- Prozeduren ----
I::Call(proc, argc) => { 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); self.push_frame(proc as usize, argc as usize);
Ok(Flow::Normal) Ok(Flow::Normal)
} }
@@ -1754,6 +2139,12 @@ impl Vm {
args.push(self.pop()?); args.push(self.pop()?);
} }
args.reverse(); 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]; let f = builtin_table()[id as usize];
match f(&mut self.rt, host, &mut args) { match f(&mut self.rt, host, &mut args) {
Ok(Some(v)) => { 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 r = p.rem_euclid(10_000);
let rounded = if r > 5_000 { let rounded = if r > 5_000 {
q + 1 q + 1
} else if r < 5_000 { } else if r < 5_000 || q % 2 == 0 {
q
} else if q % 2 == 0 {
q q
} else { } else {
q + 1 q + 1

View File

@@ -38,13 +38,11 @@ fn err_code(src: &str) -> u16 {
/// Verzeichnis, das beim Verlassen samt Inhalt verschwindet. Programme /// Verzeichnis, das beim Verlassen samt Inhalt verschwindet. Programme
/// hinterlassen dadurch nichts im Projektbaum. /// hinterlassen dadurch nichts im Projektbaum.
struct TempVerzeichnis { struct TempVerzeichnis {
vorher: std::path::PathBuf,
dir: std::path::PathBuf, dir: std::path::PathBuf,
} }
impl TempVerzeichnis { impl TempVerzeichnis {
fn neu(name: &str) -> TempVerzeichnis { fn neu(name: &str) -> TempVerzeichnis {
let vorher = std::env::current_dir().unwrap();
let dir = std::env::temp_dir().join(format!( let dir = std::env::temp_dir().join(format!(
"tb_vm_{name}_{}_{:?}", "tb_vm_{name}_{}_{:?}",
std::process::id(), std::process::id(),
@@ -52,14 +50,16 @@ impl TempVerzeichnis {
)); ));
let _ = std::fs::remove_dir_all(&dir); let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap(); std::fs::create_dir_all(&dir).unwrap();
std::env::set_current_dir(&dir).unwrap(); TempVerzeichnis { dir }
TempVerzeichnis { vorher, dir } }
fn pfad(&self, name: &str) -> String {
self.dir.join(name).to_string_lossy().replace('"', "\"\"")
} }
} }
impl Drop for TempVerzeichnis { impl Drop for TempVerzeichnis {
fn drop(&mut self) { fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.vorher);
let _ = std::fs::remove_dir_all(&self.dir); 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"); 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] #[test]
fn banker_rounding_cint() { fn banker_rounding_cint() {
// Spec-Szenario: PRINT CINT(0.5); CINT(1.5); CINT(2.5) → " 0 2 2 " // 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"); 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 /// 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 /// 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. /// findet in einem temporären Verzeichnis statt und lässt nichts zurück.
#[test] #[test]
fn open_for_isam_endet_nicht_mehr_mit_fehler_73() { fn open_for_isam_endet_nicht_mehr_mit_fehler_73() {
let _dir = TempVerzeichnis::neu("open_isam"); let dir = TempVerzeichnis::neu("open_isam");
let (ev, ausgabe) = run("TYPE T\n f AS INTEGER\nEND TYPE\n\ let (ev, ausgabe) = run(&format!(
OPEN \"db.isam\" FOR ISAM T \"Tab\" AS #1\n\ "TYPE T\n f AS INTEGER\nEND TYPE\n\
OPEN \"{}\" FOR ISAM T \"Tab\" AS #1\n\
PRINT \"offen\"\n\ PRINT \"offen\"\n\
CLOSE #1"); CLOSE #1",
dir.pfad("db.isam")
));
assert_eq!(ev, RunEvent::Ended, "unerwartetes Ende: {ev:?}\n{ausgabe}"); assert_eq!(ev, RunEvent::Ended, "unerwartetes Ende: {ev:?}\n{ausgabe}");
assert!(ausgabe.contains("offen"), "{ausgabe}"); assert!(ausgabe.contains("offen"), "{ausgabe}");
} }
@@ -594,17 +624,10 @@ fn color_prueft_wertebereich() {
} }
#[test] #[test]
fn screen_anweisung_nur_textmodus() { fn screen_anweisung_bildet_grafikmodi_auf_den_zellenpuffer_ab() {
// 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.
assert_eq!(out("SCREEN 0\nPRINT \"ok\""), "ok\n"); assert_eq!(out("SCREEN 0\nPRINT \"ok\""), "ok\n");
// Berechneter Modus erst zur Laufzeit. assert_eq!(out("SCREEN 13\nPRINT \"ok\""), "ok\n");
assert_eq!(err_code("m% = 2\nSCREEN m%"), 73); assert_eq!(err_code("m% = 14\nSCREEN m%"), 5);
} }
#[test] #[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"); 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] #[test]
fn cls_setzt_cursor_zurueck() { fn cls_setzt_cursor_zurueck() {
assert_eq!(out("PRINT \"weg\"\nCLS\nPRINT \"neu\""), "neu\n"); 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"); 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] #[test]
fn steuerarray_eigenschaften_und_objektparameter_funktionieren() { fn steuerarray_eigenschaften_und_objektparameter_funktionieren() {
let mut vm = form_vm( let mut vm = form_vm(
@@ -1175,6 +1232,31 @@ fn modales_show_setzt_nach_unload_fort() {
assert!(host.presents >= 2); 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] #[test]
fn fehlende_ereignisprozedur_verfaellt_und_typeof_prueft_klasse() { fn fehlende_ereignisprozedur_verfaellt_und_typeof_prueft_klasse() {
let mut vm = form_vm("Form1.Show\nIF TYPEOF Text1 IS TextBox THEN PRINT \"ja\""); 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(); .count();
assert_eq!(n, 1, "nur das explizite DOEVENTS ist ein Zustellopcode"); 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** Reine Textdatei ohne Kopfzeile, **eine Projektdatei pro Zeile**
(`.BAS`/`.FRM`; belegt durch die Originalbeispiele). Terminal Basic (`.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`) ## 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) ## Steuerelemente (exakte Klassennamen)
`CheckBox, ComboBox, CommandButton, DirListBox, DriveListBox, FileListBox, `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 Timer, VScrollBar` — alle außer `Menu` über die Toolbox platzierbar
(Menüs entstehen im Menu Design Window). Default-Namen: `Check1`, `Combo1`, (Menüs entstehen im Menu Design Window). Default-Namen: `Check1`, `Combo1`,
`Command1`, `Dir1`, `Drive1`, `File1`, `Frame1`, `HScroll1`, `Label1`, `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), Attached (VBDOS-Spezifikum: am Formularrand angedockt; Laufzeit read-only),
LargeChange, Min, Max, SmallChange, Value. 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 **PictureBox** — Text-Zeichenfläche (PRINT/CLS aufs Control) und Container
für OptionButton-Gruppen. Ereignisse: Click, DblClick, Drag*, GotFocus, für OptionButton-Gruppen. Ereignisse: Click, DblClick, Drag*, GotFocus,
Key*, LostFocus, Mouse*, Paint. Methoden: CLS, DRAG, MOVE, PRINT, REFRESH, 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. Index, Interval (0 = aus … 65 535 ms), Parent, Tag.
**Menu** — Ereignis Click; Eigenschaften Caption, Checked, CtlName, Enabled, **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 **DirListBox/DriveListBox/FileListBox** — Datei-Browser-Controls
(Path/Drive/FileName/Pattern/Archive/Hidden/Normal/ReadOnly/System; (Path/Drive/FileName/Pattern/Archive/Hidden/Normal/ReadOnly/System;
@@ -124,6 +129,61 @@ sie plattformneutral nach (Laufwerksliste → Wurzeln/Mounts).
| Text (TextBox/ComboBox) | — | Entwurfszeit = CtlName | | Text (TextBox/ComboBox) | — | Entwurfszeit = CtlName |
| ListIndex | 1 = keine Auswahl | 1 | | 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 **Keine Default-Eigenschaften:** `Text1 = "x"` gibt es nicht — Zugriff
immer explizit (`Text1.Text`). „Default" ist nur die CommandButton- immer explizit (`Text1.Text`). „Default" ist nur die CommandButton-
Eigenschaft (Enter-Taste); „Cancel" analog für Esc. Eigenschaft (Enter-Taste); „Cancel" analog für Esc.
@@ -188,8 +248,4 @@ Menü-Steuerelement-Arrays sind möglich.
## Offene Detailfragen ## 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) - `.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 **Fundstelle.** Bei `implementiert` das Modul, bei `Non-Feature` der
Abschnitt der Sprachreferenz, bei `offen` ein Strich. 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 | | 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 | | `FORMAT$` | Funktion | Formatierung | implementiert | tb-runtime::using | bas7advr |
| `PRINT USING` | Anweisung | Formatierung | implementiert | tb-runtime::using | bas7advr | | `PRINT USING` | Anweisung | Formatierung | implementiert | tb-runtime::using | bas7advr |
| `SetFormatCC` | Routine | Formatierung | implementiert | tb-runtime::using | bas7advr | | `SetFormatCC` | Routine | Formatierung | implementiert | tb-runtime::using | bas7advr |
| `INPUTBOX$` | Funktion | Forms | offen | - | sprachreferenz | | `INPUTBOX$` | Funktion | Forms | implementiert | tb-runtime::builtins | sprachreferenz |
| `MSGBOX` | Anweisung | Forms | offen | - | sprachreferenz | | `MSGBOX` | Anweisung | Forms | implementiert | tb-runtime::builtins | sprachreferenz |
| `MSGBOX` | Funktion | Forms | offen | - | sprachreferenz | | `MSGBOX` | Funktion | Forms | implementiert | tb-runtime::builtins | sprachreferenz |
| `CIRCLE` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr | | `CIRCLE` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `DRAW` | 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 | | `GET (Grafik)` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `LINE` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr | | `LINE` | Anweisung | Grafik | implementiert | tb-runtime::screen | bas7advr |
| `PAINT` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr | | `PAINT` | Anweisung | Grafik | implementiert | tb-runtime::screen | bas7advr |
| `PALETTE` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr | | `PALETTE` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `PCOPY` | 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 | | `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 | | `PRESET` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `PSET` | 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 | | `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 | | `WINDOW` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `ABSOLUTE` | Routine | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr | | `ABSOLUTE` | Routine | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `BLOAD` | Anweisung | 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 | | `SQR` | Funktion | Mathematik | implementiert | tb-runtime::math | qb45advr |
| `TAN` | Funktion | Mathematik | implementiert | tb-runtime::math | bas7advr | | `TAN` | Funktion | Mathematik | implementiert | tb-runtime::math | bas7advr |
| `$DYNAMIC` | Metabefehl | Metabefehl | implementiert | tb-frontend::lexer | 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 | | `$STATIC` | Metabefehl | Metabefehl | implementiert | tb-frontend::lexer | bas7advr |
| `CALL` | Anweisung | Prozeduren | implementiert | tb-frontend::sema | bas7advr | | `CALL` | Anweisung | Prozeduren | implementiert | tb-frontend::sema | bas7advr |
| `DECLARE` | 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 |
| `ERDEV$` | Funktion | System | implementiert | tb-runtime::builtins | bas7advr | | `ERDEV$` | Funktion | System | implementiert | tb-runtime::builtins | bas7advr |
| `FRE` | 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 | | `SLEEP` | Anweisung | System | implementiert | tb-runtime::builtins | bas7advr |
| `STACK` | Anweisung | System | implementiert | tb-runtime::builtins | bas7advr | | `STACK` | Anweisung | System | implementiert | tb-runtime::builtins | bas7advr |
| `STACK` | Funktion | 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.SCALEHEIGHT` | Eigenschaft | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
| `FORM.SCALEWIDTH` | 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.WINDOWSTATE` | Eigenschaft | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
| `FORM.CLS` | Methode | Forms/FORM | offen | - | forms-referenz | | `FORM.CLS` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
| `FORM.DRAG` | Methode | Forms/FORM | offen | - | 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.HIDE` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
| `FORM.LOAD` | 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.MOVE` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
| `FORM.PRINT` | Methode | Forms/FORM | offen | - | forms-referenz | | `FORM.PRINT` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
| `FORM.PRINTFORM` | Methode | Forms/FORM | offen | - | forms-referenz | | `FORM.PRINTFORM` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
| `FORM.REFRESH` | Methode | Forms/FORM | offen | - | 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.SHOW` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
| `FORM.TEXTHEIGHT` | Methode | Forms/FORM | offen | - | forms-referenz | | `FORM.TEXTHEIGHT` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
| `FORM.TEXTWIDTH` | Methode | Forms/FORM | offen | - | 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.UNLOAD` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
| `FORM_CLICK` | Ereignis | Forms/FORM | implementiert | tb-vm::interp | forms-referenz | | `FORM_CLICK` | Ereignis | Forms/FORM | implementiert | tb-vm::interp | forms-referenz |
| `FORM_DBLCLICK` | 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.DRAGMODE` | Eigenschaft | Forms/CHECKBOX | implementiert | tb-ui::forms | forms-referenz |
| `CHECKBOX.CAPTION` | 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.VALUE` | Eigenschaft | Forms/CHECKBOX | implementiert | tb-ui::forms | forms-referenz |
| `CHECKBOX.DRAG` | Methode | Forms/CHECKBOX | offen | - | forms-referenz | | `CHECKBOX.DRAG` | Methode | Forms/CHECKBOX | implementiert | tb-ui::forms | forms-referenz |
| `CHECKBOX.MOVE` | Methode | Forms/CHECKBOX | offen | - | forms-referenz | | `CHECKBOX.MOVE` | Methode | Forms/CHECKBOX | implementiert | tb-ui::forms | forms-referenz |
| `CHECKBOX.REFRESH` | Methode | Forms/CHECKBOX | offen | - | forms-referenz | | `CHECKBOX.REFRESH` | Methode | Forms/CHECKBOX | implementiert | tb-ui::forms | forms-referenz |
| `CHECKBOX.SETFOCUS` | Methode | Forms/CHECKBOX | offen | - | 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_CLICK` | Ereignis | Forms/CHECKBOX | implementiert | tb-vm::interp | forms-referenz |
| `CHECKBOX_DRAGDROP` | 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 | | `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.SELTEXT` | Eigenschaft | Forms/COMBOBOX | implementiert | tb-ui::forms | forms-referenz |
| `COMBOBOX.STYLE` | 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.TEXT` | Eigenschaft | Forms/COMBOBOX | implementiert | tb-ui::forms | forms-referenz |
| `COMBOBOX.ADDITEM` | Methode | Forms/COMBOBOX | offen | - | forms-referenz | | `COMBOBOX.ADDITEM` | Methode | Forms/COMBOBOX | implementiert | tb-ui::forms | forms-referenz |
| `COMBOBOX.DRAG` | Methode | Forms/COMBOBOX | offen | - | forms-referenz | | `COMBOBOX.DRAG` | Methode | Forms/COMBOBOX | implementiert | tb-ui::forms | forms-referenz |
| `COMBOBOX.MOVE` | Methode | Forms/COMBOBOX | offen | - | forms-referenz | | `COMBOBOX.MOVE` | Methode | Forms/COMBOBOX | implementiert | tb-ui::forms | forms-referenz |
| `COMBOBOX.REFRESH` | Methode | Forms/COMBOBOX | offen | - | forms-referenz | | `COMBOBOX.REFRESH` | Methode | Forms/COMBOBOX | implementiert | tb-ui::forms | forms-referenz |
| `COMBOBOX.REMOVEITEM` | Methode | Forms/COMBOBOX | offen | - | forms-referenz | | `COMBOBOX.REMOVEITEM` | Methode | Forms/COMBOBOX | implementiert | tb-ui::forms | forms-referenz |
| `COMBOBOX.SETFOCUS` | Methode | Forms/COMBOBOX | offen | - | 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_CHANGE` | Ereignis | Forms/COMBOBOX | implementiert | tb-vm::interp | forms-referenz |
| `COMBOBOX_CLICK` | 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 | | `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.CAPTION` | Eigenschaft | Forms/COMMANDBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `COMMANDBUTTON.DEFAULT` | 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.VALUE` | Eigenschaft | Forms/COMMANDBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `COMMANDBUTTON.DRAG` | Methode | Forms/COMMANDBUTTON | offen | - | forms-referenz | | `COMMANDBUTTON.DRAG` | Methode | Forms/COMMANDBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `COMMANDBUTTON.MOVE` | Methode | Forms/COMMANDBUTTON | offen | - | forms-referenz | | `COMMANDBUTTON.MOVE` | Methode | Forms/COMMANDBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `COMMANDBUTTON.REFRESH` | Methode | Forms/COMMANDBUTTON | offen | - | forms-referenz | | `COMMANDBUTTON.REFRESH` | Methode | Forms/COMMANDBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `COMMANDBUTTON.SETFOCUS` | Methode | Forms/COMMANDBUTTON | offen | - | 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_CLICK` | Ereignis | Forms/COMMANDBUTTON | implementiert | tb-vm::interp | forms-referenz |
| `COMMANDBUTTON_DRAGDROP` | 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 | | `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.TABINDEX` | Eigenschaft | Forms/DIRLISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `DIRLISTBOX.TABSTOP` | 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.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.PATH` | Eigenschaft | Forms/DIRLISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `DIRLISTBOX.TEXT` | 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 | | `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.TABINDEX` | Eigenschaft | Forms/DRIVELISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `DRIVELISTBOX.TABSTOP` | 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.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.DRIVE` | Eigenschaft | Forms/DRIVELISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `DRIVELISTBOX.TEXT` | 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 | | `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.TABINDEX` | Eigenschaft | Forms/FILELISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `FILELISTBOX.TABSTOP` | 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.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.FILENAME` | Eigenschaft | Forms/FILELISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `FILELISTBOX.PATH` | 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 | | `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.CTLNAME` | Eigenschaft | Forms/FRAME | implementiert | tb-ui::forms | forms-referenz |
| `FRAME.DRAGMODE` | 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.CAPTION` | Eigenschaft | Forms/FRAME | implementiert | tb-ui::forms | forms-referenz |
| `FRAME.DRAG` | Methode | Forms/FRAME | offen | - | forms-referenz | | `FRAME.DRAG` | Methode | Forms/FRAME | implementiert | tb-ui::forms | forms-referenz |
| `FRAME.MOVE` | Methode | Forms/FRAME | offen | - | forms-referenz | | `FRAME.MOVE` | Methode | Forms/FRAME | implementiert | tb-ui::forms | forms-referenz |
| `FRAME.REFRESH` | Methode | Forms/FRAME | offen | - | 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_DRAGDROP` | Ereignis | Forms/FRAME | implementiert | tb-vm::interp | forms-referenz |
| `FRAME_DRAGOVER` | 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 | | `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.MAX` | Eigenschaft | Forms/HSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
| `HSCROLLBAR.SMALLCHANGE` | 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.VALUE` | Eigenschaft | Forms/HSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
| `HSCROLLBAR.DRAG` | Methode | Forms/HSCROLLBAR | offen | - | forms-referenz | | `HSCROLLBAR.DRAG` | Methode | Forms/HSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
| `HSCROLLBAR.MOVE` | Methode | Forms/HSCROLLBAR | offen | - | forms-referenz | | `HSCROLLBAR.MOVE` | Methode | Forms/HSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
| `HSCROLLBAR.REFRESH` | Methode | Forms/HSCROLLBAR | offen | - | 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_CHANGE` | Ereignis | Forms/HSCROLLBAR | implementiert | tb-vm::interp | forms-referenz |
| `HSCROLLBAR_DRAGDROP` | 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 | | `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.AUTOSIZE` | Eigenschaft | Forms/LABEL | implementiert | tb-ui::forms | forms-referenz |
| `LABEL.BORDERSTYLE` | 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.CAPTION` | Eigenschaft | Forms/LABEL | implementiert | tb-ui::forms | forms-referenz |
| `LABEL.DRAG` | Methode | Forms/LABEL | offen | - | forms-referenz | | `LABEL.DRAG` | Methode | Forms/LABEL | implementiert | tb-ui::forms | forms-referenz |
| `LABEL.MOVE` | Methode | Forms/LABEL | offen | - | forms-referenz | | `LABEL.MOVE` | Methode | Forms/LABEL | implementiert | tb-ui::forms | forms-referenz |
| `LABEL.REFRESH` | Methode | Forms/LABEL | offen | - | 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_CHANGE` | Ereignis | Forms/LABEL | implementiert | tb-vm::interp | forms-referenz |
| `LABEL_CLICK` | 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 | | `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.LISTINDEX` | Eigenschaft | Forms/LISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `LISTBOX.SORTED` | 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.TEXT` | Eigenschaft | Forms/LISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `LISTBOX.ADDITEM` | Methode | Forms/LISTBOX | offen | - | forms-referenz | | `LISTBOX.ADDITEM` | Methode | Forms/LISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `LISTBOX.DRAG` | Methode | Forms/LISTBOX | offen | - | forms-referenz | | `LISTBOX.DRAG` | Methode | Forms/LISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `LISTBOX.MOVE` | Methode | Forms/LISTBOX | offen | - | forms-referenz | | `LISTBOX.MOVE` | Methode | Forms/LISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `LISTBOX.REFRESH` | Methode | Forms/LISTBOX | offen | - | forms-referenz | | `LISTBOX.REFRESH` | Methode | Forms/LISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `LISTBOX.REMOVEITEM` | Methode | Forms/LISTBOX | offen | - | forms-referenz | | `LISTBOX.REMOVEITEM` | Methode | Forms/LISTBOX | implementiert | tb-ui::forms | forms-referenz |
| `LISTBOX.SETFOCUS` | Methode | Forms/LISTBOX | offen | - | 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_CLICK` | Ereignis | Forms/LISTBOX | implementiert | tb-vm::interp | forms-referenz |
| `LISTBOX_DBLCLICK` | 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 | | `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.SEPARATOR` | Eigenschaft | Forms/MENU | implementiert | tb-ui::forms | forms-referenz |
| `MENU.TAG` | 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.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 | | `MENU_CLICK` | Ereignis | Forms/MENU | implementiert | tb-vm::interp | forms-referenz |
| `OPTIONBUTTON.BACKCOLOR` | Eigenschaft | Forms/OPTIONBUTTON | implementiert | tb-ui::forms | forms-referenz | | `OPTIONBUTTON.BACKCOLOR` | Eigenschaft | Forms/OPTIONBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `OPTIONBUTTON.ENABLED` | 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.DRAGMODE` | Eigenschaft | Forms/OPTIONBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `OPTIONBUTTON.CAPTION` | 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.VALUE` | Eigenschaft | Forms/OPTIONBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `OPTIONBUTTON.DRAG` | Methode | Forms/OPTIONBUTTON | offen | - | forms-referenz | | `OPTIONBUTTON.DRAG` | Methode | Forms/OPTIONBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `OPTIONBUTTON.MOVE` | Methode | Forms/OPTIONBUTTON | offen | - | forms-referenz | | `OPTIONBUTTON.MOVE` | Methode | Forms/OPTIONBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `OPTIONBUTTON.REFRESH` | Methode | Forms/OPTIONBUTTON | offen | - | forms-referenz | | `OPTIONBUTTON.REFRESH` | Methode | Forms/OPTIONBUTTON | implementiert | tb-ui::forms | forms-referenz |
| `OPTIONBUTTON.SETFOCUS` | Methode | Forms/OPTIONBUTTON | offen | - | 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_CLICK` | Ereignis | Forms/OPTIONBUTTON | implementiert | tb-vm::interp | forms-referenz |
| `OPTIONBUTTON_DBLCLICK` | 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 | | `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.CURRENTY` | Eigenschaft | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
| `PICTUREBOX.SCALEHEIGHT` | 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.SCALEWIDTH` | Eigenschaft | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
| `PICTUREBOX.CLS` | Methode | Forms/PICTUREBOX | offen | - | forms-referenz | | `PICTUREBOX.CLS` | Methode | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
| `PICTUREBOX.DRAG` | Methode | Forms/PICTUREBOX | offen | - | forms-referenz | | `PICTUREBOX.DRAG` | Methode | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
| `PICTUREBOX.MOVE` | Methode | Forms/PICTUREBOX | offen | - | forms-referenz | | `PICTUREBOX.MOVE` | Methode | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
| `PICTUREBOX.PRINT` | Methode | Forms/PICTUREBOX | offen | - | forms-referenz | | `PICTUREBOX.PRINT` | Methode | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
| `PICTUREBOX.REFRESH` | Methode | Forms/PICTUREBOX | offen | - | forms-referenz | | `PICTUREBOX.REFRESH` | Methode | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
| `PICTUREBOX.SETFOCUS` | Methode | Forms/PICTUREBOX | offen | - | forms-referenz | | `PICTUREBOX.SETFOCUS` | Methode | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
| `PICTUREBOX.TEXTHEIGHT` | Methode | Forms/PICTUREBOX | offen | - | forms-referenz | | `PICTUREBOX.TEXTHEIGHT` | Methode | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
| `PICTUREBOX.TEXTWIDTH` | Methode | Forms/PICTUREBOX | offen | - | 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_CLICK` | Ereignis | Forms/PICTUREBOX | implementiert | tb-vm::interp | forms-referenz |
| `PICTUREBOX_DBLCLICK` | 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 | | `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.SELSTART` | Eigenschaft | Forms/TEXTBOX | implementiert | tb-ui::forms | forms-referenz |
| `TEXTBOX.SELTEXT` | 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.TEXT` | Eigenschaft | Forms/TEXTBOX | implementiert | tb-ui::forms | forms-referenz |
| `TEXTBOX.DRAG` | Methode | Forms/TEXTBOX | offen | - | forms-referenz | | `TEXTBOX.DRAG` | Methode | Forms/TEXTBOX | implementiert | tb-ui::forms | forms-referenz |
| `TEXTBOX.MOVE` | Methode | Forms/TEXTBOX | offen | - | forms-referenz | | `TEXTBOX.MOVE` | Methode | Forms/TEXTBOX | implementiert | tb-ui::forms | forms-referenz |
| `TEXTBOX.REFRESH` | Methode | Forms/TEXTBOX | offen | - | forms-referenz | | `TEXTBOX.REFRESH` | Methode | Forms/TEXTBOX | implementiert | tb-ui::forms | forms-referenz |
| `TEXTBOX.SETFOCUS` | Methode | Forms/TEXTBOX | offen | - | 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_CHANGE` | Ereignis | Forms/TEXTBOX | implementiert | tb-vm::interp | forms-referenz |
| `TEXTBOX_DRAGDROP` | 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 | | `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.MAX` | Eigenschaft | Forms/VSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
| `VSCROLLBAR.SMALLCHANGE` | 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.VALUE` | Eigenschaft | Forms/VSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
| `VSCROLLBAR.DRAG` | Methode | Forms/VSCROLLBAR | offen | - | forms-referenz | | `VSCROLLBAR.DRAG` | Methode | Forms/VSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
| `VSCROLLBAR.MOVE` | Methode | Forms/VSCROLLBAR | offen | - | forms-referenz | | `VSCROLLBAR.MOVE` | Methode | Forms/VSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
| `VSCROLLBAR.REFRESH` | Methode | Forms/VSCROLLBAR | offen | - | 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_CHANGE` | Ereignis | Forms/VSCROLLBAR | implementiert | tb-vm::interp | forms-referenz |
| `VSCROLLBAR_DRAGDROP` | 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 | | `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_KEYPRESS` | Ereignis | Forms/VSCROLLBAR | implementiert | tb-vm::interp | forms-referenz |
| `VSCROLLBAR_KEYUP` | 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 | | `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.ACTIVECONTROL` | Eigenschaft | Forms/SCREEN | implementiert | tb-ui::forms | forms-referenz |
| `SCREEN.ACTIVEFORM` | 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 | | `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). - `CONST name = ausdruck` — Konstanten (konstante Ausdrücke zur Compilezeit).
- `DECLARE SUB/FUNCTION name (parameter)` — Prototyp; die IDE erzeugt sie - `DECLARE SUB/FUNCTION name (parameter)` — Prototyp; die IDE erzeugt sie
automatisch beim Speichern. 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) ## 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. als Direktmodus-Befehl der IDE (Phase 5) und ist nicht kompilierbar.
- `SLEEP [sekunden]`, `DO EVENTS`/`DOEVENTS` (Ereignisse verarbeiten — - `SLEEP [sekunden]`, `DO EVENTS`/`DOEVENTS` (Ereignisse verarbeiten —
zentral für Forms) 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 ## 6. Prozeduren
@@ -517,16 +523,12 @@ Dialoge:
Kombinationen) liefern nichts — eine eigene Kodierung dafür wäre eine Kombinationen) liefern nichts — eine eigene Kodierung dafür wäre eine
Erweiterung und gehört in Stufe 2. Erweiterung und gehört in Stufe 2.
4. **Kein `CHAIN`/Overlay-Mechanismus**; `SHELL` startet die System-Shell. 4. **Kein `CHAIN`/Overlay-Mechanismus**; `SHELL` startet die System-Shell.
5. **Grafik-Anweisungen** (`SCREEN n>0`, `PSET`, `PRESET`, `LINE`, 5. **Grafik im Zellenpuffer:** `SCREEN 0` bis `SCREEN 13`, `LINE` (auch
`CIRCLE`, `PAINT`, `DRAW`, Grafik-`GET`/`PUT`, `PALETTE`, `WINDOW`, `B`/`BF` und `STEP`), `PAINT` und `VIEW` werden ausgeführt. Historische
`VIEW`, `PMAP`, `PCOPY`, `POINT`) sind Nicht-Ziel und werden **zur Pixelkoordinaten werden dabei in 8×8-Blöcken auf Terminalzellen
Compile-Zeit** mit „Feature unavailable" abgewiesen. Die Formen, die abgebildet. `PSET`, `PRESET`, `CIRCLE`, `DRAW`, Grafik-`GET`/`PUT`,
sich ein Schlüsselwort mit einer unterstützten Anweisung teilen, erkennt `PALETTE`, `WINDOW`, `PMAP`, `PCOPY` und `POINT` bleiben Nicht-Ziel und
der Compiler an ihrer Syntax: `LINE` ohne `INPUT`, `GET`/`PUT` mit werden zur Compile-Zeit mit „Feature unavailable" abgewiesen.
`(`, `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.
6. **`PLAY`/`SOUND`**: `BEEP` = Terminal-Bell; Rest Nicht-Ziel (Fehler 73). 6. **`PLAY`/`SOUND`**: `BEEP` = Terminal-Bell; Rest Nicht-Ziel (Fehler 73).
5a. **`LPRINT` schreibt in eine Datei** (2026-09-03): einen 5a. **`LPRINT` schreibt in eine Datei** (2026-09-03): einen
plattformübergreifenden Druckerkanal gibt es nicht. Der Druckerpuffer plattformübergreifenden Druckerkanal gibt es nicht. Der Druckerpuffer
@@ -576,3 +578,13 @@ Dialoge:
werden nicht abgebildet. Unter Windows ist `SIGTERM` von außen nicht werden nicht abgebildet. Unter Windows ist `SIGTERM` von außen nicht
zustellbar — `SIGNAL(2)` bleibt dort wirkungslos. Trifft `SIGINT` ohne aktiven Trap zustellbar — `SIGNAL(2)` bleibt dort wirkungslos. Trifft `SIGINT` ohne aktiven Trap
ein, bleibt es beim bisherigen Abbruch. 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 Siehe proposal.md — Why. Dieser Change setzt auf drei fertigen Stücken
auf: dem Zellenpuffer aus Phase 3, der Ereignisschleife samt Mausquelle auf: dem Zellenpuffer aus Phase 3, der Ereignisschleife samt Mausquelle
(`phase-4-ereignisschleife`) und dem Objektmodell (`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 Klassen, Menüs, Dialoge —, aber der mit den wenigsten offenen
Architekturfragen. 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 Pfaden. `Pattern` bleibt das Muster des Vorbilds. Die Abweichung wird in
der Sprachreferenz festgehalten. 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 ## 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 jede Klasse bekommt ihren eigenen Korpusnachweis, nicht nur einen
gemeinsamen Sammeltest. gemeinsamen Sammeltest.
- **Vollständiges Neuzeichnen bei großen Terminals** → bei 200×60 sind es - **Vollständiges Neuzeichnen bei großen Terminals** → bei 200×60 sind es
@@ -109,6 +116,4 @@ der Sprachreferenz festgehalten.
## Open Questions ## Open Questions
- Ob die PictureBox als Container für OptionButton-Gruppen dieselbe Keine.
Fokuslogik wie der Frame braucht, zeigt der erste Korpusnachweis mit
gruppierten Optionsfeldern.

View File

@@ -18,7 +18,7 @@ zwischen die Aufgaben.
- **Steuerelementklassen mit Verhalten**: CommandButton, TextBox, - **Steuerelementklassen mit Verhalten**: CommandButton, TextBox,
ListBox, ComboBox, CheckBox, OptionButton, Frame, Label, HScrollBar, ListBox, ComboBox, CheckBox, OptionButton, Frame, Label, HScrollBar,
VScrollBar, PictureBox, Timer sowie die Dateisystem-Steuerelemente 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. Zuständen (normal, fokussiert, deaktiviert) und ihren Methoden.
- **Darstellung im Zellenpuffer**: Steuerelemente zeichnen in den - **Darstellung im Zellenpuffer**: Steuerelemente zeichnen in den
vorhandenen Textbildschirm; es entsteht keine zweite Zeichenschicht. vorhandenen Textbildschirm; es entsteht keine zweite Zeichenschicht.
@@ -43,10 +43,13 @@ zwischen die Aufgaben.
Positionierung in Zeichen. Positionierung in Zeichen.
- **Timer als Steuerelement**: `Interval` 065 535 ms auf der Zeitquelle - **Timer als Steuerelement**: `Interval` 065 535 ms auf der Zeitquelle
der Ereignisschleife, Ereignis `Timer`. 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); **Non-Goals:** Der Formular-Designer (Phase 5). Für den geforderten
native Erweiterungssteuerelemente (Stufe 2); grafische Ausgabe in der VBDOS-Kompatibilitätsnachweis notwendige Projektauflösung, native
PictureBox — sie ist eine Textzeichenfläche. Erweiterungssteuerelemente und Grafikausgabe gehören zu diesem Change.
## Capabilities ## Capabilities
@@ -66,10 +69,10 @@ PictureBox — sie ist eine Textzeichenfläche.
- `crates/tb-ui`: Darstellung und Eingabebehandlung je Klasse, Menüs, - `crates/tb-ui`: Darstellung und Eingabebehandlung je Klasse, Menüs,
Dialoge, Trefferprüfung, Fokusverwaltung. Dialoge, Trefferprüfung, Fokusverwaltung.
- `crates/tb-runtime`: Fehler 260480 des Forms-Bereichs als Auslöser; - `crates/tb-runtime`: Fehler 260480 des Forms-Bereichs als Auslöser,
`MSGBOX`/`INPUTBOX$` als Sprachelemente. `MSGBOX`/`INPUTBOX$` und die benötigte Zellgrafik.
- `crates/tb-frontend`: Signaturen von `MSGBOX` (Anweisung und Funktion) - `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 - `tests/compat`: Formularprogramme mit Sollausgabe; die Programme aus
`github.com/cout/vbdos` als Kompatibilitätsnachweis. `github.com/cout/vbdos` als Kompatibilitätsnachweis.
- `docs/`: `forms-referenz.md` — die offenen Detailfragen (Optik je - `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 ### Requirement: Kompatibilitätsnachweis an Fremdprogrammen
Die Formularprogramme aus dem öffentlichen Bestand des Vorbilds SHALL Die Formularprogramme aus dem öffentlichen Bestand des Vorbilds SHALL
sich ohne Non-Features übersetzen lassen und bedienbar sein. Ein sich ohne Diagnosen oder Non-Features übersetzen lassen und bedienbar
Programm, das an einem Non-Feature scheitert, MUST das Element sein. Jeder entdeckte fehlende Sprach-, Projekt-, Formular- oder
namentlich nennen; ein Scheitern aus anderem Grund MUST als Befund Laufzeitpfad MUST implementiert und als Befund festgehalten werden.
festgehalten werden.
#### Scenario: Fremdprogramm übersetzt #### Scenario: Fremdprogramm übersetzt
- **WHEN** ein Formularprogramm des öffentlichen Bestands übersetzt wird - **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. 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 - [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
- [ ] 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.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. 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 - [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
- [ ] 2.2 Eingabekette für Tastatur und Maus (D2); verifiziert durch Unit-Tests, die je Station den Vorrang prüfen - [x] 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.3 Trefferprüfung über die Z-Reihenfolge; verifiziert durch einen Test mit überlappenden Steuerelementen und einem Klick ins Formular
## 3. Fokus und Tastatur ## 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 - [x] 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 - [x] 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.3 `SETFOCUS`, `GotFocus`/`LostFocus` in der festgelegten Reihenfolge; verifiziert durch einen Test, der die Reihenfolge der beiden Ereignisse prüft
## 4. Steuerelementklassen ## 4. Steuerelementklassen
- [ ] 4.1 CommandButton, Label, Frame, CheckBox, OptionButton (inkl. Gruppierung im Container); verifiziert durch Snapshot- und Verhaltenstests je Klasse - [x] 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 - [x] 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" - [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"
- [ ] 4.4 HScrollBar/VScrollBar mit `Min`/`Max`/`Value`/`SmallChange`/`LargeChange`/`Attached` und Ereignis `Change`; verifiziert durch Tests für Tastatur- und Mausbedienung - [x] 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 - [x] 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 - [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
- [ ] 4.7 DirListBox, DriveListBox, FileListBox plattformneutral (D6) mit `Path`/`Drive`/`FileName`/`Pattern` und ihren Ereignissen; verifiziert durch Tests auf einem angelegten Verzeichnisbaum - [x] 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.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. 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) - [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)
- [ ] 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.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 - [x] 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 - [x] 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.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. 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 - [x] 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 - [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
- [ ] 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.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.4 Meilenstein: die Beispiel-Formularprogramme des Korpus laufen; verifiziert durch den grünen Korpuslauf
## 7. Inventar, Dokumentation, Abschluss ## 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 - [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
- [ ] 7.2 Abweichungen (Dateisystem-Steuerelemente, festgelegte Optik, Z- und Timer-Reihenfolge) in `docs/sprachreferenz.md` eintragen; verifiziert durch den Abschnitt „Abweichungen" - [x] 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 - [x] 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.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 #### Scenario: Fehlende Zeitdeklaration
- **WHEN** ein Korpusprogramm zeitgesteuerte Traps nutzt, aber keinen Zeitverlauf deklariert - **WHEN** ein Korpusprogramm zeitgesteuerte Traps nutzt, aber keinen Zeitverlauf deklariert
- **THEN** schlägt der Harness mit einer Meldung fehl, die die fehlende Deklaration nennt - **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