feat(kotlin-compose): finalize design system + apps + sync sub-repo gitlinks
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,15 @@
|
||||
# Tables playground - vive dentro de primitives_gallery/ (playgrounds.md).
|
||||
# No es un app del registry: no tiene app.md, no se indexa.
|
||||
# Tables playground (cpp_apps.md / playgrounds.md). NO se indexa.
|
||||
add_imgui_app(tables_playground
|
||||
main.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/table_view.cpp
|
||||
data_table.cpp
|
||||
data_table_logic.cpp
|
||||
)
|
||||
|
||||
# Self-test E2E (logica pura, sin ImGui). No depende de fn_framework.
|
||||
add_executable(tables_playground_self_test
|
||||
self_test.cpp
|
||||
data_table_logic.cpp
|
||||
)
|
||||
target_include_directories(tables_playground_self_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
#include "data_table.h"
|
||||
#include "imgui.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace data_table {
|
||||
|
||||
namespace {
|
||||
|
||||
// Estado UI por-celda/por-header — sobrevive entre frames pero NO se persiste
|
||||
// a disco. Si se promueve al registry hay que pasarlo al State del caller.
|
||||
struct UiState {
|
||||
int pending_col = -1;
|
||||
std::string pending_value;
|
||||
bool open_cell_popup = false;
|
||||
|
||||
int header_popup_col = -1;
|
||||
std::unordered_map<int, std::string> filter_inputs; // col -> buffer
|
||||
std::unordered_map<int, std::string> color_value_inputs; // col -> buffer
|
||||
std::unordered_map<int, ImVec4> color_picker_vals; // col -> color
|
||||
};
|
||||
|
||||
UiState& ui() { static UiState s; return s; }
|
||||
|
||||
void ensure_init(State& st, int cols) {
|
||||
if ((int)st.col_visible.size() != cols) st.col_visible.assign(cols, true);
|
||||
}
|
||||
|
||||
void draw_chips(State& st, const char* const* headers, int cols) {
|
||||
if (st.filters.empty()) {
|
||||
ImGui::TextDisabled("Sin filtros. Click en celda -> elige operador.");
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < st.filters.size(); ) {
|
||||
const auto& f = st.filters[i];
|
||||
const char* hdr = (f.col >= 0 && f.col < cols) ? headers[f.col] : "?";
|
||||
char buf[256];
|
||||
std::snprintf(buf, sizeof(buf), "%s %s %s x##chip%zu",
|
||||
hdr, op_label(f.op), f.value.c_str(), i);
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(60, 100, 160, 220));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(80, 130, 200, 240));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(45, 80, 130, 240));
|
||||
bool clicked = ImGui::SmallButton(buf);
|
||||
ImGui::PopStyleColor(3);
|
||||
if (clicked) { st.filters.erase(st.filters.begin() + i); continue; }
|
||||
ImGui::SameLine();
|
||||
++i;
|
||||
}
|
||||
ImGui::NewLine();
|
||||
}
|
||||
|
||||
// Devuelve true y rellena out si el usuario eligio un operador.
|
||||
bool draw_op_menu_items(Op& out) {
|
||||
const Op ops[] = {Op::Eq, Op::Neq, Op::Gt, Op::Gte, Op::Lt, Op::Lte};
|
||||
for (Op o : ops) {
|
||||
if (ImGui::MenuItem(op_label(o))) { out = o; return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void draw_header_menu(State& st, int col, const char* const* headers, int col_count) {
|
||||
auto& U = ui();
|
||||
auto& fbuf = U.filter_inputs[col];
|
||||
fbuf.resize(256, '\0');
|
||||
|
||||
if (ImGui::BeginMenu("Filter...")) {
|
||||
ImGui::SetNextItemWidth(180);
|
||||
ImGui::InputText("##filterval", fbuf.data(), fbuf.size());
|
||||
std::string val(fbuf.c_str());
|
||||
const Op ops[] = {Op::Eq, Op::Neq, Op::Gt, Op::Gte, Op::Lt, Op::Lte};
|
||||
for (size_t i = 0; i < sizeof(ops)/sizeof(ops[0]); ++i) {
|
||||
if (i > 0) ImGui::SameLine();
|
||||
if (ImGui::SmallButton(op_label(ops[i]))) {
|
||||
st.filters.push_back({col, ops[i], val});
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
if (ImGui::BeginMenu("Conditional color")) {
|
||||
auto& vbuf = U.color_value_inputs[col];
|
||||
vbuf.resize(256, '\0');
|
||||
auto it = U.color_picker_vals.find(col);
|
||||
if (it == U.color_picker_vals.end()) {
|
||||
U.color_picker_vals[col] = ImVec4(0.85f, 0.40f, 0.30f, 0.60f);
|
||||
}
|
||||
ImVec4& cv = U.color_picker_vals[col];
|
||||
ImGui::SetNextItemWidth(180);
|
||||
ImGui::InputText("equals", vbuf.data(), vbuf.size());
|
||||
ImGui::ColorEdit4("color", &cv.x, ImGuiColorEditFlags_NoInputs);
|
||||
if (ImGui::Button("Apply")) {
|
||||
ImU32 c = ImGui::ColorConvertFloat4ToU32(cv);
|
||||
st.color_rules.push_back({col, std::string(vbuf.c_str()), (unsigned int)c});
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Clear col")) {
|
||||
for (size_t i = 0; i < st.color_rules.size();) {
|
||||
if (st.color_rules[i].col == col) st.color_rules.erase(st.color_rules.begin() + i);
|
||||
else ++i;
|
||||
}
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
if (ImGui::MenuItem("Hide column")) {
|
||||
st.col_visible[col] = false;
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
if (ImGui::BeginMenu("Columns")) {
|
||||
for (int k = 0; k < col_count; ++k) {
|
||||
bool v = st.col_visible[k];
|
||||
if (ImGui::Checkbox(headers[k], &v)) st.col_visible[k] = v;
|
||||
}
|
||||
if (ImGui::MenuItem("Show all")) {
|
||||
for (int k = 0; k < col_count; ++k) st.col_visible[k] = true;
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void render(const char* id,
|
||||
const char* const* headers,
|
||||
int col_count,
|
||||
const char* const* cells,
|
||||
int row_count,
|
||||
State& st)
|
||||
{
|
||||
ensure_init(st, col_count);
|
||||
auto& U = ui();
|
||||
|
||||
draw_chips(st, headers, col_count);
|
||||
|
||||
auto visible_rows = compute_visible_rows(cells, row_count, col_count, st);
|
||||
int visible_cols = 0;
|
||||
for (bool v : st.col_visible) if (v) ++visible_cols;
|
||||
|
||||
ImGui::Text("Filas: %d / %d Columnas: %d / %d",
|
||||
(int)visible_rows.size(), row_count, visible_cols, col_count);
|
||||
|
||||
if (visible_cols == 0) {
|
||||
ImGui::TextDisabled("(todas las columnas ocultas - click derecho en cabecera anterior)");
|
||||
return;
|
||||
}
|
||||
|
||||
ImGuiTableFlags flags =
|
||||
ImGuiTableFlags_Borders |
|
||||
ImGuiTableFlags_Sortable |
|
||||
ImGuiTableFlags_SortMulti |
|
||||
ImGuiTableFlags_RowBg |
|
||||
ImGuiTableFlags_Resizable |
|
||||
ImGuiTableFlags_ScrollY |
|
||||
ImGuiTableFlags_Reorderable;
|
||||
|
||||
if (!ImGui::BeginTable(id, visible_cols, flags, ImVec2(0, 0))) return;
|
||||
|
||||
// Setup columns with UserID = column index del dataset original.
|
||||
for (int c = 0; c < col_count; ++c) {
|
||||
if (!st.col_visible[c]) continue;
|
||||
ImGui::TableSetupColumn(headers[c], ImGuiTableColumnFlags_None, 0.0f, (ImGuiID)c);
|
||||
}
|
||||
ImGui::TableSetupScrollFreeze(0, 1);
|
||||
|
||||
// Custom header row para soportar right-click context menu por columna.
|
||||
ImGui::TableNextRow(ImGuiTableRowFlags_Headers);
|
||||
int draw_col = 0;
|
||||
for (int c = 0; c < col_count; ++c) {
|
||||
if (!st.col_visible[c]) continue;
|
||||
ImGui::TableSetColumnIndex(draw_col++);
|
||||
ImGui::PushID(c);
|
||||
ImGui::TableHeader(headers[c]);
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
||||
U.header_popup_col = c;
|
||||
ImGui::OpenPopup("##hdr_menu");
|
||||
}
|
||||
if (ImGui::BeginPopup("##hdr_menu") && U.header_popup_col == c) {
|
||||
draw_header_menu(st, c, headers, col_count);
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
// Aplicar sort specs de ImGui -> State.
|
||||
if (ImGuiTableSortSpecs* specs = ImGui::TableGetSortSpecs()) {
|
||||
if (specs->SpecsDirty && specs->SpecsCount > 0) {
|
||||
const ImGuiTableColumnSortSpecs& s = specs->Specs[0];
|
||||
st.sort_col = (int)s.ColumnUserID;
|
||||
st.sort_desc = (s.SortDirection == ImGuiSortDirection_Descending);
|
||||
specs->SpecsDirty = false;
|
||||
visible_rows = compute_visible_rows(cells, row_count, col_count, st);
|
||||
} else if (specs->SpecsCount == 0 && st.sort_col >= 0) {
|
||||
st.sort_col = -1;
|
||||
visible_rows = compute_visible_rows(cells, row_count, col_count, st);
|
||||
}
|
||||
}
|
||||
|
||||
// Body.
|
||||
for (int r : visible_rows) {
|
||||
ImGui::TableNextRow();
|
||||
int dc = 0;
|
||||
for (int c = 0; c < col_count; ++c) {
|
||||
if (!st.col_visible[c]) continue;
|
||||
ImGui::TableSetColumnIndex(dc++);
|
||||
const char* cell = cells[r * col_count + c];
|
||||
for (const auto& cr : st.color_rules) {
|
||||
if (cr.col == c && cell && cr.equals == cell) {
|
||||
ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, (ImU32)cr.color);
|
||||
break;
|
||||
}
|
||||
}
|
||||
ImGui::PushID(r * col_count + c);
|
||||
if (ImGui::Selectable(cell ? cell : "", false, ImGuiSelectableFlags_AllowDoubleClick)) {
|
||||
U.pending_col = c;
|
||||
U.pending_value = cell ? cell : "";
|
||||
U.open_cell_popup = true;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
|
||||
if (U.open_cell_popup) { ImGui::OpenPopup("##cell_op"); U.open_cell_popup = false; }
|
||||
if (ImGui::BeginPopup("##cell_op")) {
|
||||
const char* hdr = (U.pending_col >= 0 && U.pending_col < col_count)
|
||||
? headers[U.pending_col] : "?";
|
||||
ImGui::TextDisabled("%s ?? \"%s\"", hdr, U.pending_value.c_str());
|
||||
ImGui::Separator();
|
||||
Op picked;
|
||||
if (draw_op_menu_items(picked)) {
|
||||
st.filters.push_back({U.pending_col, picked, U.pending_value});
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace data_table
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "data_table_logic.h"
|
||||
|
||||
namespace data_table {
|
||||
|
||||
// Render barra-de-chips + tabla. Mutates `st` en respuesta a interaccion.
|
||||
// Caller mantiene el State entre frames.
|
||||
void render(const char* id,
|
||||
const char* const* headers,
|
||||
int col_count,
|
||||
const char* const* cells,
|
||||
int row_count,
|
||||
State& st);
|
||||
|
||||
} // namespace data_table
|
||||
@@ -0,0 +1,93 @@
|
||||
#include "data_table_logic.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace data_table {
|
||||
|
||||
const char* op_label(Op o) {
|
||||
switch (o) {
|
||||
case Op::Eq: return "=";
|
||||
case Op::Neq: return "!=";
|
||||
case Op::Gt: return ">";
|
||||
case Op::Gte: return ">=";
|
||||
case Op::Lt: return "<";
|
||||
case Op::Lte: return "<=";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
bool parse_number(const char* s, double& out) {
|
||||
if (!s || !*s) return false;
|
||||
char* end = nullptr;
|
||||
double v = std::strtod(s, &end);
|
||||
if (end == s) return false;
|
||||
while (*end == ' ' || *end == '\t') end++;
|
||||
if (*end != '\0') return false;
|
||||
out = v;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool compare(const char* a, const char* b, Op op) {
|
||||
if (!a) a = "";
|
||||
if (!b) b = "";
|
||||
double na, nb;
|
||||
bool numeric = parse_number(a, na) && parse_number(b, nb);
|
||||
if (numeric) {
|
||||
switch (op) {
|
||||
case Op::Eq: return na == nb;
|
||||
case Op::Neq: return na != nb;
|
||||
case Op::Gt: return na > nb;
|
||||
case Op::Gte: return na >= nb;
|
||||
case Op::Lt: return na < nb;
|
||||
case Op::Lte: return na <= nb;
|
||||
}
|
||||
}
|
||||
int c = std::strcmp(a, b);
|
||||
switch (op) {
|
||||
case Op::Eq: return c == 0;
|
||||
case Op::Neq: return c != 0;
|
||||
case Op::Gt: return c > 0;
|
||||
case Op::Gte: return c >= 0;
|
||||
case Op::Lt: return c < 0;
|
||||
case Op::Lte: return c <= 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<int> compute_visible_rows(const char* const* cells,
|
||||
int rows, int cols,
|
||||
const State& st)
|
||||
{
|
||||
std::vector<int> out;
|
||||
out.reserve(rows);
|
||||
for (int r = 0; r < rows; ++r) {
|
||||
bool keep = true;
|
||||
for (const auto& f : st.filters) {
|
||||
if (f.col < 0 || f.col >= cols) continue;
|
||||
const char* cell = cells[r * cols + f.col];
|
||||
if (!compare(cell, f.value.c_str(), f.op)) { keep = false; break; }
|
||||
}
|
||||
if (keep) out.push_back(r);
|
||||
}
|
||||
if (st.sort_col >= 0 && st.sort_col < cols) {
|
||||
int sc = st.sort_col;
|
||||
bool desc = st.sort_desc;
|
||||
std::sort(out.begin(), out.end(), [&](int a, int b) {
|
||||
const char* ca = cells[a * cols + sc];
|
||||
const char* cb = cells[b * cols + sc];
|
||||
if (!ca) ca = "";
|
||||
if (!cb) cb = "";
|
||||
double na, nb;
|
||||
bool num = parse_number(ca, na) && parse_number(cb, nb);
|
||||
int cmp;
|
||||
if (num) cmp = (na < nb) ? -1 : (na > nb ? 1 : 0);
|
||||
else cmp = std::strcmp(ca, cb);
|
||||
return desc ? (cmp > 0) : (cmp < 0);
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace data_table
|
||||
@@ -0,0 +1,44 @@
|
||||
// Logica pura del playground data_table. Sin ImGui — testable headless.
|
||||
// Cuando se promueva al registry, esto sera la base de data_table_cpp_viz.
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace data_table {
|
||||
|
||||
enum class Op { Eq, Neq, Gt, Gte, Lt, Lte };
|
||||
const char* op_label(Op o);
|
||||
|
||||
struct Filter {
|
||||
int col;
|
||||
Op op;
|
||||
std::string value;
|
||||
};
|
||||
|
||||
struct ColorRule {
|
||||
int col;
|
||||
std::string equals;
|
||||
unsigned int color; // ImU32 (ABGR para ImGui)
|
||||
};
|
||||
|
||||
struct State {
|
||||
std::vector<Filter> filters;
|
||||
std::vector<ColorRule> color_rules;
|
||||
std::vector<bool> col_visible; // size = col_count; auto-init en render
|
||||
int sort_col = -1; // -1 = sin sort
|
||||
bool sort_desc = false;
|
||||
};
|
||||
|
||||
// Parse "1.23" -> 1.23, true. False si la celda no es numero completo.
|
||||
bool parse_number(const char* s, double& out);
|
||||
|
||||
// Compara dos celdas con operador. Numerico si ambas parseables; lexical si no.
|
||||
bool compare(const char* a, const char* b, Op op);
|
||||
|
||||
// Aplica filtros y ordena. Devuelve indices de filas visibles.
|
||||
std::vector<int> compute_visible_rows(const char* const* cells,
|
||||
int rows, int cols,
|
||||
const State& st);
|
||||
|
||||
} // namespace data_table
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# E2E playground tables. Compila + corre self-test linux + windows (si
|
||||
# mingw esta disponible). Sale 0 si todo pasa.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="${FN_REGISTRY_ROOT:-$(cd "$(dirname "$0")/../../../../.." && pwd)}"
|
||||
cd "$ROOT"
|
||||
|
||||
echo "[e2e] linux build + self_test"
|
||||
cmake -B cpp/build -S cpp >/dev/null
|
||||
cmake --build cpp/build --target tables_playground_self_test -j"$(nproc)" >/dev/null
|
||||
./cpp/build/apps/primitives_gallery/playground/tables/tables_playground_self_test
|
||||
|
||||
if command -v x86_64-w64-mingw32-g++ >/dev/null 2>&1; then
|
||||
echo "[e2e] windows cross-build (mingw)"
|
||||
source "$ROOT/bash/functions/infra/build_cpp_windows.sh"
|
||||
build_cpp_windows tables_playground_self_test >/dev/null
|
||||
echo "[e2e] windows self_test via wine si disponible"
|
||||
EXE="$ROOT/cpp/build/windows/apps/primitives_gallery/playground/tables/tables_playground_self_test.exe"
|
||||
if [ -f "$EXE" ]; then
|
||||
if command -v wine >/dev/null 2>&1; then
|
||||
wine "$EXE" || { echo "[e2e] FAIL windows self_test (wine)"; exit 1; }
|
||||
else
|
||||
echo "[e2e] SKIP wine no instalado; binario en $EXE"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[e2e] OK"
|
||||
+42
-53
@@ -1,14 +1,11 @@
|
||||
// Playground tables: visor de la funcion table_view_cpp_viz tal cual existe
|
||||
// hoy en el registry. Iteraremos mejoras encima hasta promover una API v2
|
||||
// que sustituya a los `ImGui::BeginTable` raw de las apps C++.
|
||||
// Playground tables: iterador de la fn `data_table` antes de promoverla al
|
||||
// registry y migrar las apps C++ que hoy usan `ImGui::BeginTable` raw.
|
||||
|
||||
#include "app_base.h"
|
||||
#include "imgui.h"
|
||||
#include "viz/table_view.h"
|
||||
#include "core/logger.h"
|
||||
#include "data_table.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
@@ -21,48 +18,44 @@ struct Row {
|
||||
const char* description;
|
||||
};
|
||||
|
||||
// Dataset de muestra inspirado en el registry. Filas reales-ish para
|
||||
// hacer obvias las limitaciones actuales (sin sort, sin filter, sin
|
||||
// per-cell render, alto fijo, etc.).
|
||||
const std::vector<Row>& sample_rows() {
|
||||
static const std::vector<Row> rows = {
|
||||
{"filter_slice", "go", "core", "pure", "Filtra slice con predicado"},
|
||||
{"map_slice", "go", "core", "pure", "Aplica f a cada elemento"},
|
||||
{"reduce_slice", "go", "core", "pure", "Fold con acumulador"},
|
||||
{"sma", "py", "finance", "pure", "Simple moving average"},
|
||||
{"ema", "py", "finance", "pure", "Exponential moving average"},
|
||||
{"rsi", "py", "finance", "pure", "Relative strength index"},
|
||||
{"table_view", "cpp", "viz", "pure", "Tabla ImGui actual del registry"},
|
||||
{"line_plot", "cpp", "viz", "pure", "ImPlot line wrapper"},
|
||||
{"scatter_plot", "cpp", "viz", "pure", "ImPlot scatter wrapper"},
|
||||
{"bar_chart", "cpp", "viz", "pure", "ImPlot bar wrapper"},
|
||||
{"heatmap", "cpp", "viz", "pure", "ImPlot heatmap wrapper"},
|
||||
{"sqlite_open", "go", "infra", "impure", "Open SQLite con WAL+FK"},
|
||||
{"http_json_response", "go", "infra", "impure", "Helper JSON response"},
|
||||
{"http_parse_body", "go", "infra", "impure", "Parse JSON body"},
|
||||
{"rsync_deploy", "bash", "infra", "impure", "rsync local -> remoto"},
|
||||
{"systemd_install", "go", "infra", "impure", "Sube unit + enable + start"},
|
||||
{"systemd_restart", "go", "infra", "impure", "Restart servicio remoto"},
|
||||
{"jupyter_discover", "py", "notebook", "impure", "Descubre instancias Jupyter"},
|
||||
{"jupyter_exec", "py", "notebook", "impure", "Ejecuta celda y vuelca output"},
|
||||
{"docker_pull_image", "go", "infra", "impure", "docker pull con timeout"},
|
||||
{"graph_force_layout", "cpp", "viz", "pure", "Force-directed CPU"},
|
||||
{"graph_force_layout_gpu","cpp", "viz", "pure", "Force-directed GPU (compute)"},
|
||||
{"sql_workbench", "cpp", "core", "impure", "Workbench SQL embebido"},
|
||||
{"text_editor", "cpp", "core", "impure", "Editor de texto con highlighting"},
|
||||
{"icon_font", "cpp", "core", "impure", "Carga tabler-icons.ttf"},
|
||||
{"filter_slice", "go", "core", "pure", "Filtra slice con predicado"},
|
||||
{"map_slice", "go", "core", "pure", "Aplica f a cada elemento"},
|
||||
{"reduce_slice", "go", "core", "pure", "Fold con acumulador"},
|
||||
{"sma", "py", "finance", "pure", "Simple moving average"},
|
||||
{"ema", "py", "finance", "pure", "Exponential moving average"},
|
||||
{"rsi", "py", "finance", "pure", "Relative strength index"},
|
||||
{"table_view", "cpp", "viz", "pure", "Tabla ImGui actual del registry"},
|
||||
{"line_plot", "cpp", "viz", "pure", "ImPlot line wrapper"},
|
||||
{"scatter_plot", "cpp", "viz", "pure", "ImPlot scatter wrapper"},
|
||||
{"bar_chart", "cpp", "viz", "pure", "ImPlot bar wrapper"},
|
||||
{"heatmap", "cpp", "viz", "pure", "ImPlot heatmap wrapper"},
|
||||
{"sqlite_open", "go", "infra", "impure", "Open SQLite con WAL+FK"},
|
||||
{"http_json_response", "go", "infra", "impure", "Helper JSON response"},
|
||||
{"http_parse_body", "go", "infra", "impure", "Parse JSON body"},
|
||||
{"rsync_deploy", "bash", "infra", "impure", "rsync local -> remoto"},
|
||||
{"systemd_install", "go", "infra", "impure", "Sube unit + enable + start"},
|
||||
{"systemd_restart", "go", "infra", "impure", "Restart servicio remoto"},
|
||||
{"jupyter_discover", "py", "notebook", "impure", "Descubre instancias Jupyter"},
|
||||
{"jupyter_exec", "py", "notebook", "impure", "Ejecuta celda y vuelca output"},
|
||||
{"docker_pull_image", "go", "infra", "impure", "docker pull con timeout"},
|
||||
{"graph_force_layout", "cpp", "viz", "pure", "Force-directed CPU"},
|
||||
{"graph_force_layout_gpu","cpp", "viz", "pure", "Force-directed GPU (compute)"},
|
||||
{"sql_workbench", "cpp", "core", "impure", "Workbench SQL embebido"},
|
||||
{"text_editor", "cpp", "core", "impure", "Editor de texto con highlighting"},
|
||||
{"icon_font", "cpp", "core", "impure", "Carga tabler-icons.ttf"},
|
||||
};
|
||||
return rows;
|
||||
}
|
||||
|
||||
// Aplanado row-major para alimentar table_view_cpp_viz (firma `const char* const*`).
|
||||
const char* const* flatten_cells(int& out_rows, int& out_cols) {
|
||||
const char* const* flatten_cells(int& rows, int& cols) {
|
||||
static std::vector<const char*> flat;
|
||||
static bool built = false;
|
||||
if (!built) {
|
||||
const auto& rows = sample_rows();
|
||||
flat.reserve(rows.size() * 5);
|
||||
for (const auto& r : rows) {
|
||||
const auto& src = sample_rows();
|
||||
flat.reserve(src.size() * 5);
|
||||
for (const auto& r : src) {
|
||||
flat.push_back(r.name);
|
||||
flat.push_back(r.lang);
|
||||
flat.push_back(r.domain);
|
||||
@@ -71,30 +64,26 @@ const char* const* flatten_cells(int& out_rows, int& out_cols) {
|
||||
}
|
||||
built = true;
|
||||
}
|
||||
out_rows = static_cast<int>(sample_rows().size());
|
||||
out_cols = 5;
|
||||
rows = (int)sample_rows().size();
|
||||
cols = 5;
|
||||
return flat.data();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void render() {
|
||||
if (ImGui::Begin("Tables Playground - table_view actual")) {
|
||||
static data_table::State st;
|
||||
if (ImGui::Begin("Tables Playground - data_table v0.1")) {
|
||||
ImGui::TextWrapped(
|
||||
"Esta es la funcion `table_view_cpp_viz` del registry hoy. "
|
||||
"Capacidades: borders, sortable (solo indicador, no sort real), "
|
||||
"rowBg, resizable, scrollY (alto fijo 300px), reorderable. "
|
||||
"Sin filter, sin selection, sin per-cell render, sin export. "
|
||||
"Iteraremos mejoras encima de esto.");
|
||||
"Iteracion 1: sort real al pulsar header, click en celda -> popup operador "
|
||||
"(=, !=, >, >=, <, <=) -> chip removible. Click derecho header: filter input, "
|
||||
"conditional color, hide column, show/hide columns.");
|
||||
ImGui::Separator();
|
||||
|
||||
static const char* headers[] = {"name", "lang", "domain", "purity", "description"};
|
||||
int rows = 0, cols = 0;
|
||||
const char* const* cells = flatten_cells(rows, cols);
|
||||
|
||||
ImGui::Text("Filas: %d Columnas: %d", rows, cols);
|
||||
ImGui::Spacing();
|
||||
table_view("##registry_sample", headers, cols, cells, rows);
|
||||
data_table::render("##registry_sample", headers, cols, cells, rows, st);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
@@ -106,8 +95,8 @@ int main() {
|
||||
.width = 1280,
|
||||
.height = 800,
|
||||
.about = {.name = "tables_playground",
|
||||
.version = "0.1.0",
|
||||
.description = "Playground para iterar mejoras sobre table_view_cpp_viz antes de promover a registry y migrar apps C++."},
|
||||
.version = "0.2.0",
|
||||
.description = "Playground para iterar mejoras sobre table_view antes de promover al registry."},
|
||||
.log = {.file_path = "tables_playground.log",
|
||||
.level = static_cast<int>(fn_log::Level::Info)}
|
||||
}, render);
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
// E2E self-test del playground tables. Ejercita la logica pura
|
||||
// (data_table_logic) sin ImGui. Build target separado:
|
||||
//
|
||||
// tables_playground_self_test -> linux
|
||||
// tables_playground_self_test.exe -> windows
|
||||
//
|
||||
// Exit 0 = todos los checks pasan, 1 = falla.
|
||||
|
||||
#include "data_table_logic.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
int failed = 0;
|
||||
int passed = 0;
|
||||
|
||||
void check(bool cond, const char* name) {
|
||||
if (cond) { passed++; std::printf("PASS %s\n", name); }
|
||||
else { failed++; std::printf("FAIL %s\n", name); }
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
using namespace data_table;
|
||||
|
||||
int main() {
|
||||
// --- parse_number ---
|
||||
double v = 0;
|
||||
check(parse_number("1.23", v) && v == 1.23, "parse_number 1.23");
|
||||
check(parse_number("42", v) && v == 42.0, "parse_number 42");
|
||||
check(parse_number("-7.5", v) && v == -7.5, "parse_number -7.5");
|
||||
check(!parse_number("abc", v), "parse_number abc rejected");
|
||||
check(!parse_number("12x", v), "parse_number 12x rejected");
|
||||
check(!parse_number("", v), "parse_number empty rejected");
|
||||
check(!parse_number(nullptr, v), "parse_number null rejected");
|
||||
|
||||
// --- compare numerico ---
|
||||
check( compare("10", "2", Op::Gt), "10 > 2 numerico");
|
||||
check(!compare("10", "2", Op::Lt), "10 < 2 numerico false");
|
||||
check( compare("2", "10", Op::Lt), "2 < 10 numerico");
|
||||
check( compare("5", "5", Op::Eq), "5 == 5 numerico");
|
||||
check( compare("5", "5", Op::Gte), "5 >= 5 numerico");
|
||||
check( compare("5", "5", Op::Lte), "5 <= 5 numerico");
|
||||
check( compare("5", "5", Op::Neq) == false, "5 != 5 numerico false");
|
||||
|
||||
// --- compare lexical (cuando no son numeros) ---
|
||||
check( compare("go", "go", Op::Eq), "lexical eq");
|
||||
check( compare("go", "py", Op::Neq), "lexical neq");
|
||||
check( compare("py", "go", Op::Gt), "lexical gt");
|
||||
check( compare("ab", "ac", Op::Lt), "lexical lt");
|
||||
|
||||
// --- compute_visible_rows: filter ---
|
||||
const char* cells[] = {
|
||||
"a","1",
|
||||
"b","2",
|
||||
"c","3",
|
||||
"a","4",
|
||||
};
|
||||
State st;
|
||||
st.filters.push_back({0, Op::Eq, "a"});
|
||||
auto rows = compute_visible_rows(cells, 4, 2, st);
|
||||
check(rows.size() == 2 && rows[0] == 0 && rows[1] == 3, "filter col0 = a");
|
||||
|
||||
// --- filter numerico ---
|
||||
st.filters.clear();
|
||||
st.filters.push_back({1, Op::Gt, "2"});
|
||||
rows = compute_visible_rows(cells, 4, 2, st);
|
||||
check(rows.size() == 2 && rows[0] == 2 && rows[1] == 3, "filter col1 > 2");
|
||||
|
||||
// --- combinacion: > 1 AND col0 != b ---
|
||||
st.filters.clear();
|
||||
st.filters.push_back({1, Op::Gt, "1"});
|
||||
st.filters.push_back({0, Op::Neq, "b"});
|
||||
rows = compute_visible_rows(cells, 4, 2, st);
|
||||
check(rows.size() == 2 && rows[0] == 2 && rows[1] == 3, "filter combinado AND");
|
||||
|
||||
// --- sort ascendente numerico ---
|
||||
st.filters.clear();
|
||||
st.sort_col = 1;
|
||||
st.sort_desc = false;
|
||||
rows = compute_visible_rows(cells, 4, 2, st);
|
||||
check(rows.size() == 4 && rows[0] == 0 && rows[3] == 3, "sort asc numerico");
|
||||
|
||||
// --- sort descendente numerico ---
|
||||
st.sort_desc = true;
|
||||
rows = compute_visible_rows(cells, 4, 2, st);
|
||||
check(rows.size() == 4 && rows[0] == 3 && rows[3] == 0, "sort desc numerico");
|
||||
|
||||
// --- sort lexical ---
|
||||
st.sort_col = 0;
|
||||
st.sort_desc = false;
|
||||
rows = compute_visible_rows(cells, 4, 2, st);
|
||||
check(rows.size() == 4 && std::strcmp(cells[rows[0]*2], "a") == 0
|
||||
&& std::strcmp(cells[rows[3]*2], "c") == 0, "sort asc lexical");
|
||||
|
||||
// --- filter + sort combinado ---
|
||||
st.sort_col = 1;
|
||||
st.sort_desc = true;
|
||||
st.filters.push_back({0, Op::Eq, "a"});
|
||||
rows = compute_visible_rows(cells, 4, 2, st);
|
||||
check(rows.size() == 2 && rows[0] == 3 && rows[1] == 0, "filter+sort combinado");
|
||||
|
||||
// --- filter sobre columna inexistente: se ignora ---
|
||||
st.filters.clear();
|
||||
st.filters.push_back({99, Op::Eq, "x"});
|
||||
st.sort_col = -1;
|
||||
rows = compute_visible_rows(cells, 4, 2, st);
|
||||
check(rows.size() == 4, "filter col fuera de rango ignorado");
|
||||
|
||||
std::printf("\n=== %d passed, %d failed ===\n", passed, failed);
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user