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:
2026-05-11 16:30:43 +02:00
parent f1e2c1cd19
commit 4c04162e23
8 changed files with 596 additions and 56 deletions
+12 -3
View File
@@ -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}
)
+244
View File
@@ -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
+16
View File
@@ -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
+93
View File
@@ -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
+44
View File
@@ -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
+29
View File
@@ -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
View File
@@ -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);
+116
View File
@@ -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;
}