Add a wxWidgets GUI frontend (second frontend, proves the split).

A native wx GUI built entirely on essim_core via the Frontend interface — no
Tui reuse, no command shell. Demonstrates the core/frontends architecture by
adding a real second frontend:

  - WxFrontend : public Frontend — owns the System + a console buffer; handles
    boot headlessly (restore for --restore/--batch; honest note for source);
    Run() boots wx without a generated main (SetInstance + wxEntryStart/
    CallOnInit/OnRun) so the shared frontend_main stays in control.
  - EssimFrame (wxFrame) — menu-driven window: Load (app::load_module), Restore/
    Save (persist), Export (app::export_connections), Verify (app::verify),
    rendered into a model tree (modules → parts), an overview + verify-health
    panel, and a log. Each handler is a thin wrapper over a core/app op.
  - main.cpp: construct WxFrontend, call frontend_main — same 4 lines as tui.
  - CMakeLists.txt: find_package(wxWidgets) + essim_add_frontend(wx LIBS …);
    select with -DESSIM_FRONTEND=wx. ESSIM_FRONTEND gains wx in its STRINGS.

Set LC_CTYPE from the environment at GUI init so wxString decodes the UTF-8 in
narrow literals (em dash, ellipsis); LC_NUMERIC stays "C". .gitignore: build*/.

Needs libwxgtk3.2-dev. Verified: builds clean (wx 3.2.9 GTK3), window opens and
renders with no wx asserts; --commands-md/--restore/--batch behave headlessly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 21:01:02 +02:00
parent e561c0f960
commit 4803d7d01c
8 changed files with 493 additions and 2 deletions

View File

