Files
TerminalBasic/crates/tb-ide/src/designer.rs

1967 lines
76 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Designeraktionen auf dem gemeinsamen FRM-Dokument, ohne VM oder Ereignisschleife.
use crate::{
app::{App, Dialog, DialogKind, Field, Hit, Mode, WindowKind},
commands::Command,
documents::DocumentId,
};
use anyhow::{anyhow, bail, ensure, Context, Result};
use crossterm::event::{
KeyCode as K, KeyEvent, KeyModifiers as M, MouseButton, MouseEvent, MouseEventKind,
};
use ratatui::{
layout::Rect,
style::{Modifier, Style},
widgets::Paragraph,
Frame,
};
use std::collections::{BTreeMap, BTreeSet};
use tb_frontend::{
forms::{self, ObjectClass as Class, PropertyType},
lexer::{self, TokenKind},
};
use tb_runtime::screen::TextScreen;
use tb_ui::{
forms::{FormsModel, PropertyValue as Value},
frm::{self, FormFile, FormNode},
};
use tb_vm::project_io::Content;
pub type DesignId = u64;
type Key = (String, Option<i32>);
type Drag = (u16, u16, Option<(i8, i8)>);
#[derive(Default)]
pub struct Designer {
pub document: Option<DocumentId>,
pub selections: BTreeMap<DocumentId, Vec<DesignId>>,
pub grid: bool,
pub tool: Option<(Class, i32)>,
pub property: usize,
pub value: Option<Field>,
clipboard: Vec<FormNode>,
pub viewport: Rect,
pub objects: Vec<(DesignId, Rect)>,
drag: Option<Drag>,
paint: Option<(&'static str, u8)>,
last_tool_click: Option<(usize, std::time::Instant)>,
}
pub fn node_key(n: &FormNode) -> Key {
(
n.name.to_uppercase(),
forms::property(n.class, "INDEX")
.and_then(|(p, _)| n.properties.get(&p))
.and_then(|v| {
if let Value::Integer(n) = v {
Some(*n)
} else {
None
}
}),
)
}
pub fn nodes(root: &FormNode) -> Vec<&FormNode> {
let mut out = vec![root];
for n in &root.children {
out.extend(nodes(n));
}
out
}
fn find_mut<'a>(n: &'a mut FormNode, key: &Key) -> Option<&'a mut FormNode> {
if node_key(n) == *key {
return Some(n);
}
n.children.iter_mut().find_map(|n| find_mut(n, key))
}
pub fn value(n: &FormNode, name: &str) -> Option<Value> {
let (p, s) = forms::property(n.class, name)?;
Some(
n.properties
.get(&p)
.cloned()
.unwrap_or_else(|| Value::from_default(s.default, s.ty)),
)
}
fn integer(n: &FormNode, name: &str) -> i32 {
match value(n, name) {
Some(Value::Integer(n)) => n,
_ => 0,
}
}
fn set(n: &mut FormNode, name: &str, v: Value) -> Result<()> {
let (p, _) =
forms::property(n.class, name).ok_or_else(|| anyhow!("{}.{} unbekannt", n.name, name))?;
n.properties.insert(p, v);
Ok(())
}
fn text(v: &Value) -> String {
match v {
Value::Integer(v) => v.to_string(),
Value::Single(v) => v.to_string(),
Value::String(v) => v.clone(),
Value::Boolean(v) => v.to_string(),
_ => format!("{v:?}"),
}
}
fn container(c: Class) -> bool {
matches!(c, Class::Form | Class::Frame | Class::PictureBox)
}
/// Beide Spin-Stile sind Varianten derselben Metadatenklasse.
pub fn tools() -> Vec<(&'static str, Class, i32)> {
let mut out: Vec<_> = Class::ALL
.into_iter()
.filter(|c| !matches!(c, Class::Form | Class::Screen | Class::Menu | Class::Spin))
.map(|c| (c.display_name(), c, 0))
.collect();
out.extend([("VSpin", Class::Spin, 0), ("HSpin", Class::Spin, 1)]);
out
}
pub fn preview(form: &FormFile, size: (u16, u16)) -> Result<(FormsModel, TextScreen)> {
let mut model = FormsModel::new(form.catalog().objects, size.0 as usize, size.1 as usize);
form.apply(&mut model)?;
// Timer bleiben ausschließlich im Dokument konfiguriert, nie in der Vorschau aktiv.
let enabled = forms::property(Class::Timer, "ENABLED").unwrap().0;
let catalog = form.catalog();
for node in nodes(&form.root)
.into_iter()
.filter(|n| n.class == Class::Timer)
{
let object = catalog.find(&node.name).unwrap().0;
model.set_initial_at(object, node_key(node).1, enabled, Value::Boolean(false))?;
}
model.show(0, false)?;
// Es gibt weder eine VM noch einen Timer-/Event-Pump im Designer.
model.events.clear();
let mut screen = TextScreen::with_size(size.0 as usize, size.1 as usize);
model.render(&mut screen);
Ok((model, screen))
}
fn validate(form: &FormFile) -> Result<()> {
let parsed = frm::read_text("Designer", &frm::write_text(form))?;
let catalog = parsed.catalog();
parsed.initial_values(&catalog)?;
fn visit(n: &FormNode, parent: Option<&FormNode>, depth: usize) -> Result<()> {
if let Some(p) = parent {
ensure!(
if n.class == Class::Menu {
matches!(p.class, Class::Menu | Class::Form)
} else {
container(p.class)
},
"{}: ungültiger Container {}",
n.name,
p.name
);
if !matches!(n.class, Class::Timer | Class::Menu) {
ensure!(
integer(n, "LEFT") + integer(n, "WIDTH") <= integer(p, "WIDTH")
&& integer(n, "TOP") + integer(n, "HEIGHT") <= integer(p, "HEIGHT"),
"{}: Geometrie außerhalb von {}",
n.name,
p.name
);
}
}
if n.class == Class::Menu {
ensure!(depth <= 6, "{}: höchstens sechs Menüebenen", n.name);
let shortcut = text(&value(n, "SHORTCUT").unwrap());
ensure!(
shortcut.is_empty()
|| forms::menu_shortcuts()
.iter()
.any(|s| s.eq_ignore_ascii_case(&shortcut.replace(' ', ""))),
"{}.Shortcut: nicht unterstützt",
n.name
);
ensure!(
shortcut.is_empty() || parent.is_some_and(|p| p.class == Class::Menu),
"{}.Shortcut: kein Shortcut auf Menütitel",
n.name
);
if value(n, "SEPARATOR") == Some(Value::Boolean(true))
|| value(n, "CAPTION") == Some(Value::String("-".into()))
{
ensure!(
n.children.is_empty()
&& shortcut.is_empty()
&& value(n, "CHECKED") == Some(Value::Boolean(false))
&& value(n, "ENABLED") == Some(Value::Boolean(true)),
"{}: ungültiger Separator",
n.name
);
}
}
for child in &n.children {
visit(
child,
Some(n),
depth + usize::from(child.class == Class::Menu),
)?;
}
Ok(())
}
visit(&parsed.root, None, 0)
}
impl App {
pub fn design_document(&self) -> Result<DocumentId> {
self.active_window()
.and_then(|w| {
if let WindowKind::Code(v) = w.kind {
self.project.view(v).ok().map(|v| v.document())
} else {
None
}
})
.filter(|id| {
self.project
.document(*id)
.is_ok_and(|d| matches!(d.content(), Content::Form(_)))
})
.or(self.designer.document)
.ok_or_else(|| anyhow!("Kein Formular ausgewählt"))
}
pub fn design_form(&self) -> Result<&FormFile> {
let Content::Form(f) = self.project.document(self.design_document()?)?.content() else {
bail!("Kein Formular")
};
Ok(f)
}
pub fn design_enter(&mut self, id: DocumentId) -> Result<()> {
self.designer.document = Some(id);
let ids = self.project.design_ids(id)?.clone();
let root = ids[&node_key(&self.design_form()?.root)];
let selection = self.designer.selections.entry(id).or_default();
selection.retain(|i| ids.values().any(|v| i == v));
if selection.is_empty() {
selection.push(root);
}
self.designer.value = None;
Ok(())
}
pub fn design_objects(&mut self) -> Result<Vec<(DesignId, FormNode)>> {
let id = self.design_document()?;
let ids = self.project.design_ids(id)?.clone();
Ok(nodes(&self.design_form()?.root)
.into_iter()
.map(|n| (ids[&node_key(n)], n.clone()))
.collect())
}
pub fn design_select(&mut self, id: DesignId, multiple: bool) -> Result<()> {
ensure!(
self.design_objects()?.iter().any(|(i, _)| *i == id),
"Unbekanntes Designobjekt"
);
let doc = self.design_document()?;
let selection = self.designer.selections.entry(doc).or_default();
if !multiple {
selection.clear();
}
if multiple && selection.contains(&id) {
selection.retain(|i| *i != id);
} else {
selection.push(id);
}
self.designer.property = 0;
self.designer.value = None;
self.value_focus = false;
Ok(())
}
fn selected_keys(&mut self) -> Result<Vec<Key>> {
let doc = self.design_document()?;
let selected = self
.designer
.selections
.get(&doc)
.cloned()
.unwrap_or_default();
Ok(self
.design_objects()?
.into_iter()
.filter(|(id, _)| selected.contains(id))
.map(|(_, n)| node_key(&n))
.collect())
}
fn selected_node(&mut self) -> Result<FormNode> {
let key = self
.selected_keys()?
.last()
.cloned()
.ok_or_else(|| anyhow!("Kein Control ausgewählt"))?;
Ok(nodes(&self.design_form()?.root)
.into_iter()
.find(|n| node_key(n) == key)
.unwrap()
.clone())
}
fn design_edit(&mut self, edit: impl FnOnce(&mut FormFile) -> Result<()>) -> Result<()> {
let id = self.design_document()?;
self.project.edit_form(id, |f| {
edit(f)?;
validate(f)
})?;
self.designer.value = None;
Ok(())
}
pub fn design_place(
&mut self,
class: Class,
style: i32,
parent: DesignId,
rect: Rect,
) -> Result<DesignId> {
ensure!(
!matches!(class, Class::Form | Class::Screen),
"Keine platzierbare Klasse"
);
let objects = self.design_objects()?;
let p = objects
.iter()
.find(|(id, _)| *id == parent)
.ok_or_else(|| anyhow!("Container fehlt"))?
.1
.clone();
ensure!(
if class == Class::Menu {
matches!(p.class, Class::Menu | Class::Form)
} else {
container(p.class)
},
"{} ist kein passender Container",
p.name
);
let names: BTreeSet<_> = objects.iter().map(|(_, n)| n.name.to_uppercase()).collect();
let name = (1..)
.map(|n| format!("{}{n}", class.display_name()))
.find(|n| !names.contains(&n.to_uppercase()))
.unwrap();
let mut node = FormNode {
class,
name: name.clone(),
properties: BTreeMap::new(),
children: vec![],
};
if !matches!(class, Class::Timer | Class::Menu) {
for (name, n) in [
("LEFT", rect.x),
("TOP", rect.y),
("WIDTH", rect.width),
("HEIGHT", rect.height),
] {
set(&mut node, name, Value::Integer(n as i32))?;
}
}
if class == Class::Spin {
ensure!((0..=1).contains(&style), "Spin.Style ungültig");
set(&mut node, "STYLE", Value::Integer(style))?;
set(
&mut node,
"WIDTH",
Value::Integer(if style == 1 { 2 } else { 1 }),
)?;
set(
&mut node,
"HEIGHT",
Value::Integer(if style == 1 { 1 } else { 2 }),
)?;
}
if forms::property(class, "CAPTION").is_some() {
set(&mut node, "CAPTION", Value::String(name.clone()))?;
}
if forms::property(class, "TABINDEX").is_some() {
set(
&mut node,
"TABINDEX",
Value::Integer(
objects
.iter()
.map(|(_, n)| integer(n, "TABINDEX"))
.max()
.unwrap_or(0)
+ 1,
),
)?;
}
let key = node_key(&node);
self.design_edit(|f| {
find_mut(&mut f.root, &node_key(&p))
.unwrap()
.children
.push(node);
Ok(())
})?;
let doc = self.design_document()?;
let id = self.project.design_ids(doc)?[&key];
self.design_select(id, false)?;
Ok(id)
}
pub fn design_geometry(&mut self, dx: i32, dy: i32, sizing: bool) -> Result<()> {
let keys = self.selected_keys()?;
self.design_edit(|f| {
// Ausgewählte Kinder eines ausgewählten Containers nicht doppelt verschieben.
fn edit(n: &mut FormNode, keys: &[Key], dx: i32, dy: i32, sizing: bool) -> Result<()> {
if keys.contains(&node_key(n)) && !matches!(n.class, Class::Timer | Class::Menu) {
for (name, delta) in [
(if sizing { "WIDTH" } else { "LEFT" }, dx),
(if sizing { "HEIGHT" } else { "TOP" }, dy),
] {
if delta == 0 {
continue;
}
let (_, spec) = forms::property(n.class, name).unwrap();
ensure!(spec.writable, "{}.{} ist schreibgeschützt", n.name, name);
set(n, name, Value::Integer(integer(n, name) + delta))?;
}
if !sizing {
return Ok(());
}
}
for c in &mut n.children {
edit(c, keys, dx, dy, sizing)?;
}
Ok(())
}
edit(&mut f.root, &keys, dx, dy, sizing)
})
}
fn design_resize(&mut self, dx: i32, dy: i32, edges: (i8, i8)) -> Result<()> {
let keys = self.selected_keys()?;
self.design_edit(|f| {
for key in keys {
let n = find_mut(&mut f.root, &key).ok_or_else(|| anyhow!("Control fehlt"))?;
if matches!(n.class, Class::Timer | Class::Menu) {
continue;
}
for (position, size, edge, delta) in [
("LEFT", "WIDTH", edges.0, dx),
("TOP", "HEIGHT", edges.1, dy),
] {
if edge == 0 || delta == 0 {
continue;
}
ensure!(
forms::property(n.class, size).unwrap().1.writable,
"{}.{} ist schreibgeschützt",
n.name,
size
);
if edge < 0 {
set(n, position, Value::Integer(integer(n, position) + delta))?;
}
set(
n,
size,
Value::Integer(integer(n, size) + delta * i32::from(edge)),
)?;
}
}
Ok(())
})
}
pub fn design_property(&mut self, name: &str, input: &str) -> Result<()> {
let n = self.selected_node()?;
if name.eq_ignore_ascii_case("CTLNAME") || name.eq_ignore_ascii_case("FORMNAME") {
return self.design_rename(input);
}
let (p, s) = forms::property(n.class, name)
.ok_or_else(|| anyhow!("{}.{} unbekannt", n.name, name))?;
let edit = || -> Result<Value> {
ensure!(s.writable, "schreibgeschützt");
Ok(match s.ty {
PropertyType::Integer => {
let v = input.parse::<i32>()?;
ensure!(
s.min.is_none_or(|min| v >= min) && s.max.is_none_or(|max| v <= max),
"außerhalb des Wertebereichs"
);
Value::Integer(v)
}
PropertyType::Single => {
let v = input.parse::<f32>()?;
ensure!(v.is_finite(), "nicht endlich");
Value::Single(v)
}
PropertyType::Boolean => {
Value::Boolean(match input.to_ascii_lowercase().as_str() {
"true" | "-1" | "1" => true,
"false" | "0" => false,
_ => bail!("Boolean erwartet: true/false"),
})
}
PropertyType::String => Value::String(input.into()),
_ => bail!("nicht editierbarer Propertytyp"),
})
};
let v = edit().with_context(|| format!("{}.{}", n.name, s.name))?;
let (mut preview_model, _) = preview(self.design_form()?, self.size)?;
let object = self.design_form()?.catalog().find(&n.name).unwrap().0;
preview_model
.set_at(object, node_key(&n).1, p, v.clone())
.with_context(|| format!("{}.{}", n.name, s.name))?;
let old = node_key(&n);
let doc = self.design_document()?;
let stable = self.project.design_ids(doc)?[&old];
let mut changed = n.clone();
changed.properties.insert(p, v.clone());
if n.class == Class::Spin && s.name == "STYLE" {
set(
&mut changed,
"WIDTH",
Value::Integer(if input == "1" { 2 } else { 1 }),
)?;
set(
&mut changed,
"HEIGHT",
Value::Integer(if input == "1" { 1 } else { 2 }),
)?;
}
let new = node_key(&changed);
self.design_edit(|f| {
*find_mut(&mut f.root, &old).unwrap() = changed;
Ok(())
})
.with_context(|| format!("{}.{}", n.name, s.name))?;
if old != new {
self.project.rekey_design_id(doc, &old, new, stable);
}
Ok(())
}
pub fn design_copy(&mut self, cut: bool) -> Result<()> {
let keys = self.selected_keys()?;
let mut copied = Vec::new();
fn collect(n: &FormNode, keys: &[Key], out: &mut Vec<FormNode>) {
if n.class != Class::Form && keys.contains(&node_key(n)) {
out.push(n.clone());
} else {
for c in &n.children {
collect(c, keys, out);
}
}
}
collect(&self.design_form()?.root, &keys, &mut copied);
ensure!(!copied.is_empty(), "Keine Controls ausgewählt");
if cut {
self.design_delete()?;
}
self.designer.clipboard = copied;
Ok(())
}
pub fn design_delete(&mut self) -> Result<()> {
let keys = self.selected_keys()?;
ensure!(
!keys.contains(&node_key(&self.design_form()?.root)),
"Formular über Remove File entfernen"
);
let names: Vec<_> = nodes(&self.design_form()?.root)
.into_iter()
.filter(|n| keys.contains(&node_key(n)))
.flat_map(nodes)
.map(|n| n.name.clone())
.collect();
self.design_edit(|f| {
fn remove(n: &mut FormNode, keys: &[Key]) {
n.children.retain(|c| !keys.contains(&node_key(c)));
for c in &mut n.children {
remove(c, keys);
}
}
remove(&mut f.root, &keys);
Ok(())
})?;
let id = self.design_document()?;
self.design_enter(id)?;
self.message=format!("Entfernt: {}. Vorhandener Code bleibt erhalten; zugehörige Handler sind jetzt ungebunden.",names.join(", "));
self.open_dialog(
"Löschfolgen",
DialogKind::Message,
vec![Field::text("Hinweis", self.message.clone())],
);
Ok(())
}
pub fn design_paste(&mut self) -> Result<()> {
ensure!(
!self.designer.clipboard.is_empty(),
"Control-Zwischenablage leer"
);
let parent = self.selected_node()?;
ensure!(
container(parent.class) || parent.class == Class::Menu,
"Zum Einfügen einen Container auswählen"
);
let mut copies = self.designer.clipboard.clone();
let mut names: BTreeSet<_> = nodes(&self.design_form()?.root)
.iter()
.map(|n| n.name.to_uppercase())
.collect();
let mut renamed = BTreeMap::new();
fn rename(
n: &mut FormNode,
names: &mut BTreeSet<String>,
renamed: &mut BTreeMap<String, String>,
) -> Result<()> {
let old = n.name.to_uppercase();
let name = renamed
.entry(old.clone())
.or_insert_with(|| {
let chosen = (1..)
.map(|i| format!("{old}Copy{i}"))
.find(|n| !names.contains(n))
.unwrap();
names.insert(chosen.clone());
chosen
})
.clone();
n.name = name.clone();
if let Some((p, _)) = forms::property(n.class, "CTLNAME") {
if n.properties.contains_key(&p) {
n.properties.insert(p, Value::String(name));
}
}
for c in &mut n.children {
rename(c, names, renamed)?;
}
Ok(())
}
for n in &mut copies {
rename(n, &mut names, &mut renamed)?;
}
let pasted: Vec<_> = copies.iter().map(node_key).collect();
self.design_edit(|f| {
find_mut(&mut f.root, &node_key(&parent))
.unwrap()
.children
.extend(copies);
Ok(())
})?;
let doc = self.design_document()?;
let ids = self.project.design_ids(doc)?;
self.designer
.selections
.insert(doc, pasted.iter().map(|k| ids[k]).collect());
Ok(())
}
pub fn design_menu_move(&mut self, direction: &str) -> Result<()> {
let key = node_key(&self.selected_node()?);
self.design_edit(|f| {
fn change(n: &mut FormNode, key: &Key, d: &str) -> Result<bool> {
if let Some(i) = n.children.iter().position(|c| node_key(c) == *key) {
ensure!(n.children[i].class == Class::Menu, "Kein Menüelement");
match d {
"up" => {
ensure!(
i > 0 && n.children[i - 1].class == Class::Menu,
"Kein Vorgänger"
);
n.children.swap(i, i - 1);
}
"down" => {
ensure!(
i + 1 < n.children.len() && n.children[i + 1].class == Class::Menu,
"Kein Nachfolger"
);
n.children.swap(i, i + 1);
}
"in" => {
ensure!(
i > 0 && n.children[i - 1].class == Class::Menu,
"Kein Menüvorgänger"
);
let c = n.children.remove(i);
n.children[i - 1].children.push(c);
}
_ => bail!("Keine äußere Menüebene"),
}
return Ok(true);
}
for i in 0..n.children.len() {
if d == "out" {
if let Some(j) = n.children[i]
.children
.iter()
.position(|c| node_key(c) == *key)
{
let c = n.children[i].children.remove(j);
n.children.insert(i + 1, c);
return Ok(true);
}
}
if change(&mut n.children[i], key, d)? {
return Ok(true);
}
}
Ok(false)
}
ensure!(change(&mut f.root, &key, direction)?, "Menüelement fehlt");
Ok(())
})
}
pub fn design_rename(&mut self, new: &str) -> Result<()> {
self.design_rename_with(new, None)
}
fn design_rename_with(&mut self, new: &str, extra: Option<FormFile>) -> Result<()> {
let selected = self.selected_node()?;
let old = selected.name.to_uppercase();
if old == new.to_uppercase() {
return Ok(());
}
// Benutzt dieselbe Namensprüfung wie die Prozedurerzeugung.
tb_frontend::editing::new_procedure("", 0, new, false).map_err(|e| anyhow!(e))?;
let doc = self.design_document()?;
let ids = self.project.design_ids(doc)?.clone();
let input = self.project.sources()?;
let mut compiler = tb_vm::project::ProjectCompiler::default();
let compiled = input.compile(&mut compiler, "RENAME").map_err(|d| {
anyhow!(
"Umbenennen erfordert eindeutig auflösbare Quellen: {}",
d.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("; ")
)
})?;
let mut catalog = forms::FormCatalog::default();
for f in &input.forms {
catalog.append(&f.catalog());
}
let form_name = self.design_form()?.root.name.clone();
ensure!(
catalog.objects.iter().filter(|o| o.name.eq_ignore_ascii_case(new)).all(|o| {
node_key(&selected).1.is_some() && o.array && o.class == selected.class
&& o.parent_form.as_deref().is_some_and(|name| name.eq_ignore_ascii_case(&form_name))
}),
"Name {new} bereits vorhanden; nur indizierte Controls derselben Klasse und Form können ein Array bilden"
);
let target = if selected.class == Class::Form {
catalog.find(&old)
} else {
catalog.find_in(&old, &form_name)
}
.ok_or_else(|| anyhow!("Objektbindung fehlt"))?
.0;
let mut changes: BTreeMap<DocumentId, Content> = BTreeMap::new();
let mut edits: BTreeMap<DocumentId, BTreeMap<usize, (usize, String)>> = BTreeMap::new();
let mut handlers = BTreeMap::new();
let mut references = Vec::new();
let mut procedures = Vec::new();
let source_files = compiled.sources;
let bound = compiler.bound_form_references().map_err(|d| {
anyhow!(
"Unklare Bindungen: {}",
d.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("; ")
)
})?;
procedures.extend(bound.procedures);
for (pos, name, object) in &bound.objects {
if *object != target {
continue;
}
if *name == old {
references.push((*pos, name.clone(), new.to_owned()));
} else if let Some(event) = name.strip_prefix(&format!("{old}_")) {
let replacement = format!("{new}_{event}");
handlers.insert(name.clone(), replacement.clone());
references.push((*pos, name.clone(), replacement));
}
}
for (pos, name) in procedures {
if let Some(replacement) = handlers.get(&name) {
references.push((pos, name, replacement.clone()));
}
}
for (pos, name, replacement) in references {
let path = &source_files[pos.source as usize].path;
let id = self
.project
.find_document(std::path::Path::new(path))
.ok_or_else(|| anyhow!("Betroffene Include-Datei nicht geöffnet: {path}"))?;
let d = self.project.document(id)?;
let first = match d.content() {
Content::Form(f) => frm::read_text(path, &frm::write_text(f))?.code_line(),
_ => 1,
};
let line = pos
.line
.checked_sub(first)
.ok_or_else(|| anyhow!("Referenz außerhalb des Codebereichs"))?
+ 1;
let lexed = lexer::lex(d.code());
let t = lexed
.tokens
.iter()
.find(|t| {
t.pos.line == line
&& t.pos.column >= pos.column
&& matches!(&t.kind,TokenKind::Ident{name:n,..} if n.split(['.','!']).any(|part|part==name))
})
.ok_or_else(|| anyhow!("{path}:{line}: {name} nicht sicher lokalisierbar"))?;
let token_start = token_offset(d.code(), t.pos.line, t.pos.column)?;
let TokenKind::Ident {
name: path_name, ..
} = &t.kind
else {
unreachable!()
};
let mut component = 0;
for part in path_name.split(['.', '!']) {
if part == name {
break;
}
component += part.len() + 1;
}
let start = token_start + component;
ensure!(
d.code()
.get(start..start + name.len())
.is_some_and(|s| s.eq_ignore_ascii_case(&name)),
"Referenz nicht bytegenau auflösbar"
);
edits
.entry(id)
.or_default()
.insert(start, (start + name.len(), replacement));
}
for (id, replacements) in edits {
let mut content = self.project.document(id)?.content().clone();
for (start, (end, replacement)) in replacements.into_iter().rev() {
content.code_mut().replace_range(start..end, &replacement);
}
changes.insert(id, content);
}
let content = changes
.entry(doc)
.or_insert_with(|| self.project.document(doc).unwrap().content().clone());
let Content::Form(f) = content else {
unreachable!()
};
fn rename(n: &mut FormNode, old: &str, new: &str) {
if n.name.eq_ignore_ascii_case(old) {
n.name = new.into();
let property = if n.class == Class::Form {
"FORMNAME"
} else {
"CTLNAME"
};
if let Some((p, _)) = forms::property(n.class, property) {
if n.properties.contains_key(&p) {
n.properties.insert(p, Value::String(new.into()));
}
}
}
for c in &mut n.children {
rename(c, old, new);
}
}
if let Some(extra) = extra {
f.root = extra.root;
}
rename(&mut f.root, &old, new);
validate(f)?;
let mut loader = self.project.loader()?;
for (id, c) in &changes {
loader
.insert(self.project.document(*id)?.source_path(), c.clone())
.map_err(|e| anyhow!(e))?;
}
let candidate = loader
.load_manifest(self.project.manifest().clone())
.map_err(|e| anyhow!(e))?;
let mut candidate_compiler = tb_vm::project::ProjectCompiler::default();
let candidate_compiled = candidate
.compile(&mut candidate_compiler, "RENAME")
.map_err(|d| {
anyhow!(
"Namenskonflikt; nichts geändert: {}",
d.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("; ")
)
})?;
// Dieselben vollständig aufgelösten Importkontexte vor und nach der Änderung.
let candidate_bound = candidate_compiler.bound_form_references().map_err(|d| {
anyhow!(
"Unklare neue Bindungen: {}",
d.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("; ")
)
})?;
let mut candidate_catalog = forms::FormCatalog::default();
for form in &candidate.forms {
candidate_catalog.append(&form.catalog());
}
let bindings = |references: &[(tb_frontend::SourcePos, String, u16)],
files: &[tb_frontend::source::SourceFile],
objects: &forms::FormCatalog,
rename_old: bool| {
let mut result = Vec::new();
for (p, n, o) in references {
let mut n = n.clone();
let object = &objects.objects[*o as usize];
let mut name = object.name.clone();
let mut owner = object.parent_form.clone();
if rename_old && *o == target {
name = new.to_uppercase();
if n == old {
n = new.to_uppercase();
} else if let Some(r) = handlers.get(&n) {
n = r.to_uppercase();
}
}
if rename_old && selected.class == Class::Form && owner.as_deref() == Some(&old) {
owner = Some(new.to_uppercase());
}
// Merging arrays changes catalog indices. Compare semantic object identities.
result.push((
files[p.source as usize].path.clone(),
n,
name,
owner,
object.class.name(),
object.array,
));
}
result.sort();
result
};
ensure!(
bindings(&bound.objects, &source_files, &catalog, true)
== bindings(
&candidate_bound.objects,
&candidate_compiled.sources,
&candidate_catalog,
false
),
"Unklare oder verdeckte Objektverweise; nichts geändert"
);
self.project
.commit_transaction(changes.into_iter().collect())?;
self.project.rename_design_ids(doc, &old, new, &ids);
self.designer.value = None;
Ok(())
}
pub fn design_event(&mut self, object: DesignId, event: &str) -> Result<()> {
let doc = self.design_document()?;
let n = self
.design_objects()?
.into_iter()
.find(|(id, _)| *id == object)
.ok_or_else(|| anyhow!("Objekt fehlt"))?
.1;
let event = event.to_uppercase();
ensure!(
forms::events(n.class).contains(&event.as_str()),
"{}.{} ist kein Ereignis",
n.name,
event
);
let prefix = if n.class == Class::Form {
"Form"
} else {
&n.name
};
let name = format!("{prefix}_{event}");
let input = self.project.sources()?;
let mut catalog = forms::FormCatalog::default();
for f in &input.forms {
catalog.append(&f.catalog());
}
let target_object = if n.class == Class::Form {
catalog.find(&n.name)
} else {
catalog.find_in(&n.name, &self.design_form()?.root.name)
}
.ok_or_else(|| anyhow!("Objektbindung fehlt"))?
.0;
let mut found = None;
for unit in &input.units {
let mut files = Vec::new();
let (module, diags) = unit.parse(0, &mut files);
ensure!(
diags.is_empty(),
"Unvollständiger Quelltext vor Ereigniserzeugung"
);
let bound = tb_frontend::sema::bound_form_references(&module, &catalog);
let exports = tb_frontend::sema::export_declarations(&module, &[]);
for p in module.procs.iter().filter(|p| {
p.sig.name.rsplit_once('_').is_some_and(|(_, e)| e == event)
&& bound.objects.iter().any(|(pos, name, object)| {
*pos == p.pos && *name == p.sig.name && *object == target_object
})
}) {
let sig = exports
.iter()
.find_map(|s| match s {
tb_frontend::ast::Stmt::Declare { sig, .. } if sig.name == p.sig.name => {
Some(sig)
}
_ => None,
})
.unwrap();
ensure!(found.is_none(), "Mehrdeutige Ereignisprozedur {name}");
let target = self
.project
.find_document(std::path::Path::new(&files[p.pos.source as usize].path))
.ok_or_else(|| anyhow!("Ereignisdatei fehlt"))?;
let params = event_parameters(&n, &event);
ensure!(
sig.kind == tb_frontend::ast::ProcKind::Sub
&& sig.params.len() == params.len()
&& sig
.params
.iter()
.zip(&params)
.all(
|(actual, (name, ty))| actual.name.eq_ignore_ascii_case(name)
&& actual
.as_type
.as_ref()
.map(tb_frontend::editing::type_text)
.or_else(|| actual.suffix.map(|s| match s {
lexer::Suffix::Integer => "INTEGER",
lexer::Suffix::Single => "SINGLE",
_ => "",
}
.into()))
.is_some_and(|t| t == *ty)
),
"{name}: vorhandene Ereignissignatur stimmt nicht"
);
let first = match self.project.document(target)?.content() {
Content::Form(f) => frm::read_text("Event", &frm::write_text(f))?.code_line(),
_ => 1,
};
found = Some((
target,
crate::editor::line_offset(
self.project.document(target)?.code(),
p.pos.line - first + 1,
),
));
}
}
let (target, at) = if let Some(found) = found {
found
} else {
let code = self.project.document(doc)?.code();
let mut addition = self.new_procedure_text(doc, code.len(), &name, false, true)?;
let params = event_parameters(&n, &event)
.iter()
.map(|(name, ty)| format!("{name} AS {ty}"))
.collect::<Vec<_>>()
.join(", ");
// BASIC-Parameter sind BYREF; derselbe Prozedurrumpf wie New Sub.
addition = addition.replacen(" ()", &format!(" ({params})"), 1);
let at = code.len();
self.project.replace_text(doc, at..at, &addition)?;
(doc, at + 1)
};
self.designer.document = Some(doc);
self.designer.selections.insert(doc, vec![object]);
self.show_document(target, false)?;
let v = self.editor_view()?;
self.project.view_mut(v)?.cursor = at;
self.editor_scroll()?;
Ok(())
}
pub(crate) fn designer_command(&mut self, command: Command) -> Result<bool> {
use Command::*;
match command {
Grid => self.designer.grid = !self.designer.grid,
Toolbox | Palette | MenuDesign => {
self.design_document()?;
self.show_tool(match command {
Toolbox => WindowKind::Toolbox,
Palette => WindowKind::Palette,
_ => WindowKind::MenuDesign,
});
}
Tool(name) => {
let (_, class, style) = tools()
.into_iter()
.find(|(n, _, _)| n.eq_ignore_ascii_case(name))
.ok_or_else(|| anyhow!("Unbekanntes Werkzeug"))?;
let doc = self.design_document()?;
self.show_document(doc, true)?;
self.designer.tool = Some((class, style));
self.value_focus = false;
}
Events => {
let objects = self.design_objects()?;
let selected = self.selected_keys()?.last().cloned();
let at = objects
.iter()
.position(|(_, n)| Some(node_key(n)) == selected)
.unwrap_or(0);
self.open_dialog(
"Event Procedures · Objekt",
DialogKind::DesignObject(objects.iter().map(|(id, _)| *id).collect()),
vec![Field::choice(
"Objekt",
objects
.iter()
.map(|(_, n)| format!("{} {:?}", n.name, node_key(n).1))
.collect(),
at,
)],
);
}
Cut | Copy | Paste | Clear if self.mode == Mode::Designer => match command {
Cut => self.design_copy(true)?,
Copy => self.design_copy(false)?,
Paste => self.design_paste()?,
_ => self.design_delete()?,
},
Undo if self.mode == Mode::Designer => {
let doc = self.design_document()?;
self.project.undo(doc)?;
self.design_enter(doc)?;
}
Form if self.designer.document.is_some() && self.mode == Mode::Environment => {
self.show_document(self.designer.document.unwrap(), true)?;
}
Code if self.mode == Mode::Designer => {
let doc = self.design_document()?;
self.show_document(doc, false)?;
}
_ => return Ok(false),
}
Ok(true)
}
pub(crate) fn design_help_context(&mut self) -> Result<(String, String)> {
let node = self.selected_node()?;
let specs = forms::properties(node.class);
let property = if self.properties || self.value_focus {
specs.get(self.designer.property).map_or("", |p| p.name)
} else {
""
};
Ok((node.class.name().to_owned(), property.into()))
}
pub(crate) fn design_value_focus(&mut self) -> Result<()> {
let node = self.selected_node()?;
let specs = forms::properties(node.class);
self.designer.property = self.designer.property.min(specs.len() - 1);
let name = specs[self.designer.property].name;
let v = if matches!(name, "CTLNAME" | "FORMNAME") {
node.name.clone()
} else {
text(&value(&node, name).unwrap())
};
self.designer.value = Some(Field::text(name, v));
self.properties = true;
self.value_focus = true;
Ok(())
}
pub(crate) fn design_action(&mut self, action: &str) -> Result<()> {
match action {
"property" => {
let n = self.selected_node()?;
let specs = forms::properties(n.class);
self.open_dialog(
"Properties",
DialogKind::DesignProperty,
vec![Field::choice(
"Property",
specs.iter().map(|p| p.name.into()).collect(),
self.designer.property.min(specs.len() - 1),
)],
);
}
"value" => self.design_value_focus()?,
"palette" => self.open_dialog(
"Color Palette",
DialogKind::DesignPalette,
vec![
Field::choice("ForeColor", (0..16).map(|n| n.to_string()).collect(), 0),
Field::choice("BackColor", (0..16).map(|n| n.to_string()).collect(), 7),
Field::toggle("Auf Formular auftragen", false),
],
),
"insert" => {
let objects = self.design_objects()?;
let selected = self.selected_node()?;
let parent = if selected.class == Class::Menu {
objects
.iter()
.find(|(_, n)| node_key(n) == node_key(&selected))
.unwrap()
.0
} else {
objects[0].0
};
self.design_place(Class::Menu, 0, parent, Rect::default())?;
}
"delete" => self.design_delete()?,
"up" | "down" | "in" | "out" => self.design_menu_move(action)?,
"menu" => {
let n = self.selected_node()?;
ensure!(n.class == Class::Menu, "Menüelement auswählen");
let shortcuts = forms::menu_shortcuts();
let current = text(&value(&n, "SHORTCUT").unwrap());
let at = shortcuts
.iter()
.position(|s| s.eq_ignore_ascii_case(&current.replace(' ', "")))
.unwrap_or(0);
self.open_dialog(
"Menu Design",
DialogKind::DesignMenu(node_key(&n)),
vec![
Field::text("CtlName", n.name.clone()),
Field::text("Caption", text(&value(&n, "CAPTION").unwrap())),
Field::text("Tag", text(&value(&n, "TAG").unwrap())),
Field::text(
"Index (leer: skalar)",
node_key(&n).1.map(|i| i.to_string()).unwrap_or_default(),
),
Field::toggle(
"Checked",
value(&n, "CHECKED") == Some(Value::Boolean(true)),
),
Field::toggle(
"Enabled",
value(&n, "ENABLED") == Some(Value::Boolean(true)),
),
Field::toggle(
"Visible",
value(&n, "VISIBLE") == Some(Value::Boolean(true)),
),
Field::toggle(
"Separator",
value(&n, "SEPARATOR") == Some(Value::Boolean(true)),
),
Field::choice("Shortcut", shortcuts, at),
],
);
}
_ => bail!("Unbekannte Designeraktion"),
}
Ok(())
}
pub(crate) fn design_submit(&mut self, d: &mut Dialog) -> Result<bool> {
match &d.kind {
DialogKind::DesignProperty => {
self.designer.property = d.fields[0].index();
self.design_value_focus()?;
}
DialogKind::DesignObject(ids) => {
let id = ids[d.fields[0].index()];
let n = self
.design_objects()?
.into_iter()
.find(|(i, _)| *i == id)
.unwrap()
.1;
self.open_dialog(
"Event Procedures · Ereignis",
DialogKind::DesignEvent(id),
vec![Field::choice(
"Ereignis",
forms::events(n.class)
.iter()
.map(|s| s.to_string())
.collect(),
0,
)],
);
}
DialogKind::DesignEvent(id) => self.design_event(*id, &d.fields[0].string())?,
DialogKind::DesignPalette => {
let keys = if d.fields[2].flag() {
vec![node_key(&self.design_form()?.root)]
} else {
self.selected_keys()?
};
let fg = d.fields[0].index() as i32;
let bg = d.fields[1].index() as i32;
self.design_edit(|f| {
for key in keys {
let n = find_mut(&mut f.root, &key).unwrap();
for (name, v) in [("FORECOLOR", fg), ("BACKCOLOR", bg)] {
if forms::property(n.class, name).is_some() {
set(n, name, Value::Integer(v))?;
}
}
}
Ok(())
})?;
}
DialogKind::DesignMenu(key) => {
let key = key.clone();
let new = d.fields[0].string();
let mut candidate = self.design_form()?.clone();
let n = find_mut(&mut candidate.root, &key)
.ok_or_else(|| anyhow!("Menüelement fehlt"))?;
for (name, field) in [("CAPTION", 1), ("TAG", 2), ("SHORTCUT", 8)] {
set(n, name, Value::String(d.fields[field].string()))?;
}
for (name, field) in [
("CHECKED", 4),
("ENABLED", 5),
("VISIBLE", 6),
("SEPARATOR", 7),
] {
set(n, name, Value::Boolean(d.fields[field].flag()))?;
}
let p = forms::property(Class::Menu, "INDEX").unwrap().0;
if d.fields[3].string().is_empty() {
n.properties.remove(&p);
} else {
n.properties.insert(
p,
Value::Integer(
d.fields[3]
.string()
.parse()
.context("Menu.Index: Integer erwartet")?,
),
);
}
let final_key = node_key(n);
let updated = n.clone();
validate(&candidate)?;
// Eine Namensänderung und die übrigen Felder werden zusammen validiert.
// Die Rename-Transaktion muss vor jedem Commit alle Kandidaten kennen.
self.design_menu_commit(&key, &new, updated, final_key)?;
}
_ => return Ok(false),
}
Ok(true)
}
pub(crate) fn design_key(&mut self, key: KeyEvent) -> Result<bool> {
if self.mode != Mode::Designer {
return Ok(false);
}
if self.value_focus {
if key.code == K::Esc {
self.value_focus = false;
self.designer.value = None;
return Ok(true);
}
if key.code == K::Enter {
let field = self
.designer
.value
.clone()
.ok_or_else(|| anyhow!("Keine Property ausgewählt"))?;
self.design_property(&field.label, &field.string())?;
self.value_focus = false;
return Ok(true);
}
if let Some(v) = &mut self.designer.value {
v.key(key);
}
return Ok(true);
}
match self.active_window().map(|w| w.kind) {
Some(WindowKind::Palette) if key.code == K::Enter => {
self.design_action("palette")?;
return Ok(true);
}
Some(WindowKind::Toolbox) => {
let list = tools();
let at = self
.designer
.tool
.and_then(|t| list.iter().position(|(_, c, s)| (*c, *s) == t))
.unwrap_or(0);
match key.code {
K::Up | K::Down => {
let i = (at
+ list.len()
+ if key.code == K::Down {
1
} else {
list.len() - 1
})
% list.len();
self.designer.tool = Some((list[i].1, list[i].2));
}
K::Enter => {
self.design_tool_double(at)?;
}
_ => return Ok(false),
}
return Ok(true);
}
Some(WindowKind::MenuDesign) => {
let control = key.modifiers.contains(M::CONTROL);
match key.code {
K::Insert => self.design_action("insert")?,
K::Delete => self.design_action("delete")?,
K::Enter => self.design_action("menu")?,
K::Left if control => self.design_action("out")?,
K::Right if control => self.design_action("in")?,
K::Up if control => self.design_action("up")?,
K::Down if control => self.design_action("down")?,
K::Up | K::Down => self.design_tab(key.code == K::Down, true)?,
_ => return Ok(false),
}
return Ok(true);
}
_ => {}
}
match key.code {
K::Tab | K::BackTab => self.design_tab(
key.code == K::Tab && !key.modifiers.contains(M::SHIFT),
false,
)?,
K::PageUp | K::PageDown => {
let n = forms::properties(self.selected_node()?.class).len();
self.designer.property =
(self.designer.property + n + if key.code == K::PageDown { 1 } else { n - 1 })
% n;
}
K::Left | K::Right | K::Up | K::Down => {
let step = if key.modifiers.contains(M::CONTROL) {
5
} else {
1
};
let (dx, dy) = match key.code {
K::Left => (-step, 0),
K::Right => (step, 0),
K::Up => (0, -step),
_ => (0, step),
};
self.design_geometry(dx, dy, key.modifiers.contains(M::SHIFT))?;
}
K::Delete => self.design_delete()?,
K::Esc => self.designer.tool = None,
_ => return Ok(false),
}
Ok(true)
}
fn design_tab(&mut self, forward: bool, menus: bool) -> Result<()> {
let mut objects = self.design_objects()?;
objects.retain(|(_, n)| {
if menus {
n.class == Class::Menu
} else {
!matches!(n.class, Class::Form | Class::Menu)
}
});
if objects.is_empty() {
return Ok(());
}
if !menus {
objects.sort_by_key(|(_, n)| integer(n, "TABINDEX"));
}
let selected = self.selected_keys()?.last().cloned();
let i = objects
.iter()
.position(|(_, n)| Some(node_key(n)) == selected);
let at = i.map_or(if forward { 0 } else { objects.len() - 1 }, |i| {
(i + objects.len() + if forward { 1 } else { objects.len() - 1 }) % objects.len()
});
self.design_select(objects[at].0, false)
}
fn design_tool_double(&mut self, index: usize) -> Result<()> {
let (_, class, style) = tools()[index];
let objects = self.design_objects()?;
let selected = self.selected_node()?;
let parent = objects
.iter()
.find(|(_, n)| node_key(n) == node_key(&selected) && container(n.class))
.unwrap_or(&objects[0])
.0;
self.design_place(
class,
style,
parent,
Rect::new(
1,
1,
if class == Class::VScrollBar { 1 } else { 8 },
if class == Class::VScrollBar { 5 } else { 1 },
),
)?;
self.designer.tool = None;
Ok(())
}
pub(crate) fn design_mouse(&mut self, m: MouseEvent) -> Result<bool> {
if self.mode != Mode::Designer || self.dialog.is_some() || self.menu.is_some() {
return Ok(false);
}
if let Some((property, color)) = self.designer.paint {
if m.kind == MouseEventKind::Up(MouseButton::Left) {
self.designer.paint = None;
let id = self
.designer
.objects
.iter()
.rev()
.find(|(_, r)| r.contains((m.column, m.row).into()))
.map(|(id, _)| *id)
.ok_or_else(|| anyhow!("Farbe auf die Designfläche ziehen"))?;
let n = self
.design_objects()?
.into_iter()
.find(|(i, _)| *i == id)
.unwrap()
.1;
ensure!(
forms::property(n.class, property).is_some(),
"{}.{} nicht vorhanden",
n.name,
property
);
self.design_edit(|f| {
set(
find_mut(&mut f.root, &node_key(&n)).unwrap(),
property,
Value::Integer(color as i32),
)
})?;
}
return Ok(true);
}
if m.kind == MouseEventKind::Down(MouseButton::Left) {
if let Some((_, Hit::DesignPaint(property, color))) = self
.hits
.iter()
.rev()
.find(|(r, _)| r.contains((m.column, m.row).into()))
{
self.designer.paint = Some((*property, *color));
return Ok(true);
}
}
if m.kind == MouseEventKind::Down(MouseButton::Left) {
if let Some((_, Hit::DesignTool(index))) = self
.hits
.iter()
.rev()
.find(|(r, _)| r.contains((m.column, m.row).into()))
{
let index = *index;
let double = self
.designer
.last_tool_click
.is_some_and(|(i, t)| i == index && t.elapsed().as_millis() < 500);
self.designer.last_tool_click = Some((index, std::time::Instant::now()));
let (_, class, style) = tools()[index];
self.designer.tool = Some((class, style));
if double {
self.designer.last_tool_click = None;
self.design_tool_double(index)?;
}
return Ok(true);
}
}
if self.designer.drag.is_none() {
if let Some((_, hit)) = self
.hits
.iter()
.rev()
.find(|(r, _)| r.contains((m.column, m.row).into()))
{
match hit {
Hit::Window(id)
if self
.windows
.iter()
.any(|w| w.id == *id && matches!(w.kind, WindowKind::Code(_))) => {}
_ => return Ok(false),
}
}
}
let inside = self.designer.viewport.contains((m.column, m.row).into());
if !inside && self.designer.drag.is_none() {
return Ok(false);
}
match m.kind {
MouseEventKind::Down(MouseButton::Left) => {
if let Some(doc) = self.designer.document {
if let Some(w)=self.windows.iter().find(|w|matches!(w.kind,WindowKind::Code(v) if self.project.view(v).is_ok_and(|v|v.document()==doc))) {self.active=w.id;}
}
let hit = self
.designer
.objects
.iter()
.rev()
.find(|(_, r)| r.contains((m.column, m.row).into()))
.copied();
if let Some((id, r)) = hit {
let selected = self
.designer
.selections
.get(&self.design_document()?)
.is_some_and(|s| s.contains(&id));
let resize = if selected && self.designer.tool.is_none() {
handle_at(r, m.column, m.row)
} else {
None
};
if !selected || m.modifiers.contains(M::CONTROL) {
self.design_select(id, m.modifiers.contains(M::CONTROL))?;
}
self.designer.drag = Some((m.column, m.row, resize));
} else if self.designer.tool.is_some() {
self.designer.drag = Some((m.column, m.row, None));
}
self.value_focus = false;
}
MouseEventKind::Up(MouseButton::Left) => {
if let Some((x, y, handle)) = self.designer.drag.take() {
if let Some((class, style)) = self.designer.tool.take() {
let objects = self.design_objects()?;
let parent = self
.designer
.objects
.iter()
.rev()
.find(|(id, r)| {
r.contains((x, y).into())
&& objects.iter().any(|(i, n)| i == id && container(n.class))
})
.copied()
.ok_or_else(|| anyhow!("Innerhalb eines Containers aufziehen"))?;
let left = x
.min(m.column)
.checked_sub(parent.1.x)
.ok_or_else(|| anyhow!("Außerhalb des Containers"))?;
let top = y
.min(m.row)
.checked_sub(parent.1.y)
.ok_or_else(|| anyhow!("Außerhalb des Containers"))?;
self.design_place(
class,
style,
parent.0,
Rect::new(left, top, x.abs_diff(m.column) + 1, y.abs_diff(m.row) + 1),
)?;
} else {
let dx = m.column as i32 - x as i32;
let dy = m.row as i32 - y as i32;
if dx != 0 || dy != 0 {
if let Some(edges) = handle {
self.design_resize(dx, dy, edges)?;
} else {
self.design_geometry(dx, dy, false)?;
}
}
}
}
}
MouseEventKind::Drag(MouseButton::Left) => {}
_ => return Ok(false),
}
Ok(true)
}
fn design_menu_commit(
&mut self,
key: &Key,
new: &str,
updated: FormNode,
final_key: Key,
) -> Result<()> {
let doc = self.design_document()?;
let stable = self.project.design_ids(doc)?[key];
let mut candidate = self.design_form()?.clone();
*find_mut(&mut candidate.root, key).unwrap() = updated;
if !key.0.eq_ignore_ascii_case(new) {
self.design_rename_with(new, Some(candidate))?;
} else {
self.design_edit(|f| {
f.root = candidate.root;
Ok(())
})?;
}
self.project
.rekey_design_id(doc, key, (new.to_uppercase(), final_key.1), stable);
Ok(())
}
pub(crate) fn design_render(&mut self, f: &mut Frame, area: Rect) {
self.designer.viewport = area;
self.designer.objects.clear();
let render = || -> Result<_> {
let form = self.design_form()?.clone();
let (model, screen) = preview(&form, (area.width, area.height))?;
Ok((form, model, screen))
};
let Ok((form, model, screen)) = render() else {
return;
};
f.render_widget(tb_ui::screen::ScreenWidget(&screen), area);
let Ok(objects) = self.design_objects() else {
return;
};
let catalog = form.catalog();
let mut timers = 0;
for (id, n) in objects {
if n.class == Class::Menu {
continue;
}
let object = catalog.find(&n.name).unwrap().0;
let Some((x, y, w, h)) = model.rect((object, node_key(&n).1)) else {
continue;
};
let r = if n.class == Class::Timer {
let r = Rect::new(area.x + timers * 4, area.bottom().saturating_sub(1), 3, 1);
timers += 1;
f.render_widget(
Paragraph::new("[T]").style(crate::render::pair(0, 14)),
r.intersection(area),
);
r
} else {
Rect::new(
area.x + x.saturating_sub(1) as u16,
area.y + y.saturating_sub(1) as u16,
w as u16,
h as u16,
)
};
let r = r.intersection(area);
if r.is_empty() {
continue;
}
if self.designer.grid && n.class == Class::Form {
for y in r.y..r.bottom() {
for x in r.x..r.right() {
let cell = &mut f.buffer_mut()[(x, y)];
if cell.symbol() == " " {
cell.set_symbol("·");
}
}
}
}
self.designer.objects.push((id, r));
}
let selection = self
.design_document()
.ok()
.and_then(|id| self.designer.selections.get(&id))
.cloned()
.unwrap_or_default();
for (id, r) in &self.designer.objects {
if selection.contains(id) {
for (x, y) in [
(r.x, r.y),
(r.right() - 1, r.y),
(r.x, r.bottom() - 1),
(r.right() - 1, r.bottom() - 1),
(r.x + r.width / 2, r.y),
(r.x + r.width / 2, r.bottom() - 1),
(r.x, r.y + r.height / 2),
(r.right() - 1, r.y + r.height / 2),
] {
f.render_widget(
Paragraph::new("").style(crate::render::pair(0, 14)),
Rect::new(x, y, 1, 1),
);
}
}
}
}
pub(crate) fn design_tool_render(&mut self, f: &mut Frame, kind: WindowKind, area: Rect) {
match kind {
WindowKind::Toolbox => {
for (i, (name, class, style)) in
tools().iter().enumerate().take(area.height as usize)
{
let r = Rect::new(area.x, area.y + i as u16, area.width, 1);
f.render_widget(
Paragraph::new(format!(
"{} {name}",
if self.designer.tool == Some((*class, *style)) {
">"
} else {
" "
}
)),
r,
);
self.hits.push((r, Hit::DesignTool(i)));
}
}
WindowKind::Palette => {
for (row, property) in [(0, "FORECOLOR"), (2, "BACKCOLOR")] {
f.render_widget(
Paragraph::new(property),
Rect::new(area.x, area.y + row, area.width, 1).intersection(area),
);
for n in 0..16u16 {
let r =
Rect::new(area.x + n * 2, area.y + row + 1, 2, 1).intersection(area);
if !r.is_empty() {
f.render_widget(
Paragraph::new(format!("{n:X} ")).style(
Style::default().bg(crate::render::dos(n as u8)).fg(
crate::render::dos(
if matches!(n, 0 | 1 | 4 | 5 | 6 | 8 | 9) {
15
} else {
0
},
),
),
),
r,
);
self.hits.push((r, Hit::DesignPaint(property, n as u8)));
}
}
}
let r = Rect::new(area.x, area.y + 4, area.width, 1).intersection(area);
f.render_widget(Paragraph::new("Enter: Fore/Back auftragen"), r);
self.hits.push((r, Hit::DesignAction("palette")));
}
WindowKind::MenuDesign => {
let labels = [
("+", "insert"),
("Edit", "menu"),
("Del", "delete"),
("", "up"),
("", "down"),
("", "in"),
("", "out"),
];
let mut x = area.x;
for (label, action) in labels {
let r = Rect::new(x, area.y, label.len() as u16 + 1, 1).intersection(area);
f.render_widget(Paragraph::new(label), r);
self.hits.push((r, Hit::DesignAction(action)));
x += r.width;
}
if let Ok(objects) = self.design_objects() {
let selected = self
.design_document()
.ok()
.and_then(|d| self.designer.selections.get(&d))
.cloned()
.unwrap_or_default();
let menus: Vec<_> = objects
.into_iter()
.filter(|(_, n)| n.class == Class::Menu)
.collect();
let current = menus
.iter()
.position(|(id, _)| selected.contains(id))
.unwrap_or(0);
let start = current.saturating_sub(area.height.saturating_sub(3) as usize);
for (row, (id, n)) in menus
.iter()
.skip(start)
.take(area.height.saturating_sub(1) as usize)
.enumerate()
{
fn depth(n: &FormNode, key: &Key) -> Option<usize> {
if node_key(n) == *key {
Some(0)
} else {
n.children.iter().find_map(|c| depth(c, key).map(|d| d + 1))
}
}
let depth = self
.design_form()
.ok()
.and_then(|f| depth(&f.root, &node_key(n)))
.unwrap_or(1);
let r = Rect::new(area.x, area.y + 1 + row as u16, area.width, 1);
f.render_widget(
Paragraph::new(format!(
"{}{} {}{}",
" ".repeat(depth.saturating_sub(1) * 2),
text(&value(n, "CAPTION").unwrap()),
n.name,
node_key(n).1.map(|i| format!("({i})")).unwrap_or_default()
))
.style(if selected.contains(id) {
Style::default().add_modifier(Modifier::REVERSED)
} else {
Style::default()
}),
r,
);
self.hits.push((r, Hit::DesignObject(*id)));
}
}
}
_ => {}
}
}
pub(crate) fn design_bar(&mut self) -> (String, String) {
let Ok(n) = self.selected_node() else {
return ("Properties: kein Control ausgewählt".into(), String::new());
};
let specs = forms::properties(n.class);
self.designer.property = self.designer.property.min(specs.len() - 1);
let name = specs[self.designer.property].name;
let v = self
.designer
.value
.as_ref()
.filter(|_| self.value_focus)
.map(Field::string)
.unwrap_or_else(|| {
if matches!(name, "CTLNAME" | "FORMNAME") {
n.name.clone()
} else {
text(&value(&n, name).unwrap())
}
});
(
format!(
"Property: {} [{}]↓ Value: [{}]{}",
n.name,
name,
v,
if self.value_focus { " *" } else { "" }
),
format!(
"{},{}{}×{}",
integer(&n, "LEFT"),
integer(&n, "TOP"),
integer(&n, "WIDTH"),
integer(&n, "HEIGHT")
),
)
}
}
fn token_offset(code: &str, line: u32, col: u32) -> Result<usize> {
let start = crate::editor::line_offset(code, line);
let offset = code[start..]
.char_indices()
.nth(col.saturating_sub(1) as usize)
.map(|(i, _)| i)
.ok_or_else(|| anyhow!("Ungültige Quellposition"))?;
Ok(start + offset)
}
fn event_parameters(n: &FormNode, event: &str) -> Vec<(String, String)> {
let mut params = Vec::new();
if node_key(n).1.is_some() {
params.push(("Index".into(), "INTEGER".into()));
}
for (name, ty) in forms::event_params(event).unwrap_or_default() {
params.push((
(*name).into(),
match ty {
forms::EventParamType::Integer => "INTEGER",
forms::EventParamType::Single => "SINGLE",
forms::EventParamType::Control => "CONTROL",
}
.into(),
));
}
params
}
fn handle_at(r: Rect, x: u16, y: u16) -> Option<(i8, i8)> {
let columns = [(r.x, -1), (r.right() - 1, 1), (r.x + r.width / 2, 0)];
let rows = [(r.y, -1), (r.bottom() - 1, 1), (r.y + r.height / 2, 0)];
for (col, horizontal) in columns {
for (row, vertical) in rows {
if (horizontal != 0 || vertical != 0) && x == col && y == row {
return Some((
if r.width == 1 { 0 } else { horizontal },
if r.height == 1 { 0 } else { vertical },
));
}
}
}
None
}