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