merge data_table migration (issue 0081-J)

This commit is contained in:
2026-05-15 14:42:10 +02:00
4 changed files with 95 additions and 275 deletions
+1 -1
View File
@@ -60,7 +60,7 @@ target_include_directories(graph_explorer PRIVATE
${FN_CPP_ROOT_DIR}/functions ${FN_CPP_ROOT_DIR}/functions
) )
target_link_libraries(graph_explorer PRIVATE SQLite::SQLite3 DuckDB::DuckDB) target_link_libraries(graph_explorer PRIVATE SQLite::SQLite3 DuckDB::DuckDB fn_table_viz)
duckdb_copy_runtime(graph_explorer) duckdb_copy_runtime(graph_explorer)
# Threads — issue 0026 (jobs system) usa std::thread + std::mutex + condvar. # Threads — issue 0026 (jobs system) usa std::thread + std::mutex + condvar.
+13
View File
@@ -17,6 +17,19 @@ uses_functions:
- graph_icons_cpp_viz - graph_icons_cpp_viz
- graph_sources_cpp_viz - graph_sources_cpp_viz
- graph_types_cpp_viz - graph_types_cpp_viz
# data_table stack — issue 0081-J: panel Table migrado a data_table::render
- data_table_cpp_viz
- viz_render_cpp_viz
- compute_stage_cpp_core
- compute_pipeline_cpp_core
- tql_emit_cpp_core
- tql_apply_cpp_core
- lua_engine_cpp_core
- join_tables_cpp_core
- auto_detect_type_cpp_core
- compute_column_stats_cpp_core
- llm_anthropic_cpp_core
- tql_to_sql_cpp_core
# core # core
- graph_spatial_hash_cpp_core - graph_spatial_hash_cpp_core
- button_cpp_core - button_cpp_core
+75 -274
View File
@@ -17,6 +17,9 @@
#include "core/tokens.h" #include "core/tokens.h"
#include "core/icons_tabler.h" #include "core/icons_tabler.h"
// data_table — issue 0081-J: migracion panel Table a data_table::render.
#include "viz/data_table.h"
#include "imgui.h" #include "imgui.h"
#include <algorithm> #include <algorithm>
@@ -1537,200 +1540,25 @@ void views_table_refresh_indices(AppState& app) {
} }
} }
namespace { // ----------------------------------------------------------------------------
// Table view (issue 0004) — migrated to data_table::render (issue 0081-J).
// Comparador estable para ImGuiTableSortSpecs. //
struct TableSortCtx { // OLD: ImGui::BeginTable("##tablev", 6, ...) with manual sort/filter/clipper.
const ImGuiTableSortSpecs* specs; // Per-column filter popups + chips toolbar + TabBar per type.
}; // Click on row selected node in graph viewport.
TableSortCtx g_table_sort_ctx; //
// NEW: data_table::render() provides sort + filter + viz + stages.
bool table_row_lt(const AppState::TableRow& a, const AppState::TableRow& b) { // AppState::table_dt_state persists the UI state between frames.
const ImGuiTableSortSpecs* specs = g_table_sort_ctx.specs; // Viewport selection via Inspector (not via table row click).
if (!specs) return a.name < b.name; //
for (int n = 0; n < specs->SpecsCount; ++n) { // Removed helpers: render_one_table, render_table_headers_with_filters,
const ImGuiTableColumnSortSpecs& s = specs->Specs[n]; // table_row_lt, table_row_field, table_col_name_by_id, k_table_cols,
int delta = 0; // TableColMeta, TableSortCtx, ci_contains, build_visible lambda.
switch (s.ColumnUserID) { //
case 0: delta = a.id.compare(b.id); break; // TODO(future): if viewport selection from table is wanted again, expose
case 1: delta = a.name.compare(b.name); break; // a read-only "last hovered row id" from data_table::State or add a
case 2: delta = a.type_ref.compare(b.type_ref); break; // post-render row-id lookup via ImGui::IsItemHovered on a named child.
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;
}
// Mapeo column_user_id -> nombre legible y string-getter sobre TableRow.
struct TableColMeta {
int user_id;
const char* name;
};
const TableColMeta k_table_cols[] = {
{0, "id"}, {1, "name"}, {2, "type"}, {3, "status"},
{4, "updated_at"}, {5, "neighbors"},
};
constexpr int k_table_col_n = (int)(sizeof(k_table_cols) / sizeof(k_table_cols[0]));
const std::string& table_row_field(const AppState::TableRow& r, int user_id) {
static const std::string empty_str;
static thread_local std::string scratch;
switch (user_id) {
case 0: return r.id;
case 1: return r.name;
case 2: return r.type_ref;
case 3: return r.status;
case 4: return r.updated_at;
case 5: scratch = std::to_string(r.neighbors); return scratch;
}
return empty_str;
}
const char* table_col_name_by_id(int user_id) {
for (int i = 0; i < k_table_col_n; ++i)
if (k_table_cols[i].user_id == user_id) return k_table_cols[i].name;
return "?";
}
// Render header row con popup right-click por columna para anadir filtro.
void render_table_headers_with_filters(AppState& app) {
ImGui::TableNextRow(ImGuiTableRowFlags_Headers);
for (int i = 0; i < k_table_col_n; ++i) {
ImGui::TableSetColumnIndex(i);
const char* name = ImGui::TableGetColumnName(i);
ImGui::PushID(i);
ImGui::TableHeader(name);
if (ImGui::BeginPopupContextItem("##colfilt",
ImGuiPopupFlags_MouseButtonRight)) {
int user_id = k_table_cols[i].user_id;
ImGui::TextDisabled("Filter %s", k_table_cols[i].name);
ImGui::Separator();
// Si reabrimos el popup para esta columna, sembrar el buffer.
if (app.table_filter_pending_col != user_id) {
app.table_filter_pending_col = user_id;
auto it = app.table_col_filters.find(user_id);
std::snprintf(app.table_filter_input, sizeof(app.table_filter_input),
"%s", it == app.table_col_filters.end() ? "" : it->second.c_str());
ImGui::SetKeyboardFocusHere();
}
ImGui::SetNextItemWidth(220);
ImGuiInputTextFlags fflags = ImGuiInputTextFlags_EnterReturnsTrue;
bool commit = ImGui::InputTextWithHint("##filt_in", "substring (case-insensitive)",
app.table_filter_input,
sizeof(app.table_filter_input), fflags);
ImGui::SameLine();
if (ImGui::SmallButton("Apply") || commit) {
if (app.table_filter_input[0]) {
app.table_col_filters[user_id] = app.table_filter_input;
} else {
app.table_col_filters.erase(user_id);
}
app.table_filter_pending_col = -1;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::SmallButton("Clear")) {
app.table_col_filters.erase(user_id);
app.table_filter_input[0] = 0;
app.table_filter_pending_col = -1;
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
} else if (app.table_filter_pending_col == k_table_cols[i].user_id) {
// popup se cerro sin aplicar — limpiar pending.
app.table_filter_pending_col = -1;
}
ImGui::PopID();
}
}
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);
render_table_headers_with_filters(app);
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) { void views_table(AppState& app) {
if (!app.panel_table) return; if (!app.panel_table) return;
@@ -1739,97 +1567,70 @@ void views_table(AppState& app) {
return; 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());
// Chips de filtros activos por columna (right-click sobre header lo anade).
if (!app.table_col_filters.empty()) {
ImGui::TextDisabled("Filters:");
ImGui::SameLine();
int del_col = -1;
for (auto& kv : app.table_col_filters) {
ImGui::SameLine();
char chip[160];
std::snprintf(chip, sizeof(chip), TI_FILTER " %s: %s " TI_X "##chip_%d",
table_col_name_by_id(kv.first), kv.second.c_str(), kv.first);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.18f, 0.30f, 0.50f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.40f, 0.65f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.35f, 0.50f, 0.75f, 1.0f));
if (ImGui::SmallButton(chip)) del_col = kv.first;
ImGui::PopStyleColor(3);
}
if (del_col >= 0) app.table_col_filters.erase(del_col);
ImGui::SameLine();
if (fn_ui::button("Clear all", fn_ui::ButtonVariant::Subtle)) {
app.table_col_filters.clear();
}
}
if (app.table_rows.empty()) { if (app.table_rows.empty()) {
ImGui::TextDisabled("(no entities loaded)"); ImGui::TextDisabled("(no entities loaded)");
ImGui::End(); ImGui::End();
return; return;
} }
// Indices por tipo presentes en el snapshot. // Construir TableInput a partir de app.table_rows.
std::vector<std::string> types_present; // Columnas: id, name, type, status, updated_at, neighbors.
types_present.reserve(8); // Los datos se copian en backing strings cada frame (barato para N<50k
{ // ya que son strings cortos y el vector se reutiliza por move).
std::unordered_set<std::string> seen; // data_table::render gestiona sort, filtros, busqueda y viz internamente.
for (const auto& r : app.table_rows) { static const std::vector<std::string> k_headers = {
if (seen.insert(r.type_ref).second) types_present.push_back(r.type_ref); "id", "name", "type", "status", "updated_at", "neighbors"
}
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;
// Filtros por columna (AND de todos).
bool reject = false;
for (auto& kv : app.table_col_filters) {
const std::string& field = table_row_field(r, kv.first);
if (!ci_contains(field, kv.second.c_str())) { reject = true; break; }
}
if (reject) continue;
v.push_back((int)i);
}
return v;
}; };
constexpr int k_ncols = 6;
if (app.table_show_all) { // cell_backing: propietario de los strings de la tabla.
auto visible = build_visible(nullptr); // Se reconstruye solo cuando table_cache_dirty (tras reload).
ImGui::TextDisabled("All types — %zu visible", visible.size()); // Para mantenerlo entre frames: miembro de AppState seria lo optimo,
render_one_table(app, visible); // pero para simplicidad lo hacemos static local (unica instancia OK
} else if (ImGui::BeginTabBar("##ttabs")) { // porque views_table se llama desde un unico lugar del render loop).
for (size_t i = 0; i < types_present.size(); ++i) { static std::vector<std::string> s_cell_backing;
const std::string& t = types_present[i]; static std::vector<const char*> s_cells;
char lbl[96]; static int s_rows_cached = -1;
std::snprintf(lbl, sizeof(lbl), "%s##tt%zu",
t.empty() ? "(none)" : t.c_str(), i); // Rebuilding the cache on dirty flag or size change.
if (ImGui::BeginTabItem(lbl)) { if (app.table_cache_dirty || (int)app.table_rows.size() != s_rows_cached) {
app.table_active_tab = (int)i; const int nrows = (int)app.table_rows.size();
auto visible = build_visible(t.c_str()); s_cell_backing.resize((size_t)nrows * k_ncols);
ImGui::TextDisabled("%zu rows visible", visible.size()); s_cells.resize((size_t)nrows * k_ncols);
render_one_table(app, visible); for (int i = 0; i < nrows; ++i) {
ImGui::EndTabItem(); const auto& r = app.table_rows[i];
} s_cell_backing[(size_t)i * k_ncols + 0] = r.id;
s_cell_backing[(size_t)i * k_ncols + 1] = r.name;
s_cell_backing[(size_t)i * k_ncols + 2] = r.type_ref;
s_cell_backing[(size_t)i * k_ncols + 3] = r.status;
s_cell_backing[(size_t)i * k_ncols + 4] = r.updated_at;
s_cell_backing[(size_t)i * k_ncols + 5] = std::to_string(r.neighbors);
} }
ImGui::EndTabBar(); for (size_t k = 0; k < s_cell_backing.size(); ++k)
s_cells[k] = s_cell_backing[k].c_str();
s_rows_cached = nrows;
app.table_cache_dirty = false;
} }
data_table::TableInput tbl;
tbl.name = "entities";
tbl.headers = k_headers;
tbl.types = {
data_table::ColumnType::String, // id
data_table::ColumnType::String, // name
data_table::ColumnType::String, // type
data_table::ColumnType::String, // status
data_table::ColumnType::String, // updated_at
data_table::ColumnType::Int, // neighbors
};
tbl.cells = s_cells.empty() ? nullptr : s_cells.data();
tbl.rows = s_rows_cached;
tbl.cols = k_ncols;
// Render con chrome completo (barra de chips + breadcrumb).
// app.table_dt_state persiste entre frames.
data_table::render("##tablev_dt", {tbl}, app.table_dt_state);
ImGui::End(); ImGui::End();
} }
+6
View File
@@ -7,6 +7,8 @@
#include "entity_ops.h" #include "entity_ops.h"
#include "node_groups.h" #include "node_groups.h"
#include "core/data_table_types.h"
#include <cstdint> #include <cstdint>
#include <unordered_map> #include <unordered_map>
@@ -246,6 +248,10 @@ struct AppState {
char table_filter_input[96] = {}; // buffer del popup activo char table_filter_input[96] = {}; // buffer del popup activo
int table_filter_pending_col = -1; // col_user_id en edicion int table_filter_pending_col = -1; // col_user_id en edicion
// data_table::State para el panel Table (issue 0081-J).
// Persiste filters/sort/stages entre frames; uno por instancia de render.
data_table::State table_dt_state;
// ---- Type Editor (issue 0007) ------------------------------------------ // ---- Type Editor (issue 0007) ------------------------------------------
// Draft del editor de tipos. Se inicializa con una copia de parsed_types // Draft del editor de tipos. Se inicializa con una copia de parsed_types
// tras cargar el grafo. Save reescribe `types.yaml` y dispara // tras cargar el grafo. Save reescribe `types.yaml` y dispara