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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff