Files
fn_registry/cpp/functions/viz/data_table_grid.cpp
T
egutierrez b9716a7cd6 chore: snapshot WIP previo + flow 0008 + 7 sub-issues (0112-0119)
Snapshot de WIP acumulado de sesiones previas antes de merge wave 1
del flow 0008 (kanban_cpp + agent_runner_api + DoD schema).

Incluye:
- dev/flows/0008-kanban-cpp-and-agent-workflows.md
- dev/issues/0112-0119*.md (7 sub-issues)
- WIP previo en cmd/fn/doctor.go, registry/*, modules/, cpp/, etc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:17:08 +02:00

1015 lines
46 KiB
C++

// data_table_grid — render del grid de datos con renderers declarativos.
// Sub-funcion extraida de modules/data_table/data_table.cpp (issue 0107c).
//
// Contiene:
// - Helpers estaticos de celda: hex_to_imcolor, lerp_color_along_stops,
// icon_name_to_glyph, column_type_icon (copia local para este TU).
// - draw_cell_custom: renderer declarativo por ColumnSpec.
// - draw_row_inspector_modal: modal de inspeccion de fila.
// - render_grid_stage0: grid completo para stage == 0.
// - render_grid_stage_n: grid para stage > 0 (datos materializados).
//
// Dependencias:
// viz/data_table_grid.h
// viz/data_table_drill.h (make_drill_filter, apply_drill_step, drill_into)
// viz/data_table_color_rules.h (parse_hex_color, apply_color_rules_for_cell)
// viz/data_table_chips.h (apply_header_sort_click, draw_header_menu)
// data_table/data_table_internal.h (UiState, ui(), reorder_column via data_table.cpp)
// core/data_table_types.h
// core/tql_helpers.h (parse_breakout_granularity)
// core/auto_detect_type.h (parse_number)
// imgui.h, core/icons_tabler.h
#include "viz/data_table_grid.h"
#include "viz/data_table_drill.h"
#include "viz/data_table_color_rules.h"
#include "viz/data_table_chips.h"
#include "data_table/data_table_internal.h"
#include "core/data_table_types.h"
#include "core/tql_helpers.h"
#include "core/auto_detect_type.h"
#include "imgui.h"
#include "core/icons_tabler.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>
#include <unordered_map>
#include <vector>
namespace data_table {
// ---------------------------------------------------------------------------
// Static helpers (only used by this TU)
// ---------------------------------------------------------------------------
static const char* column_type_icon_g(ColumnType t) {
switch (t) {
case ColumnType::Auto: return "\xef\xa4\x9d"; // TI_HELP_CIRCLE
case ColumnType::String: return "\xef\x95\xa7"; // TI_ABC
case ColumnType::Int: return "\xef\x95\x94"; // TI_123
case ColumnType::Float: return "\xef\xa8\xa6"; // TI_DECIMAL
case ColumnType::Bool: return "\xee\xae\xa6"; // TI_CHECKBOX
case ColumnType::Date: return "\xee\xa9\x93"; // TI_CALENDAR
case ColumnType::Json: return "\xee\xaf\x8c"; // TI_BRACES
}
return "?";
}
static ImVec4 hex_to_imcolor_g(const std::string& hex) {
const char* p = hex.c_str();
if (*p == '#') ++p;
unsigned int r = 0, g = 0, b = 0;
if (std::sscanf(p, "%02x%02x%02x", &r, &g, &b) != 3)
return ImVec4(-1.f, -1.f, -1.f, -1.f);
return ImVec4(r / 255.f, g / 255.f, b / 255.f, 1.f);
}
static ImU32 lerp_color_along_stops_g(
const std::vector<ColorStop>& stops, float t, float alpha)
{
static const std::vector<ColorStop> kDefault = {
{0.0f, "#22c55e"},
{0.5f, "#f59e0b"},
{1.0f, "#ef4444"},
};
const auto& sv = stops.empty() ? kDefault : stops;
std::vector<ColorStop> sorted_sv = sv;
std::sort(sorted_sv.begin(), sorted_sv.end(),
[](const ColorStop& a, const ColorStop& b) {
return a.position < b.position;
});
t = t < 0.f ? 0.f : (t > 1.f ? 1.f : t);
if (t <= sorted_sv.front().position)
return parse_hex_color(sorted_sv.front().color, alpha);
if (t >= sorted_sv.back().position)
return parse_hex_color(sorted_sv.back().color, alpha);
for (size_t i = 0; i + 1 < sorted_sv.size(); ++i) {
const auto& lo = sorted_sv[i];
const auto& hi = sorted_sv[i + 1];
if (t >= lo.position && t <= hi.position) {
float span = hi.position - lo.position;
float f = (span > 1e-6f) ? (t - lo.position) / span : 0.f;
ImVec4 ca = hex_to_imcolor_g(lo.color);
ImVec4 cb = hex_to_imcolor_g(hi.color);
if (ca.x < 0.f) ca = ImVec4(0.5f, 0.5f, 0.5f, 1.f);
if (cb.x < 0.f) cb = ImVec4(0.5f, 0.5f, 0.5f, 1.f);
float rv = ca.x + f * (cb.x - ca.x);
float gv = ca.y + f * (cb.y - ca.y);
float bv = ca.z + f * (cb.z - ca.z);
unsigned int ri = (unsigned int)(rv * 255.f + 0.5f);
unsigned int gi = (unsigned int)(gv * 255.f + 0.5f);
unsigned int bi = (unsigned int)(bv * 255.f + 0.5f);
unsigned int ai = (unsigned int)(alpha * 255.f + 0.5f);
return IM_COL32(ri, gi, bi, ai);
}
}
return parse_hex_color(sorted_sv.back().color, alpha);
}
static const char* icon_name_to_glyph_g(const std::string& name) {
static const std::unordered_map<std::string, const char*> kMap = {
{"TI_CHECK", TI_CHECK},
{"TI_X", TI_X},
{"TI_ALERT_CIRCLE", TI_ALERT_CIRCLE},
{"TI_CIRCLE_DOT", TI_CIRCLE_DOT},
{"TI_CLOCK", TI_CLOCK},
{"TI_LOADER", TI_LOADER},
{"TI_BAN", TI_BAN},
{"TI_PLAYER_PLAY", TI_PLAYER_PLAY},
{"TI_PLAYER_PAUSE", TI_PLAYER_PAUSE},
{"TI_PLAYER_STOP", TI_PLAYER_STOP},
{"TI_DATABASE", TI_DATABASE},
{"TI_SETTINGS", TI_SETTINGS},
{"TI_USER", TI_USER},
{"TI_USERS", TI_USERS},
{"TI_FILE", TI_FILE},
{"TI_FOLDER", TI_FOLDER},
{"TI_REFRESH", TI_REFRESH},
{"TI_BOLT", TI_BOLT},
{"TI_INFO_CIRCLE", TI_INFO_CIRCLE},
{"TI_ARROW_UP", TI_ARROW_UP},
{"TI_ARROW_DOWN", TI_ARROW_DOWN},
{"TI_ARROW_RIGHT", TI_ARROW_RIGHT},
{"TI_ARROW_LEFT", TI_ARROW_LEFT},
{"TI_DOTS", TI_DOTS},
{"TI_EYE", TI_EYE},
{"TI_EYE_OFF", TI_EYE_OFF},
{"TI_EDIT", TI_EDIT},
{"TI_TRASH", TI_TRASH},
{"TI_COPY", TI_COPY},
{"TI_EXTERNAL_LINK", TI_EXTERNAL_LINK},
};
auto it = kMap.find(name);
return it != kMap.end() ? it->second : nullptr;
}
// row_to_tsv_g: serializa una fila a header\tvalues.
static std::string row_to_tsv_g(const char* const* cells, int rows, int cols,
int row_idx, const std::vector<std::string>& headers) {
if (row_idx < 0 || row_idx >= rows || cols <= 0) return "";
std::string out;
for (int c = 0; c < cols; ++c) {
if (c > 0) out += '\t';
if (c < (int)headers.size()) out += headers[c];
}
out += "\r\n";
for (int c = 0; c < cols; ++c) {
if (c > 0) out += '\t';
const char* v = cells[row_idx * cols + c];
if (v) out += v;
}
out += "\r\n";
return out;
}
// reorder_column_g: mueve col src a posicion de col dst en st.col_order.
static void reorder_column_g(State& st, int src, int dst) {
if (src == dst) return;
auto it_s = std::find(st.col_order.begin(), st.col_order.end(), src);
auto it_d = std::find(st.col_order.begin(), st.col_order.end(), dst);
if (it_s == st.col_order.end() || it_d == st.col_order.end()) return;
int si = (int)(it_s - st.col_order.begin());
int di = (int)(it_d - st.col_order.begin());
int v = st.col_order[si];
st.col_order.erase(st.col_order.begin() + si);
if (di > (int)st.col_order.size()) di = (int)st.col_order.size();
st.col_order.insert(st.col_order.begin() + di, v);
}
// ---------------------------------------------------------------------------
// draw_cell_custom
// ---------------------------------------------------------------------------
void draw_cell_custom(const ColumnSpec& spec, const char* value,
int row_idx, int col_idx,
std::vector<TableEvent>* events_out)
{
if (!value) value = "";
switch (spec.renderer) {
case CellRenderer::Badge: {
const BadgeRule* matched = nullptr;
for (const auto& br : spec.badges) {
if (br.value == value) { matched = &br; break; }
}
if (matched) {
ImVec4 col = hex_to_imcolor_g(matched->color_hex);
const char* label = matched->label.empty() ? value : matched->label.c_str();
if (col.x >= 0.f) {
ImGui::PushStyleColor(ImGuiCol_Header, col);
ImVec4 hover_col = ImVec4(
std::min(col.x + 0.1f, 1.f),
std::min(col.y + 0.1f, 1.f),
std::min(col.z + 0.1f, 1.f),
col.w);
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, hover_col);
ImGui::PushStyleColor(ImGuiCol_HeaderActive, hover_col);
ImGui::Selectable(label, false);
ImGui::PopStyleColor(3);
} else {
ImGui::TextUnformatted(label);
}
} else {
ImGui::TextUnformatted(value);
}
break;
}
case CellRenderer::Progress: {
double v_raw = 0.0;
if (!parse_number(value, v_raw)) {
ImGui::TextUnformatted(value);
break;
}
float fv = (float)v_raw;
if (spec.progress_scale_100) fv /= 100.f;
fv = fv < 0.f ? 0.f : (fv > 1.f ? 1.f : fv);
bool has_color = !spec.progress_color_hex.empty();
ImVec4 bar_col;
if (has_color) {
bar_col = hex_to_imcolor_g(spec.progress_color_hex);
if (bar_col.x < 0.f) has_color = false;
}
if (has_color) ImGui::PushStyleColor(ImGuiCol_PlotHistogram, bar_col);
ImGui::ProgressBar(fv, ImVec2(-1.f, 0.f));
if (has_color) ImGui::PopStyleColor();
break;
}
case CellRenderer::Duration: {
double v_raw = 0.0;
if (!parse_number(value, v_raw)) {
ImGui::TextUnformatted(value);
break;
}
float ms = (float)v_raw;
ImVec4 text_col;
if (ms <= spec.duration_warn_ms) {
text_col = hex_to_imcolor_g("#22c55e");
} else if (ms <= spec.duration_error_ms) {
text_col = hex_to_imcolor_g("#f59e0b");
} else {
text_col = hex_to_imcolor_g("#ef4444");
}
char buf[32];
std::snprintf(buf, sizeof(buf), "%.0f ms", ms);
ImGui::TextColored(text_col, "%s", buf);
break;
}
case CellRenderer::Icon: {
const char* glyph = nullptr;
ImVec4 icon_col(-1.f, -1.f, -1.f, -1.f);
for (const auto& entry : spec.icon_map) {
if (entry.value == value) {
glyph = icon_name_to_glyph_g(entry.icon_name);
if (!entry.color_hex.empty())
icon_col = hex_to_imcolor_g(entry.color_hex);
break;
}
}
if (glyph) {
if (icon_col.x >= 0.f)
ImGui::TextColored(icon_col, "%s", glyph);
else
ImGui::TextUnformatted(glyph);
} else {
ImGui::TextUnformatted(value);
}
break;
}
case CellRenderer::Button: {
if (value[0] == '\0') break;
const char* label = spec.button_label.empty() ? value : spec.button_label.c_str();
bool has_color = !spec.button_color_hex.empty();
if (has_color) {
ImVec4 btn_col = hex_to_imcolor_g(spec.button_color_hex);
if (btn_col.x >= 0.f) {
ImGui::PushStyleColor(ImGuiCol_Button, btn_col);
ImVec4 hov = ImVec4(
std::min(btn_col.x + 0.12f, 1.f),
std::min(btn_col.y + 0.12f, 1.f),
std::min(btn_col.z + 0.12f, 1.f),
btn_col.w);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, hov);
ImGui::PushStyleColor(ImGuiCol_ButtonActive, hov);
} else {
has_color = false;
}
}
char btn_id[128];
std::snprintf(btn_id, sizeof(btn_id), "%s##btn_%d_%d",
label, row_idx, col_idx);
if (ImGui::SmallButton(btn_id) && events_out) {
TableEvent ev;
ev.kind = TableEventKind::ButtonClick;
ev.row = row_idx;
ev.col = col_idx;
ev.column_id = spec.id;
ev.action_id = spec.button_action;
ev.value = value;
events_out->push_back(std::move(ev));
}
if (has_color) ImGui::PopStyleColor(3);
break;
}
case CellRenderer::Dots: {
std::string s = value;
std::vector<std::string> tokens;
{
std::string cur;
char sep = spec.dots_separator ? spec.dots_separator : ',';
for (char c : s) {
if (c == sep) { tokens.push_back(cur); cur.clear(); }
else cur.push_back(c);
}
if (!cur.empty()) tokens.push_back(cur);
}
int limit = (spec.dots_max > 0)
? std::min((int)tokens.size(), spec.dots_max)
: (int)tokens.size();
float font_h = ImGui::GetTextLineHeight();
float radius = (spec.dots_glyph_size > 0.f ? spec.dots_glyph_size : font_h * 0.32f);
float spacing = radius * 2.3f;
ImVec2 origin = ImGui::GetCursorScreenPos();
float dot_y = origin.y + font_h * 0.5f;
ImDrawList* dl = ImGui::GetWindowDrawList();
ImU32 default_col = IM_COL32(110, 110, 125, 255);
for (int t = 0; t < limit; ++t) {
ImU32 col = default_col;
for (const auto& br : spec.badges) {
if (br.value == tokens[t]) {
ImVec4 c = hex_to_imcolor_g(br.color_hex);
if (c.x >= 0.f) col = ImGui::ColorConvertFloat4ToU32(c);
break;
}
}
float cx = origin.x + radius + t * spacing;
dl->AddCircleFilled(ImVec2(cx, dot_y), radius, col, 18);
}
float total_w = (limit > 0 ? (limit * spacing) : 0.f);
ImGui::Dummy(ImVec2(total_w, font_h));
if (spec.tooltip_on_hover && ImGui::IsItemHovered()) {
ImVec2 mp = ImGui::GetMousePos();
for (int t = 0; t < limit; ++t) {
float cx = origin.x + radius + t * spacing;
float dx = mp.x - cx, dy = mp.y - dot_y;
if (dx*dx + dy*dy <= radius * radius * 1.5f) {
ImGui::SetTooltip("%s", tokens[t].c_str());
break;
}
}
}
if (spec.dots_show_count) {
ImGui::SameLine(0, 6.0f);
ImGui::TextDisabled("(%d)", (int)tokens.size());
}
break;
}
case CellRenderer::CategoricalChip: {
const ChipRule* matched_chip = nullptr;
for (const auto& cr : spec.chips) {
if (cr.match == value) { matched_chip = &cr; break; }
}
if (matched_chip) {
float font_h = ImGui::GetTextLineHeight();
ImVec2 cursor = ImGui::GetCursorScreenPos();
float radius = 4.0f;
float cy = cursor.y + font_h * 0.5f;
float cx = cursor.x + radius;
ImDrawList* dl = ImGui::GetWindowDrawList();
ImU32 chip_col = parse_hex_color(matched_chip->color, 1.0f);
dl->AddCircleFilled(ImVec2(cx, cy), radius, chip_col, 18);
ImGui::Dummy(ImVec2(radius * 2.0f + 4.0f, font_h));
ImGui::SameLine(0, 0);
}
ImGui::TextUnformatted(value);
break;
}
case CellRenderer::ColorScale: {
double v_raw = 0.0;
if (!parse_number(value, v_raw)) {
ImGui::TextUnformatted(value);
break;
}
double span = spec.range_max - spec.range_min;
float t = 0.f;
if (span > 1e-12) {
t = (float)((v_raw - spec.range_min) / span);
}
t = t < 0.f ? 0.f : (t > 1.f ? 1.f : t);
ImU32 bg_col = lerp_color_along_stops_g(spec.range_stops, t, spec.range_alpha);
ImVec2 cell_min = ImGui::GetCursorScreenPos();
float row_h = ImGui::GetTextLineHeight();
float col_w = ImGui::GetContentRegionAvail().x;
ImVec2 cell_max = ImVec2(cell_min.x + col_w, cell_min.y + row_h);
ImGui::GetWindowDrawList()->AddRectFilled(cell_min, cell_max, bg_col);
ImGui::TextUnformatted(value);
break;
}
default:
ImGui::TextUnformatted(value);
break;
}
// Tooltip (non-Dots renderers).
if (spec.renderer != CellRenderer::Dots &&
spec.tooltip_on_hover && ImGui::IsItemHovered()) {
const char* tip = (spec.tooltip == "auto") ? value : spec.tooltip.c_str();
if (tip && tip[0]) ImGui::SetTooltip("%s", tip);
}
}
// ---------------------------------------------------------------------------
// draw_row_inspector_modal (static — solo usada por render_grid_stage_n)
// ---------------------------------------------------------------------------
static void draw_row_inspector_modal(State& st, int active,
const char* const* cells, int rows, int cols,
const std::vector<std::string>& headers,
const std::vector<ColumnType>& types,
const std::vector<std::string>& prev_input_headers)
{
if (!st.inspect_open) return;
if (st.inspect_row < 0 || st.inspect_row >= rows) {
st.inspect_open = false;
return;
}
ImGui::OpenPopup("##row_inspector");
ImGui::SetNextWindowSize(ImVec2(560, 400), ImGuiCond_Appearing);
if (ImGui::BeginPopupModal("##row_inspector", &st.inspect_open,
ImGuiWindowFlags_NoSavedSettings)) {
ImGui::Text("Row %d", st.inspect_row);
ImGui::SameLine(0, 20);
if (ImGui::SmallButton("Copy TSV")) {
std::string tsv = row_to_tsv_g(cells, rows, cols, st.inspect_row, headers);
ImGui::SetClipboardText(tsv.c_str());
}
ImGui::SameLine();
bool can_filter = (active > 0 && !prev_input_headers.empty());
ImGui::BeginDisabled(!can_filter);
if (ImGui::SmallButton("Filter prev stage by this row")) {
int target = active - 1;
for (int c = 0; c < cols; ++c) {
const char* v = cells[st.inspect_row * cols + c];
if (!v || !*v) continue;
const std::string& h = headers[c];
std::string h_clean;
parse_breakout_granularity(h, h_clean);
int ci = -1;
for (size_t i = 0; i < prev_input_headers.size(); ++i) {
if (prev_input_headers[i] == h_clean) { ci = (int)i; break; }
}
if (ci < 0) continue;
DrillStep step;
step.target_stage = target;
step.filter_pos = (int)st.stages[target].filters.size();
step.prev_active_stage = st.active_stage;
step.added = make_drill_filter(ci, v);
if (apply_drill_step(st, step)) {
st.drill_back.push_back(step);
}
}
st.drill_forward.clear();
st.inspect_open = false;
}
ImGui::EndDisabled();
ImGui::Separator();
ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg
| ImGuiTableFlags_ScrollY | ImGuiTableFlags_Resizable;
if (ImGui::BeginTable("##inspector_tbl", 2, flags, ImVec2(-1, -1))) {
ImGui::TableSetupColumn("col");
ImGui::TableSetupColumn("value");
ImGui::TableHeadersRow();
for (int c = 0; c < cols; ++c) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ColumnType t = (c < (int)types.size()) ? types[c] : ColumnType::String;
ImGui::Text("%s %s", column_type_icon_g(t),
(c < (int)headers.size()) ? headers[c].c_str() : "?");
ImGui::TableSetColumnIndex(1);
const char* v = cells[st.inspect_row * cols + c];
ImGui::TextWrapped("%s", v ? v : "");
}
ImGui::EndTable();
}
ImGui::EndPopup();
}
}
// ---------------------------------------------------------------------------
// render_grid_stage0
// ---------------------------------------------------------------------------
void render_grid_stage0(const char* id,
State& st,
const char* const* cells,
int row_count, int orig_cols, int eff_cols,
const char* const* eff_headers,
const ColumnType* eff_types,
const int* src_for_eff,
const std::vector<int>& visible_rows,
const TableInput& main_t,
std::vector<TableEvent>* events_out)
{
UiState& U = ui();
Stage& act = st.stages[0];
// draw_header_menu takes const std::vector<ColumnType>&; build a local view.
std::vector<ColumnType> eff_types_vec(eff_types, eff_types + eff_cols);
int visible_cols = 0;
for (int c = 0; c < eff_cols; ++c)
if (st.col_visible[c]) ++visible_cols;
if (visible_cols == 0) return;
ImGuiTableFlags flags =
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY;
// Issue 0081-O.7: disable ALL Selectable bg painting; hover + selection
// painted via TableSetBgColor below.
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0));
if (ImGui::BeginTable(id, visible_cols, flags, ImVec2(0, 0))) {
for (int dc = 0; dc < (int)st.col_order.size(); ++dc) {
int c = st.col_order[dc];
if (c < 0 || c >= eff_cols) continue;
if (!st.col_visible[c]) continue;
ImGui::TableSetupColumn(eff_headers[c], ImGuiTableColumnFlags_None, 0.0f, (ImGuiID)c);
}
ImGui::TableSetupScrollFreeze(0, 1);
// Custom header row
ImGui::TableNextRow(ImGuiTableRowFlags_Headers);
for (int dc = 0; dc < (int)st.col_order.size(); ++dc) {
int c = st.col_order[dc];
if (c < 0 || c >= eff_cols) continue;
if (!st.col_visible[c]) continue;
ImGui::TableSetColumnIndex(dc);
ImGui::PushID(c);
int sort_pos = -1;
bool sort_desc = false;
for (size_t si = 0; si < act.sorts.size(); ++si) {
if (act.sorts[si].col == eff_headers[c]) {
sort_pos = (int)si; sort_desc = act.sorts[si].desc; break;
}
}
char arrow[16] = "";
if (sort_pos == 0) std::snprintf(arrow, sizeof(arrow), " %s", sort_desc ? "v" : "^");
else if (sort_pos > 0) std::snprintf(arrow, sizeof(arrow), " %s%d", sort_desc ? "v" : "^", sort_pos + 1);
char label[200];
std::snprintf(label, sizeof(label), "%s %s%s",
column_type_icon_g(eff_types[c]), eff_headers[c], arrow);
ImGui::PushStyleColor(ImGuiCol_Header, IM_COL32(45, 50, 65, 200));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, IM_COL32(65, 75, 95, 220));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, IM_COL32(80, 95, 130, 240));
bool clicked = ImGui::Selectable(label, false, ImGuiSelectableFlags_DontClosePopups);
ImGui::PopStyleColor(3);
if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) {
ImGui::SetDragDropPayload("##colreorder", &c, sizeof(int));
ImGui::Text("Move %s", eff_headers[c]);
ImGui::EndDragDropSource();
}
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* p = ImGui::AcceptDragDropPayload("##colreorder")) {
int src = *(const int*)p->Data;
reorder_column_g(st, src, c);
}
ImGui::EndDragDropTarget();
}
if (clicked) {
bool shift = ImGui::GetIO().KeyShift;
apply_header_sort_click(act, eff_headers[c], shift);
}
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
U.header_popup_col = c;
ImGui::OpenPopup("##hdr_menu");
}
if (ImGui::BeginPopup("##hdr_menu") && U.header_popup_col == c) {
// Restore visible hover inside popup (outer table pushes to transparent).
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, IM_COL32( 70, 95, 140, 230));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, IM_COL32( 55, 80, 120, 240));
ImGui::PushStyleColor(ImGuiCol_Header, IM_COL32( 0, 0, 0, 0));
draw_header_menu(st, act, c, eff_headers, eff_cols, eff_types_vec, orig_cols, true);
ImGui::PopStyleColor(3);
ImGui::EndPopup();
}
// Stats overlay
if (st.stats_mode && c < (int)st.stats_cache.size()) {
const ColStats& s = st.stats_cache[c];
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(170, 190, 220, 220));
ImGui::Text("missing: %d", s.empty_count);
ImGui::Text("uniq: %d%s", s.unique_count, s.unique_capped ? "+" : "");
if (s.numeric) {
ImGui::Text("mean: %.2f", s.mean);
ImGui::Text("p25: %.2f", s.p25);
ImGui::Text("p50: %.2f", s.p50);
ImGui::Text("p75: %.2f", s.p75);
if (!s.hist.empty()) {
char overlay[64];
std::snprintf(overlay, sizeof(overlay), "[%.2f..%.2f]", s.min, s.max);
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, IM_COL32(70, 140, 220, 230));
ImGui::PlotHistogram("##hist", s.hist.data(), (int)s.hist.size(),
0, overlay, 0.0f, FLT_MAX, ImVec2(-1, 36));
ImGui::PopStyleColor();
}
} else if (!s.top_categories.empty()) {
int mx = 0;
for (const auto& kv : s.top_categories) if (kv.second > mx) mx = kv.second;
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, IM_COL32(70, 140, 220, 220));
for (const auto& kv : s.top_categories) {
float frac = mx > 0 ? (float)kv.second / (float)mx : 0.f;
char ovl[96];
std::snprintf(ovl, sizeof(ovl), "%s (%d)", kv.first.c_str(), kv.second);
ImGui::ProgressBar(frac, ImVec2(-1, 12), ovl);
}
ImGui::PopStyleColor();
}
ImGui::PopStyleColor();
}
ImGui::PopID();
}
int sel_rmin = std::min(st.sel_anchor_row, st.sel_end_row);
int sel_rmax = std::max(st.sel_anchor_row, st.sel_end_row);
int sel_cmin = std::min(st.sel_anchor_col, st.sel_end_col);
int sel_cmax = std::max(st.sel_anchor_col, st.sel_end_col);
ImGuiListClipper clipper;
clipper.Begin((int)visible_rows.size());
while (clipper.Step()) {
for (int ri = clipper.DisplayStart; ri < clipper.DisplayEnd; ++ri) {
int r = visible_rows[ri];
ImGui::TableNextRow();
int draw_idx = 0;
for (int oc = 0; oc < (int)st.col_order.size(); ++oc) {
int c = st.col_order[oc];
if (c < 0 || c >= eff_cols) continue;
if (!st.col_visible[c]) continue;
ImGui::TableSetColumnIndex(draw_idx++);
// Capture cell rect before render (for per-cell hover overlay).
ImVec2 cell_min = ImGui::GetCursorScreenPos();
float cell_w = ImGui::GetContentRegionAvail().x;
float cell_h = ImGui::GetTextLineHeight();
ImVec2 cell_max(cell_min.x + cell_w, cell_min.y + cell_h);
int src = src_for_eff[c];
// NOTE: Lua-derived cell evaluation is intentionally NOT done here.
// The caller (data_table.cpp entrypoint) evaluates derived cells and
// passes a pre-materialized `cells` array to render_grid_stage0, OR
// this function operates on the raw cells with src mapping.
// For stage 0, derived cols are evaluated inline in the entrypoint
// and NOT passed through this function path — the entrypoint calls
// render_grid_stage0 only for the raw-cell + src_for_eff path, while
// derived evaluation stays in the entrypoint context.
// This avoids passing Lua state through the grid API boundary.
const char* cell = cells[r * orig_cols + src];
float dot_advance = 0.f;
apply_color_rules_for_cell(st, c, cell,
cell_min, cell_w, cell_h,
dot_advance);
if (dot_advance > 0.f) {
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + dot_advance);
}
bool in_sel = (st.sel_active &&
ri >= sel_rmin && ri <= sel_rmax &&
oc >= sel_cmin && oc <= sel_cmax);
ImGui::PushID(r * eff_cols + c);
{
bool custom_rendered = false;
const ColumnSpec* cell_cs = nullptr;
if (!main_t.column_specs.empty() &&
c < (int)main_t.column_specs.size()) {
cell_cs = &main_t.column_specs[(size_t)c];
if (cell_cs->renderer != CellRenderer::Text) {
draw_cell_custom(*cell_cs, cell, r, c, events_out);
custom_rendered = true;
}
}
if (!custom_rendered) {
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0, 0, 0, 0));
ImGui::Selectable(cell ? cell : "", in_sel,
ImGuiSelectableFlags_AllowDoubleClick,
ImVec2(0, ImGui::GetTextLineHeight()));
ImGui::PopStyleColor();
if (cell_cs && cell_cs->tooltip_on_hover &&
ImGui::IsItemHovered()) {
const char* tip = (cell_cs->tooltip == "auto")
? (cell ? cell : "")
: cell_cs->tooltip.c_str();
if (tip && tip[0]) ImGui::SetTooltip("%s", tip);
}
}
}
// Per-cell bg via TableSetBgColor (edge-to-edge, no Selectable padding).
{
ImVec2 mp = ImGui::GetMousePos();
ImVec2 pad = ImGui::GetStyle().CellPadding;
bool is_hovered =
mp.x >= cell_min.x - pad.x && mp.x < cell_min.x + cell_w + pad.x &&
mp.y >= cell_min.y - pad.y && mp.y < cell_min.y + cell_h + pad.y &&
ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
if (in_sel) {
ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg,
is_hovered
? IM_COL32(102, 140, 217, 80)
: IM_COL32(102, 140, 217, 60));
} else if (is_hovered) {
ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg,
IM_COL32(255, 255, 255, 22));
}
}
bool hovered = ImGui::IsItemHovered(
ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
if (hovered) {
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
st.sel_anchor_row = ri; st.sel_anchor_col = oc;
st.sel_end_row = ri; st.sel_end_col = oc;
st.sel_active = true;
st.sel_dragging = true;
} else if (st.sel_dragging) {
st.sel_end_row = ri; st.sel_end_col = oc;
}
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)
&& events_out) {
TableEvent ev;
ev.kind = TableEventKind::RowDoubleClick;
ev.row = r;
ev.col = c;
ev.value = cell ? cell : "";
if (!main_t.column_specs.empty() &&
c < (int)main_t.column_specs.size())
ev.column_id = main_t.column_specs[(size_t)c].id;
events_out->push_back(std::move(ev));
}
if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
U.pending_col = c;
U.pending_value = cell ? cell : "";
U.open_cell_popup = true;
if (events_out) {
TableEvent ev;
ev.kind = TableEventKind::RowRightClick;
ev.row = r;
ev.col = c;
ev.value = cell ? cell : "";
if (!main_t.column_specs.empty() &&
c < (int)main_t.column_specs.size())
ev.column_id = main_t.column_specs[(size_t)c].id;
events_out->push_back(std::move(ev));
}
}
}
ImGui::PopID();
}
}
}
if (st.sel_dragging && !ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
st.sel_dragging = false;
}
ImGui::EndTable();
}
ImGui::PopStyleColor(3); // HeaderHovered/HeaderActive/Header
// Ctrl+C -> TSV copy
if (st.sel_active && ImGui::GetIO().KeyCtrl &&
ImGui::IsKeyPressed(ImGuiKey_C, false))
{
int rmin = std::min(st.sel_anchor_row, st.sel_end_row);
int rmax = std::max(st.sel_anchor_row, st.sel_end_row);
int cmin = std::min(st.sel_anchor_col, st.sel_end_col);
int cmax = std::max(st.sel_anchor_col, st.sel_end_col);
std::string out;
bool first = true;
for (int oc = cmin; oc <= cmax; ++oc) {
if (oc < 0 || oc >= (int)st.col_order.size()) continue;
int c = st.col_order[oc];
if (c < 0 || c >= eff_cols) continue;
if (!st.col_visible[c]) continue;
if (!first) out += '\t';
out += eff_headers[c];
first = false;
}
out += '\n';
for (int ri = rmin; ri <= rmax; ++ri) {
if (ri < 0 || ri >= (int)visible_rows.size()) continue;
int r = visible_rows[ri];
first = true;
for (int oc = cmin; oc <= cmax; ++oc) {
if (oc < 0 || oc >= (int)st.col_order.size()) continue;
int c = st.col_order[oc];
if (c < 0 || c >= eff_cols) continue;
if (!st.col_visible[c]) continue;
int src = src_for_eff[c];
const char* v = cells[r * orig_cols + src];
std::string sv = v ? v : "";
for (char& ch : sv) if (ch == '\t' || ch == '\n') ch = ' ';
if (!first) out += '\t';
out += sv;
first = false;
}
out += '\n';
}
ImGui::SetClipboardText(out.c_str());
}
}
// ---------------------------------------------------------------------------
// render_grid_stage_n
// ---------------------------------------------------------------------------
void render_grid_stage_n(const char* id,
State& st,
const char* const* cur_cells,
int cur_rows, int cur_cols_n,
const std::vector<std::string>& cur_headers,
const std::vector<ColumnType>& cur_types,
const std::vector<std::string>& input_headers_active,
int n_breakouts,
const TableInput& main_t,
std::vector<TableEvent>* events_out)
{
UiState& U = ui();
int active = st.active_stage;
Stage& act = st.stages[active];
ImGuiTableFlags flags =
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY;
// Issue 0081-O.7: tone down row hover.
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(1.0f, 1.0f, 1.0f, 0.05f));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(1.0f, 1.0f, 1.0f, 0.08f));
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.40f, 0.55f, 0.85f, 0.20f));
if (cur_cols_n > 0 && ImGui::BeginTable(id, cur_cols_n, flags, ImVec2(0, 0))) {
for (int c = 0; c < cur_cols_n; ++c) {
ImGui::TableSetupColumn(cur_headers[c].c_str(),
ImGuiTableColumnFlags_None, 0.0f, (ImGuiID)c);
}
ImGui::TableSetupScrollFreeze(0, 1);
// Custom header row: icon + name + sort indicator + stats overlay.
ImGui::TableNextRow(ImGuiTableRowFlags_Headers);
for (int c = 0; c < cur_cols_n; ++c) {
ImGui::TableSetColumnIndex(c);
int sort_pos = -1;
bool sort_desc = false;
for (size_t si = 0; si < act.sorts.size(); ++si) {
if (act.sorts[si].col == cur_headers[c]) {
sort_pos = (int)si; sort_desc = act.sorts[si].desc; break;
}
}
char arrow[16] = "";
if (sort_pos == 0) std::snprintf(arrow, sizeof(arrow), " %s", sort_desc ? "v" : "^");
else if (sort_pos > 0) std::snprintf(arrow, sizeof(arrow), " %s%d", sort_desc ? "v" : "^", sort_pos + 1);
ImGui::PushStyleColor(ImGuiCol_Header, IM_COL32(45, 50, 65, 200));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, IM_COL32(65, 75, 95, 220));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, IM_COL32(80, 95, 130, 240));
char lbl[200];
std::snprintf(lbl, sizeof(lbl), "%s %s%s",
column_type_icon_g(cur_types[c]),
cur_headers[c].c_str(), arrow);
bool h_clicked = ImGui::Selectable(lbl, false, ImGuiSelectableFlags_DontClosePopups);
ImGui::PopStyleColor(3);
if (h_clicked) {
bool shift = ImGui::GetIO().KeyShift;
apply_header_sort_click(act, cur_headers[c], shift);
}
if (st.stats_mode && c < (int)st.stats_cache.size()) {
const ColStats& s = st.stats_cache[c];
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(170, 190, 220, 220));
ImGui::Text("missing: %d", s.empty_count);
ImGui::Text("uniq: %d%s", s.unique_count, s.unique_capped ? "+" : "");
if (s.numeric) {
ImGui::Text("mean: %.2f", s.mean);
ImGui::Text("p25: %.2f", s.p25);
ImGui::Text("p50: %.2f", s.p50);
ImGui::Text("p75: %.2f", s.p75);
if (!s.hist.empty()) {
char overlay[64];
std::snprintf(overlay, sizeof(overlay), "[%.2f..%.2f]", s.min, s.max);
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, IM_COL32(70, 140, 220, 230));
ImGui::PlotHistogram("##hist", s.hist.data(), (int)s.hist.size(),
0, overlay, 0.0f, FLT_MAX, ImVec2(-1, 36));
ImGui::PopStyleColor();
}
} else if (!s.top_categories.empty()) {
int mx = 0;
for (const auto& kv : s.top_categories) if (kv.second > mx) mx = kv.second;
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, IM_COL32(70, 140, 220, 220));
for (const auto& kv : s.top_categories) {
float frac = mx > 0 ? (float)kv.second / (float)mx : 0.f;
char ovl[96];
std::snprintf(ovl, sizeof(ovl), "%s (%d)", kv.first.c_str(), kv.second);
ImGui::ProgressBar(frac, ImVec2(-1, 12), ovl);
}
ImGui::PopStyleColor();
}
ImGui::PopStyleColor();
}
}
for (int r = 0; r < cur_rows; ++r) {
ImGui::TableNextRow();
for (int c = 0; c < cur_cols_n; ++c) {
ImGui::TableSetColumnIndex(c);
const char* cell = cur_cells[r * cur_cols_n + c];
ImGui::PushID(r * cur_cols_n + c);
{
bool custom_rendered = false;
const ColumnSpec* cell_cs2 = nullptr;
if (!main_t.column_specs.empty() &&
c < (int)main_t.column_specs.size()) {
cell_cs2 = &main_t.column_specs[(size_t)c];
if (cell_cs2->renderer != CellRenderer::Text) {
draw_cell_custom(*cell_cs2, cell, r, c, events_out);
custom_rendered = true;
}
}
if (!custom_rendered) {
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0, 0, 0, 0));
ImGui::Selectable(cell ? cell : "", false, 0,
ImVec2(0, ImGui::GetTextLineHeight()));
ImGui::PopStyleColor();
if (cell_cs2 && cell_cs2->tooltip_on_hover &&
ImGui::IsItemHovered()) {
const char* tip = (cell_cs2->tooltip == "auto")
? (cell ? cell : "")
: cell_cs2->tooltip.c_str();
if (tip && tip[0]) ImGui::SetTooltip("%s", tip);
}
}
}
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
U.pending_col = c;
U.pending_value = cell ? cell : "";
st.inspect_row = r;
ImGui::OpenPopup("##drill_popup");
if (events_out) {
TableEvent ev;
ev.kind = TableEventKind::RowRightClick;
ev.row = r;
ev.col = c;
ev.value = cell ? cell : "";
if (!main_t.column_specs.empty() &&
c < (int)main_t.column_specs.size())
ev.column_id = main_t.column_specs[(size_t)c].id;
events_out->push_back(std::move(ev));
}
}
if (ImGui::BeginPopup("##drill_popup")) {
if (c < n_breakouts) {
char lbl[256];
std::snprintf(lbl, sizeof(lbl), "Drill into: %s = %s",
cur_headers[c].c_str(), cell ? cell : "");
if (ImGui::MenuItem(lbl)) {
drill_into(st, active, cur_headers[c],
cell ? std::string(cell) : "",
input_headers_active);
ImGui::CloseCurrentPopup();
}
ImGui::Separator();
}
if (ImGui::MenuItem("Inspect row...")) {
st.inspect_row = r;
st.inspect_open = true;
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
ImGui::PopID();
}
}
ImGui::EndTable();
}
ImGui::PopStyleColor(3); // HeaderHovered/HeaderActive/Header
// Row inspector modal
draw_row_inspector_modal(st, active, cur_cells, cur_rows, cur_cols_n,
cur_headers, cur_types, input_headers_active);
}
} // namespace data_table