@@ -0,0 +1,285 @@
#include "frontends/wx/wx_frame.hpp"
#include "frontends/wx/wx_frontend.hpp"
#include "core/app/export.hpp"
#include "core/app/load.hpp"
#include "core/app/verify.hpp"
#include "core/domain/analysis.hpp"
#include "core/domain/connect.hpp"
#include "core/domain/modules.hpp"
#include "core/domain/parts.hpp"
#include "core/domain/persist.hpp"
#include "core/domain/signal_type.hpp"
#include "core/domain/signals.hpp"
#include "core/domain/system.hpp"
#include <wx/wx.h>
#include <wx/choicdlg.h>
#include <wx/filedlg.h>
#include <wx/filename.h>
#include <wx/textdlg.h>
#include <wx/treectrl.h>
#include <algorithm>
#include <string>
#include <vector>
namespace {
enum {
ID_LOAD = wxID_HIGHEST + 1,
ID_RESTORE,
ID_SAVE,
ID_EXPORT,
ID_VERIFY,
ID_QUIT,
ID_ABOUT,
};
// Core (UTF-8 std::string) -> wxString, and back for paths.
inline wxString wx(const std::string &s) { return wxString::FromUTF8(s.c_str()); }
} // namespace
EssimFrame::EssimFrame(WxFrontend &fe)
: wxFrame(nullptr, wxID_ANY, "essim — system digital twin",
wxDefaultPosition, wxSize(960, 640)),
fe_(fe) {
auto *file = new wxMenu;
file->Append(ID_LOAD, "&Load module…\tCtrl-L");
file->Append(ID_RESTORE, "&Restore snapshot…\tCtrl-R");
file->Append(ID_SAVE, "&Save snapshot…\tCtrl-S");
file->AppendSeparator();
file->Append(ID_EXPORT, "&Export connections…\tCtrl-E");
file->AppendSeparator();
file->Append(ID_QUIT, "&Quit\tCtrl-Q");
auto *sysm = new wxMenu;
sysm->Append(ID_VERIFY, "&Verify\tCtrl-K");
auto *help = new wxMenu;
help->Append(ID_ABOUT, "&About");
auto *bar = new wxMenuBar;
bar->Append(file, "&File");
bar->Append(sysm, "&System");
bar->Append(help, "&Help");
SetMenuBar(bar);
CreateStatusBar();
SetStatusText("essim — wx frontend");
auto *panel = new wxPanel(this);
tree_ = new wxTreeCtrl(panel, wxID_ANY);
overview_ = new wxTextCtrl(panel, wxID_ANY, "", wxDefaultPosition,
wxDefaultSize, wxTE_MULTILINE | wxTE_READONLY);
log_ = new wxTextCtrl(panel, wxID_ANY, "", wxDefaultPosition,
wxDefaultSize, wxTE_MULTILINE | wxTE_READONLY);
wxFont mono(wxFontInfo().Family(wxFONTFAMILY_TELETYPE));
overview_->SetFont(mono);
log_->SetFont(mono);
auto *top = new wxBoxSizer(wxHORIZONTAL);
top->Add(tree_, 1, wxEXPAND | wxALL, 4);
top->Add(overview_, 1, wxEXPAND | wxALL, 4);
auto *root = new wxBoxSizer(wxVERTICAL);
root->Add(top, 2, wxEXPAND);
root->Add(new wxStaticText(panel, wxID_ANY, " Log"), 0, wxLEFT | wxTOP, 6);
root->Add(log_, 1, wxEXPAND | wxALL, 4);
panel->SetSizer(root);
Bind(wxEVT_MENU, &EssimFrame::OnLoad, this, ID_LOAD);
Bind(wxEVT_MENU, &EssimFrame::OnRestore, this, ID_RESTORE);
Bind(wxEVT_MENU, &EssimFrame::OnSave, this, ID_SAVE);
Bind(wxEVT_MENU, &EssimFrame::OnExport, this, ID_EXPORT);
Bind(wxEVT_MENU, &EssimFrame::OnVerify, this, ID_VERIFY);
Bind(wxEVT_MENU, &EssimFrame::OnQuit, this, ID_QUIT);
Bind(wxEVT_MENU, &EssimFrame::OnAbout, this, ID_ABOUT);
RebuildModelView();
}
void EssimFrame::Log(const wxString &line) {
log_->AppendText(line + "\n");
}
void EssimFrame::RebuildModelView() {
System *sys = fe_.system();
tree_->DeleteAllItems();
wxTreeItemId root = tree_->AddRoot("System");
int n_mods = 0, n_parts = 0, n_sigs = 0;
if (sys) {
std::vector<std::string> mods;
for (auto &mkv : *sys->modules()) mods.push_back(mkv.first);
std::sort(mods.begin(), mods.end());
n_mods = (int)mods.size();
for (const auto &mname : mods) {
Module *m = sys->modules()->get(mname);
int mp = (int)m->size();
int ms = (int)m->signals->size();
n_parts += mp;
n_sigs += ms;
wxTreeItemId mid = tree_->AppendItem(
root, wx(mname) + wxString::Format(" — %d part(s), %d signal(s)",
mp, ms));
std::vector<std::string> parts;
for (auto &pkv : *m) parts.push_back(pkv.first);
std::sort(parts.begin(), parts.end());
for (const auto &pname : parts) {
Part *p = m->get(pname);
wxString label = wx(pname)
+ wxString::Format(" (%d pin(s))", (int)p->size());
if (!p->connector_type.empty())
label += " [" + wx(p->connector_type) + "]";
tree_->AppendItem(mid, label);
}
}
}
tree_->Expand(root);
int n_conn = sys ? (int)sys->connections()->size() : 0;
wxString ov;
ov << "Modules: " << n_mods << "\n"
<< "Parts: " << n_parts << "\n"
<< "Signals: " << n_sigs << "\n"
<< "Connections: " << n_conn << "\n";
if (sys) {
app::VerifyReport r = app::verify(sys);
ov << "\nHealth (verify):\n"
<< wxString::Format(" pin-role mismatches: %d / %d typed pin(s)\n",
(int)r.role_mismatches.size(), r.typed_pins)
<< wxString::Format(" net inconsistencies: %d / %d bridged net(s)\n",
(int)r.net_inconsistencies.size(), r.bridged_nets)
<< wxString::Format(" orphan pins: %d (%d imported, %d dropped)\n",
r.orphan_total(), r.orphan_imported, r.orphan_dropped)
<< wxString::Format(" model anomalies: %d\n", r.model_total());
}
overview_->SetValue(ov);
}
void EssimFrame::OnLoad(wxCommandEvent &) {
wxFileDialog dlg(this, "Load a netlist / pinout file", "", "",
"All files (*.*)|*.*", wxFD_OPEN | wxFD_FILE_MUST_EXIST);
if (dlg.ShowModal() != wxID_OK) return;
const wxString path = dlg.GetPath();
wxString modname = wxGetTextFromUser("Module name:", "Load module",
wxFileName(path).GetName(), this);
if (modname.empty()) return;
static const wxString kinds[] = {"mentor", "altium", "ods"};
int ki = wxGetSingleChoiceIndex("Import type:", "Load module",
WXSIZEOF(kinds), kinds, this);
if (ki < 0) return;
ImportType type;
app::import_type_from_name(kinds[ki].ToStdString(), type); // choice is valid
app::LoadResult r = app::load_module(
fe_.system(), modname.utf8_string(), path.utf8_string(), type);
if (!r.ok) {
Log("load failed: " + wx(r.error));
wxMessageBox(wx(r.error), "Load failed", wxOK | wxICON_ERROR, this);
return;
}
Log(wxString::Format(
"loaded '%s' from %s — %d part(s), %d signal(s)"
" (dropped %d; types: %d power / %d gnd / %d suspect)",
modname, path, r.parts, r.signals, r.dropped, r.power, r.gnd,
r.kept_other));
RebuildModelView();
}
void EssimFrame::OnRestore(wxCommandEvent &) {
wxFileDialog dlg(this, "Restore a system snapshot", "", "",
"essim snapshots (*.essim)|*.essim|All files (*.*)|*.*",
wxFD_OPEN | wxFD_FILE_MUST_EXIST);
if (dlg.ShowModal() != wxID_OK) return;
std::string err;
System *fresh = restore_system(dlg.GetPath().utf8_string(), err);
if (!fresh) {
Log("restore failed: " + wx(err));
wxMessageBox(wx(err), "Restore failed", wxOK | wxICON_ERROR, this);
return;
}
fe_.set_system(fresh);
Log("restored from " + dlg.GetPath());
RebuildModelView();
}
void EssimFrame::OnSave(wxCommandEvent &) {
wxFileDialog dlg(this, "Save system snapshot", "", "system.essim",
"essim snapshots (*.essim)|*.essim|All files (*.*)|*.*",
wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
if (dlg.ShowModal() != wxID_OK) return;
std::string err;
if (save_system(fe_.system(), dlg.GetPath().utf8_string(), err)) {
Log("saved to " + dlg.GetPath());
} else {
Log("save failed: " + wx(err));
wxMessageBox(wx(err), "Save failed", wxOK | wxICON_ERROR, this);
}
}
void EssimFrame::OnExport(wxCommandEvent &) {
wxFileDialog dlg(this, "Export connections", "", "connections.csv",
"CSV (*.csv)|*.csv|OpenDocument sheet (*.ods)|*.ods",
wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
if (dlg.ShowModal() != wxID_OK) return;
const std::string path = dlg.GetPath().utf8_string();
app::ExportFormat fmt;
if (!app::export_format_from_path(path, fmt)) {
wxMessageBox("Unknown export extension (use .csv or .ods).",
"Export failed", wxOK | wxICON_ERROR, this);
return;
}
app::ExportResult r = app::export_connections(fe_.system(), path, fmt);
if (r.ok) {
Log(wxString::Format("exported %d row(s) to %s", r.rows, dlg.GetPath()));
} else {
Log("export failed: " + wx(r.error));
wxMessageBox(wx(r.error), "Export failed", wxOK | wxICON_ERROR, this);
}
}
void EssimFrame::OnVerify(wxCommandEvent &) {
app::VerifyReport r = app::verify(fe_.system());
Log("verify:");
Log(wxString::Format(" %d pin-role mismatch(es) over %d typed pin(s)",
(int)r.role_mismatches.size(), r.typed_pins));
for (const auto &m : r.role_mismatches)
Log(wx(" " + m.module + "/" + m.part + "/" + m.pin + ": expected "
+ signal_type_name(m.expected) + ", got "
+ signal_type_name(m.actual)));
Log(wxString::Format(" %d inconsistent net(s) over %d bridged net(s)",
(int)r.net_inconsistencies.size(), r.bridged_nets));
Log(wxString::Format(" %d orphan pin(s) (%d imported, %d dropped)",
r.orphan_total(), r.orphan_imported, r.orphan_dropped));
auto log_anoms = [this](const std::vector<Anomaly> &v, const char *tail) {
Log(wxString::Format(" %d %s", (int)v.size(), tail));
for (const auto &a : v)
Log(wx(std::string(" [") + anomaly_kind_name(a.kind) + "] "
+ a.message));
};
log_anoms(r.pin_anomalies, "model-driven pin anomaly(ies)");
log_anoms(r.jtag_anomalies, "JTAG chain anomaly(ies)");
log_anoms(r.conflict_anomalies, "source-conflict(s)");
log_anoms(r.completeness_anomalies, "BSDL completeness issue(s)");
RebuildModelView();
}
void EssimFrame::OnQuit(wxCommandEvent &) { Close(true); }
void EssimFrame::OnAbout(wxCommandEvent &) {
wxMessageBox("essim — system digital twin\n\n"
"wxWidgets frontend over essim_core.",
"About essim", wxOK | wxICON_INFORMATION, this);
}