0b9af8f1bb
- Replace TextColored+glyph with ImDrawList::AddCircleFilled in CellRenderer::Dots. Dots are now font-independent: no dependency on Unicode glyph coverage. Fixes "dots show as ?" on Karla/Roboto/Inter fonts that lack Geometric Shapes block. - dots_glyph_size now controls circle radius (px) instead of font scale. - BadgeRule.label is ignored for Dots (documented in data_table_types.h + docs). - data_table.md bumped to v1.3.1 with capability growth log entry. - docs/capabilities/data_table_renderers.md: Dots section updated + Common pitfalls entry added: "Asumir que cualquier glyph Unicode renderea". - dag_engine_ui/tabs.cpp: removed stale "● glyph" comment from BadgeRule. - Recompiled: dag_engine_ui, registry_dashboard, graph_explorer, navegator_dashboard, odr_console. All 5 apps deployed to Desktop/apps/. Build Linux + tests 4/4 green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
303 lines
12 KiB
Markdown
303 lines
12 KiB
Markdown
# data_table_renderers — declarative cell renderers (v1.3.1)
|
|
|
|
Tag: `cpp-tables` (mismo grupo que TQL; los renderers son parte del stack `data_table`).
|
|
|
|
Extiende `data_table_cpp_viz` con una API declarativa para renderizar columnas con
|
|
Badge, Progress, Duration, Icon, **Button** (Phase 2) y **Dots** (Phase 2.5), emitir
|
|
eventos de interaccion (ButtonClick, RowDoubleClick, RowRightClick), mostrar tooltips
|
|
por celda y persistir los specs en TQL (`aux_column_specs` roundtrip). Back-compat
|
|
100%: apps sin `column_specs` ni `events_out` no necesitan cambios.
|
|
|
|
## Decision tree: que renderer elegir?
|
|
|
|
```
|
|
Que muestra la celda?
|
|
|
|
|
+-- Texto libre arbitrario -> Text (default)
|
|
|
|
|
+-- Enum-like status discreto (1 val)
|
|
| |
|
|
| +-- Solo color de fondo -> Badge
|
|
| +-- Necesita icono -> Icon
|
|
| +-- Es accion clickable -> Button
|
|
|
|
|
+-- Lista/secuencia de status -> Dots
|
|
|
|
|
+-- Numero 0..1 o 0..100 -> Progress
|
|
|
|
|
+-- Milisegundos con threshold -> Duration
|
|
|
|
|
+-- Editable inline -> TextInput (Fase 3)
|
|
|
|
|
+-- Widget propio fuera del set -> Custom (Fase 3, escape hatch)
|
|
```
|
|
|
|
## Tipos nuevos / actualizados en `data_table_types.h`
|
|
|
|
| Tipo | Que es |
|
|
|---|---|
|
|
| `CellRenderer` | enum class: `Text=0`, `Badge=1`, `Progress=2`, `Duration=3`, `Icon=4`, **`Button=5`**, **`Dots=8`** |
|
|
| `BadgeRule` | value (exact match) + color_hex + label opcional (usado como glyph override en Dots) |
|
|
| `IconMapEntry` | value + icon_name (ej. `"TI_BOLT"`) + color_hex opcional |
|
|
| `ColumnSpec` | id + renderer + badges / progress / duration / icon_map / button_action, button_label, button_color_hex / tooltip, tooltip_on_hover / **dots_separator, dots_max, dots_show_count, dots_glyph_size** |
|
|
| `TableInput::column_specs` | `std::vector<ColumnSpec>` sidecar opcional (size 0 o == cols) |
|
|
| **`TableEventKind`** | enum class: ButtonClick=1, RowDoubleClick=2, RowRightClick=3, CellEdit=4 (reservado) |
|
|
| **`TableEvent`** | kind + row + col + column_id + action_id + value |
|
|
| **`State::aux_column_specs`** | specs persistidos en TQL (roundtrip via tql_emit/tql_apply) |
|
|
|
|
## Ejemplo canonico: Recent Executions (status Badge + duration Duration)
|
|
|
|
```cpp
|
|
#include "viz/data_table.h"
|
|
#include "core/data_table_types.h"
|
|
|
|
// --- Datos (owner externo) ---
|
|
static const char* g_exec_cells[] = {
|
|
"ok", "142",
|
|
"error", "3850",
|
|
"ok", "72",
|
|
"warn", "1100",
|
|
};
|
|
|
|
// --- Setup (una vez, en init de la app o antes del loop) ---
|
|
data_table::TableInput t;
|
|
t.name = "executions";
|
|
t.rows = 4;
|
|
t.cols = 2;
|
|
t.cells = g_exec_cells;
|
|
t.headers = {"status", "duration_ms"};
|
|
t.types = {data_table::ColumnType::String, data_table::ColumnType::Float};
|
|
|
|
// Columna 0: Badge por valor de status
|
|
data_table::ColumnSpec cs_status;
|
|
cs_status.id = "status";
|
|
cs_status.renderer = data_table::CellRenderer::Badge;
|
|
cs_status.badges = {
|
|
data_table::BadgeRule{"ok", "#22c55e", "OK"},
|
|
data_table::BadgeRule{"error", "#ef4444", "ERROR"},
|
|
data_table::BadgeRule{"warn", "#f59e0b", "WARN"},
|
|
};
|
|
|
|
// Columna 1: Duration con gradiente verde/amarillo/rojo
|
|
data_table::ColumnSpec cs_dur;
|
|
cs_dur.id = "duration_ms";
|
|
cs_dur.renderer = data_table::CellRenderer::Duration;
|
|
cs_dur.duration_warn_ms = 500.0f;
|
|
cs_dur.duration_error_ms = 2000.0f;
|
|
|
|
t.column_specs = {cs_status, cs_dur};
|
|
|
|
data_table::State st; // persiste entre frames
|
|
|
|
// --- Render loop ---
|
|
ImGui::Begin("Executions");
|
|
ImGui::BeginChild("##exec_tbl", ImVec2(-1, -1));
|
|
data_table::render("##exec", {t}, st);
|
|
ImGui::EndChild();
|
|
ImGui::End();
|
|
```
|
|
|
|
## Ejemplo: Progress bar + Icon
|
|
|
|
```cpp
|
|
data_table::ColumnSpec cs_progress;
|
|
cs_progress.id = "completion";
|
|
cs_progress.renderer = data_table::CellRenderer::Progress;
|
|
cs_progress.progress_scale_100 = true; // cell value es 0..100
|
|
cs_progress.progress_color_hex = "#3b82f6"; // azul; "" -> ImPlot auto
|
|
|
|
data_table::ColumnSpec cs_icon;
|
|
cs_icon.id = "kind";
|
|
cs_icon.renderer = data_table::CellRenderer::Icon;
|
|
cs_icon.icon_map = {
|
|
data_table::IconMapEntry{"fn", "TI_BOLT", "#3b82f6"},
|
|
data_table::IconMapEntry{"type", "TI_DATABASE", ""},
|
|
data_table::IconMapEntry{"app", "TI_SETTINGS", "#6b7280"},
|
|
};
|
|
|
|
t.column_specs = {cs_progress, cs_icon};
|
|
```
|
|
|
|
## Iconos soportados en CellRenderer::Icon
|
|
|
|
El lookup es una tabla estatica de ~30 nombres frecuentes:
|
|
|
|
```
|
|
TI_CHECK TI_X TI_ALERT_CIRCLE TI_CIRCLE_DOT
|
|
TI_CLOCK TI_LOADER TI_BAN TI_PLAYER_PLAY
|
|
TI_PLAYER_PAUSE TI_PLAYER_STOP TI_DATABASE TI_SETTINGS
|
|
TI_USER TI_USERS TI_FILE TI_FOLDER
|
|
TI_REFRESH TI_BOLT TI_INFO_CIRCLE TI_ARROW_UP
|
|
TI_ARROW_DOWN TI_ARROW_RIGHT TI_ARROW_LEFT TI_DOTS
|
|
TI_EYE TI_EYE_OFF TI_EDIT TI_TRASH
|
|
TI_COPY TI_EXTERNAL_LINK
|
|
```
|
|
|
|
Si el `icon_name` no esta en la tabla, la celda se renderiza como texto plano.
|
|
|
|
## Ejemplo Phase 2: Button + events + tooltip
|
|
|
|
```cpp
|
|
// --- Setup ---
|
|
t.column_specs.resize(t.cols);
|
|
|
|
// Columna "actions": boton Cancel por fila
|
|
t.column_specs[col_actions].renderer = data_table::CellRenderer::Button;
|
|
t.column_specs[col_actions].button_action = "cancel";
|
|
t.column_specs[col_actions].button_label = "Cancel";
|
|
t.column_specs[col_actions].button_color_hex = "#ef4444";
|
|
|
|
// Columna "status": tooltip automatico (muestra valor truncado)
|
|
t.column_specs[col_status].tooltip = "auto";
|
|
t.column_specs[col_status].tooltip_on_hover = true;
|
|
|
|
// --- Render loop ---
|
|
events.clear();
|
|
data_table::render("##t", {t}, st, &events);
|
|
|
|
// --- Procesar eventos ---
|
|
for (const auto& ev : events) {
|
|
if (ev.kind == data_table::TableEventKind::ButtonClick &&
|
|
ev.action_id == "cancel") {
|
|
cancel_row(ev.row);
|
|
}
|
|
if (ev.kind == data_table::TableEventKind::RowDoubleClick) {
|
|
open_detail(ev.row);
|
|
}
|
|
if (ev.kind == data_table::TableEventKind::RowRightClick) {
|
|
// Abrir menu propio via ImGui::BeginPopup
|
|
ctx_menu_row = ev.row;
|
|
ImGui::OpenPopup("##ctx");
|
|
}
|
|
}
|
|
```
|
|
|
|
## Ejemplo Phase 2: TQL roundtrip de column_specs
|
|
|
|
```cpp
|
|
// Persistir specs en State.aux_column_specs para guardar en .tql
|
|
data_table::ColumnSpec cs;
|
|
cs.id = "status"; cs.renderer = data_table::CellRenderer::Badge;
|
|
cs.badges = {{"ok","#22c55e","OK"},{"error","#ef4444",""}};
|
|
st.aux_column_specs = {{cs}}; // [tabla_0: {spec_col_0}]
|
|
|
|
// tql_emit serializa aux_column_specs automaticamente
|
|
std::string tql = tql::emit(st, headers, types);
|
|
// tql_apply lo recupera en res.state.aux_column_specs
|
|
auto res = tql::apply(tql, headers);
|
|
// render() lo aplica si TableInput.column_specs esta vacio
|
|
data_table::render("##t", {t}, res.state, &events);
|
|
```
|
|
|
|
## Fronteras
|
|
|
|
- **Solo Column 0..N posicional**: `column_specs[i]` aplica a la columna en posicion `i` del `TableInput` original. No se mapea por nombre.
|
|
- **Button con celda vacia**: si el cell value es vacio, NO se dibuja boton. Poner un valor no vacio en la celda para habilitar el boton.
|
|
- **No implementa TextInput/Custom** (Phase 3 separado).
|
|
- **Stage N (agregado)**: los renderers se aplican por posicion de columna del output agregado — si el breakout cambia el numero de columnas, revisar los indices.
|
|
- **RowRightClick**: en el raw table (stage 0) el evento se emite pero el popup de drill-down interno sigue abriendose. La app puede abrir su propio popup al detectar el evento.
|
|
|
|
## Gotchas
|
|
|
|
- `column_specs.size()` debe ser 0 (sin specs) o igual a `t.cols`. Mezcla de tamaños puede causar out-of-bounds silencioso (el render hace `c < column_specs.size()` guard, pero es mejor ser explicitoo).
|
|
- `hex_to_imcolor` acepta `"#rrggbb"` o `"rrggbb"`. Alpha siempre 1.0. Sin soporte para `rgba`.
|
|
- El ColorRule existente de State (`st.color_rules`) sigue funcionando — ambos sistemas coexisten. Si hay conflicto, `column_specs` toma prioridad para el contenido de la celda; `color_rules` pinta el fondo via `TableSetBgColor`.
|
|
- En el renderer Badge el `Selectable` con background coloreado consume el item para hover/click — la seleccion de rango con drag puede verse afectada visualmente en columnas Badge.
|
|
- `events_out` no se limpia en `render()` — el caller debe llamar `events.clear()` antes de cada frame.
|
|
- `aux_column_specs` solo se persiste para `tables[0]` (el main table). Specs para tablas extra deben gestionarse por el caller.
|
|
|
|
## CellRenderer::Dots — timeline inline
|
|
|
|
Para sparkline-like de status (ultimas N ejecuciones, salud de checks, eventos):
|
|
|
|
```cpp
|
|
ColumnSpec recent;
|
|
recent.id = "recent";
|
|
recent.renderer = CellRenderer::Dots;
|
|
recent.badges = { {"success", "#22c55e"}, {"failed", "#ef4444"},
|
|
{"running", "#eab308"}, {"pending", "#94a3b8"} };
|
|
recent.dots_max = 5;
|
|
recent.tooltip_on_hover = true; // hover sobre cada dot muestra el status string
|
|
|
|
// Cell value: comma-separated status strings (most-recent first)
|
|
// "success,success,failed,running,pending"
|
|
```
|
|
|
|
Resultado visual: 5 circulos solidos coloreados (verde verde rojo amarillo gris).
|
|
|
|
Cada dot se dibuja con `ImDrawList::AddCircleFilled` — primitiva ImGui pura, sin dependencia de glyph Unicode. Funciona con cualquier fuente (Karla, Roboto, Inter, Tabler).
|
|
|
|
Campos de `ColumnSpec` para Dots:
|
|
|
|
| Campo | Default | Descripcion |
|
|
|---|---|---|
|
|
| `dots_separator` | `','` | Caracter separador de tokens en el cell value |
|
|
| `dots_max` | `0` (sin limite) | Limite hard de dots a mostrar |
|
|
| `dots_show_count` | `false` | Si true, añade ` (N)` al final con el total de tokens |
|
|
| `dots_glyph_size` | `0.0` | Radio del circulo en px; 0 = `font_height * 0.32` (auto) |
|
|
|
|
Color lookup: igual que Badge — cada token se busca exactamente en `badges.value`.
|
|
Sin match: gris tenue `IM_COL32(110, 110, 125, 255)`. `BadgeRule.label` es ignorado
|
|
en Dots (solo aplica al renderer Badge).
|
|
|
|
TQL: `renderer = "dots"`, campos opcionales `dots_separator`, `dots_max`, `dots_show_count`, `dots_glyph_size`.
|
|
|
|
## Notas
|
|
|
|
- Tests: `cpp/tests/test_column_specs.cpp` (10 tests: 1 back-compat + 4 renderer types + 3 Phase 2 + 2 Dots Phase 2.5). Smoke/linker; no requieren ImGui context.
|
|
- TQL roundtrip implementado en Phase 2 (issue 0081-O, v1.2.0): `tql_emit_test` (3 tests) + `tql_apply_test` (9 tests nuevos).
|
|
- Dots TQL roundtrip cubierto en `test_column_specs.cpp` test 10 (struct + aux_column_specs).
|
|
|
|
## Common pitfalls
|
|
|
|
### Multiple columns for a status timeline
|
|
|
|
**Incorrecto:**
|
|
|
|
```cpp
|
|
ti.headers = {"Name", "R1", "R2", "R3", "R4", "R5", ...};
|
|
for (int i = 1; i <= 5; ++i)
|
|
ti.column_specs[i].renderer = CellRenderer::Badge;
|
|
```
|
|
|
|
Problemas:
|
|
- 5 columnas separadas = 5x sort headers + 5x filter chips + filas desalineadas.
|
|
- Cells `"-"` para rellenar cuando hay menos de 5 runs.
|
|
- Imposible filtrar por "muestra DAGs con >=1 failure en ultimas 5".
|
|
- Bug real: dag_engine_ui v1 (ver issue 0081-O.5).
|
|
|
|
**Correcto:**
|
|
|
|
```cpp
|
|
ColumnSpec recent;
|
|
recent.renderer = CellRenderer::Dots;
|
|
recent.badges = { {"success","#22c55e"}, {"failed","#ef4444"}, ... };
|
|
ti.headers = {"Name", "Recent", ...};
|
|
// Cell value: "success,failed,success,running,pending"
|
|
```
|
|
|
|
Una sola columna, N dots inline. Tooltip por dot muestra el status concreto. Filtrable como string (`Recent contains failed`).
|
|
|
|
### Badge para texto libre
|
|
|
|
Badge espera **valores discretos** (enum-like: ok/error/running). Para texto libre con muchos valores unicos: Text + opcionalmente Custom renderer (Fase 3).
|
|
|
|
### Buttons para navegacion cuando RowDoubleClick es suficiente
|
|
|
|
Si la accion es "abrir detalle del row", no uses Button por row. Usa `RowDoubleClick` event + handler comun. Mas limpio y sin IDs de ImGui por fila.
|
|
|
|
### Asumir que cualquier glyph Unicode renderea
|
|
|
|
Las fuentes del proyecto (Karla, Roboto, Inter, Tabler) cubren Latin-1 + Tabler private use area. NO incluyen Geometric Shapes (●, ▲), Misc Symbols (★) ni Dingbats (✓). Si quieres usar uno de estos:
|
|
- **Para dots de status:** usa `CellRenderer::Dots` (primitiva `ImDrawList`, no depende de fuente).
|
|
- **Para checkmarks/X:** usa `TI_CHECK` / `TI_X` via `icon_name` en `IconMapEntry`.
|
|
- **Para texto puro con ●:** considera añadir un font fallback con Geometric Shapes (no recomendado — usa Dots renderer).
|
|
|
|
### Progress con valores fuera de 0..1
|
|
|
|
Progress espera `0..1`. Si tu valor es `0..100`, set `progress_scale_100 = true` o normaliza antes.
|
|
|
|
### Color hardcodeado fuera de column_specs
|
|
|
|
El coloreado debe vivir en `column_specs[i].badges`, no en `if (status=="ok") PushStyleColor(GREEN)` antes de `TextUnformatted`. Esto compila pero rompe la promesa declarativa: el TQL serializado no tendra la info y Ask AI no podra razonar sobre la tabla.
|