feat(table): vista tabla por tipo de entidad (issue 0004)

- entity_ops: entity_list_rows (bulk pull id/name/type_ref/status/updated_at).
- AppState::TableRow + cache + filtros (search substring + show_all toggle).
- views_table: tabs por type_ref (alfabetico) o tabla unica con todos los
  tipos. ImGui::BeginTable con sort + clipper para >10k filas. Click en
  Selectable selecciona el nodo en el viewport (clear + add via
  graph_viewport_*).
- views_table_refresh_indices: degree + node_idx por user_data hash.
- main.cpp: panel "Table" en g_panels; cache build tras load_input y
  reload_after_mutation.
This commit is contained in:
2026-05-01 01:05:03 +02:00
parent 078947a2b8
commit 84afa4ce70
6 changed files with 339 additions and 1 deletions
+208
View File
@@ -1391,6 +1391,214 @@ bool views_open_modal(AppState& app) {
return opened;
}
// ----------------------------------------------------------------------------
// Table view (issue 0004)
// ----------------------------------------------------------------------------
void views_table_refresh_indices(AppState& app) {
if (!app.graph) return;
GraphData& g = *app.graph;
// Degree map: user_data -> count.
std::unordered_map<uint64_t, int> deg;
deg.reserve((size_t)g.node_count * 2);
for (int i = 0; i < g.edge_count; ++i) {
const GraphEdge& e = g.edges[i];
if (e.source < (uint32_t)g.node_count) deg[g.nodes[e.source].user_data]++;
if (e.target < (uint32_t)g.node_count) deg[g.nodes[e.target].user_data]++;
}
for (auto& r : app.table_rows) {
uint64_t h = fnv1a64_id(r.id.c_str());
r.node_idx = g.find_node_by_user_data(h);
auto it = deg.find(h);
r.neighbors = (it == deg.end()) ? 0 : it->second;
}
}
namespace {
// Comparador estable para ImGuiTableSortSpecs.
struct TableSortCtx {
const ImGuiTableSortSpecs* specs;
};
TableSortCtx g_table_sort_ctx;
bool table_row_lt(const AppState::TableRow& a, const AppState::TableRow& b) {
const ImGuiTableSortSpecs* specs = g_table_sort_ctx.specs;
if (!specs) return a.name < b.name;
for (int n = 0; n < specs->SpecsCount; ++n) {
const ImGuiTableColumnSortSpecs& s = specs->Specs[n];
int delta = 0;
switch (s.ColumnUserID) {
case 0: delta = a.id.compare(b.id); break;
case 1: delta = a.name.compare(b.name); break;
case 2: delta = a.type_ref.compare(b.type_ref); break;
case 3: delta = a.status.compare(b.status); break;
case 4: delta = a.updated_at.compare(b.updated_at); break;
case 5: delta = (a.neighbors < b.neighbors) ? -1
: (a.neighbors > b.neighbors) ? 1 : 0; break;
default: break;
}
if (delta != 0) {
return (s.SortDirection == ImGuiSortDirection_Ascending) ? (delta < 0) : (delta > 0);
}
}
return false;
}
bool ci_contains(const std::string& hay, const char* needle) {
if (!needle || !*needle) return true;
auto lower = [](char c){ return (char)std::tolower((unsigned char)c); };
std::string h; h.reserve(hay.size());
for (char c : hay) h.push_back(lower(c));
std::string n;
for (const char* p = needle; *p; ++p) n.push_back(lower(*p));
return h.find(n) != std::string::npos;
}
void render_one_table(AppState& app, std::vector<int>& visible_indices) {
ImGuiTableFlags flags =
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable |
ImGuiTableFlags_Sortable | ImGuiTableFlags_ScrollY |
ImGuiTableFlags_SizingStretchProp;
if (!ImGui::BeginTable("##tablev", 6, flags)) return;
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableSetupColumn("id", ImGuiTableColumnFlags_DefaultSort, 0, 0);
ImGui::TableSetupColumn("name", ImGuiTableColumnFlags_None, 0, 1);
ImGui::TableSetupColumn("type", ImGuiTableColumnFlags_None, 0, 2);
ImGui::TableSetupColumn("status", ImGuiTableColumnFlags_None, 0, 3);
ImGui::TableSetupColumn("updated_at", ImGuiTableColumnFlags_None, 0, 4);
ImGui::TableSetupColumn("neighbors", ImGuiTableColumnFlags_WidthFixed,
80.0f, 5);
ImGui::TableHeadersRow();
ImGuiTableSortSpecs* specs = ImGui::TableGetSortSpecs();
if (specs && specs->SpecsDirty) {
g_table_sort_ctx.specs = specs;
std::sort(visible_indices.begin(), visible_indices.end(),
[&app](int a, int b) {
return table_row_lt(app.table_rows[a], app.table_rows[b]);
});
specs->SpecsDirty = false;
}
ImGuiListClipper clipper;
clipper.Begin((int)visible_indices.size());
while (clipper.Step()) {
for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
int ri = visible_indices[row];
const auto& r = app.table_rows[ri];
ImGui::TableNextRow();
ImGui::PushID(ri);
ImGui::TableSetColumnIndex(0);
char sel_lbl[256];
std::snprintf(sel_lbl, sizeof(sel_lbl), "%s##sel", r.id.c_str());
bool is_sel = (app.viewport && r.node_idx >= 0
&& graph_viewport_is_selected(*app.viewport, r.node_idx));
if (ImGui::Selectable(sel_lbl, is_sel,
ImGuiSelectableFlags_SpanAllColumns)) {
if (r.node_idx >= 0 && app.graph && app.viewport) {
graph_viewport_clear_selection(*app.graph, *app.viewport);
graph_viewport_add_to_selection(*app.graph, *app.viewport,
r.node_idx);
app.filter_focus_target = r.node_idx;
}
}
ImGui::TableSetColumnIndex(1);
ImGui::TextUnformatted(r.name.c_str());
ImGui::TableSetColumnIndex(2);
ImGui::TextUnformatted(r.type_ref.c_str());
ImGui::TableSetColumnIndex(3);
ImGui::TextUnformatted(r.status.c_str());
ImGui::TableSetColumnIndex(4);
ImGui::TextUnformatted(r.updated_at.c_str());
ImGui::TableSetColumnIndex(5);
ImGui::Text("%d", r.neighbors);
ImGui::PopID();
}
}
ImGui::EndTable();
}
} // namespace
void views_table(AppState& app) {
if (!app.panel_table) return;
if (!ImGui::Begin("Table", &app.panel_table)) {
ImGui::End();
return;
}
// Toolbar superior: search + show all.
ImGui::SetNextItemWidth(220);
ImGui::InputTextWithHint("##tsearch", TI_SEARCH " filter name/id...",
app.table_search_buf, sizeof(app.table_search_buf));
ImGui::SameLine();
ImGui::Checkbox("Show all types", &app.table_show_all);
ImGui::SameLine();
ImGui::TextDisabled("%zu rows", app.table_rows.size());
if (app.table_rows.empty()) {
ImGui::TextDisabled("(no entities loaded)");
ImGui::End();
return;
}
// Indices por tipo.
std::vector<std::string> types_present;
types_present.reserve(8);
{
std::unordered_set<std::string> seen;
for (const auto& r : app.table_rows) {
if (seen.insert(r.type_ref).second) types_present.push_back(r.type_ref);
}
std::sort(types_present.begin(), types_present.end());
}
auto build_visible = [&](const char* type_filter) {
std::vector<int> v;
v.reserve(app.table_rows.size());
for (size_t i = 0; i < app.table_rows.size(); ++i) {
const auto& r = app.table_rows[i];
if (type_filter && r.type_ref != type_filter) continue;
if (app.table_search_buf[0]
&& !ci_contains(r.name, app.table_search_buf)
&& !ci_contains(r.id, app.table_search_buf)) continue;
v.push_back((int)i);
}
return v;
};
if (app.table_show_all) {
auto visible = build_visible(nullptr);
ImGui::TextDisabled("All types — %zu visible", visible.size());
render_one_table(app, visible);
} else if (ImGui::BeginTabBar("##ttabs")) {
for (size_t i = 0; i < types_present.size(); ++i) {
const std::string& t = types_present[i];
char lbl[96];
std::snprintf(lbl, sizeof(lbl), "%s##tt%zu",
t.empty() ? "(none)" : t.c_str(), i);
if (ImGui::BeginTabItem(lbl)) {
app.table_active_tab = (int)i;
auto visible = build_visible(t.c_str());
ImGui::TextDisabled("%zu rows visible", visible.size());
render_one_table(app, visible);
ImGui::EndTabItem();
}
}
ImGui::EndTabBar();
}
ImGui::End();
}
// ----------------------------------------------------------------------------
// Type Editor (issue 0007)
// ----------------------------------------------------------------------------