diff --git a/CMakeLists.txt b/CMakeLists.txt index f7f6c0b..377d6e9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,7 +60,7 @@ target_include_directories(graph_explorer PRIVATE ${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) # Threads — issue 0026 (jobs system) usa std::thread + std::mutex + condvar. diff --git a/app.md b/app.md index 139309e..6535880 100644 --- a/app.md +++ b/app.md @@ -17,6 +17,19 @@ uses_functions: - graph_icons_cpp_viz - graph_sources_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 - graph_spatial_hash_cpp_core - button_cpp_core diff --git a/views.cpp b/views.cpp index 949f8cc..806dc7e 100644 --- a/views.cpp +++ b/views.cpp @@ -17,6 +17,9 @@ #include "core/tokens.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 @@ -1537,200 +1540,25 @@ void views_table_refresh_indices(AppState& app) { } } -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; -} - -// 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& 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 +// ---------------------------------------------------------------------------- +// Table view (issue 0004) — migrated to data_table::render (issue 0081-J). +// +// OLD: ImGui::BeginTable("##tablev", 6, ...) with manual sort/filter/clipper. +// Per-column filter popups + chips toolbar + TabBar per type. +// Click on row selected node in graph viewport. +// +// NEW: data_table::render() provides sort + filter + viz + stages. +// AppState::table_dt_state persists the UI state between frames. +// Viewport selection via Inspector (not via table row click). +// +// Removed helpers: render_one_table, render_table_headers_with_filters, +// table_row_lt, table_row_field, table_col_name_by_id, k_table_cols, +// TableColMeta, TableSortCtx, ci_contains, build_visible lambda. +// +// TODO(future): if viewport selection from table is wanted again, expose +// a read-only "last hovered row id" from data_table::State or add a +// post-render row-id lookup via ImGui::IsItemHovered on a named child. +// ---------------------------------------------------------------------------- void views_table(AppState& app) { if (!app.panel_table) return; @@ -1739,97 +1567,70 @@ void views_table(AppState& app) { 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()) { ImGui::TextDisabled("(no entities loaded)"); ImGui::End(); return; } - // Indices por tipo presentes en el snapshot. - std::vector types_present; - types_present.reserve(8); - { - std::unordered_set 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 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; + // Construir TableInput a partir de app.table_rows. + // Columnas: id, name, type, status, updated_at, neighbors. + // 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). + // data_table::render gestiona sort, filtros, busqueda y viz internamente. + static const std::vector k_headers = { + "id", "name", "type", "status", "updated_at", "neighbors" }; + constexpr int k_ncols = 6; - 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(); - } + // cell_backing: propietario de los strings de la tabla. + // Se reconstruye solo cuando table_cache_dirty (tras reload). + // Para mantenerlo entre frames: miembro de AppState seria lo optimo, + // pero para simplicidad lo hacemos static local (unica instancia OK + // porque views_table se llama desde un unico lugar del render loop). + static std::vector s_cell_backing; + static std::vector s_cells; + static int s_rows_cached = -1; + + // Rebuilding the cache on dirty flag or size change. + if (app.table_cache_dirty || (int)app.table_rows.size() != s_rows_cached) { + const int nrows = (int)app.table_rows.size(); + s_cell_backing.resize((size_t)nrows * k_ncols); + s_cells.resize((size_t)nrows * k_ncols); + for (int i = 0; i < nrows; ++i) { + 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(); } diff --git a/views.h b/views.h index e83015d..377dfe4 100644 --- a/views.h +++ b/views.h @@ -7,6 +7,8 @@ #include "entity_ops.h" #include "node_groups.h" +#include "core/data_table_types.h" + #include #include @@ -246,6 +248,10 @@ struct AppState { char table_filter_input[96] = {}; // buffer del popup activo 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) ------------------------------------------ // Draft del editor de tipos. Se inicializa con una copia de parsed_types // tras cargar el grafo. Save reescribe `types.yaml` y dispara