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:
@@ -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)
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user